From 5cbb47dde04d156ec484b6d055bd2e3469a92f2d Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Tue, 11 Aug 2026 17:18:40 +0000 Subject: [PATCH 01/18] feat(supervisor): add managed Pi admission bridge Signed-off-by: Johnny Greco --- Cargo.lock | 1 + architecture/sandbox.md | 2 + crates/openshell-core/src/middleware.rs | 11 +- crates/openshell-sandbox/Cargo.toml | 2 + crates/openshell-sandbox/src/agent_bridge.rs | 209 +++++++++ crates/openshell-sandbox/src/lib.rs | 87 ++++ .../src/regex.rs | 2 + .../src/lib.rs | 403 +++++++++++++++++- .../src/remote.rs | 22 +- .../src/l7/relay.rs | 14 + .../src/l7/websocket.rs | 9 + .../openshell-supervisor-network/src/opa.rs | 2 +- .../openshell-supervisor-network/src/proxy.rs | 12 + docs/extensibility/supervisor-middleware.mdx | 8 +- .../src/main.rs | 24 +- proto/supervisor_middleware.proto | 64 ++- 16 files changed, 840 insertions(+), 32 deletions(-) create mode 100644 crates/openshell-sandbox/src/agent_bridge.rs diff --git a/Cargo.lock b/Cargo.lock index 3ae582a12a..a3f5439068 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4248,6 +4248,7 @@ dependencies = [ name = "openshell-sandbox" version = "0.0.0" dependencies = [ + "axum", "clap", "futures", "miette", diff --git a/architecture/sandbox.md b/architecture/sandbox.md index 786ed5194d..5347039399 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -181,6 +181,8 @@ middleware registry validates implementation-owned config. The generic registry and chain runner live in `openshell-supervisor-middleware`; first-party implementations live in `openshell-supervisor-middleware-builtins`. +Managed agent admission reuses the operator middleware registry without joining the HTTP chain. When a configured operator service advertises the required Pi rendered-prompt hook, the sandbox supervisor binds a loopback-only bridge inside the workload network namespace. The bridge stamps sandbox and provider identity, forwards the bounded versioned request to the selected gRPC service, and returns only its structured decision, optional replacement, and opaque receipt. + The supervisor installs policy and middleware registry changes as one runtime generation and preserves the last-known-good generation if preparation fails. Policy-only updates reuse the connected registry, so an external middleware diff --git a/crates/openshell-core/src/middleware.rs b/crates/openshell-core/src/middleware.rs index 2b3fb18982..c2f49e1c8e 100644 --- a/crates/openshell-core/src/middleware.rs +++ b/crates/openshell-core/src/middleware.rs @@ -11,8 +11,9 @@ use tokio::sync::mpsc; use tonic::{Request, Response, Status}; use crate::proto::{ - HttpHeader, HttpRequestEvaluation, HttpRequestResult, HttpRequestTarget, MiddlewareManifest, - RequestContext, SupervisorMiddlewarePhase, ValidateConfigRequest, ValidateConfigResponse, + AgentConversationEvaluation, AgentConversationResult, HttpHeader, HttpRequestEvaluation, + HttpRequestResult, HttpRequestTarget, MiddlewareManifest, RequestContext, + SupervisorMiddlewarePhase, ValidateConfigRequest, ValidateConfigResponse, WebSocketSessionEvent, WebSocketSessionEventResult, }; @@ -43,6 +44,11 @@ pub trait SupervisorMiddlewareEndpoint: Send + Sync { request: Request, ) -> Result, Status>; + async fn evaluate_agent_conversation( + &self, + request: Request, + ) -> Result, Status>; + async fn open_websocket_session( &self, requests: mpsc::Receiver, @@ -176,6 +182,7 @@ impl<'a> HttpRequestView<'a> { /// phase: SupervisorMiddlewarePhase::PreCredentials as i32, /// max_payload_bytes: 1024, /// timeout: String::new(), +/// ..Default::default() /// }], /// expected_audience: String::new(), /// } diff --git a/crates/openshell-sandbox/Cargo.toml b/crates/openshell-sandbox/Cargo.toml index c653db84dd..035130a362 100644 --- a/crates/openshell-sandbox/Cargo.toml +++ b/crates/openshell-sandbox/Cargo.toml @@ -26,6 +26,7 @@ openshell-supervisor-process = { path = "../openshell-supervisor-process" } # Async runtime tokio = { workspace = true } +axum = { workspace = true } # gRPC (tonic::Status downcast in error mapping) tonic = { workspace = true, features = ["channel", "tls-native-roots"] } @@ -47,6 +48,7 @@ rustls = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } prost = { workspace = true } +uuid = { workspace = true } # Logging tracing = { workspace = true } diff --git a/crates/openshell-sandbox/src/agent_bridge.rs b/crates/openshell-sandbox/src/agent_bridge.rs new file mode 100644 index 0000000000..70e3ecdd3a --- /dev/null +++ b/crates/openshell-sandbox/src/agent_bridge.rs @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Supervisor-owned loopback bridge for managed agent admission. + +use std::sync::Arc; + +use axum::extract::{DefaultBodyLimit, State}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::routing::post; +use axum::{Json, Router}; +use openshell_core::proto::{ + AgentConversationEvaluation, AgentConversationTarget, Decision, RequestContext, + SupervisorMiddlewarePhase, +}; +use serde::{Deserialize, Serialize}; +use tokio::net::TcpListener; +use tracing::{debug, warn}; + +pub const BRIDGE_ADDR: &str = "127.0.0.1:8193"; +pub const BRIDGE_PATH: &str = "/v1/agent/conversation"; +pub const BRIDGE_URL: &str = "http://127.0.0.1:8193/v1/agent/conversation"; +pub const BRIDGE_URL_ENV: &str = "OPENSHELL_PI_CONVERSATION_URL"; + +const MAX_BRIDGE_BODY_BYTES: usize = 256 * 1024; +const MAX_ADMISSION_BODY_BYTES: usize = 32 * 1024; +const PI_HARNESS_VERSION: &str = "extension-v1"; + +#[derive(Debug, Clone)] +pub struct BridgeConfig { + pub middleware_name: String, + pub sandbox_id: String, + pub provider_host: String, + pub middleware_config: prost_types::Struct, +} + +#[derive(Clone)] +struct BridgeState { + runner: openshell_supervisor_middleware::ChainRunner, + config: Arc, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct BridgeRequest { + harness_version: String, + #[serde(default)] + session_id: String, + #[serde(default)] + submission_id: String, + request_body: Vec, +} + +#[derive(Debug, Serialize)] +struct BridgeResponse { + decision: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + replacement_body: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + receipt: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + reason_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + metadata: Option>, +} + +#[derive(Debug, Serialize)] +struct BridgeError { + error: &'static str, +} + +pub fn spawn( + listener: TcpListener, + runner: openshell_supervisor_middleware::ChainRunner, + config: BridgeConfig, +) -> tokio::task::JoinHandle<()> { + let state = BridgeState { + runner, + config: Arc::new(config), + }; + tokio::spawn(async move { + let app = Router::new() + .route(BRIDGE_PATH, post(evaluate)) + .layer(DefaultBodyLimit::max(MAX_BRIDGE_BODY_BYTES)) + .with_state(state); + if let Err(error) = axum::serve(listener, app).await { + warn!(%error, "Pi admission bridge stopped"); + } + }) +} + +async fn evaluate(State(state): State, Json(input): Json) -> Response { + if !is_valid_admission_request(&input) { + return ( + StatusCode::BAD_REQUEST, + Json(BridgeError { + error: "invalid_admission_request", + }), + ) + .into_response(); + } + + let evaluation = AgentConversationEvaluation { + phase: SupervisorMiddlewarePhase::AgentContext as i32, + context: Some(RequestContext { + request_id: uuid::Uuid::new_v4().to_string(), + sandbox_id: state.config.sandbox_id.clone(), + originating_process: None, + }), + config: Some(state.config.middleware_config.clone()), + target: Some(AgentConversationTarget { + harness: "pi".into(), + harness_version: input.harness_version, + hook: "rendered_prompt_admission".into(), + schema_version: "openshell.pi-input.v1".into(), + scheme: "https".into(), + host: state.config.provider_host.clone(), + port: 443, + path: "/v1/chat/completions".into(), + }), + middleware_name: state.config.middleware_name.clone(), + session_id: input.session_id, + turn_id: input.submission_id, + request_body: input.request_body, + ..Default::default() + }; + + let result = match state.runner.evaluate_agent_conversation(evaluation).await { + Ok(result) => result, + Err(error) => { + debug!(error = %error, "Pi admission evaluation failed"); + return ( + StatusCode::BAD_GATEWAY, + Json(BridgeError { + error: "admission_unavailable", + }), + ) + .into_response(); + } + }; + + match Decision::try_from(result.decision).unwrap_or(Decision::Unspecified) { + Decision::Allow => Json(BridgeResponse { + decision: "allow", + replacement_body: result + .has_replacement_body + .then_some(result.replacement_body), + receipt: (!result.attestation.is_empty()).then_some(result.attestation), + reason_code: None, + metadata: Some(result.metadata), + }) + .into_response(), + Decision::Deny => Json(BridgeResponse { + decision: "deny", + replacement_body: None, + receipt: None, + reason_code: (!result.reason_code.is_empty()).then_some(result.reason_code), + metadata: None, + }) + .into_response(), + Decision::Unspecified => ( + StatusCode::BAD_GATEWAY, + Json(BridgeError { + error: "invalid_admission_response", + }), + ) + .into_response(), + } +} + +fn is_valid_admission_request(input: &BridgeRequest) -> bool { + input.harness_version == PI_HARNESS_VERSION + && !input.request_body.is_empty() + && input.request_body.len() <= MAX_ADMISSION_BODY_BYTES +} + +#[cfg(test)] +mod tests { + use super::*; + + fn request(harness_version: &str, body_len: usize) -> BridgeRequest { + BridgeRequest { + harness_version: harness_version.into(), + session_id: String::new(), + submission_id: String::new(), + request_body: vec![b'x'; body_len], + } + } + + #[test] + fn admission_request_requires_the_pinned_harness_version() { + assert!(!is_valid_admission_request(&request("0.84.1", 1))); + assert!(is_valid_admission_request(&request(PI_HARNESS_VERSION, 1))); + } + + #[test] + fn admission_request_enforces_the_logical_body_limit() { + assert!(!is_valid_admission_request(&request(PI_HARNESS_VERSION, 0))); + assert!(is_valid_admission_request(&request( + PI_HARNESS_VERSION, + MAX_ADMISSION_BODY_BYTES, + ))); + assert!(!is_valid_admission_request(&request( + PI_HARNESS_VERSION, + MAX_ADMISSION_BODY_BYTES + 1, + ))); + } +} diff --git a/crates/openshell-sandbox/src/lib.rs b/crates/openshell-sandbox/src/lib.rs index d96f141cc8..a8ae905ad1 100644 --- a/crates/openshell-sandbox/src/lib.rs +++ b/crates/openshell-sandbox/src/lib.rs @@ -6,6 +6,7 @@ //! This crate provides process sandboxing and monitoring capabilities. mod activity_aggregator; +mod agent_bridge; mod denial_aggregator; #[cfg_attr(not(target_os = "linux"), allow(dead_code))] mod google_cloud_metadata; @@ -60,7 +61,9 @@ pub(crate) use openshell_ocsf::ctx::ctx as ocsf_ctx; use openshell_core::denial::DenialEvent; use openshell_core::policy::{NetworkMode, NetworkPolicy, ProxyPolicy, SandboxPolicy}; use openshell_core::proposals::AgentProposals; +use openshell_core::proto::NetworkMiddlewareConfig; use openshell_core::provider_credentials::ProviderCredentialState; +use openshell_supervisor_middleware::ChainRunner; use openshell_supervisor_network::opa::OpaEngine; use openshell_supervisor_process::process::ProcessEnforcementMode; pub use openshell_supervisor_process::process::{ProcessHandle, ProcessStatus}; @@ -84,6 +87,46 @@ fn has_network_runtime_capability(capabilities: Option<&str>, required: &str) -> } const SIDECAR_PROCESS_PROXY_ADDR: &str = "127.0.0.1:3128"; const SIDECAR_READY_TIMEOUT_SECS: u64 = 120; +const PI_AGENT_HOOKS: &[&str] = &["rendered_prompt_admission"]; + +struct PiBridgeSelection { + middleware_name: String, + provider_host: String, + middleware_config: prost_types::Struct, +} + +async fn select_pi_bridge( + runner: &ChainRunner, + configs: &std::collections::HashMap, +) -> Result> { + let supported = runner + .agent_conversation_middleware_names("pi", "openshell.pi-input.v1", PI_AGENT_HOOKS) + .await?; + let mut matches = configs + .iter() + .filter(|(_, config)| supported.contains(&config.middleware)); + let Some((config_name, config)) = matches.next() else { + return Ok(None); + }; + if matches.next().is_some() { + return Err(miette::miette!( + "managed Pi admission requires exactly one matching middleware config" + )); + } + let endpoints = config.endpoints.as_ref().ok_or_else(|| { + miette::miette!("Pi admission middleware config '{config_name}' requires endpoints") + })?; + if endpoints.include.len() != 1 || endpoints.include[0].contains('*') { + return Err(miette::miette!( + "Pi admission middleware config '{config_name}' requires one exact provider host" + )); + } + Ok(Some(PiBridgeSelection { + middleware_name: config.middleware.clone(), + provider_host: endpoints.include[0].clone(), + middleware_config: config.config.clone().unwrap_or_default(), + })) +} /// Run a command in the sandbox. /// @@ -549,6 +592,50 @@ pub async fn run_sandbox( None }; + let pi_bridge = match (opa_engine.as_ref(), retained_proto.as_ref()) { + (Some(engine), Some(proto)) => { + let runner = engine.middleware_runner()?; + select_pi_bridge(&runner, &proto.network_middlewares) + .await? + .map(|selection| (selection, runner)) + } + _ => None, + }; + if let Some((selection, runner)) = pi_bridge { + if sidecar_network_enforcement { + return Err(miette::miette!( + "managed Pi admission is not supported in sidecar topology" + )); + } + #[cfg(target_os = "linux")] + let listener = netns + .as_ref() + .ok_or_else(|| miette::miette!("Pi admission bridge requires network enforcement"))? + .bind_tcp_in_netns(agent_bridge::BRIDGE_ADDR) + .await + .into_diagnostic() + .wrap_err("failed to bind Pi admission bridge")?; + #[cfg(not(target_os = "linux"))] + let listener = tokio::net::TcpListener::bind(agent_bridge::BRIDGE_ADDR) + .await + .into_diagnostic()?; + agent_bridge::spawn( + listener, + runner, + agent_bridge::BridgeConfig { + middleware_name: selection.middleware_name, + sandbox_id: sandbox_id.clone().unwrap_or_default(), + provider_host: selection.provider_host, + middleware_config: selection.middleware_config, + }, + ); + provider_env.insert( + agent_bridge::BRIDGE_URL_ENV.into(), + agent_bridge::BRIDGE_URL.into(), + ); + info!(url = agent_bridge::BRIDGE_URL, "Pi admission bridge ready"); + } + #[cfg(target_os = "linux")] let sidecar_control_server = if network_enabled && sidecar_network_enforcement { if !matches!(policy.network.mode, NetworkMode::Proxy) { diff --git a/crates/openshell-supervisor-middleware-builtins/src/regex.rs b/crates/openshell-supervisor-middleware-builtins/src/regex.rs index a5c2df882a..7ac8f3fb67 100644 --- a/crates/openshell-supervisor-middleware-builtins/src/regex.rs +++ b/crates/openshell-supervisor-middleware-builtins/src/regex.rs @@ -56,12 +56,14 @@ pub fn describe() -> Vec { phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: MAX_PAYLOAD_BYTES, timeout: String::new(), + ..Default::default() }, MiddlewareBinding { operation: SupervisorMiddlewareOperation::WebsocketMessage as i32, phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: MAX_PAYLOAD_BYTES, timeout: String::new(), + ..Default::default() }, ] } diff --git a/crates/openshell-supervisor-middleware/src/lib.rs b/crates/openshell-supervisor-middleware/src/lib.rs index 902b5165c2..8568f71534 100644 --- a/crates/openshell-supervisor-middleware/src/lib.rs +++ b/crates/openshell-supervisor-middleware/src/lib.rs @@ -24,7 +24,8 @@ use prost::Message; use openshell_core::proto::middleware::v1::supervisor_middleware_server::SupervisorMiddleware; use openshell_core::proto::{ - Decision, Finding, HeaderMutation, HttpHeader, HttpRequestEvaluation, HttpRequestTarget, + AgentConversationEvaluation, AgentConversationResult, AgentConversationTarget, Decision, + Finding, HeaderMutation, HttpHeader, HttpRequestEvaluation, HttpRequestTarget, MiddlewareBinding, MiddlewareManifest, NetworkMiddlewareConfig, RequestContext, SandboxPolicy, SupervisorMiddlewareOperation, SupervisorMiddlewarePhase, SupervisorMiddlewareService, ValidateConfigRequest, ValidateConfigResponse, @@ -66,6 +67,13 @@ impl SupervisorMiddlewareEndpoint for GeneratedMiddlewareEndpoint { self.service.evaluate_http_request(request).await } + async fn evaluate_agent_conversation( + &self, + request: Request, + ) -> std::result::Result, TonicStatus> { + self.service.evaluate_agent_conversation(request).await + } + async fn open_websocket_session( &self, _receiver: tokio::sync::mpsc::Receiver, @@ -288,6 +296,8 @@ pub const MAX_MIDDLEWARE_FINDING_BYTES: usize = 4 * 1024; pub const MAX_MIDDLEWARE_METADATA_ENTRIES: usize = 64; /// Largest combined metadata key/value payload accepted from one middleware stage. pub const MAX_MIDDLEWARE_METADATA_BYTES: usize = 32 * 1024; +/// Largest opaque agent receipt accepted from one middleware result. +pub const MAX_AGENT_ATTESTATION_BYTES: usize = 8 * 1024; const MAX_MIDDLEWARE_HEADER_MUTATION_WIRE_BYTES: usize = 64 * 1024; const MAX_MIDDLEWARE_PROTOBUF_OVERHEAD_BYTES: usize = 64 * 1024; @@ -609,6 +619,18 @@ impl MiddlewareDispatch { } } + async fn evaluate_agent_conversation( + &self, + request: AgentConversationEvaluation, + ) -> std::result::Result, tonic::Status> { + match self { + Self::InProcess(_) => Err(tonic::Status::unimplemented( + "in-process middleware does not support agent conversations", + )), + Self::Grpc(service) => service.evaluate_agent_conversation(request).await, + } + } + async fn open_websocket_session( &self, receiver: tokio::sync::mpsc::Receiver, @@ -824,6 +846,7 @@ fn validate_payload_limit(source: &str, binding: &MiddlewareBinding) -> Result Result { @@ -834,11 +857,30 @@ fn supported_binding(source: &str, binding: &MiddlewareBinding) -> Result Ok(SupportedBinding::HttpPreCredentials), + ) if binding.harness.is_empty() + && binding.hook.is_empty() + && binding.schema_version.is_empty() => + { + Ok(SupportedBinding::HttpPreCredentials) + } ( Some(SupervisorMiddlewareOperation::WebsocketMessage), Some(SupervisorMiddlewarePhase::PreCredentials), - ) => Ok(SupportedBinding::WebSocketPreCredentials), + ) if binding.harness.is_empty() + && binding.hook.is_empty() + && binding.schema_version.is_empty() => + { + Ok(SupportedBinding::WebSocketPreCredentials) + } + ( + Some(SupervisorMiddlewareOperation::AgentConversation), + Some(SupervisorMiddlewarePhase::AgentContext), + ) if is_stable_identifier(&binding.harness) + && is_stable_identifier(&binding.hook) + && is_stable_identifier(&binding.schema_version) => + { + Ok(SupportedBinding::AgentContext) + } ( Some(SupervisorMiddlewareOperation::WebsocketMessage), Some(SupervisorMiddlewarePhase::PreReturn), @@ -860,13 +902,17 @@ fn validate_manifest_bindings( return Err(miette!("{source} describes no bindings")); } - let mut described_pairs = HashSet::with_capacity(manifest.bindings.len()); + let mut described_bindings = HashSet::with_capacity(manifest.bindings.len()); for binding in &manifest.bindings { supported_binding(source, binding)?; - if !described_pairs.insert((binding.operation, binding.phase)) { - return Err(miette!( - "{source} describes a duplicate middleware operation/phase pair" - )); + if !described_bindings.insert(( + binding.operation, + binding.phase, + binding.harness.as_str(), + binding.hook.as_str(), + binding.schema_version.as_str(), + )) { + return Err(miette!("{source} describes a duplicate middleware binding")); } let advertised = validate_payload_limit(source, binding)?; if !binding.timeout.trim().is_empty() { @@ -1410,6 +1456,149 @@ impl ChainRunner { .find(|binding| binding.operation == operation as i32 && binding.phase == phase as i32) } + fn agent_context_binding<'a>( + manifest: &'a MiddlewareManifest, + target: &AgentConversationTarget, + ) -> Option<&'a MiddlewareBinding> { + manifest.bindings.iter().find(|binding| { + binding.operation == SupervisorMiddlewareOperation::AgentConversation as i32 + && binding.phase == SupervisorMiddlewarePhase::AgentContext as i32 + && binding.harness == target.harness + && binding.hook == target.hook + && binding.schema_version == target.schema_version + }) + } + + /// Return middleware attachment names that advertise every requested hook. + pub async fn agent_conversation_middleware_names( + &self, + harness: &str, + schema_version: &str, + hooks: &[&str], + ) -> Result> { + let manifests = self.manifests().await?; + Ok(manifests + .iter() + .filter(|(_, manifest)| { + hooks.iter().all(|hook| { + manifest.bindings.iter().any(|binding| { + binding.operation == SupervisorMiddlewareOperation::AgentConversation as i32 + && binding.phase == SupervisorMiddlewarePhase::AgentContext as i32 + && binding.harness == harness + && binding.hook == *hook + && binding.schema_version == schema_version + }) + }) + }) + .map(|(state, manifest)| Self::attachment_name(state, manifest).to_string()) + .collect()) + } + + /// Evaluate one supervisor-stamped agent request through its named middleware. + pub async fn evaluate_agent_conversation( + &self, + evaluation: AgentConversationEvaluation, + ) -> Result { + if evaluation.middleware_name.is_empty() { + return Err(miette!( + "agent conversation middleware name cannot be empty" + )); + } + let target = evaluation + .target + .as_ref() + .ok_or_else(|| miette!("agent conversation target is required"))?; + let manifests = self.manifests().await?; + let Some((state, binding)) = manifests.iter().find_map(|(state, manifest)| { + (Self::attachment_name(state, manifest) == evaluation.middleware_name) + .then(|| Self::agent_context_binding(manifest, target)) + .flatten() + .map(|binding| (state, binding)) + }) else { + return Err(miette!( + "middleware '{}' has no matching agent hook binding", + evaluation.middleware_name + )); + }; + if evaluation.phase != SupervisorMiddlewarePhase::AgentContext as i32 { + return Err(miette!("agent conversation phase must be AGENT_CONTEXT")); + } + if evaluation + .context + .as_ref() + .is_none_or(|context| context.sandbox_id.is_empty()) + { + return Err(miette!("trusted sandbox context is required")); + } + if evaluation.request_body.is_empty() { + return Err(miette!("agent conversation request body cannot be empty")); + } + let max_payload_bytes = validate_payload_limit("agent conversation binding", binding)?; + if evaluation.request_body.len() > max_payload_bytes { + return Err(miette!( + "agent conversation request exceeds binding capacity" + )); + } + if evaluation + .config + .as_ref() + .is_some_and(|config| config.encoded_len() > MAX_MIDDLEWARE_CONFIG_BYTES) + { + return Err(miette!( + "agent conversation config exceeds platform capacity" + )); + } + if evaluation + .context + .as_ref() + .is_some_and(|context| context.encoded_len() > MAX_MIDDLEWARE_CONTEXT_BYTES) + || target.encoded_len() > MAX_MIDDLEWARE_TARGET_BYTES + { + return Err(miette!( + "agent conversation envelope exceeds platform capacity" + )); + } + + let mut result = call_with_timeout( + state.timeout_for_binding(binding)?, + "EvaluateAgentConversation", + state.service.evaluate_agent_conversation(evaluation), + ) + .await + .map(tonic::Response::into_inner) + .map_err(|error| { + miette!( + "middleware EvaluateAgentConversation failed: {}", + safe_reason(&error.to_string()) + ) + })?; + let decision = Decision::try_from(result.decision).unwrap_or(Decision::Unspecified); + if decision == Decision::Unspecified { + return Err(miette!( + "agent conversation response has unspecified decision" + )); + } + if result.reason.len() > MAX_MIDDLEWARE_REASON_BYTES + || (!result.reason_code.is_empty() && !is_stable_reason_code(&result.reason_code)) + || result.attestation.len() > MAX_AGENT_ATTESTATION_BYTES + || result.replacement_body.len() > max_payload_bytes + { + return Err(miette!("agent conversation response is invalid")); + } + if decision == Decision::Deny + && (result.has_replacement_body || !result.attestation.is_empty()) + { + return Err(miette!( + "denied agent conversation cannot include replacement or receipt" + )); + } + if !result.has_replacement_body && !result.replacement_body.is_empty() { + return Err(miette!("agent replacement body requires its presence flag")); + } + result.reason = safe_reason(&result.reason); + Ok(result) + } + pub async fn describe_chain(&self, entries: &[ChainEntry]) -> Result> { Ok(self .describe_chain_for( @@ -2113,6 +2302,7 @@ mod tests { phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, timeout: String::new(), + ..Default::default() }], expected_audience: String::new(), } @@ -2249,6 +2439,7 @@ mod tests { phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, timeout: String::new(), + ..Default::default() }], expected_audience: String::new(), } @@ -2340,6 +2531,7 @@ mod tests { phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, timeout: "10ms".into(), + ..Default::default() }], expected_audience: String::new(), } @@ -2600,6 +2792,7 @@ mod tests { phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: self.max_body_bytes, timeout: String::new(), + ..Default::default() }], expected_audience: String::new(), })) @@ -2624,6 +2817,13 @@ mod tests { > { Ok(tonic::Response::new(self.result.clone())) } + + async fn evaluate_agent_conversation( + &self, + _request: Request, + ) -> std::result::Result, tonic::Status> { + Err(tonic::Status::unimplemented("HTTP-only test middleware")) + } } struct SlowService { @@ -2631,6 +2831,127 @@ mod tests { binding_timeout: String, } + struct AgentService { + received: std::sync::Mutex>, + } + + #[tonic::async_trait] + impl SupervisorMiddleware for AgentService { + type EvaluateWebSocketSessionStream = WebSocketResponseStream; + + async fn evaluate_web_socket_session( + &self, + _request: Request>, + ) -> std::result::Result, tonic::Status> + { + Err(tonic::Status::unimplemented("agent-only test middleware")) + } + + async fn describe( + &self, + _request: Request<()>, + ) -> std::result::Result, tonic::Status> { + let binding = |hook: &str| MiddlewareBinding { + operation: SupervisorMiddlewareOperation::AgentConversation as i32, + phase: SupervisorMiddlewarePhase::AgentContext as i32, + max_payload_bytes: 4096, + timeout: String::new(), + harness: "pi".into(), + hook: hook.into(), + schema_version: "openshell.pi-input.v1".into(), + }; + Ok(tonic::Response::new(MiddlewareManifest { + name: "test/agent".into(), + service_version: "test".into(), + bindings: vec![binding("rendered_prompt_admission")], + expected_audience: String::new(), + })) + } + + async fn validate_config( + &self, + _request: Request, + ) -> std::result::Result, tonic::Status> { + Ok(tonic::Response::new(ValidateConfigResponse { + valid: true, + reason: String::new(), + })) + } + + async fn evaluate_http_request( + &self, + _request: Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + Err(tonic::Status::unimplemented("agent-only test middleware")) + } + + async fn evaluate_agent_conversation( + &self, + request: Request, + ) -> std::result::Result, tonic::Status> { + self.received + .lock() + .expect("agent request lock") + .push(request.into_inner()); + Ok(tonic::Response::new(AgentConversationResult { + decision: Decision::Allow as i32, + attestation: b"receipt".to_vec(), + ..Default::default() + })) + } + } + + #[tokio::test] + async fn agent_request_uses_exact_matching_registered_binding() { + let service = Arc::new(AgentService { + received: std::sync::Mutex::new(Vec::new()), + }); + let runner = ChainRunner::new_protobuf_for_tests(service.clone()); + let names = runner + .agent_conversation_middleware_names( + "pi", + "openshell.pi-input.v1", + &["rendered_prompt_admission"], + ) + .await + .expect("discover agent middleware"); + assert_eq!(names, vec!["test/agent"]); + + let result = runner + .evaluate_agent_conversation(AgentConversationEvaluation { + phase: SupervisorMiddlewarePhase::AgentContext as i32, + context: Some(RequestContext { + request_id: "request-1".into(), + sandbox_id: "sandbox-1".into(), + originating_process: None, + }), + config: Some(prost_types::Struct::default()), + target: Some(AgentConversationTarget { + harness: "pi".into(), + harness_version: "test".into(), + hook: "rendered_prompt_admission".into(), + schema_version: "openshell.pi-input.v1".into(), + scheme: "https".into(), + host: "api.openai.com".into(), + port: 443, + path: "/v1/chat/completions".into(), + }), + middleware_name: "test/agent".into(), + request_body: b"{}".to_vec(), + ..Default::default() + }) + .await + .expect("evaluate agent request"); + assert_eq!(result.attestation, b"receipt"); + assert_eq!( + service.received.lock().expect("agent request lock").len(), + 1 + ); + } + #[tonic::async_trait] impl SupervisorMiddleware for SlowService { type EvaluateWebSocketSessionStream = WebSocketResponseStream; @@ -2655,6 +2976,7 @@ mod tests { phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, timeout: self.binding_timeout.clone(), + ..Default::default() }], expected_audience: String::new(), })) @@ -2681,6 +3003,13 @@ mod tests { tokio::time::sleep(self.delay).await; Ok(tonic::Response::new(allow_result())) } + + async fn evaluate_agent_conversation( + &self, + _request: Request, + ) -> std::result::Result, tonic::Status> { + Err(tonic::Status::unimplemented("HTTP-only test middleware")) + } } /// A middleware attached twice for exercising per-stage validation. The @@ -2714,6 +3043,7 @@ mod tests { phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 256 * 1024, timeout: String::new(), + ..Default::default() }], expected_audience: String::new(), })) @@ -2754,6 +3084,13 @@ mod tests { } Ok(tonic::Response::new(result)) } + + async fn evaluate_agent_conversation( + &self, + _request: Request, + ) -> std::result::Result, tonic::Status> { + Err(tonic::Status::unimplemented("HTTP-only test middleware")) + } } #[tokio::test] @@ -2984,6 +3321,7 @@ mod tests { phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, timeout: String::new(), + ..Default::default() }], expected_audience: String::new(), })) @@ -3016,6 +3354,13 @@ mod tests { .push(request.into_inner()); Ok(tonic::Response::new(allow_result())) } + + async fn evaluate_agent_conversation( + &self, + _request: Request, + ) -> std::result::Result, tonic::Status> { + Err(tonic::Status::unimplemented("HTTP-only test middleware")) + } } /// Three-stage service used to verify that each stage observes the header @@ -3049,6 +3394,7 @@ mod tests { phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, timeout: String::new(), + ..Default::default() }], expected_audience: String::new(), })) @@ -3094,6 +3440,13 @@ mod tests { } Ok(tonic::Response::new(result)) } + + async fn evaluate_agent_conversation( + &self, + _request: Request, + ) -> std::result::Result, tonic::Status> { + Err(tonic::Status::unimplemented("HTTP-only test middleware")) + } } #[tokio::test] @@ -3503,6 +3856,7 @@ mod tests { phase: PRE_CREDENTIALS_PHASE as i32, max_payload_bytes: 4096, timeout: String::new(), + ..Default::default() }], expected_audience: String::new(), }; @@ -3530,6 +3884,7 @@ mod tests { phase: PRE_CREDENTIALS_PHASE as i32, max_payload_bytes: u64::MAX, timeout: String::new(), + ..Default::default() }], expected_audience: String::new(), }; @@ -3539,13 +3894,14 @@ mod tests { } #[test] - fn manifest_rejects_duplicate_operation_phase_pairs() { + fn manifest_rejects_duplicate_bindings() { let registration = external_registration(4096); let binding = || MiddlewareBinding { operation: HTTP_REQUEST_OPERATION as i32, phase: PRE_CREDENTIALS_PHASE as i32, max_payload_bytes: 4096, timeout: String::new(), + ..Default::default() }; let manifest = MiddlewareManifest { name: "example/service".into(), @@ -3555,12 +3911,8 @@ mod tests { }; let error = validate_external_manifest(®istration, &manifest, 4096, false) - .expect_err("one service cannot advertise two bindings for the same pair"); - assert!( - error - .to_string() - .contains("duplicate middleware operation/phase pair") - ); + .expect_err("one service cannot advertise the same binding twice"); + assert!(error.to_string().contains("duplicate middleware binding")); } #[test] @@ -3570,6 +3922,7 @@ mod tests { phase: phase as i32, max_payload_bytes: MAX_MIDDLEWARE_PAYLOAD_BYTES as u64, timeout: "500ms".into(), + ..Default::default() }; let mut manifest = MiddlewareManifest { name: "example/websocket".into(), @@ -3597,6 +3950,7 @@ mod tests { phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, timeout: String::new(), + ..Default::default() }], expected_audience: String::new(), }; @@ -3621,6 +3975,7 @@ mod tests { phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 4096, timeout: String::new(), + ..Default::default() }], expected_audience: String::new(), }; @@ -3695,6 +4050,7 @@ mod tests { phase: PRE_CREDENTIALS_PHASE as i32, max_payload_bytes: 4096, timeout: timeout.into(), + ..Default::default() }], expected_audience: String::new(), }; @@ -4720,6 +5076,7 @@ mod tests { phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: MAX_MIDDLEWARE_PAYLOAD_BYTES as u64, timeout: "1s".into(), + ..Default::default() }], expected_audience: String::new(), })) @@ -4756,6 +5113,15 @@ mod tests { self.websocket_stream(request.into_inner()), )) } + + async fn evaluate_agent_conversation( + &self, + _request: Request, + ) -> std::result::Result, tonic::Status> { + Err(tonic::Status::unimplemented( + "WebSocket-only test middleware", + )) + } } #[tonic::async_trait] @@ -4784,6 +5150,13 @@ mod tests { SupervisorMiddleware::evaluate_http_request(self, request).await } + async fn evaluate_agent_conversation( + &self, + request: Request, + ) -> std::result::Result, tonic::Status> { + SupervisorMiddleware::evaluate_agent_conversation(self, request).await + } + async fn open_websocket_session( &self, receiver: tokio::sync::mpsc::Receiver, diff --git a/crates/openshell-supervisor-middleware/src/remote.rs b/crates/openshell-supervisor-middleware/src/remote.rs index edc1e8066c..fda17e083b 100644 --- a/crates/openshell-supervisor-middleware/src/remote.rs +++ b/crates/openshell-supervisor-middleware/src/remote.rs @@ -7,8 +7,8 @@ use openshell_core::middleware::{ }; use openshell_core::proto::middleware::v1::supervisor_middleware_client::SupervisorMiddlewareClient; use openshell_core::proto::{ - HttpRequestEvaluation, HttpRequestResult, MiddlewareManifest, ValidateConfigRequest, - ValidateConfigResponse, WebSocketSessionEvent, + AgentConversationEvaluation, AgentConversationResult, HttpRequestEvaluation, HttpRequestResult, + MiddlewareManifest, ValidateConfigRequest, ValidateConfigResponse, WebSocketSessionEvent, }; use openshell_extension_core::{ BearerTokenInterceptor, BearerTokenSlot, ExtensionChannelConfig, ExtensionServerTrust, @@ -94,6 +94,16 @@ impl GrpcMiddlewareService { .await } + /// Forward an owned agent evaluation through the protobuf service contract. + pub async fn evaluate_agent_conversation( + &self, + request: AgentConversationEvaluation, + ) -> std::result::Result, Status> { + self.service + .evaluate_agent_conversation(Request::new(request)) + .await + } + /// Open a remote WebSocket middleware stream through the gRPC adapter. pub async fn open_websocket_session( &self, @@ -166,6 +176,14 @@ impl SupervisorMiddlewareEndpoint for RemoteMiddlewareService { client.evaluate_http_request(request).await } + async fn evaluate_agent_conversation( + &self, + request: Request, + ) -> std::result::Result, Status> { + let mut client = self.client.clone(); + client.evaluate_agent_conversation(request).await + } + async fn open_websocket_session( &self, receiver: tokio::sync::mpsc::Receiver, diff --git a/crates/openshell-supervisor-network/src/l7/relay.rs b/crates/openshell-supervisor-network/src/l7/relay.rs index 44f3f8023a..82284730a8 100644 --- a/crates/openshell-supervisor-network/src/l7/relay.rs +++ b/crates/openshell-supervisor-network/src/l7/relay.rs @@ -3510,6 +3510,7 @@ network_policies: max_payload_bytes: openshell_supervisor_middleware::MAX_MIDDLEWARE_PAYLOAD_BYTES as u64, timeout: "2s".into(), + ..Default::default() }], expected_audience: String::new(), }, @@ -3541,6 +3542,16 @@ network_policies: Err(tonic::Status::unimplemented("WebSocket-only middleware")) } + async fn evaluate_agent_conversation( + &self, + _request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + Err(tonic::Status::unimplemented("WebSocket-only middleware")) + } + async fn evaluate_web_socket_session( &self, request: tonic::Request>, @@ -5320,6 +5331,7 @@ network_policies: phase: openshell_core::proto::SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 8192, timeout: String::new(), + ..Default::default() }], expected_audience: String::new(), } @@ -5467,6 +5479,7 @@ network_policies: phase: openshell_core::proto::SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 8192, timeout: String::new(), + ..Default::default() }], expected_audience: String::new(), } @@ -5938,6 +5951,7 @@ network_policies: phase: SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: self.max_body_bytes, timeout: String::new(), + ..Default::default() }], expected_audience: String::new(), } diff --git a/crates/openshell-supervisor-network/src/l7/websocket.rs b/crates/openshell-supervisor-network/src/l7/websocket.rs index 6cd8aa4818..60ebfcc725 100644 --- a/crates/openshell-supervisor-network/src/l7/websocket.rs +++ b/crates/openshell-supervisor-network/src/l7/websocket.rs @@ -3524,6 +3524,7 @@ network_policies: max_payload_bytes: openshell_supervisor_middleware::MAX_MIDDLEWARE_PAYLOAD_BYTES as u64, timeout: "1s".into(), + ..Default::default() }], expected_audience: String::new(), })) @@ -3550,6 +3551,14 @@ network_policies: Err(Status::unimplemented("WebSocket-only test middleware")) } + async fn evaluate_agent_conversation( + &self, + _request: Request, + ) -> std::result::Result, Status> + { + Err(Status::unimplemented("WebSocket-only test middleware")) + } + async fn evaluate_web_socket_session( &self, request: Request>, diff --git a/crates/openshell-supervisor-network/src/opa.rs b/crates/openshell-supervisor-network/src/opa.rs index 35cef601d8..f5d8e11436 100644 --- a/crates/openshell-supervisor-network/src/opa.rs +++ b/crates/openshell-supervisor-network/src/opa.rs @@ -844,7 +844,7 @@ impl OpaEngine { Ok(()) } - pub(crate) fn middleware_runner(&self) -> Result { + pub fn middleware_runner(&self) -> Result { self.middleware_runner .read() .map(|runner| runner.clone()) diff --git a/crates/openshell-supervisor-network/src/proxy.rs b/crates/openshell-supervisor-network/src/proxy.rs index bc3ad8b3db..f8060ca569 100644 --- a/crates/openshell-supervisor-network/src/proxy.rs +++ b/crates/openshell-supervisor-network/src/proxy.rs @@ -6390,6 +6390,7 @@ mod tests { as i32, max_payload_bytes: 1024, timeout: "1s".into(), + ..Default::default() }], expected_audience: String::new(), }, @@ -6421,6 +6422,16 @@ mod tests { Err(tonic::Status::unimplemented("WebSocket-only test service")) } + async fn evaluate_agent_conversation( + &self, + _request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + Err(tonic::Status::unimplemented("WebSocket-only test service")) + } + async fn open_websocket_session( &self, mut receiver: mpsc::Receiver, @@ -6474,6 +6485,7 @@ mod tests { phase: openshell_core::proto::SupervisorMiddlewarePhase::PreCredentials as i32, max_payload_bytes: 8192, timeout: String::new(), + ..Default::default() }], expected_audience: String::new(), } diff --git a/docs/extensibility/supervisor-middleware.mdx b/docs/extensibility/supervisor-middleware.mdx index 68cdbd29a5..172d532006 100644 --- a/docs/extensibility/supervisor-middleware.mdx +++ b/docs/extensibility/supervisor-middleware.mdx @@ -3,12 +3,14 @@ # SPDX-License-Identifier: Apache-2.0 title: "Supervisor Middleware" sidebar-title: "Supervisor Middleware" -description: "Configure and operate built-in and operator-run middleware for sandbox HTTP requests and WebSocket messages." +description: "Configure and operate built-in and operator-run middleware for sandbox HTTP, WebSocket, and agent requests." keywords: "Generative AI, Cybersecurity, AI Agents, Supervisor Middleware, Extensibility, Request Filtering" --- Supervisor middleware adds ordered processing stages to allowed HTTP and WebSocket egress. Middleware runs after network and L7 policy admit traffic and before OpenShell injects provider credentials. A stage can allow or deny an HTTP request or client WebSocket text message, replace its payload, add approved HTTP headers, and report audit-safe findings. +Operator middleware can also advertise versioned agent-harness bindings. For managed Pi, the sandbox supervisor exposes a loopback bridge only when one configured middleware advertises `rendered_prompt_admission` for `openshell.pi-input.v1`. The supervisor sets `OPENSHELL_PI_CONVERSATION_URL`; the sandbox deployment loads the middleware's Pi extension through Pi's standard extension mechanism. Pi itself remains unaware of OpenShell. The MVP admits a rendered, idle, text-only user prompt before Pi stores it; denied prompts never enter chat history, while replacement bodies support redaction. The supervisor stamps sandbox and provider identity before dispatch. Agent admission is fail closed and does not use the HTTP or WebSocket middleware chains. Admission bodies are limited to 32 KiB. Images, queued input, retries, compaction, and automatic continuations after tool calls are intentionally unsupported until a separate model-request boundary is added. + Middleware selection is independent of the network policy rule that admitted the request. OpenShell matches middleware by destination host, so the same middleware applies consistently across broad, specific, user-authored, and provider-derived network policies. ## Request Flow @@ -52,7 +54,7 @@ The request context identifies the originating sandbox to operator-run services. `openshell/regex` is an example built-in middleware. It replaces only simple, self-contained token patterns in UTF-8 HTTP bodies and client WebSocket text messages; the initial pattern recognizes `sk-` tokens. It does not infer values from keyword assignments such as JSON `password` fields. This best-effort text transformation is not parser-aware and does not guarantee that it will detect or fully remove sensitive values. Its `config` accepts one field, `mode: redact`, which is also the default when the field is omitted. Unknown config fields and non-string values are rejected at policy validation. Custom expressions are not configurable yet. -Operator-run services expose bindings for supported operation and phase pairs. A binding is identified by its operation and phase. V1 supports `HttpRequest/pre_credentials` and `WebSocketMessage/pre_credentials`; a service may expose either or both. Policies attach the complete middleware by its operator-owned gateway registration name. +Operator-run services expose bindings for supported operation and phase pairs. HTTP and WebSocket bindings use `HttpRequest/pre_credentials` and `WebSocketMessage/pre_credentials`. Agent bindings use `AgentConversation/agent_context` plus a harness, hook, and schema version. Policies attach the complete middleware by its operator-owned gateway registration name. ## Register a Middleware Service @@ -223,7 +225,7 @@ See [Logging](/observability/logging) for log access and [OCSF JSON Export](/obs ## Current Limitations - Middleware applies only through operation bindings advertised by each implementation. For protocols that have no supported middleware operation at all, such as HTTP/2 prior knowledge or non-HTTP TCP, the existing uninspectable-traffic gate denies a host match containing `fail_closed` and relays an all-`fail_open` match with a detection finding. -- The typed operation and phase pairs are `HTTP_REQUEST/PRE_CREDENTIALS` and `WEBSOCKET_MESSAGE/PRE_CREDENTIALS`. +- The typed operation and phase pairs are `HTTP_REQUEST/PRE_CREDENTIALS`, `WEBSOCKET_MESSAGE/PRE_CREDENTIALS`, and `AGENT_CONVERSATION/AGENT_CONTEXT`. - A host match does not imply every advertised operation: an HTTP-only attachment can inspect the upgrade GET, then post-upgrade traffic passes with `binding_not_selected` coverage. - The V1 WebSocket binding inspects complete client text messages only. Binary messages pass with `unsupported_message_type` coverage for active stages; control frames and upstream-to-client messages remain outside the middleware operation. - Selection uses destination host include and exclude patterns. diff --git a/examples/supervisor-middleware-content-guard/src/main.rs b/examples/supervisor-middleware-content-guard/src/main.rs index c527537e74..9239481695 100644 --- a/examples/supervisor-middleware-content-guard/src/main.rs +++ b/examples/supervisor-middleware-content-guard/src/main.rs @@ -11,12 +11,13 @@ use openshell_core::proto::middleware::v1::supervisor_middleware_server::{ SupervisorMiddleware, SupervisorMiddlewareServer, }; use openshell_core::proto::{ - Decision, Finding, HttpRequestEvaluation, HttpRequestResult, MiddlewareBinding, - MiddlewareManifest, SupervisorMiddlewareOperation, SupervisorMiddlewarePhase, - ValidateConfigRequest, ValidateConfigResponse, WebSocketMessage, WebSocketMessageResult, - WebSocketPreflightAction, WebSocketPreflightDecision, WebSocketSessionEvent, - WebSocketSessionEventResult, web_socket_message, web_socket_message_result, - web_socket_session_event, web_socket_session_event_result, + AgentConversationEvaluation, AgentConversationResult, Decision, Finding, + HttpRequestEvaluation, HttpRequestResult, MiddlewareBinding, MiddlewareManifest, + SupervisorMiddlewareOperation, SupervisorMiddlewarePhase, ValidateConfigRequest, + ValidateConfigResponse, WebSocketMessage, WebSocketMessageResult, WebSocketPreflightAction, + WebSocketPreflightDecision, WebSocketSessionEvent, WebSocketSessionEventResult, + web_socket_message, web_socket_message_result, web_socket_session_event, + web_socket_session_event_result, }; use prost_types::Struct; use prost_types::value::Kind; @@ -231,12 +232,14 @@ impl SupervisorMiddleware for ContentGuard { phase: PHASE as i32, max_payload_bytes: MAX_PAYLOAD_BYTES, timeout: String::new(), + ..Default::default() }, MiddlewareBinding { operation: SupervisorMiddlewareOperation::WebsocketMessage as i32, phase: PHASE as i32, max_payload_bytes: MAX_PAYLOAD_BYTES, timeout: String::new(), + ..Default::default() }, ], expected_audience: String::new(), @@ -274,6 +277,15 @@ impl SupervisorMiddleware for ContentGuard { Ok(Response::new(evaluate(&config, &body))) } + async fn evaluate_agent_conversation( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "content guard does not implement agent admission", + )) + } + async fn evaluate_web_socket_session( &self, request: Request>, diff --git a/proto/supervisor_middleware.proto b/proto/supervisor_middleware.proto index 27fd804bdf..b60c4f5304 100644 --- a/proto/supervisor_middleware.proto +++ b/proto/supervisor_middleware.proto @@ -10,7 +10,7 @@ import "google/protobuf/struct.proto"; // SupervisorMiddleware lets an operator-run service inspect and transform // sandbox HTTP requests and client WebSocket text messages before OpenShell -// injects credentials. +// injects credentials, or evaluate a supported agent-harness request. service SupervisorMiddleware { // Describe returns the service manifest and declared bindings. rpc Describe(google.protobuf.Empty) returns (MiddlewareManifest); @@ -22,6 +22,10 @@ service SupervisorMiddleware { // buffered HTTP request. rpc EvaluateHttpRequest(HttpRequestEvaluation) returns (HttpRequestResult); + // EvaluateAgentConversation returns an allow, deny, or replacement decision for + // one versioned, harness-native request before the harness commits or sends it. + rpc EvaluateAgentConversation(AgentConversationEvaluation) returns (AgentConversationResult); + // 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 @@ -62,8 +66,9 @@ message MiddlewareBinding { // manifest validation. SupervisorMiddlewarePhase phase = 2; // Maximum logical payload or replacement this binding can process. For - // HTTP_REQUEST this is the request body; for WEBSOCKET_MESSAGE this is one - // complete message. Required for every payload-bearing operation. + // HTTP_REQUEST and AGENT_CONVERSATION this is the request body; for + // WEBSOCKET_MESSAGE this is one complete message. Required for every + // payload-bearing operation. 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. @@ -71,6 +76,12 @@ message MiddlewareBinding { // Values use an integer with an `ms` or `s` suffix and must be between // 10ms and 30s. string timeout = 4; + // Agent harness supported by an AGENT_CONVERSATION binding. Empty otherwise. + string harness = 5; + // Harness hook supported by an AGENT_CONVERSATION binding. Empty otherwise. + string hook = 6; + // Version of the harness-native request schema. Empty otherwise. + string schema_version = 7; } // ValidateConfigRequest contains one policy configuration to validate. @@ -126,6 +137,7 @@ enum SupervisorMiddlewareOperation { SUPERVISOR_MIDDLEWARE_OPERATION_UNSPECIFIED = 0; SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_REQUEST = 1; SUPERVISOR_MIDDLEWARE_OPERATION_WEBSOCKET_MESSAGE = 2; + SUPERVISOR_MIDDLEWARE_OPERATION_AGENT_CONVERSATION = 3; } // Ordered phase within a supervisor operation. @@ -133,6 +145,7 @@ enum SupervisorMiddlewarePhase { SUPERVISOR_MIDDLEWARE_PHASE_UNSPECIFIED = 0; SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS = 1; SUPERVISOR_MIDDLEWARE_PHASE_PRE_RETURN = 2; + SUPERVISOR_MIDDLEWARE_PHASE_AGENT_CONTEXT = 3; } // Why OpenShell is ending a middleware stream. @@ -316,6 +329,51 @@ message Process { repeated string ancestors = 3; } +// AgentConversationTarget identifies the harness hook and provider destination for +// which an allowed model request may receive a receipt. +message AgentConversationTarget { + string harness = 1; + string harness_version = 2; + string hook = 3; + string schema_version = 4; + string scheme = 5; + string host = 6; + uint32 port = 7; + string path = 8; +} + +// AgentConversationEvaluation is stamped by the supervisor-owned bridge. Workload +// callers supply only the harness request and untrusted request provenance. +message AgentConversationEvaluation { + SupervisorMiddlewarePhase phase = 1; + RequestContext context = 2; + google.protobuf.Struct config = 3; + AgentConversationTarget target = 4; + reserved 5; + string middleware_name = 6; + string session_id = 7; + string turn_id = 8; + bytes request_body = 9; + string source = 10; + string delivery = 11; + string request_kind = 12; + optional uint32 candidate_index = 13; +} + +// AgentConversationResult carries the authority decision, an optional complete +// replacement body, and a model-request receipt opaque to OpenShell. +message AgentConversationResult { + Decision decision = 1; + string reason = 2; + reserved 3, 4; + bytes attestation = 5; + repeated Finding findings = 6; + map metadata = 7; + string reason_code = 8; + bytes replacement_body = 9; + bool has_replacement_body = 10; +} + // Decision controls whether OpenShell continues processing the current // evaluation unit. enum Decision { From 30dda213b3d31be93ddd465ca5a3ceaf018ae44f Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Tue, 18 Aug 2026 15:51:18 +0000 Subject: [PATCH 02/18] refactor(supervisor): generalize agent admission bridge Signed-off-by: Johnny Greco --- .../skills/debug-openshell-cluster/SKILL.md | 12 +- .../skills/generate-sandbox-policy/SKILL.md | 8 +- .agents/skills/openshell-cli/SKILL.md | 10 +- architecture/sandbox.md | 12 +- crates/openshell-sandbox/Cargo.toml | 1 - crates/openshell-sandbox/src/agent_bridge.rs | 272 +++++++++++++----- crates/openshell-sandbox/src/lib.rs | 270 +++++++++++++---- .../src/lib.rs | 169 +++++++---- docs/extensibility/supervisor-middleware.mdx | 28 +- proto/supervisor_middleware.proto | 5 +- 10 files changed, 603 insertions(+), 184 deletions(-) diff --git a/.agents/skills/debug-openshell-cluster/SKILL.md b/.agents/skills/debug-openshell-cluster/SKILL.md index 3fc53df904..cbc4b6920f 100644 --- a/.agents/skills/debug-openshell-cluster/SKILL.md +++ b/.agents/skills/debug-openshell-cluster/SKILL.md @@ -109,7 +109,17 @@ journalctl -u openshell-gateway --no-pager --lines=200 openshell logs --tail --source sandbox ``` -The middleware service must start before the gateway and be reachable from both the gateway and sandbox supervisors. Gateway startup fails if `Describe` is unavailable, a manifest exposes duplicate operation/phase bindings, the registration claims the reserved `openshell/` namespace, or payload and timeout limits are invalid. Supported V1 bindings are `HTTP_REQUEST/PRE_CREDENTIALS` and `WEBSOCKET_MESSAGE/PRE_CREDENTIALS`. When gateway JWT signing is disabled, supervisors preserve the legacy unauthenticated connector and do not request extension credentials. When signing is enabled, credential acquisition and verification failures are fail closed: check HTTPS trust and hostname validation, audience and issuer agreement, the token `kid`, gateway `RefreshSandboxToken` errors, and middleware logs. Changing a registration requires a gateway restart. A policy update can also fail before persistence if the selected implementation rejects its `network_middlewares` config. +The middleware service must start before the gateway and be reachable from both the gateway and sandbox supervisors. Gateway startup fails if `Describe` is unavailable, a manifest exposes a duplicate binding identity, the registration claims the reserved `openshell/` namespace, or payload and timeout limits are invalid. Agent bindings with different harness, hook, or schema identifiers may share their operation and phase. Supported V1 bindings are `HTTP_REQUEST/PRE_CREDENTIALS`, `WEBSOCKET_MESSAGE/PRE_CREDENTIALS`, and `AGENT_CONVERSATION/AGENT_CONTEXT`. When gateway JWT signing is disabled, supervisors preserve the legacy unauthenticated connector and do not request extension credentials. When signing is enabled, credential acquisition and verification failures are fail closed: check HTTPS trust and hostname validation, audience and issuer agreement, the token `kid`, gateway `RefreshSandboxToken` errors, and middleware logs. Changing a registration requires a gateway restart. A policy update can also fail before persistence if the selected implementation rejects its `network_middlewares` config. + +For agent admission failures, verify that exactly one configured middleware has +an agent-conversation binding, uses `fail_closed`, selects one exact provider +host without exclusions, and that the host resolves to one admitted network +endpoint. The loopback bridge is available only in combined topology and is +started when the sandbox starts with an agent binding. Check the sandbox +environment for `OPENSHELL_AGENT_CONVERSATION_URL`; managed Pi may use the +compatibility alias `OPENSHELL_PI_CONVERSATION_URL`. A generation mismatch after +a rejected or fail-closed policy reload intentionally returns admission +unavailable instead of evaluating against stale middleware state. At request time, distinguish attachment, binding selection, coverage, denial, and failure. A host-matched HTTP-only attachment can inspect the upgrade GET but does not join the WebSocket chain; the connection proceeds under either `on_error` mode and emits `binding_not_selected` coverage. A selected WebSocket stage receives text messages only. Binary messages pass under both modes, emit `unsupported_message_type` coverage, and consume a session sequence without an RPC. An explicit `middleware_denied` result is always enforced. WebSocket preflight returns `INSPECT`, voluntary `SKIP`, or authoritative `DENY`; `DENY` rejects the upgrade before upstream contact under both `on_error` modes. A selected-stage failure follows the policy-local `on_error`: `fail_closed` blocks the HTTP request or closes the WebSocket, while `fail_open` bypasses only that stage and emits a detection finding. A fail-open per-message capacity failure bypasses that message without disabling the stage. A timeout, transport failure, stream closure, missing or invalid response, duplicate or regressed sequence, or other failure that makes an established WebSocket stream unreliable disables that stage for later messages on the connection and emits `openshell.middleware.websocket_stage_disabled`. Confirm preflight, session-start, and session-end in service logs. OpenShell best-effort sends at most one session-end to each still-writable opened stage, including a preflight that terminates before session start; distinguish `MIDDLEWARE_DENIAL` from `MIDDLEWARE_FAILURE`. WebSocket message sequences are allocated session-wide; each stage receives a strictly increasing subset, so gaps are valid when binary messages or other units are not delivered to that stage. Zero, duplicate, or regressed sequences are protocol errors. If a running supervisor cannot install a new registry, it preserves its last-known-good generation and emits a configuration failure event. diff --git a/.agents/skills/generate-sandbox-policy/SKILL.md b/.agents/skills/generate-sandbox-policy/SKILL.md index f455590eca..16fcf39772 100644 --- a/.agents/skills/generate-sandbox-policy/SKILL.md +++ b/.agents/skills/generate-sandbox-policy/SKILL.md @@ -221,11 +221,17 @@ Is L7 inspection needed? ### Middleware Decision -Add `network_middlewares` only when the user asks to inspect, transform, redact, or independently authorize admitted HTTP requests or client WebSocket text messages. Middleware runs after network and L7 policy admission and before provider credential injection. +Add `network_middlewares` only when the user asks to inspect, transform, redact, +or independently authorize admitted HTTP requests, client WebSocket text +messages, or a supported agent-conversation hook. HTTP and WebSocket middleware +runs after network and L7 policy admission and before provider credential +injection. Agent admission uses a separate loopback bridge but the same +middleware registration and runtime generation. - Use `openshell/regex` without gateway registration for fixed-pattern redaction of UTF-8 HTTP request bodies or complete client-to-upstream WebSocket text messages. - Use an operator-owned middleware name only when it is already registered under `[[openshell.supervisor.middleware]]` and reachable from both the gateway and sandbox supervisors. - Confirm that a requested WebSocket implementation exposes a `WEBSOCKET_MESSAGE/PRE_CREDENTIALS` binding. `openshell/regex` exposes this binding. A host-matched HTTP-only implementation may inspect the upgrade GET but does not join the post-upgrade chain; messages pass and OpenShell emits `binding_not_selected` coverage regardless of `on_error`. +- For agent admission, confirm that the operator implementation exposes one `AGENT_CONVERSATION/AGENT_CONTEXT` binding. Use `fail_closed`, select exactly one provider host without globs or exclusions, and ensure that host has exactly one admitted network endpoint. The manifest supplies the harness, hook, and schema identifiers; do not encode a harness-specific contract in policy. The initial bridge requires combined sandbox topology and supports exactly one configured agent binding. - WebSocket middleware runs for both `ws://` and `wss://` and receives complete client text messages only. Binary messages pass under both error modes and emit `unsupported_message_type` coverage for active stages. Upstream-to-client messages remain uninspected. Do not claim that V1 provides all-message WebSocket inspection. - Treat `fail_open` on WebSocket as a session-scoped bypass: if the stage stream fails, OpenShell disables it for later messages on that connection and emits a state-change finding. Prefer `fail_closed` for required redaction or authorization. - `on_error` governs failures after an advertised operation binding is selected. It does not apply to an unadvertised WebSocket binding or binary-message pass-through. An explicit HTTP, WebSocket preflight, or WebSocket message denial is authoritative under both `fail_open` and `fail_closed`. diff --git a/.agents/skills/openshell-cli/SKILL.md b/.agents/skills/openshell-cli/SKILL.md index 69a889a8e2..594eeb6645 100644 --- a/.agents/skills/openshell-cli/SKILL.md +++ b/.agents/skills/openshell-cli/SKILL.md @@ -427,12 +427,20 @@ Edit `current-policy.yaml` to allow the blocked actions. **For policy content au - TLS termination configuration - Enforcement modes (`audit` vs `enforce`) - Binary matching patterns -- Ordered `network_middlewares`, host selection, HTTP and WebSocket bindings, and `fail_open` or `fail_closed` behavior +- Ordered `network_middlewares`, host selection, HTTP, WebSocket, and agent-conversation bindings, and `fail_open` or `fail_closed` behavior `network_policies` and `network_middlewares` can be modified at runtime. If `filesystem_policy`, `landlock`, or `process` need changes, the sandbox must be recreated. Built-in middleware such as `openshell/regex` needs no gateway registration. An operator-run middleware must already be registered under `[[openshell.supervisor.middleware]]`; changing that static registration requires a gateway restart. Middleware can inspect parsed HTTP request bodies and complete client-to-upstream WebSocket text messages over both `ws://` and `wss://` when the implementation advertises the matching binding. The built-in `openshell/regex` advertises both bindings and applies its fixed patterns to UTF-8 text. A host-matched HTTP-only attachment can inspect the upgrade GET but does not join the WebSocket chain; look for `binding_not_selected` coverage. Binary messages pass under both `on_error` modes and active stages emit `unsupported_message_type` coverage; upstream-to-client messages remain uninspected. A broken fail-open WebSocket stage is disabled for the rest of that connection; inspect sandbox OCSF logs for `openshell.middleware.websocket_stage_disabled`. +An operator service can also advertise `AGENT_CONVERSATION/AGENT_CONTEXT`. +Agent admission requires `fail_closed`, exactly one configured agent binding, +and one exact provider host that resolves to one admitted network endpoint. The +supervisor exposes `OPENSHELL_AGENT_CONVERSATION_URL` in combined topology and +keeps `OPENSHELL_PI_CONVERSATION_URL` as a compatibility alias. Adding the first +agent binding to a running sandbox requires recreating the sandbox so the +bridge listener and environment are installed. + ### Step 5: Push the updated policy ```bash diff --git a/architecture/sandbox.md b/architecture/sandbox.md index 5347039399..f511c7fbc7 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -181,7 +181,17 @@ middleware registry validates implementation-owned config. The generic registry and chain runner live in `openshell-supervisor-middleware`; first-party implementations live in `openshell-supervisor-middleware-builtins`. -Managed agent admission reuses the operator middleware registry without joining the HTTP chain. When a configured operator service advertises the required Pi rendered-prompt hook, the sandbox supervisor binds a loopback-only bridge inside the workload network namespace. The bridge stamps sandbox and provider identity, forwards the bounded versioned request to the selected gRPC service, and returns only its structured decision, optional replacement, and opaque receipt. +Managed agent admission reuses the operator middleware registry without joining +the HTTP chain. When exactly one configured operator service advertises an +`AGENT_CONVERSATION/AGENT_CONTEXT` binding, the sandbox supervisor binds a +loopback-only bridge inside the workload network namespace. The manifest owns +the harness, hook, and schema identifiers; policy owns the exact provider host; +and the supervisor derives the provider scheme and port from the admitted +network endpoint. The bridge stamps sandbox and provider identity, forwards the +bounded versioned request to the selected service, and returns only its +structured decision, optional replacement, and opaque receipt. The bridge and +egress middleware runner share one runtime generation, so a partial policy or +registry reload fails closed instead of mixing admission and egress state. The supervisor installs policy and middleware registry changes as one runtime generation and preserves the last-known-good generation if preparation fails. diff --git a/crates/openshell-sandbox/Cargo.toml b/crates/openshell-sandbox/Cargo.toml index 035130a362..5cb7ca8a84 100644 --- a/crates/openshell-sandbox/Cargo.toml +++ b/crates/openshell-sandbox/Cargo.toml @@ -52,7 +52,6 @@ uuid = { workspace = true } # Logging tracing = { workspace = true } -uuid = { workspace = true } tracing-subscriber = { workspace = true } tracing-appender = { workspace = true } diff --git a/crates/openshell-sandbox/src/agent_bridge.rs b/crates/openshell-sandbox/src/agent_bridge.rs index 70e3ecdd3a..4b1ce1d483 100644 --- a/crates/openshell-sandbox/src/agent_bridge.rs +++ b/crates/openshell-sandbox/src/agent_bridge.rs @@ -11,34 +11,50 @@ use axum::response::{IntoResponse, Response}; use axum::routing::post; use axum::{Json, Router}; use openshell_core::proto::{ - AgentConversationEvaluation, AgentConversationTarget, Decision, RequestContext, - SupervisorMiddlewarePhase, + AgentConversationEvaluation, AgentConversationResult, AgentConversationTarget, Decision, + RequestContext, SupervisorMiddlewarePhase, }; +use openshell_supervisor_network::opa::OpaEngine; use serde::{Deserialize, Serialize}; use tokio::net::TcpListener; +use tokio::sync::watch; use tracing::{debug, warn}; pub const BRIDGE_ADDR: &str = "127.0.0.1:8193"; pub const BRIDGE_PATH: &str = "/v1/agent/conversation"; pub const BRIDGE_URL: &str = "http://127.0.0.1:8193/v1/agent/conversation"; -pub const BRIDGE_URL_ENV: &str = "OPENSHELL_PI_CONVERSATION_URL"; +pub const BRIDGE_URL_ENV: &str = "OPENSHELL_AGENT_CONVERSATION_URL"; +pub const LEGACY_PI_BRIDGE_URL_ENV: &str = "OPENSHELL_PI_CONVERSATION_URL"; const MAX_BRIDGE_BODY_BYTES: usize = 256 * 1024; const MAX_ADMISSION_BODY_BYTES: usize = 32 * 1024; -const PI_HARNESS_VERSION: &str = "extension-v1"; #[derive(Debug, Clone)] -pub struct BridgeConfig { +pub struct BridgeSelection { pub middleware_name: String, - pub sandbox_id: String, + pub harness: String, + pub hook: String, + pub schema_version: String, + pub provider_scheme: String, pub provider_host: String, + pub provider_port: u32, + pub max_payload_bytes: usize, pub middleware_config: prost_types::Struct, } +#[derive(Debug, Clone)] +pub struct BridgeRuntimeSnapshot { + pub generation: u64, + pub selection: Option, +} + #[derive(Clone)] struct BridgeState { - runner: openshell_supervisor_middleware::ChainRunner, - config: Arc, + engine: Arc, + runtime: watch::Receiver, + sandbox_id: Arc, + sandbox_name: Arc, + workspace: watch::Receiver, } #[derive(Debug, Deserialize)] @@ -52,7 +68,7 @@ struct BridgeRequest { request_body: Vec, } -#[derive(Debug, Serialize)] +#[derive(Debug, PartialEq, Serialize)] struct BridgeResponse { decision: &'static str, #[serde(skip_serializing_if = "Option::is_none")] @@ -72,12 +88,18 @@ struct BridgeError { pub fn spawn( listener: TcpListener, - runner: openshell_supervisor_middleware::ChainRunner, - config: BridgeConfig, + engine: Arc, + runtime: watch::Receiver, + sandbox_id: String, + sandbox_name: String, + workspace: watch::Receiver, ) -> tokio::task::JoinHandle<()> { let state = BridgeState { - runner, - config: Arc::new(config), + engine, + runtime, + sandbox_id: Arc::from(sandbox_id), + sandbox_name: Arc::from(sandbox_name), + workspace, }; tokio::spawn(async move { let app = Router::new() @@ -85,13 +107,20 @@ pub fn spawn( .layer(DefaultBodyLimit::max(MAX_BRIDGE_BODY_BYTES)) .with_state(state); if let Err(error) = axum::serve(listener, app).await { - warn!(%error, "Pi admission bridge stopped"); + warn!(%error, "agent admission bridge stopped"); } }) } async fn evaluate(State(state): State, Json(input): Json) -> Response { - if !is_valid_admission_request(&input) { + let runtime = state.runtime.borrow().clone(); + if runtime.generation != state.engine.current_generation() { + return admission_unavailable(); + } + let Some(selection) = runtime.selection else { + return admission_unavailable(); + }; + if !is_valid_admission_request(&input, selection.max_payload_bytes) { return ( StatusCode::BAD_REQUEST, Json(BridgeError { @@ -101,78 +130,120 @@ async fn evaluate(State(state): State, Json(input): Json result, + Err(error) => { + debug!(error = %error, "agent admission evaluation failed"); + return admission_unavailable(); + } + }; + if generation.ensure_current().is_err() { + return admission_unavailable(); + } + + match bridge_result(result) { + Ok(result) => Json(result).into_response(), + Err(error) => (StatusCode::BAD_GATEWAY, Json(BridgeError { error })).into_response(), + } +} + +fn build_evaluation( + sandbox_id: &str, + sandbox_name: &str, + workspace: &str, + selection: &BridgeSelection, + input: BridgeRequest, +) -> AgentConversationEvaluation { + AgentConversationEvaluation { phase: SupervisorMiddlewarePhase::AgentContext as i32, context: Some(RequestContext { request_id: uuid::Uuid::new_v4().to_string(), - sandbox_id: state.config.sandbox_id.clone(), + sandbox_id: sandbox_id.to_string(), + sandbox_name: sandbox_name.to_string(), + workspace: workspace.to_string(), originating_process: None, }), - config: Some(state.config.middleware_config.clone()), + config: Some(selection.middleware_config.clone()), target: Some(AgentConversationTarget { - harness: "pi".into(), + harness: selection.harness.clone(), harness_version: input.harness_version, - hook: "rendered_prompt_admission".into(), - schema_version: "openshell.pi-input.v1".into(), - scheme: "https".into(), - host: state.config.provider_host.clone(), - port: 443, - path: "/v1/chat/completions".into(), + hook: selection.hook.clone(), + schema_version: selection.schema_version.clone(), + scheme: selection.provider_scheme.clone(), + host: selection.provider_host.clone(), + port: selection.provider_port, + path: String::new(), }), - middleware_name: state.config.middleware_name.clone(), + middleware_name: selection.middleware_name.clone(), session_id: input.session_id, turn_id: input.submission_id, request_body: input.request_body, - ..Default::default() - }; - - let result = match state.runner.evaluate_agent_conversation(evaluation).await { - Ok(result) => result, - Err(error) => { - debug!(error = %error, "Pi admission evaluation failed"); - return ( - StatusCode::BAD_GATEWAY, - Json(BridgeError { - error: "admission_unavailable", - }), - ) - .into_response(); - } - }; + } +} +fn bridge_result(result: AgentConversationResult) -> Result { match Decision::try_from(result.decision).unwrap_or(Decision::Unspecified) { - Decision::Allow => Json(BridgeResponse { + Decision::Allow => Ok(BridgeResponse { decision: "allow", replacement_body: result .has_replacement_body .then_some(result.replacement_body), receipt: (!result.attestation.is_empty()).then_some(result.attestation), reason_code: None, - metadata: Some(result.metadata), - }) - .into_response(), - Decision::Deny => Json(BridgeResponse { + metadata: (!result.metadata.is_empty()).then_some(result.metadata), + }), + Decision::Deny => Ok(BridgeResponse { decision: "deny", replacement_body: None, receipt: None, reason_code: (!result.reason_code.is_empty()).then_some(result.reason_code), metadata: None, - }) - .into_response(), - Decision::Unspecified => ( - StatusCode::BAD_GATEWAY, - Json(BridgeError { - error: "invalid_admission_response", - }), - ) - .into_response(), + }), + Decision::Unspecified => Err("invalid_admission_response"), } } -fn is_valid_admission_request(input: &BridgeRequest) -> bool { - input.harness_version == PI_HARNESS_VERSION +fn admission_unavailable() -> Response { + ( + StatusCode::BAD_GATEWAY, + Json(BridgeError { + error: "admission_unavailable", + }), + ) + .into_response() +} + +fn is_valid_admission_request(input: &BridgeRequest, binding_limit: usize) -> bool { + is_stable_identifier(&input.harness_version) && !input.request_body.is_empty() && input.request_body.len() <= MAX_ADMISSION_BODY_BYTES + && input.request_body.len() <= binding_limit +} + +fn is_stable_identifier(value: &str) -> bool { + !value.is_empty() + && value.len() <= 128 + && value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-' | b'/')) } #[cfg(test)] @@ -189,21 +260,82 @@ mod tests { } #[test] - fn admission_request_requires_the_pinned_harness_version() { - assert!(!is_valid_admission_request(&request("0.84.1", 1))); - assert!(is_valid_admission_request(&request(PI_HARNESS_VERSION, 1))); + fn admission_request_requires_a_stable_harness_version() { + assert!(!is_valid_admission_request(&request("", 1), 1)); + assert!(!is_valid_admission_request(&request("bad version", 1), 1)); + assert!(is_valid_admission_request(&request("extension-v1", 1), 1)); } #[test] fn admission_request_enforces_the_logical_body_limit() { - assert!(!is_valid_admission_request(&request(PI_HARNESS_VERSION, 0))); - assert!(is_valid_admission_request(&request( - PI_HARNESS_VERSION, - MAX_ADMISSION_BODY_BYTES, - ))); - assert!(!is_valid_admission_request(&request( - PI_HARNESS_VERSION, - MAX_ADMISSION_BODY_BYTES + 1, - ))); + assert!(!is_valid_admission_request( + &request("extension-v1", 0), + usize::MAX + )); + assert!(is_valid_admission_request( + &request("extension-v1", MAX_ADMISSION_BODY_BYTES,), + usize::MAX + )); + assert!(!is_valid_admission_request( + &request("extension-v1", MAX_ADMISSION_BODY_BYTES + 1,), + usize::MAX + )); + assert!(!is_valid_admission_request(&request("extension-v1", 2), 1)); + } + + #[test] + fn evaluation_uses_advertised_binding_and_trusted_destination() { + let selection = BridgeSelection { + middleware_name: "operator/guard".into(), + harness: "another-agent".into(), + hook: "user_input".into(), + schema_version: "example.input.v1".into(), + provider_scheme: "https".into(), + provider_host: "api.example.com".into(), + provider_port: 8443, + max_payload_bytes: 1024, + middleware_config: prost_types::Struct::default(), + }; + let evaluation = build_evaluation( + "sandbox-1", + "friendly-sandbox", + "workspace-1", + &selection, + request("plugin-v2", 2), + ); + let target = evaluation.target.expect("target"); + assert_eq!(target.harness, "another-agent"); + assert_eq!(target.harness_version, "plugin-v2"); + assert_eq!(target.hook, "user_input"); + assert_eq!(target.schema_version, "example.input.v1"); + assert_eq!(target.host, "api.example.com"); + assert_eq!(target.port, 8443); + assert!(target.path.is_empty()); + } + + #[test] + fn result_mapping_preserves_only_operation_outputs() { + let allow = bridge_result(AgentConversationResult { + decision: Decision::Allow as i32, + attestation: b"receipt".to_vec(), + replacement_body: b"redacted".to_vec(), + has_replacement_body: true, + ..Default::default() + }) + .expect("allow"); + assert_eq!(allow.decision, "allow"); + assert_eq!(allow.receipt, Some(b"receipt".to_vec())); + assert_eq!(allow.replacement_body, Some(b"redacted".to_vec())); + + let deny = bridge_result(AgentConversationResult { + decision: Decision::Deny as i32, + reason_code: "policy_denied".into(), + ..Default::default() + }) + .expect("deny"); + assert_eq!(deny.decision, "deny"); + assert_eq!(deny.reason_code.as_deref(), Some("policy_denied")); + assert_eq!(deny.receipt, None); + assert_eq!(deny.replacement_body, None); } } diff --git a/crates/openshell-sandbox/src/lib.rs b/crates/openshell-sandbox/src/lib.rs index a8ae905ad1..8abd92e056 100644 --- a/crates/openshell-sandbox/src/lib.rs +++ b/crates/openshell-sandbox/src/lib.rs @@ -61,9 +61,8 @@ pub(crate) use openshell_ocsf::ctx::ctx as ocsf_ctx; use openshell_core::denial::DenialEvent; use openshell_core::policy::{NetworkMode, NetworkPolicy, ProxyPolicy, SandboxPolicy}; use openshell_core::proposals::AgentProposals; -use openshell_core::proto::NetworkMiddlewareConfig; use openshell_core::provider_credentials::ProviderCredentialState; -use openshell_supervisor_middleware::ChainRunner; +use openshell_supervisor_middleware::{ChainRunner, OnError}; use openshell_supervisor_network::opa::OpaEngine; use openshell_supervisor_process::process::ProcessEnforcementMode; pub use openshell_supervisor_process::process::{ProcessHandle, ProcessStatus}; @@ -87,47 +86,100 @@ fn has_network_runtime_capability(capabilities: Option<&str>, required: &str) -> } const SIDECAR_PROCESS_PROXY_ADDR: &str = "127.0.0.1:3128"; const SIDECAR_READY_TIMEOUT_SECS: u64 = 120; -const PI_AGENT_HOOKS: &[&str] = &["rendered_prompt_admission"]; - -struct PiBridgeSelection { - middleware_name: String, - provider_host: String, - middleware_config: prost_types::Struct, -} - -async fn select_pi_bridge( +async fn select_agent_bridge( runner: &ChainRunner, - configs: &std::collections::HashMap, -) -> Result> { - let supported = runner - .agent_conversation_middleware_names("pi", "openshell.pi-input.v1", PI_AGENT_HOOKS) - .await?; - let mut matches = configs + policy: &openshell_core::proto::SandboxPolicy, +) -> Result> { + let bindings = runner.agent_conversation_bindings().await?; + let mut matches = policy + .network_middlewares .iter() - .filter(|(_, config)| supported.contains(&config.middleware)); - let Some((config_name, config)) = matches.next() else { + .flat_map(|(config_name, config)| { + bindings + .iter() + .filter(move |binding| binding.middleware_name == config.middleware) + .map(move |binding| (config_name, config, binding)) + }); + let Some((config_name, config, advertised)) = matches.next() else { return Ok(None); }; if matches.next().is_some() { return Err(miette::miette!( - "managed Pi admission requires exactly one matching middleware config" + "managed agent admission requires exactly one configured agent-conversation binding" + )); + } + if OnError::parse(&config.on_error)? != OnError::FailClosed { + return Err(miette::miette!( + "agent admission middleware config '{config_name}' must use fail_closed" )); } let endpoints = config.endpoints.as_ref().ok_or_else(|| { - miette::miette!("Pi admission middleware config '{config_name}' requires endpoints") + miette::miette!("agent admission middleware config '{config_name}' requires endpoints") })?; - if endpoints.include.len() != 1 || endpoints.include[0].contains('*') { + if endpoints.include.len() != 1 + || !endpoints.exclude.is_empty() + || endpoints.include[0].contains('*') + { return Err(miette::miette!( - "Pi admission middleware config '{config_name}' requires one exact provider host" + "agent admission middleware config '{config_name}' requires one exact provider host without exclusions" )); } - Ok(Some(PiBridgeSelection { + let provider_host = &endpoints.include[0]; + let (provider_scheme, provider_port) = exact_provider_endpoint(policy, provider_host)?; + let max_payload_bytes = usize::try_from(advertised.binding.max_payload_bytes) + .map_err(|_| miette::miette!("agent admission binding payload limit is unsupported"))?; + Ok(Some(agent_bridge::BridgeSelection { middleware_name: config.middleware.clone(), - provider_host: endpoints.include[0].clone(), + harness: advertised.binding.harness.clone(), + hook: advertised.binding.hook.clone(), + schema_version: advertised.binding.schema_version.clone(), + provider_scheme, + provider_host: provider_host.clone(), + provider_port, + max_payload_bytes, middleware_config: config.config.clone().unwrap_or_default(), })) } +fn exact_provider_endpoint( + policy: &openshell_core::proto::SandboxPolicy, + provider_host: &str, +) -> Result<(String, u32)> { + let mut endpoints = policy + .network_policies + .values() + .flat_map(|rule| &rule.endpoints) + .filter(|endpoint| endpoint.host.eq_ignore_ascii_case(provider_host)) + .flat_map(|endpoint| { + let ports = if endpoint.ports.is_empty() { + vec![endpoint.port] + } else { + endpoint.ports.clone() + }; + ports.into_iter().map(move |port| { + let scheme = if endpoint.tls == "terminate" || port == 443 { + "https" + } else { + "http" + }; + (scheme.to_string(), port) + }) + }) + .filter(|(_, port)| *port > 0) + .collect::>(); + endpoints.sort(); + endpoints.dedup(); + match endpoints.as_slice() { + [endpoint] => Ok(endpoint.clone()), + [] => Err(miette::miette!( + "agent admission provider host '{provider_host}' requires one exact network policy endpoint" + )), + _ => Err(miette::miette!( + "agent admission provider host '{provider_host}' resolves to multiple network policy endpoints" + )), + } +} + /// Run a command in the sandbox. /// /// # Errors @@ -592,49 +644,64 @@ pub async fn run_sandbox( None }; - let pi_bridge = match (opa_engine.as_ref(), retained_proto.as_ref()) { + let agent_bridge = match (opa_engine.as_ref(), retained_proto.as_ref()) { (Some(engine), Some(proto)) => { let runner = engine.middleware_runner()?; - select_pi_bridge(&runner, &proto.network_middlewares) - .await? - .map(|selection| (selection, runner)) + select_agent_bridge(&runner, proto).await? } _ => None, }; - if let Some((selection, runner)) = pi_bridge { + let agent_bridge_runtime = if let Some(selection) = agent_bridge { if sidecar_network_enforcement { return Err(miette::miette!( - "managed Pi admission is not supported in sidecar topology" + "managed agent admission is not supported in sidecar topology" )); } #[cfg(target_os = "linux")] let listener = netns .as_ref() - .ok_or_else(|| miette::miette!("Pi admission bridge requires network enforcement"))? + .ok_or_else(|| miette::miette!("agent admission bridge requires network enforcement"))? .bind_tcp_in_netns(agent_bridge::BRIDGE_ADDR) .await .into_diagnostic() - .wrap_err("failed to bind Pi admission bridge")?; + .wrap_err("failed to bind agent admission bridge")?; #[cfg(not(target_os = "linux"))] let listener = tokio::net::TcpListener::bind(agent_bridge::BRIDGE_ADDR) .await .into_diagnostic()?; + let engine = opa_engine + .as_ref() + .expect("agent bridge requires OPA") + .clone(); + let (runtime_tx, runtime_rx) = + tokio::sync::watch::channel(agent_bridge::BridgeRuntimeSnapshot { + generation: engine.current_generation(), + selection: Some(selection), + }); agent_bridge::spawn( listener, - runner, - agent_bridge::BridgeConfig { - middleware_name: selection.middleware_name, - sandbox_id: sandbox_id.clone().unwrap_or_default(), - provider_host: selection.provider_host, - middleware_config: selection.middleware_config, - }, + engine, + runtime_rx, + sandbox_id.clone().unwrap_or_default(), + sandbox_name_for_agg.clone().unwrap_or_default(), + workspace_rx.clone(), ); provider_env.insert( agent_bridge::BRIDGE_URL_ENV.into(), agent_bridge::BRIDGE_URL.into(), ); - info!(url = agent_bridge::BRIDGE_URL, "Pi admission bridge ready"); - } + provider_env.insert( + agent_bridge::LEGACY_PI_BRIDGE_URL_ENV.into(), + agent_bridge::BRIDGE_URL.into(), + ); + info!( + url = agent_bridge::BRIDGE_URL, + "agent admission bridge ready" + ); + Some(runtime_tx) + } else { + None + }; #[cfg(target_os = "linux")] let sidecar_control_server = if network_enabled && sidecar_network_enforcement { @@ -846,6 +913,7 @@ pub async fn run_sandbox( capable: transparent_tcp_capable, substrate_ready: transparent_tcp_substrate_ready, }, + agent_bridge_runtime: agent_bridge_runtime.clone(), }; tokio::spawn(async move { @@ -2663,7 +2731,7 @@ async fn reload_gateway_policy_runtime( entrypoint_pid: u32, middleware: MiddlewareReloadContext<'_>, transparent_tcp: TransparentTcpReloadState, -) -> std::result::Result<(), GatewayRuntimeReloadError> { +) -> std::result::Result, GatewayRuntimeReloadError> { if let Some(policy) = policy && policy_contains_explicit_tcp(policy) { @@ -2690,16 +2758,33 @@ async fn reload_gateway_policy_runtime( ) .await .map_err(GatewayRuntimeReloadError::MiddlewareRegistry)?; + let candidate_runner = engine + .middleware_runner() + .map_err(GatewayRuntimeReloadError::PolicyValidation)? + .with_replacement_registry(registry.clone()); + let selection = select_agent_bridge(&candidate_runner, policy) + .await + .map_err(GatewayRuntimeReloadError::PolicyValidation)?; engine .reload_policy_and_middleware_from_proto_with_pid(policy, entrypoint_pid, registry) - .map_err(GatewayRuntimeReloadError::PolicyValidation) + .map_err(GatewayRuntimeReloadError::PolicyValidation)?; + Ok(selection) } // Policy-only change: the installed registry already matches the // delivered service set, so swap the engine alone. This must not // require middleware reachability. - Some(policy) => engine - .reload_from_proto_with_pid(policy, entrypoint_pid) - .map_err(GatewayRuntimeReloadError::PolicyValidation), + Some(policy) => { + let runner = engine + .middleware_runner() + .map_err(GatewayRuntimeReloadError::PolicyValidation)?; + let selection = select_agent_bridge(&runner, policy) + .await + .map_err(GatewayRuntimeReloadError::PolicyValidation)?; + engine + .reload_from_proto_with_pid(policy, entrypoint_pid) + .map_err(GatewayRuntimeReloadError::PolicyValidation)?; + Ok(selection) + } None => Err(GatewayRuntimeReloadError::PolicyValidation( miette::miette!("runtime reload requires a policy payload but none was returned"), )), @@ -3238,6 +3323,7 @@ struct PolicyPollLoopContext { middleware_connector: MiddlewareConnector, /// Immutable driver capability and startup substrate state. transparent_tcp: TransparentTcpReloadState, + agent_bridge_runtime: Option>, } type MiddlewareConnector = Arc< @@ -3524,6 +3610,20 @@ fn apply_policy_validation_failure( } } +fn synchronize_agent_bridge_generation( + runtime: Option<&tokio::sync::watch::Sender>, + disposition: &PolicyValidationFailureDisposition, +) { + if !disposition.previous_policy_active { + return; + } + if let Some(runtime) = runtime { + let mut snapshot = runtime.borrow().clone(); + snapshot.generation = disposition.active_generation; + runtime.send_replace(snapshot); + } +} + fn policy_validation_failure_events( disposition: &PolicyValidationFailureDisposition, version: u32, @@ -3861,6 +3961,10 @@ async fn run_policy_poll_loop_with_client( rejected.version, &rejected.validation_error, )?; + synchronize_agent_bridge_generation( + ctx.agent_bridge_runtime.as_ref(), + &disposition, + ); emit_policy_validation_failure( &disposition, rejected.version, @@ -3980,7 +4084,7 @@ async fn run_policy_poll_loop_with_client( .await; match runtime_result { - Ok(()) => { + Ok(agent_selection) => { policy_runtime_reconciled = true; let policy = result .policy @@ -4083,6 +4187,12 @@ async fn run_policy_poll_loop_with_client( ); middleware_registry_status = MiddlewareRegistryStatus::Synchronized; last_failed_runtime_revision = None; + if let Some(runtime) = ctx.agent_bridge_runtime.as_ref() { + runtime.send_replace(agent_bridge::BridgeRuntimeSnapshot { + generation: ctx.opa_engine.current_generation(), + selection: agent_selection, + }); + } } Err(failure) => { let failed_revision = FailedRuntimeRevision::new( @@ -4103,6 +4213,10 @@ async fn run_policy_poll_loop_with_client( error, disposition, } => { + synchronize_agent_bridge_generation( + ctx.agent_bridge_runtime.as_ref(), + &disposition, + ); emit_policy_validation_failure( &disposition, result.version, @@ -4406,6 +4520,65 @@ mod tests { } } + #[test] + fn agent_provider_endpoint_must_be_exact_and_unambiguous() { + let mut policy = openshell_core::proto::SandboxPolicy::default(); + policy.network_policies.insert( + "provider".into(), + openshell_core::proto::NetworkPolicyRule { + endpoints: vec![openshell_core::proto::NetworkEndpoint { + host: "api.example.com".into(), + ports: vec![443], + ..Default::default() + }], + ..Default::default() + }, + ); + + assert_eq!( + exact_provider_endpoint(&policy, "API.EXAMPLE.COM").unwrap(), + ("https".into(), 443) + ); + assert!(exact_provider_endpoint(&policy, "missing.example.com").is_err()); + + policy + .network_policies + .get_mut("provider") + .expect("provider rule") + .endpoints[0] + .ports + .push(8443); + assert!(exact_provider_endpoint(&policy, "api.example.com").is_err()); + } + + #[test] + fn retained_policy_generation_keeps_agent_bridge_in_sync() { + let (runtime, receiver) = + tokio::sync::watch::channel(agent_bridge::BridgeRuntimeSnapshot { + generation: 2, + selection: None, + }); + let retained = PolicyValidationFailureDisposition { + configured_mode: PolicyValidationFailureMode::RetainLastValid, + mode: PolicyValidationFailureMode::RetainLastValid, + previous_policy_active: true, + active_generation: 3, + }; + + synchronize_agent_bridge_generation(Some(&runtime), &retained); + + assert_eq!(receiver.borrow().generation, 3); + + let fail_closed = PolicyValidationFailureDisposition { + configured_mode: PolicyValidationFailureMode::FailClosed, + mode: PolicyValidationFailureMode::FailClosed, + previous_policy_active: false, + active_generation: 4, + }; + synchronize_agent_bridge_generation(Some(&runtime), &fail_closed); + assert_eq!(receiver.borrow().generation, 3); + } + #[test] fn sidecar_process_policy_sets_loopback_proxy_addr() { let policy = proxy_policy(None); @@ -4946,6 +5119,7 @@ network_policies: extension_authentication_enabled: false, middleware_connector, transparent_tcp: TransparentTcpReloadState::default(), + agent_bridge_runtime: None, } } diff --git a/crates/openshell-supervisor-middleware/src/lib.rs b/crates/openshell-supervisor-middleware/src/lib.rs index 8568f71534..f3382aff50 100644 --- a/crates/openshell-supervisor-middleware/src/lib.rs +++ b/crates/openshell-supervisor-middleware/src/lib.rs @@ -356,6 +356,13 @@ pub struct ChainEntry { pub on_error: OnError, } +/// One exact agent-conversation capability advertised by a registered service. +#[derive(Debug, Clone)] +pub struct AgentConversationBinding { + pub middleware_name: String, + pub binding: MiddlewareBinding, +} + impl TryFrom<(&str, &NetworkMiddlewareConfig)> for ChainEntry { type Error = miette::Report; @@ -1041,12 +1048,12 @@ fn validate_response_envelope( if result.body.len() > MAX_MIDDLEWARE_PAYLOAD_BYTES { return Err("response_body_over_capacity"); } - if result.reason.len() > MAX_MIDDLEWARE_REASON_BYTES { - return Err("response_reason_over_capacity"); - } - if !result.reason_code.is_empty() && !is_stable_reason_code(&result.reason_code) { - return Err("response_reason_code_invalid"); - } + validate_common_response_fields( + &result.reason, + &result.reason_code, + &result.findings, + &result.metadata, + )?; if result.header_mutations.len() > headers::MAX_HEADER_MUTATIONS { return Err("header_mutation_count_over_capacity"); } @@ -1059,28 +1066,42 @@ fn validate_response_envelope( if mutation_bytes > MAX_MIDDLEWARE_HEADER_MUTATION_WIRE_BYTES { return Err("header_mutation_bytes_over_capacity"); } - if result.findings.len() > MAX_MIDDLEWARE_FINDINGS_PER_STAGE { + if result.encoded_len() > MIDDLEWARE_GRPC_MESSAGE_BYTES { + return Err("response_envelope_over_capacity"); + } + Ok(()) +} + +fn validate_common_response_fields( + reason: &str, + reason_code: &str, + findings: &[Finding], + metadata: &HashMap, +) -> std::result::Result<(), &'static str> { + if reason.len() > MAX_MIDDLEWARE_REASON_BYTES { + return Err("response_reason_over_capacity"); + } + if !reason_code.is_empty() && !is_stable_reason_code(reason_code) { + return Err("response_reason_code_invalid"); + } + if findings.len() > MAX_MIDDLEWARE_FINDINGS_PER_STAGE { return Err("response_findings_over_capacity"); } - if result - .findings + if findings .iter() .any(|finding| finding.encoded_len() > MAX_MIDDLEWARE_FINDING_BYTES) { return Err("response_finding_over_capacity"); } - if result.metadata.len() > MAX_MIDDLEWARE_METADATA_ENTRIES { + if metadata.len() > MAX_MIDDLEWARE_METADATA_ENTRIES { return Err("response_metadata_count_over_capacity"); } - let metadata_bytes = result.metadata.iter().fold(0usize, |total, (key, value)| { + let metadata_bytes = metadata.iter().fold(0usize, |total, (key, value)| { total.saturating_add(key.len()).saturating_add(value.len()) }); if metadata_bytes > MAX_MIDDLEWARE_METADATA_BYTES { return Err("response_metadata_bytes_over_capacity"); } - if result.encoded_len() > MIDDLEWARE_GRPC_MESSAGE_BYTES { - return Err("response_envelope_over_capacity"); - } Ok(()) } @@ -1469,28 +1490,22 @@ impl ChainRunner { }) } - /// Return middleware attachment names that advertise every requested hook. - pub async fn agent_conversation_middleware_names( - &self, - harness: &str, - schema_version: &str, - hooks: &[&str], - ) -> Result> { + /// Return every exact agent-conversation binding advertised by registered middleware. + pub async fn agent_conversation_bindings(&self) -> Result> { let manifests = self.manifests().await?; Ok(manifests - .iter() - .filter(|(_, manifest)| { - hooks.iter().all(|hook| { - manifest.bindings.iter().any(|binding| { - binding.operation == SupervisorMiddlewareOperation::AgentConversation as i32 - && binding.phase == SupervisorMiddlewarePhase::AgentContext as i32 - && binding.harness == harness - && binding.hook == *hook - && binding.schema_version == schema_version - }) + .into_iter() + .flat_map(|(state, manifest)| { + let middleware_name = Self::attachment_name(&state, &manifest).to_string(); + manifest.bindings.into_iter().filter_map(move |binding| { + (binding.operation == SupervisorMiddlewareOperation::AgentConversation as i32 + && binding.phase == SupervisorMiddlewarePhase::AgentContext as i32) + .then(|| AgentConversationBinding { + middleware_name: middleware_name.clone(), + binding, + }) }) }) - .map(|(state, manifest)| Self::attachment_name(state, manifest).to_string()) .collect()) } @@ -1578,10 +1593,16 @@ impl ChainRunner { "agent conversation response has unspecified decision" )); } - if result.reason.len() > MAX_MIDDLEWARE_REASON_BYTES - || (!result.reason_code.is_empty() && !is_stable_reason_code(&result.reason_code)) + if validate_common_response_fields( + &result.reason, + &result.reason_code, + &result.findings, + &result.metadata, + ) + .is_err() || result.attestation.len() > MAX_AGENT_ATTESTATION_BYTES || result.replacement_body.len() > max_payload_bytes + || result.encoded_len() > MIDDLEWARE_GRPC_MESSAGE_BYTES { return Err(miette!("agent conversation response is invalid")); } @@ -2833,6 +2854,7 @@ mod tests { struct AgentService { received: std::sync::Mutex>, + result: AgentConversationResult, } #[tonic::async_trait] @@ -2856,14 +2878,14 @@ mod tests { phase: SupervisorMiddlewarePhase::AgentContext as i32, max_payload_bytes: 4096, timeout: String::new(), - harness: "pi".into(), + harness: "example-agent".into(), hook: hook.into(), - schema_version: "openshell.pi-input.v1".into(), + schema_version: "example.user-input.v1".into(), }; Ok(tonic::Response::new(MiddlewareManifest { name: "test/agent".into(), service_version: "test".into(), - bindings: vec![binding("rendered_prompt_admission")], + bindings: vec![binding("user_input_admission")], expected_audience: String::new(), })) } @@ -2896,11 +2918,7 @@ mod tests { .lock() .expect("agent request lock") .push(request.into_inner()); - Ok(tonic::Response::new(AgentConversationResult { - decision: Decision::Allow as i32, - attestation: b"receipt".to_vec(), - ..Default::default() - })) + Ok(tonic::Response::new(self.result.clone())) } } @@ -2908,17 +2926,22 @@ mod tests { async fn agent_request_uses_exact_matching_registered_binding() { let service = Arc::new(AgentService { received: std::sync::Mutex::new(Vec::new()), + result: AgentConversationResult { + decision: Decision::Allow as i32, + attestation: b"receipt".to_vec(), + ..Default::default() + }, }); let runner = ChainRunner::new_protobuf_for_tests(service.clone()); - let names = runner - .agent_conversation_middleware_names( - "pi", - "openshell.pi-input.v1", - &["rendered_prompt_admission"], - ) + let bindings = runner + .agent_conversation_bindings() .await .expect("discover agent middleware"); - assert_eq!(names, vec!["test/agent"]); + assert_eq!(bindings.len(), 1); + assert_eq!(bindings[0].middleware_name, "test/agent"); + assert_eq!(bindings[0].binding.harness, "example-agent"); + assert_eq!(bindings[0].binding.hook, "user_input_admission"); + assert_eq!(bindings[0].binding.schema_version, "example.user-input.v1"); let result = runner .evaluate_agent_conversation(AgentConversationEvaluation { @@ -2926,14 +2949,16 @@ mod tests { context: Some(RequestContext { request_id: "request-1".into(), sandbox_id: "sandbox-1".into(), + sandbox_name: String::new(), + workspace: String::new(), originating_process: None, }), config: Some(prost_types::Struct::default()), target: Some(AgentConversationTarget { - harness: "pi".into(), + harness: "example-agent".into(), harness_version: "test".into(), - hook: "rendered_prompt_admission".into(), - schema_version: "openshell.pi-input.v1".into(), + hook: "user_input_admission".into(), + schema_version: "example.user-input.v1".into(), scheme: "https".into(), host: "api.openai.com".into(), port: 443, @@ -2952,6 +2977,46 @@ mod tests { ); } + #[tokio::test] + async fn agent_request_rejects_an_oversized_response() { + let service = Arc::new(AgentService { + received: std::sync::Mutex::new(Vec::new()), + result: AgentConversationResult { + decision: Decision::Allow as i32, + reason: "r".repeat(MAX_MIDDLEWARE_REASON_BYTES + 1), + ..Default::default() + }, + }); + let runner = ChainRunner::new_protobuf_for_tests(service); + let error = runner + .evaluate_agent_conversation(AgentConversationEvaluation { + phase: SupervisorMiddlewarePhase::AgentContext as i32, + context: Some(RequestContext { + request_id: "request-2".into(), + sandbox_id: "sandbox-1".into(), + sandbox_name: String::new(), + workspace: String::new(), + originating_process: None, + }), + config: Some(prost_types::Struct::default()), + target: Some(AgentConversationTarget { + harness: "example-agent".into(), + hook: "user_input_admission".into(), + schema_version: "example.user-input.v1".into(), + ..Default::default() + }), + middleware_name: "test/agent".into(), + request_body: b"{}".to_vec(), + ..Default::default() + }) + .await + .expect_err("oversized response must fail"); + assert!( + format!("{error:?}").contains("response is invalid"), + "unexpected error: {error:?}" + ); + } + #[tonic::async_trait] impl SupervisorMiddleware for SlowService { type EvaluateWebSocketSessionStream = WebSocketResponseStream; diff --git a/docs/extensibility/supervisor-middleware.mdx b/docs/extensibility/supervisor-middleware.mdx index 172d532006..643326b6b8 100644 --- a/docs/extensibility/supervisor-middleware.mdx +++ b/docs/extensibility/supervisor-middleware.mdx @@ -9,7 +9,24 @@ keywords: "Generative AI, Cybersecurity, AI Agents, Supervisor Middleware, Exten Supervisor middleware adds ordered processing stages to allowed HTTP and WebSocket egress. Middleware runs after network and L7 policy admit traffic and before OpenShell injects provider credentials. A stage can allow or deny an HTTP request or client WebSocket text message, replace its payload, add approved HTTP headers, and report audit-safe findings. -Operator middleware can also advertise versioned agent-harness bindings. For managed Pi, the sandbox supervisor exposes a loopback bridge only when one configured middleware advertises `rendered_prompt_admission` for `openshell.pi-input.v1`. The supervisor sets `OPENSHELL_PI_CONVERSATION_URL`; the sandbox deployment loads the middleware's Pi extension through Pi's standard extension mechanism. Pi itself remains unaware of OpenShell. The MVP admits a rendered, idle, text-only user prompt before Pi stores it; denied prompts never enter chat history, while replacement bodies support redaction. The supervisor stamps sandbox and provider identity before dispatch. Agent admission is fail closed and does not use the HTTP or WebSocket middleware chains. Admission bodies are limited to 32 KiB. Images, queued input, retries, compaction, and automatic continuations after tool calls are intentionally unsupported until a separate model-request boundary is added. +Operator middleware can also advertise versioned agent-harness bindings. The +sandbox supervisor exposes a loopback bridge when exactly one configured +middleware advertises `AGENT_CONVERSATION/AGENT_CONTEXT`. The manifest supplies +the harness, hook, and schema identifiers. The selected policy config must use +`fail_closed` and select one exact provider host; the supervisor derives its +scheme and port from the corresponding admitted network endpoint. The bridge +sets `OPENSHELL_AGENT_CONVERSATION_URL`, stamps sandbox and provider identity, +and returns only the structured decision, optional replacement, and opaque +receipt. It shares the policy and middleware registry generation used for +provider egress, so an incomplete reload makes admission unavailable. + +Managed Pi is the first client of this general contract. For compatibility, the +supervisor also sets `OPENSHELL_PI_CONVERSATION_URL`. A Pi extension submits a +rendered, idle, text-only user prompt before Pi stores it; denied prompts never +enter chat history, while replacement bodies support redaction. Pi remains +unaware of OpenShell-specific behavior. Admission bodies are limited to 32 KiB +and to the advertised binding limit. Images, queued input, retries, compaction, +and automatic continuations after tool calls remain outside this initial hook. Middleware selection is independent of the network policy rule that admitted the request. OpenShell matches middleware by destination host, so the same middleware applies consistently across broad, specific, user-authored, and provider-derived network policies. @@ -77,12 +94,12 @@ timeout = "500ms" | `tls_ca_cert_path` | Optional PEM trust roots for a private HTTPS service. Custom roots replace platform roots and retain hostname verification. | | `audience` | Exact audience expected by the service. Defaults to `urn:openshell:extension:middleware:`. | | `allow_insecure_transport` | Opt this registration out of extension authentication, permitting a plaintext `http://` endpoint with no bearer credential. Defaults to `false`. Development and trusted-network deployments only. | -| `max_payload_bytes` | Shared operator limit applied to inspectable logical payloads across every binding exposed by the service, up to the 4 MiB platform maximum. It caps HTTP bodies and complete WebSocket text messages. | +| `max_payload_bytes` | Shared operator limit applied to inspectable logical payloads across every binding exposed by the service, up to the 4 MiB platform maximum. It caps HTTP bodies, complete WebSocket text messages, and agent-conversation bodies. | | `timeout` | Optional service-wide RPC timeout using an integer with an `ms` or `s` suffix. Defaults to `500ms`; valid values range from `10ms` through `30s`. | Each binding returned by `Describe` may advertise a shorter `timeout` using the same syntax and bounds. The operator-configured service timeout is a ceiling: OpenShell uses the smaller of the binding and service values. An omitted binding timeout inherits the service setting, and an omitted service setting uses the 500 ms platform default. OpenShell rejects an invalid timeout before accepting the manifest. The operator-configured service timeout applies to `Describe` and `ValidateConfig`. The effective binding timeout applies only to `EvaluateHttpRequest`, WebSocket preflight, and each WebSocket message. WebSocket streams have no connection-wide deadline. -The gateway connects to every registered service and verifies its capabilities before accepting traffic. Gateway startup fails when a service is unavailable, reports an invalid capability, or exposes more than one binding for the same operation and phase. The manifest `name` is diagnostic metadata and does not need to match the operator registration name. Operator-run registration names cannot claim the reserved `openshell/` namespace. +The gateway connects to every registered service and verifies its capabilities before accepting traffic. Gateway startup fails when a service is unavailable, reports an invalid capability, or exposes a duplicate binding identity. Agent bindings with distinct harness, hook, or schema identifiers may share the same operation and phase. The manifest `name` is diagnostic metadata and does not need to match the operator registration name. Operator-run registration names cannot claim the reserved `openshell/` namespace. Registration is static. Restart the gateway after adding, removing, or changing a service. See [Gateway Configuration](/reference/gateway-config#supervisor-middleware-services) for the complete gateway TOML context. @@ -168,7 +185,7 @@ Middleware decisions are enforced regardless of the endpoint's `enforcement` mod ## Set Payload Limits -Every middleware binding declares the largest logical payload or replacement it supports through `max_payload_bytes`. For `HTTP_REQUEST`, that payload is one request body. For `WEBSOCKET_MESSAGE`, it is one complete message rather than the whole session. +Every middleware binding declares the largest logical payload or replacement it supports through `max_payload_bytes`. For `HTTP_REQUEST`, that payload is one request body. For `WEBSOCKET_MESSAGE`, it is one complete message rather than the whole session. For `AGENT_CONVERSATION`, it is one versioned hook body or replacement; the initial loopback bridge applies an additional 32 KiB limit. - Built-in middleware uses its OpenShell-defined limit. - Each operator-run registration sets one `max_payload_bytes` ceiling no higher than any binding's advertised `max_payload_bytes` capability. @@ -205,7 +222,7 @@ Plan startup and updates around these boundaries: - Keep required services available before creating or updating policies. The gateway validates implementation-owned config before persisting a policy. - Treat `fail_open` as an explicit availability-over-enforcement decision. -When the effective sandbox configuration changes, a running supervisor validates the new service registry before installing it. If the reload fails, the supervisor keeps its last-known-good registry and emits a configuration failure event. +When the effective sandbox configuration changes, a running supervisor validates the new service registry and agent-admission selection before installing either one. If the reload fails, the supervisor keeps its last-known-good runtime generation and emits a configuration failure event. A fail-closed policy quarantine intentionally invalidates the bridge generation as well. ## Observe Middleware @@ -226,6 +243,7 @@ See [Logging](/observability/logging) for log access and [OCSF JSON Export](/obs - Middleware applies only through operation bindings advertised by each implementation. For protocols that have no supported middleware operation at all, such as HTTP/2 prior knowledge or non-HTTP TCP, the existing uninspectable-traffic gate denies a host match containing `fail_closed` and relays an all-`fail_open` match with a detection finding. - The typed operation and phase pairs are `HTTP_REQUEST/PRE_CREDENTIALS`, `WEBSOCKET_MESSAGE/PRE_CREDENTIALS`, and `AGENT_CONVERSATION/AGENT_CONTEXT`. +- The initial agent bridge supports exactly one configured agent-conversation binding, one exact provider host, and combined sandbox topology. Adding the first agent binding to a running sandbox requires recreating that sandbox so the loopback listener and environment variable are present. - A host match does not imply every advertised operation: an HTTP-only attachment can inspect the upgrade GET, then post-upgrade traffic passes with `binding_not_selected` coverage. - The V1 WebSocket binding inspects complete client text messages only. Binary messages pass with `unsupported_message_type` coverage for active stages; control frames and upstream-to-client messages remain outside the middleware operation. - Selection uses destination host include and exclude patterns. diff --git a/proto/supervisor_middleware.proto b/proto/supervisor_middleware.proto index b60c4f5304..b388eec856 100644 --- a/proto/supervisor_middleware.proto +++ b/proto/supervisor_middleware.proto @@ -354,10 +354,7 @@ message AgentConversationEvaluation { string session_id = 7; string turn_id = 8; bytes request_body = 9; - string source = 10; - string delivery = 11; - string request_kind = 12; - optional uint32 candidate_index = 13; + reserved 10 to 13; } // AgentConversationResult carries the authority decision, an optional complete From 2819502e30293dcfbab2fb9bf1f5981fd6539558 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 27 Aug 2026 23:31:21 -0400 Subject: [PATCH 03/18] feat(gateway): append local config fragment --- .../scripts/append-gateway-config-fragment.sh | 20 +++++++++++++++++++ tasks/scripts/gateway-docker.sh | 2 ++ tasks/scripts/gateway-podman.sh | 2 ++ tasks/scripts/gateway-vm.sh | 2 ++ tasks/scripts/gateway.sh | 2 ++ 5 files changed, 28 insertions(+) create mode 100644 tasks/scripts/append-gateway-config-fragment.sh diff --git a/tasks/scripts/append-gateway-config-fragment.sh b/tasks/scripts/append-gateway-config-fragment.sh new file mode 100644 index 0000000000..2eccb61a6d --- /dev/null +++ b/tasks/scripts/append-gateway-config-fragment.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +CONFIG_PATH=${1:?gateway config path is required} +FRAGMENT_PATH=${OPENSHELL_GATEWAY_CONFIG_FRAGMENT:-} + +if [[ -z "${FRAGMENT_PATH}" ]]; then + exit 0 +fi +if [[ ! -s "${FRAGMENT_PATH}" ]]; then + echo "ERROR: OPENSHELL_GATEWAY_CONFIG_FRAGMENT is missing or empty: ${FRAGMENT_PATH}" >&2 + exit 2 +fi + +printf '\n' >>"${CONFIG_PATH}" +cat -- "${FRAGMENT_PATH}" >>"${CONFIG_PATH}" diff --git a/tasks/scripts/gateway-docker.sh b/tasks/scripts/gateway-docker.sh index ef51f37182..9a5c7628fb 100644 --- a/tasks/scripts/gateway-docker.sh +++ b/tasks/scripts/gateway-docker.sh @@ -220,6 +220,8 @@ grpc_endpoint = "${GRPC_ENDPOINT}" supervisor_bin = "${SUPERVISOR_BIN}" EOF +bash "${ROOT}/tasks/scripts/append-gateway-config-fragment.sh" "${CONFIG_PATH}" + GATEWAY_ENDPOINT="http://127.0.0.1:${PORT}" register_gateway_metadata "${GATEWAY_NAME}" "${GATEWAY_ENDPOINT}" "${PORT}" diff --git a/tasks/scripts/gateway-podman.sh b/tasks/scripts/gateway-podman.sh index 2b70e3a85b..320562f9a0 100644 --- a/tasks/scripts/gateway-podman.sh +++ b/tasks/scripts/gateway-podman.sh @@ -263,6 +263,8 @@ if [[ -n "${OPENSHELL_SANDBOX_PROXY_CA_BUNDLE+x}" ]]; then printf 'proxy_ca_bundle = "%s"\n' "$(toml_escape "${OPENSHELL_SANDBOX_PROXY_CA_BUNDLE}")" >>"${CONFIG_PATH}" fi +bash "${ROOT}/tasks/scripts/append-gateway-config-fragment.sh" "${CONFIG_PATH}" + GATEWAY_ENDPOINT="http://${CLI_ENDPOINT_HOST}:${PORT}" register_gateway_metadata "${GATEWAY_NAME}" "${GATEWAY_ENDPOINT}" "${PORT}" diff --git a/tasks/scripts/gateway-vm.sh b/tasks/scripts/gateway-vm.sh index 22ba1b039f..6027c89782 100755 --- a/tasks/scripts/gateway-vm.sh +++ b/tasks/scripts/gateway-vm.sh @@ -345,6 +345,8 @@ driver_dir = "${DRIVER_DIR}" state_dir = "${VM_DRIVER_STATE_DIR}" EOF +bash "${ROOT}/tasks/scripts/append-gateway-config-fragment.sh" "${CONFIG_PATH}" + GATEWAY_ENDPOINT="http://127.0.0.1:${PORT}" register_gateway_metadata "${GATEWAY_NAME}" "${GATEWAY_ENDPOINT}" "${PORT}" "${VM_DRIVER_STATE_DIR}" save_active_gateway "${GATEWAY_NAME}" diff --git a/tasks/scripts/gateway.sh b/tasks/scripts/gateway.sh index 604476a4b6..382224d4f3 100644 --- a/tasks/scripts/gateway.sh +++ b/tasks/scripts/gateway.sh @@ -272,6 +272,8 @@ if [[ -n "${GRPC_ENDPOINT}" ]]; then printf 'grpc_endpoint = "%s"\n' "${GRPC_ENDPOINT}" >>"${CONFIG_PATH}" fi +bash "${ROOT}/tasks/scripts/append-gateway-config-fragment.sh" "${CONFIG_PATH}" + GATEWAY_ENDPOINT="http://127.0.0.1:${PORT}" register_gateway_metadata "${GATEWAY_NAME}" "${GATEWAY_ENDPOINT}" "${PORT}" From 219ecc725fd3c4a02bd52f1fc5a4c6c845544e80 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 27 Aug 2026 23:31:21 -0400 Subject: [PATCH 04/18] fix(sandbox): synchronize rebuilt policy generation --- crates/openshell-sandbox/src/lib.rs | 61 +++++++++++++++++++ .../openshell-supervisor-network/src/run.rs | 6 ++ 2 files changed, 67 insertions(+) diff --git a/crates/openshell-sandbox/src/lib.rs b/crates/openshell-sandbox/src/lib.rs index 578a107cd7..3de4fac9c7 100644 --- a/crates/openshell-sandbox/src/lib.rs +++ b/crates/openshell-sandbox/src/lib.rs @@ -680,6 +680,16 @@ pub async fn run_sandbox( generation: engine.current_generation(), selection: Some(selection), }); + if let Some(engine_ready) = networking + .as_ref() + .map(|networking| networking.engine_ready.clone()) + { + synchronize_agent_bridge_after_engine_ready( + runtime_tx.clone(), + engine.clone(), + engine_ready, + ); + } agent_bridge::spawn( listener, engine, @@ -3764,6 +3774,42 @@ fn synchronize_agent_bridge_generation( } } +fn synchronize_agent_bridge_after_engine_ready( + runtime: tokio::sync::watch::Sender, + engine: Arc, + mut engine_ready: tokio::sync::watch::Receiver, +) { + let initial_generation = engine.current_generation(); + tokio::spawn(async move { + if !*engine_ready.borrow() && engine_ready.changed().await.is_err() { + return; + } + if !*engine_ready.borrow() { + return; + } + let active_generation = engine.current_generation(); + synchronize_agent_bridge_generation_if_unchanged( + &runtime, + initial_generation, + active_generation, + ); + }); +} + +fn synchronize_agent_bridge_generation_if_unchanged( + runtime: &tokio::sync::watch::Sender, + expected_generation: u64, + active_generation: u64, +) { + runtime.send_if_modified(|snapshot| { + if snapshot.generation != expected_generation || snapshot.generation == active_generation { + return false; + } + snapshot.generation = active_generation; + true + }); +} + fn policy_validation_failure_events( disposition: &PolicyValidationFailureDisposition, version: u32, @@ -4719,6 +4765,21 @@ mod tests { assert_eq!(receiver.borrow().generation, 3); } + #[test] + fn entrypoint_policy_rebuild_only_updates_the_generation_it_started_from() { + let (runtime, receiver) = + tokio::sync::watch::channel(agent_bridge::BridgeRuntimeSnapshot { + generation: 2, + selection: None, + }); + + synchronize_agent_bridge_generation_if_unchanged(&runtime, 2, 3); + assert_eq!(receiver.borrow().generation, 3); + + synchronize_agent_bridge_generation_if_unchanged(&runtime, 2, 4); + assert_eq!(receiver.borrow().generation, 3); + } + #[test] fn sidecar_process_policy_sets_loopback_proxy_addr() { let policy = proxy_policy(None); diff --git a/crates/openshell-supervisor-network/src/run.rs b/crates/openshell-supervisor-network/src/run.rs index 2a71702b4b..c2d14810ec 100644 --- a/crates/openshell-supervisor-network/src/run.rs +++ b/crates/openshell-supervisor-network/src/run.rs @@ -151,6 +151,10 @@ pub struct Networking { pub proxy: Option, pub ca_file_paths: Option<(std::path::PathBuf, std::path::PathBuf)>, + /// Becomes true after the entrypoint-aware policy rebuild finishes or is + /// skipped. Consumers whose state is generation-bound must synchronize + /// after this transition. + pub engine_ready: tokio::sync::watch::Receiver, /// Policy-local route context: shared with the orchestrator's policy poll /// loop so it can publish updated `SandboxPolicy` snapshots that the /// `policy.local` route handler returns to the workload. @@ -217,6 +221,7 @@ pub async fn run_networking( // the race where an in-flight request observes a generation transition // during the OPA engine reload. let (engine_ready_tx, engine_ready_rx) = tokio::sync::watch::channel(false); + let networking_engine_ready_rx = engine_ready_rx.clone(); #[cfg(target_os = "linux")] let transparent_engine_ready_rx = engine_ready_rx.clone(); #[cfg(target_os = "linux")] @@ -491,6 +496,7 @@ pub async fn run_networking( Ok(Networking { proxy: proxy_handle, ca_file_paths, + engine_ready: networking_engine_ready_rx, policy_local_ctx, #[cfg(target_os = "linux")] _policy_dns: policy_dns, From b0b9381936aeed41e67125c8289130600b92ed9b Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 27 Aug 2026 23:59:46 -0400 Subject: [PATCH 05/18] fix(middleware): apply payload ceilings per binding Signed-off-by: Johnny Greco --- .../src/lib.rs | 60 ++++++++++--------- docs/extensibility/supervisor-middleware.mdx | 4 +- docs/reference/gateway-config.mdx | 2 +- rfc/0009-supervisor-middleware/README.md | 2 +- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/crates/openshell-supervisor-middleware/src/lib.rs b/crates/openshell-supervisor-middleware/src/lib.rs index f3382aff50..bc4be8b725 100644 --- a/crates/openshell-supervisor-middleware/src/lib.rs +++ b/crates/openshell-supervisor-middleware/src/lib.rs @@ -921,17 +921,11 @@ fn validate_manifest_bindings( )) { return Err(miette!("{source} describes a duplicate middleware binding")); } - let advertised = validate_payload_limit(source, binding)?; + validate_payload_limit(source, binding)?; if !binding.timeout.trim().is_empty() { parse_middleware_timeout(&binding.timeout) .map_err(|reason| miette!("{source} has invalid timeout for binding: {reason}"))?; } - if operator_max_payload_bytes.is_some_and(|limit| limit > advertised) { - return Err(miette!( - "{source} max_payload_bytes ({}) exceeds the binding capability ({advertised})", - operator_max_payload_bytes.expect("operator limit checked above") - )); - } if operator_max_payload_bytes == Some(0) { return Err(miette!( "{source} must configure max_payload_bytes for every payload-bearing binding" @@ -1678,7 +1672,9 @@ impl ChainRunner { }; let timeout = state.timeout_for_binding(&binding)?; let advertised = validate_payload_limit("middleware manifest", &binding)?; - let max_payload_bytes = state.operator_max_payload_bytes.unwrap_or(advertised); + let max_payload_bytes = state + .operator_max_payload_bytes + .map_or(advertised, |operator_limit| operator_limit.min(advertised)); described_entries.push(DescribedChainEntry { entry, service: Some(Arc::clone(state)), @@ -3910,24 +3906,31 @@ mod tests { assert!(outcome.applied[1].failed); } - #[test] - fn external_manifest_rejects_operator_limit_above_capability() { + #[tokio::test] + async fn external_binding_caps_operator_limit_at_advertised_capability() { let registration = external_registration(4097); - let manifest = MiddlewareManifest { - name: "example/service".into(), - service_version: "test".into(), - bindings: vec![MiddlewareBinding { - operation: HTTP_REQUEST_OPERATION as i32, - phase: PRE_CREDENTIALS_PHASE as i32, - max_payload_bytes: 4096, - timeout: String::new(), - ..Default::default() - }], - expected_audience: String::new(), - }; - let error = validate_external_manifest(®istration, &manifest, 4097, false) - .expect_err("operator limit must fit capability"); - assert!(error.to_string().contains("exceeds")); + let registry = registry_with_external( + Arc::new(ScriptedService { + manifest_name: "example/service".into(), + max_body_bytes: 4096, + result: allow_result(), + }), + registration, + ) + .await; + let runner = ChainRunner::from_registry(registry); + let described = runner + .describe_chain(&[ChainEntry { + name: "external".into(), + implementation: "local-guard-service".into(), + order: 0, + config: prost_types::Struct::default(), + on_error: OnError::FailClosed, + }]) + .await + .expect("describe external binding"); + + assert_eq!(described[0].max_payload_bytes(), 4096); } #[test] @@ -4030,7 +4033,7 @@ mod tests { } #[test] - fn external_websocket_binding_rejects_operator_limit_above_capability() { + fn external_websocket_binding_accepts_operator_limit_above_capability() { let registration = external_registration(4097); let manifest = MiddlewareManifest { name: "example/websocket".into(), @@ -4045,9 +4048,8 @@ mod tests { expected_audience: String::new(), }; - let error = validate_external_manifest(®istration, &manifest, 4097, false) - .expect_err("operator payload limit must fit WebSocket capability"); - assert!(error.to_string().contains("exceeds")); + validate_external_manifest(®istration, &manifest, 4097, false) + .expect("binding capability provides the effective WebSocket limit"); } #[test] diff --git a/docs/extensibility/supervisor-middleware.mdx b/docs/extensibility/supervisor-middleware.mdx index 643326b6b8..559360500e 100644 --- a/docs/extensibility/supervisor-middleware.mdx +++ b/docs/extensibility/supervisor-middleware.mdx @@ -188,11 +188,11 @@ Middleware decisions are enforced regardless of the endpoint's `enforcement` mod Every middleware binding declares the largest logical payload or replacement it supports through `max_payload_bytes`. For `HTTP_REQUEST`, that payload is one request body. For `WEBSOCKET_MESSAGE`, it is one complete message rather than the whole session. For `AGENT_CONVERSATION`, it is one versioned hook body or replacement; the initial loopback bridge applies an additional 32 KiB limit. - Built-in middleware uses its OpenShell-defined limit. -- Each operator-run registration sets one `max_payload_bytes` ceiling no higher than any binding's advertised `max_payload_bytes` capability. +- Each operator-run registration sets one `max_payload_bytes` ceiling. OpenShell uses the smaller of that ceiling and each binding's advertised `max_payload_bytes` capability. - A selected chain buffers using its largest stage limit, so every stage that can process the body receives it. - The same per-stage limit applies to request bodies and replacement bodies. -The gateway rejects a registration whose operator limit exceeds the service capability or the 4 MiB platform maximum instead of silently clamping it. OpenShell also bounds the non-payload protobuf components: 64 KiB for service config, 4 KiB for request context, 32 KiB for the target, and 128 request header lines totaling at most 64 KiB encoded. Results allow a 4 KiB discarded free-form reason, a 64-byte validated reason code, 64 header mutations totaling at most 64 KiB encoded, 32 findings of at most 4 KiB encoded each, and 64 metadata entries totaling at most 32 KiB. Middleware gRPC servers should configure request and response message limits to at least 4 MiB plus 293 KiB so every platform-valid envelope fits. +The gateway rejects a registration whose operator limit exceeds the 4 MiB platform maximum. A lower binding capability remains authoritative for that operation. OpenShell also bounds the non-payload protobuf components: 64 KiB for service config, 4 KiB for request context, 32 KiB for the target, and 128 request header lines totaling at most 64 KiB encoded. Results allow a 4 KiB discarded free-form reason, a 64-byte validated reason code, 64 header mutations totaling at most 64 KiB encoded, 32 findings of at most 4 KiB encoded each, and 64 metadata entries totaling at most 32 KiB. Middleware gRPC servers should configure request and response message limits to at least 4 MiB plus 293 KiB so every platform-valid envelope fits. At request time, exceeding a selected stage's limit is a middleware failure for that stage alone and follows that config's `on_error` behavior; other stages in the chain still run against their own limits. OpenShell can apply `fail_open` to an oversized `Content-Length` before consuming body bytes. A chunked body can cross the limit only after bytes have been consumed, so OpenShell denies that request because it cannot safely resume the original stream. diff --git a/docs/reference/gateway-config.mdx b/docs/reference/gateway-config.mdx index d1fed9ae44..358d06fea2 100644 --- a/docs/reference/gateway-config.mdx +++ b/docs/reference/gateway-config.mdx @@ -275,7 +275,7 @@ Each service implements the supervisor middleware gRPC contract and exposes bind The gateway connects to every registered service and validates `Describe` before it starts. The service must therefore be running before the gateway. Policy creation and full policy updates call `ValidateConfig`; an unavailable service or invalid middleware configuration rejects the policy before persistence. -`max_payload_bytes` is the shared operator limit for inspectable logical payloads across every binding exposed by the service. It caps HTTP request and replacement bodies as well as complete WebSocket text messages and replacements. The value must be greater than zero, no larger than each binding's advertised `max_payload_bytes` capability, and no larger than the 4 MiB platform maximum. OpenShell rejects oversized values instead of silently clamping them. Binary WebSocket messages are not exposed to V1 middleware, so this field does not limit binary pass-through. Middleware gRPC servers should allow messages of at least 4 MiB plus 293 KiB so a maximum-size payload and its protobuf envelope fit on the transport. +`max_payload_bytes` is the shared operator ceiling for inspectable logical payloads across every binding exposed by the service. It caps HTTP request and replacement bodies as well as complete WebSocket text messages and replacements. The value must be greater than zero and no larger than the 4 MiB platform maximum. Each binding's effective limit is the smaller of this ceiling and the binding's advertised `max_payload_bytes` capability. Binary WebSocket messages are not exposed to V1 middleware, so this field does not limit binary pass-through. Middleware gRPC servers should allow messages of at least 4 MiB plus 293 KiB so a maximum-size payload and its protobuf envelope fit on the transport. `timeout` is the operator-configured service-wide RPC timeout. It accepts the same compact duration syntax as gateway interceptors: an integer followed by `ms` or `s`, such as `500ms` or `2s`. Values must be between `10ms` and `30s`, inclusive. Omit the field to use the 500 ms platform default. A binding may advertise a shorter `timeout` in the `Describe` manifest, but it cannot extend the operator-configured deadline; OpenShell uses the smaller value. OpenShell validates both levels before accepting the service. The operator-configured service timeout applies to `Describe` and `ValidateConfig`. The effective binding timeout applies only to `EvaluateHttpRequest`, WebSocket preflight, and each WebSocket message. An accepted WebSocket stream has no connection-wide RPC deadline. diff --git a/rfc/0009-supervisor-middleware/README.md b/rfc/0009-supervisor-middleware/README.md index 1f6eacde7b..a84c95560e 100644 --- a/rfc/0009-supervisor-middleware/README.md +++ b/rfc/0009-supervisor-middleware/README.md @@ -348,7 +348,7 @@ max_payload_bytes = 1048576 The stable transport requirement is confidentiality plus authentication of the intended middleware service. Phase 1 may temporarily accept a plaintext `http://` endpoint only when the same entry explicitly sets `allow_insecure = true`. OpenShell rejects plaintext without that opt-in, warns prominently, and records the insecure registration as auditable configuration state. This escape hatch is limited to trusted local development and isolated research environments. Phase 2 removes plaintext support and the `allow_insecure` field, requiring authenticated encrypted transport. That removal is an intentional research-preview breaking change with no long-term compatibility obligation. The exact phase 2 mechanism, such as mTLS or TLS plus explicit caller authentication, is follow-up protocol work (see [appendices/protocol-extensions.md](appendices/protocol-extensions.md#middleware-authentication)). -For each binding, the operator's `max_payload_bytes` must not exceed the binding capability returned by `Describe` or the 4 MiB platform maximum. The gateway rejects an invalid registration rather than silently clamping it. The resulting operator limit applies to every binding exposed by that registration. +The operator's `max_payload_bytes` must not exceed the 4 MiB platform maximum. For each binding, OpenShell uses the smaller of the operator ceiling and the capability returned by `Describe`. This lets one service expose operations with different safe payload capacities without forcing every operation down to the smallest capability. RPC timeouts use an integer with an `ms` or `s` suffix, range from 10 ms through 30 s, and default to 500 ms. A binding may advertise its own timeout through `Describe`; that value overrides the service registration timeout. The service timeout applies to `Describe`, while the effective binding timeout applies to `ValidateConfig` and `EvaluateHttpRequest`. From af3173bdefc2875ede46a695ee747aec17312752 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 28 Aug 2026 12:54:31 -0400 Subject: [PATCH 06/18] feat(middleware): isolate managed agent attestations Signed-off-by: Johnny Greco --- Cargo.lock | 1 + architecture/sandbox.md | 20 +- crates/openshell-core/src/middleware.rs | 16 ++ crates/openshell-sandbox/src/agent_bridge.rs | 240 +++++++++++++----- crates/openshell-sandbox/src/lib.rs | 39 +-- .../Cargo.toml | 1 + .../src/lib.rs | 195 +++++++++++++- .../src/remote.rs | 1 + .../src/l7/middleware.rs | 97 ++++++- .../src/l7/rest.rs | 2 +- proto/supervisor_middleware.proto | 3 + 11 files changed, 512 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a3f5439068..1d25b28696 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4418,6 +4418,7 @@ dependencies = [ "tokio", "tokio-stream", "tonic", + "uuid", ] [[package]] diff --git a/architecture/sandbox.md b/architecture/sandbox.md index f511c7fbc7..bf7b3a70e3 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -182,16 +182,16 @@ registry and chain runner live in `openshell-supervisor-middleware`; first-party implementations live in `openshell-supervisor-middleware-builtins`. Managed agent admission reuses the operator middleware registry without joining -the HTTP chain. When exactly one configured operator service advertises an -`AGENT_CONVERSATION/AGENT_CONTEXT` binding, the sandbox supervisor binds a -loopback-only bridge inside the workload network namespace. The manifest owns -the harness, hook, and schema identifiers; policy owns the exact provider host; -and the supervisor derives the provider scheme and port from the admitted -network endpoint. The bridge stamps sandbox and provider identity, forwards the -bounded versioned request to the selected service, and returns only its -structured decision, optional replacement, and opaque receipt. The bridge and -egress middleware runner share one runtime generation, so a partial policy or -registry reload fails closed instead of mixing admission and egress state. +the HTTP chain. When one configured operator middleware advertises exact +`AGENT_CONVERSATION/AGENT_CONTEXT` hook and schema bindings, the sandbox +supervisor binds a loopback-only bridge inside the workload network namespace. +The bridge stamps sandbox and provider identity, retains an allowed response's +attestation, and returns a bounded opaque handle with the decision and optional +replacement. Provider egress strips and resolves that handle, exposing the +attestation only to the matching middleware stage. Handles are scoped to the +sandbox, middleware, provider target, and runtime generation and remain +retryable only for their bounded lifetime, so partial policy or registry reloads +cannot mix admission and egress state. The supervisor installs policy and middleware registry changes as one runtime generation and preserves the last-known-good generation if preparation fails. diff --git a/crates/openshell-core/src/middleware.rs b/crates/openshell-core/src/middleware.rs index c2f49e1c8e..10fdd0fd4d 100644 --- a/crates/openshell-core/src/middleware.rs +++ b/crates/openshell-core/src/middleware.rs @@ -68,6 +68,7 @@ pub struct HttpRequestView<'a> { headers: &'a [HttpHeader], body: &'a [u8], middleware_name: &'a str, + agent_attestation: &'a [u8], } impl<'a> HttpRequestView<'a> { @@ -90,9 +91,17 @@ impl<'a> HttpRequestView<'a> { headers, body, middleware_name, + agent_attestation: &[], } } + /// Attach a supervisor-resolved agent admission attestation to this view. + #[must_use] + pub fn with_agent_attestation(mut self, agent_attestation: &'a [u8]) -> Self { + self.agent_attestation = agent_attestation; + self + } + /// Return the typed middleware phase selected for this invocation. #[must_use] pub fn phase(self) -> SupervisorMiddlewarePhase { @@ -135,6 +144,13 @@ impl<'a> HttpRequestView<'a> { pub fn middleware_name(self) -> &'a str { self.middleware_name } + + /// Return the supervisor-resolved agent admission attestation for this + /// middleware stage. The value is empty for ordinary requests. + #[must_use] + pub fn agent_attestation(self) -> &'a [u8] { + self.agent_attestation + } } /// Asynchronous contract for supervisor middleware that runs in-process. diff --git a/crates/openshell-sandbox/src/agent_bridge.rs b/crates/openshell-sandbox/src/agent_bridge.rs index 4b1ce1d483..828d780d8e 100644 --- a/crates/openshell-sandbox/src/agent_bridge.rs +++ b/crates/openshell-sandbox/src/agent_bridge.rs @@ -26,22 +26,30 @@ pub const BRIDGE_URL: &str = "http://127.0.0.1:8193/v1/agent/conversation"; pub const BRIDGE_URL_ENV: &str = "OPENSHELL_AGENT_CONVERSATION_URL"; pub const LEGACY_PI_BRIDGE_URL_ENV: &str = "OPENSHELL_PI_CONVERSATION_URL"; -const MAX_BRIDGE_BODY_BYTES: usize = 256 * 1024; -const MAX_ADMISSION_BODY_BYTES: usize = 32 * 1024; +// Vec is encoded as a JSON number array by the existing bridge contract, +// so its transport envelope can be roughly five times the logical payload. +const MAX_ADMISSION_BODY_BYTES: usize = + openshell_supervisor_middleware::MAX_MIDDLEWARE_PAYLOAD_BYTES; +const MAX_BRIDGE_BODY_BYTES: usize = MAX_ADMISSION_BODY_BYTES * 5 + 64 * 1024; #[derive(Debug, Clone)] pub struct BridgeSelection { pub middleware_name: String, - pub harness: String, - pub hook: String, - pub schema_version: String, + pub bindings: Vec, pub provider_scheme: String, pub provider_host: String, pub provider_port: u32, - pub max_payload_bytes: usize, pub middleware_config: prost_types::Struct, } +#[derive(Debug, Clone)] +pub struct BridgeBinding { + pub harness: String, + pub hook: String, + pub schema_version: String, + pub max_payload_bytes: usize, +} + #[derive(Debug, Clone)] pub struct BridgeRuntimeSnapshot { pub generation: u64, @@ -61,6 +69,8 @@ struct BridgeState { #[serde(deny_unknown_fields)] struct BridgeRequest { harness_version: String, + hook: String, + schema_version: String, #[serde(default)] session_id: String, #[serde(default)] @@ -74,7 +84,7 @@ struct BridgeResponse { #[serde(skip_serializing_if = "Option::is_none")] replacement_body: Option>, #[serde(skip_serializing_if = "Option::is_none")] - receipt: Option>, + handle: Option, #[serde(skip_serializing_if = "Option::is_none")] reason_code: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -120,7 +130,7 @@ async fn evaluate(State(state): State, Json(input): Json, Json(input): Json, Json(input): Json, Json(input): Json Json(result).into_response(), Err(error) => (StatusCode::BAD_GATEWAY, Json(BridgeError { error })).into_response(), } @@ -170,6 +187,7 @@ fn build_evaluation( sandbox_name: &str, workspace: &str, selection: &BridgeSelection, + binding: &BridgeBinding, input: BridgeRequest, ) -> AgentConversationEvaluation { AgentConversationEvaluation { @@ -183,10 +201,10 @@ fn build_evaluation( }), config: Some(selection.middleware_config.clone()), target: Some(AgentConversationTarget { - harness: selection.harness.clone(), + harness: binding.harness.clone(), harness_version: input.harness_version, - hook: selection.hook.clone(), - schema_version: selection.schema_version.clone(), + hook: binding.hook.clone(), + schema_version: binding.schema_version.clone(), scheme: selection.provider_scheme.clone(), host: selection.provider_host.clone(), port: selection.provider_port, @@ -199,21 +217,47 @@ fn build_evaluation( } } -fn bridge_result(result: AgentConversationResult) -> Result { +fn bridge_result( + runner: &openshell_supervisor_middleware::ChainRunner, + sandbox_id: &str, + policy_generation: u64, + selection: &BridgeSelection, + result: AgentConversationResult, +) -> Result { match Decision::try_from(result.decision).unwrap_or(Decision::Unspecified) { - Decision::Allow => Ok(BridgeResponse { - decision: "allow", - replacement_body: result - .has_replacement_body - .then_some(result.replacement_body), - receipt: (!result.attestation.is_empty()).then_some(result.attestation), - reason_code: None, - metadata: (!result.metadata.is_empty()).then_some(result.metadata), - }), + Decision::Allow => { + if result.attestation.is_empty() { + return Err("missing_admission_attestation"); + } + let port = + u16::try_from(selection.provider_port).map_err(|_| "invalid_provider_port")?; + let handle = runner + .issue_agent_admission_handle( + openshell_supervisor_middleware::AgentAdmissionGrantInput { + sandbox_id, + middleware_name: &selection.middleware_name, + scheme: &selection.provider_scheme, + host: &selection.provider_host, + port, + policy_generation, + attestation: result.attestation, + }, + ) + .map_err(|_| "admission_store_unavailable")?; + Ok(BridgeResponse { + decision: "allow", + replacement_body: result + .has_replacement_body + .then_some(result.replacement_body), + handle: Some(handle), + reason_code: None, + metadata: (!result.metadata.is_empty()).then_some(result.metadata), + }) + } Decision::Deny => Ok(BridgeResponse { decision: "deny", replacement_body: None, - receipt: None, + handle: None, reason_code: (!result.reason_code.is_empty()).then_some(result.reason_code), metadata: None, }), @@ -231,11 +275,21 @@ fn admission_unavailable() -> Response { .into_response() } -fn is_valid_admission_request(input: &BridgeRequest, binding_limit: usize) -> bool { - is_stable_identifier(&input.harness_version) - && !input.request_body.is_empty() - && input.request_body.len() <= MAX_ADMISSION_BODY_BYTES - && input.request_body.len() <= binding_limit +fn selected_binding<'a>( + selection: &'a BridgeSelection, + input: &BridgeRequest, +) -> Option<&'a BridgeBinding> { + if !is_stable_identifier(&input.harness_version) + || input.request_body.is_empty() + || input.request_body.len() > MAX_ADMISSION_BODY_BYTES + { + return None; + } + selection.bindings.iter().find(|binding| { + binding.hook == input.hook + && binding.schema_version == input.schema_version + && input.request_body.len() <= binding.max_payload_bytes + }) } fn is_stable_identifier(value: &str) -> bool { @@ -253,6 +307,8 @@ mod tests { fn request(harness_version: &str, body_len: usize) -> BridgeRequest { BridgeRequest { harness_version: harness_version.into(), + hook: "user_input".into(), + schema_version: "example.input.v1".into(), session_id: String::new(), submission_id: String::new(), request_body: vec![b'x'; body_len], @@ -261,46 +317,59 @@ mod tests { #[test] fn admission_request_requires_a_stable_harness_version() { - assert!(!is_valid_admission_request(&request("", 1), 1)); - assert!(!is_valid_admission_request(&request("bad version", 1), 1)); - assert!(is_valid_admission_request(&request("extension-v1", 1), 1)); + let selection = selection(1); + assert!(selected_binding(&selection, &request("", 1)).is_none()); + assert!(selected_binding(&selection, &request("bad version", 1)).is_none()); + assert!(selected_binding(&selection, &request("sdk-v1", 1)).is_some()); + let mut unadvertised = request("sdk-v1", 1); + unadvertised.schema_version = "other.v1".into(); + assert!(selected_binding(&selection, &unadvertised).is_none()); } #[test] fn admission_request_enforces_the_logical_body_limit() { - assert!(!is_valid_admission_request( - &request("extension-v1", 0), - usize::MAX - )); - assert!(is_valid_admission_request( - &request("extension-v1", MAX_ADMISSION_BODY_BYTES,), - usize::MAX - )); - assert!(!is_valid_admission_request( - &request("extension-v1", MAX_ADMISSION_BODY_BYTES + 1,), - usize::MAX - )); - assert!(!is_valid_admission_request(&request("extension-v1", 2), 1)); + let max_selection = selection(MAX_ADMISSION_BODY_BYTES); + assert!(selected_binding(&max_selection, &request("sdk-v1", 0)).is_none()); + assert!( + selected_binding(&max_selection, &request("sdk-v1", MAX_ADMISSION_BODY_BYTES)) + .is_some() + ); + assert!( + selected_binding( + &max_selection, + &request("sdk-v1", MAX_ADMISSION_BODY_BYTES + 1) + ) + .is_none() + ); + assert!(selected_binding(&selection(1), &request("sdk-v1", 2)).is_none()); } - #[test] - fn evaluation_uses_advertised_binding_and_trusted_destination() { - let selection = BridgeSelection { + fn selection(limit: usize) -> BridgeSelection { + BridgeSelection { middleware_name: "operator/guard".into(), - harness: "another-agent".into(), - hook: "user_input".into(), - schema_version: "example.input.v1".into(), + bindings: vec![BridgeBinding { + harness: "another-agent".into(), + hook: "user_input".into(), + schema_version: "example.input.v1".into(), + max_payload_bytes: limit, + }], provider_scheme: "https".into(), provider_host: "api.example.com".into(), provider_port: 8443, - max_payload_bytes: 1024, middleware_config: prost_types::Struct::default(), - }; + } + } + + #[test] + fn evaluation_uses_advertised_binding_and_trusted_destination() { + let selection = selection(1024); + let binding = &selection.bindings[0]; let evaluation = build_evaluation( "sandbox-1", "friendly-sandbox", "workspace-1", &selection, + binding, request("plugin-v2", 2), ); let target = evaluation.target.expect("target"); @@ -314,28 +383,63 @@ mod tests { } #[test] - fn result_mapping_preserves_only_operation_outputs() { - let allow = bridge_result(AgentConversationResult { - decision: Decision::Allow as i32, - attestation: b"receipt".to_vec(), - replacement_body: b"redacted".to_vec(), - has_replacement_body: true, - ..Default::default() - }) + fn result_mapping_returns_an_opaque_retryable_handle() { + let runner = openshell_supervisor_middleware::ChainRunner::default(); + let selection = selection(1024); + let allow = bridge_result( + &runner, + "sandbox-1", + 7, + &selection, + AgentConversationResult { + decision: Decision::Allow as i32, + attestation: b"receipt".to_vec(), + replacement_body: b"redacted".to_vec(), + has_replacement_body: true, + ..Default::default() + }, + ) .expect("allow"); assert_eq!(allow.decision, "allow"); - assert_eq!(allow.receipt, Some(b"receipt".to_vec())); + let handle = allow.handle.expect("handle"); + assert_ne!(handle.as_bytes(), b"receipt"); + let request = openshell_supervisor_middleware::AgentAdmissionRequest { + sandbox_id: "sandbox-1", + middleware_name: "operator/guard", + scheme: "https", + host: "api.example.com", + port: 8443, + policy_generation: 7, + }; + assert_eq!( + runner + .resolve_agent_admission_handle(&handle, &request) + .unwrap(), + Some(b"receipt".to_vec()) + ); + assert_eq!( + runner + .resolve_agent_admission_handle(&handle, &request) + .unwrap(), + Some(b"receipt".to_vec()) + ); assert_eq!(allow.replacement_body, Some(b"redacted".to_vec())); - let deny = bridge_result(AgentConversationResult { - decision: Decision::Deny as i32, - reason_code: "policy_denied".into(), - ..Default::default() - }) + let deny = bridge_result( + &runner, + "sandbox-1", + 7, + &selection, + AgentConversationResult { + decision: Decision::Deny as i32, + reason_code: "policy_denied".into(), + ..Default::default() + }, + ) .expect("deny"); assert_eq!(deny.decision, "deny"); assert_eq!(deny.reason_code.as_deref(), Some("policy_denied")); - assert_eq!(deny.receipt, None); + assert_eq!(deny.handle, None); assert_eq!(deny.replacement_body, None); } } diff --git a/crates/openshell-sandbox/src/lib.rs b/crates/openshell-sandbox/src/lib.rs index 3de4fac9c7..be5b5bf62b 100644 --- a/crates/openshell-sandbox/src/lib.rs +++ b/crates/openshell-sandbox/src/lib.rs @@ -93,21 +93,17 @@ async fn select_agent_bridge( policy: &openshell_core::proto::SandboxPolicy, ) -> Result> { let bindings = runner.agent_conversation_bindings().await?; - let mut matches = policy - .network_middlewares - .iter() - .flat_map(|(config_name, config)| { - bindings - .iter() - .filter(move |binding| binding.middleware_name == config.middleware) - .map(move |binding| (config_name, config, binding)) - }); - let Some((config_name, config, advertised)) = matches.next() else { + let mut matches = policy.network_middlewares.iter().filter(|(_, config)| { + bindings + .iter() + .any(|binding| binding.middleware_name == config.middleware) + }); + let Some((config_name, config)) = matches.next() else { return Ok(None); }; if matches.next().is_some() { return Err(miette::miette!( - "managed agent admission requires exactly one configured agent-conversation binding" + "managed agent admission requires exactly one configured agent-conversation middleware" )); } if OnError::parse(&config.on_error)? != OnError::FailClosed { @@ -128,17 +124,26 @@ async fn select_agent_bridge( } let provider_host = &endpoints.include[0]; let (provider_scheme, provider_port) = exact_provider_endpoint(policy, provider_host)?; - let max_payload_bytes = usize::try_from(advertised.binding.max_payload_bytes) - .map_err(|_| miette::miette!("agent admission binding payload limit is unsupported"))?; + let advertised = bindings + .into_iter() + .filter(|binding| binding.middleware_name == config.middleware) + .map(|advertised| { + Ok(agent_bridge::BridgeBinding { + harness: advertised.binding.harness, + hook: advertised.binding.hook, + schema_version: advertised.binding.schema_version, + max_payload_bytes: usize::try_from(advertised.binding.max_payload_bytes).map_err( + |_| miette::miette!("agent admission binding payload limit is unsupported"), + )?, + }) + }) + .collect::>>()?; Ok(Some(agent_bridge::BridgeSelection { middleware_name: config.middleware.clone(), - harness: advertised.binding.harness.clone(), - hook: advertised.binding.hook.clone(), - schema_version: advertised.binding.schema_version.clone(), + bindings: advertised, provider_scheme, provider_host: provider_host.clone(), provider_port, - max_payload_bytes, middleware_config: config.config.clone().unwrap_or_default(), })) } diff --git a/crates/openshell-supervisor-middleware/Cargo.toml b/crates/openshell-supervisor-middleware/Cargo.toml index 453520c3eb..36f018ea11 100644 --- a/crates/openshell-supervisor-middleware/Cargo.toml +++ b/crates/openshell-supervisor-middleware/Cargo.toml @@ -21,6 +21,7 @@ prost-types = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } tonic = { workspace = true, features = ["channel", "server", "tls-native-roots"] } +uuid = { workspace = true } [dev-dependencies] openshell-supervisor-middleware-builtins = { path = "../openshell-supervisor-middleware-builtins" } diff --git a/crates/openshell-supervisor-middleware/src/lib.rs b/crates/openshell-supervisor-middleware/src/lib.rs index bc4be8b725..da3fc24395 100644 --- a/crates/openshell-supervisor-middleware/src/lib.rs +++ b/crates/openshell-supervisor-middleware/src/lib.rs @@ -14,10 +14,10 @@ pub use websocket::{ WebSocketSessionStartOutcome, }; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use std::future::Future; -use std::sync::Arc; -use std::time::Duration; +use std::sync::{Arc, Mutex as StdMutex}; +use std::time::{Duration, Instant}; use miette::{Result, miette}; use prost::Message; @@ -426,6 +426,10 @@ impl DescribedChainEntry { pub fn is_resolved(&self) -> bool { self.binding.is_some() } + + pub fn implementation(&self) -> &str { + &self.entry.implementation + } } /// Re-checks a middleware-transformed request body against sandbox policy. @@ -471,6 +475,8 @@ pub struct HttpRequestInput { /// still treat these dynamically hop-by-hop fields as protected. pub connection_nominated_headers: Vec, pub body: Vec, + /// Supervisor-resolved admission scoped to one middleware stage. + pub agent_attestation: Option<(String, Vec)>, } #[derive(Debug, Clone)] @@ -563,9 +569,87 @@ fn request_view_to_evaluation(request: HttpRequestView<'_>) -> HttpRequestEvalua headers: request.headers().to_vec(), body: request.body().to_vec(), middleware_name: request.middleware_name().to_string(), + agent_attestation: request.agent_attestation().to_vec(), } } +const MAX_AGENT_ADMISSION_HANDLES: usize = 1024; +const AGENT_ADMISSION_HANDLE_TTL: Duration = Duration::from_secs(300); + +#[derive(Debug, Clone)] +struct AgentAdmissionGrant { + sandbox_id: String, + middleware_name: String, + scheme: String, + host: String, + port: u16, + policy_generation: u64, + attestation: Vec, + expires_at: Instant, +} + +#[derive(Debug, Default)] +struct AgentAdmissionStore { + grants: HashMap, + insertion_order: VecDeque, +} + +impl AgentAdmissionStore { + fn issue(&mut self, grant: AgentAdmissionGrant) -> String { + self.remove_expired(Instant::now()); + while self.grants.len() >= MAX_AGENT_ADMISSION_HANDLES { + let Some(oldest) = self.insertion_order.pop_front() else { + break; + }; + self.grants.remove(&oldest); + } + let handle = uuid::Uuid::new_v4().to_string(); + self.insertion_order.push_back(handle.clone()); + self.grants.insert(handle.clone(), grant); + handle + } + + fn resolve(&mut self, handle: &str, request: &AgentAdmissionRequest<'_>) -> Option> { + let now = Instant::now(); + self.remove_expired(now); + let grant = self.grants.get(handle)?; + (grant.sandbox_id == request.sandbox_id + && grant.middleware_name == request.middleware_name + && grant.scheme.eq_ignore_ascii_case(request.scheme) + && grant.host.eq_ignore_ascii_case(request.host) + && grant.port == request.port + && grant.policy_generation == request.policy_generation) + .then(|| grant.attestation.clone()) + } + + fn remove_expired(&mut self, now: Instant) { + self.grants.retain(|_, grant| grant.expires_at > now); + self.insertion_order + .retain(|handle| self.grants.contains_key(handle)); + } +} + +/// Trusted context used to issue one opaque agent admission handle. +pub struct AgentAdmissionGrantInput<'a> { + pub sandbox_id: &'a str, + pub middleware_name: &'a str, + pub scheme: &'a str, + pub host: &'a str, + pub port: u16, + pub policy_generation: u64, + pub attestation: Vec, +} + +/// Trusted request context used to resolve an opaque agent admission handle. +pub struct AgentAdmissionRequest<'a> { + pub sandbox_id: &'a str, + pub middleware_name: &'a str, + pub scheme: &'a str, + pub host: &'a str, + pub port: u16, + pub policy_generation: u64, +} + #[derive(Clone)] pub struct ChainRunner { registry: Arc, @@ -729,6 +813,7 @@ pub struct MiddlewareRegistry { work_admission: Arc, work_admission_waiters: Arc, session_admission: Arc, + agent_admissions: Arc>, } impl std::fmt::Debug for MiddlewareRegistry { @@ -764,6 +849,7 @@ impl Default for MiddlewareRegistry { work_admission: Arc::new(Semaphore::new(MAX_CONCURRENT_MIDDLEWARE_WORK)), work_admission_waiters: Arc::new(Semaphore::new(MAX_QUEUED_MIDDLEWARE_WORK)), session_admission: Arc::new(Semaphore::new(MAX_CONCURRENT_MIDDLEWARE_SESSIONS)), + agent_admissions: Arc::new(StdMutex::new(AgentAdmissionStore::default())), } } } @@ -1252,6 +1338,7 @@ impl MiddlewareRegistry { work_admission: Arc::new(Semaphore::new(MAX_CONCURRENT_MIDDLEWARE_WORK)), work_admission_waiters: Arc::new(Semaphore::new(MAX_QUEUED_MIDDLEWARE_WORK)), session_admission: Arc::new(Semaphore::new(MAX_CONCURRENT_MIDDLEWARE_SESSIONS)), + agent_admissions: Arc::new(StdMutex::new(AgentAdmissionStore::default())), }) } @@ -1348,6 +1435,7 @@ impl ChainRunner { work_admission: Arc::new(Semaphore::new(MAX_CONCURRENT_MIDDLEWARE_WORK)), work_admission_waiters: Arc::new(Semaphore::new(MAX_QUEUED_MIDDLEWARE_WORK)), session_admission: Arc::new(Semaphore::new(MAX_CONCURRENT_MIDDLEWARE_SESSIONS)), + agent_admissions: Arc::new(StdMutex::new(AgentAdmissionStore::default())), }), } } @@ -1374,9 +1462,47 @@ impl ChainRunner { registry.work_admission = Arc::clone(&self.registry.work_admission); registry.work_admission_waiters = Arc::clone(&self.registry.work_admission_waiters); registry.session_admission = Arc::clone(&self.registry.session_admission); + registry.agent_admissions = Arc::clone(&self.registry.agent_admissions); Self::from_registry(registry) } + /// Store a middleware attestation and return the only value exposed to the + /// sandbox workload. + pub fn issue_agent_admission_handle( + &self, + input: AgentAdmissionGrantInput<'_>, + ) -> Result { + let grant = AgentAdmissionGrant { + sandbox_id: input.sandbox_id.to_string(), + middleware_name: input.middleware_name.to_string(), + scheme: input.scheme.to_string(), + host: input.host.to_string(), + port: input.port, + policy_generation: input.policy_generation, + attestation: input.attestation, + expires_at: Instant::now() + AGENT_ADMISSION_HANDLE_TTL, + }; + self.registry + .agent_admissions + .lock() + .map_err(|_| miette!("agent admission store lock poisoned")) + .map(|mut store| store.issue(grant)) + } + + /// Resolve a workload handle without consuming it so provider retries can + /// reuse the same admitted turn within the short handle lifetime. + pub fn resolve_agent_admission_handle( + &self, + handle: &str, + request: &AgentAdmissionRequest<'_>, + ) -> Result>> { + self.registry + .agent_admissions + .lock() + .map_err(|_| miette!("agent admission store lock poisoned")) + .map(|mut store| store.resolve(handle, request)) + } + /// Reserve one unit of short-lived middleware work. /// /// The bounded waiter queue provides backpressure for work expected to @@ -1800,6 +1926,7 @@ impl ChainRunner { headers, connection_nominated_headers, body, + agent_attestation, } = input; // The request envelope is moved into one stable chain state. Built-ins // borrow these values for every stage; only the gRPC adapter clones them @@ -1874,6 +2001,12 @@ impl ChainRunner { &headers, &body, &entry.entry.implementation, + ) + .with_agent_attestation( + agent_attestation + .as_ref() + .filter(|(middleware, _)| middleware == &entry.entry.implementation) + .map_or(&[], |(_, attestation)| attestation.as_slice()), ); if let Err(reason) = validate_request_view(request) { match apply_on_error(entry, reason, &mut applied) { @@ -2271,6 +2404,59 @@ mod tests { headers: Vec::new(), connection_nominated_headers: Vec::new(), body: body.as_bytes().to_vec(), + agent_attestation: None, + } + } + + #[test] + fn agent_admission_handles_are_scoped_and_retryable() { + let runner = ChainRunner::default(); + let handle = runner + .issue_agent_admission_handle(AgentAdmissionGrantInput { + sandbox_id: "sandbox-1", + middleware_name: "operator/guard", + scheme: "https", + host: "api.example.com", + port: 443, + policy_generation: 7, + attestation: b"trusted".to_vec(), + }) + .expect("issue handle"); + let request = + |sandbox_id, middleware_name, host, policy_generation| AgentAdmissionRequest { + sandbox_id, + middleware_name, + scheme: "https", + host, + port: 443, + policy_generation, + }; + + let valid = request("sandbox-1", "operator/guard", "api.example.com", 7); + assert_eq!( + runner + .resolve_agent_admission_handle(&handle, &valid) + .unwrap(), + Some(b"trusted".to_vec()) + ); + assert_eq!( + runner + .resolve_agent_admission_handle(&handle, &valid) + .unwrap(), + Some(b"trusted".to_vec()) + ); + for invalid in [ + request("sandbox-2", "operator/guard", "api.example.com", 7), + request("sandbox-1", "operator/other", "api.example.com", 7), + request("sandbox-1", "operator/guard", "other.example.com", 7), + request("sandbox-1", "operator/guard", "api.example.com", 8), + ] { + assert_eq!( + runner + .resolve_agent_admission_handle(&handle, &invalid) + .unwrap(), + None + ); } } @@ -3610,6 +3796,7 @@ mod tests { ("x-api-key".into(), "second-value".into()), ]; request.query = "page=2".into(); + request.agent_attestation = Some(("test/recorder".into(), b"trusted".to_vec())); let original_body = request.body.as_ptr().addr(); let outcome = runner @@ -3634,6 +3821,7 @@ mod tests { SupervisorMiddlewarePhase::PreCredentials as i32 ); assert_eq!(received[0].middleware_name, "test/recorder"); + assert_eq!(received[0].agent_attestation, b"trusted"); assert_eq!(received[0].config.as_ref(), Some(&evaluation_config)); let context = received[0].context.as_ref().expect("request context"); assert_eq!(context.request_id, "req"); @@ -3727,6 +3915,7 @@ mod tests { work_admission: Arc::new(Semaphore::new(MAX_CONCURRENT_MIDDLEWARE_WORK)), work_admission_waiters: Arc::new(Semaphore::new(MAX_QUEUED_MIDDLEWARE_WORK)), session_admission: Arc::new(Semaphore::new(MAX_CONCURRENT_MIDDLEWARE_SESSIONS)), + agent_admissions: Arc::new(StdMutex::new(AgentAdmissionStore::default())), } } diff --git a/crates/openshell-supervisor-middleware/src/remote.rs b/crates/openshell-supervisor-middleware/src/remote.rs index fda17e083b..f74a60526f 100644 --- a/crates/openshell-supervisor-middleware/src/remote.rs +++ b/crates/openshell-supervisor-middleware/src/remote.rs @@ -90,6 +90,7 @@ impl GrpcMiddlewareService { headers: request.headers().to_vec(), body: request.body().to_vec(), middleware_name: request.middleware_name().to_string(), + agent_attestation: request.agent_attestation().to_vec(), })) .await } diff --git a/crates/openshell-supervisor-network/src/l7/middleware.rs b/crates/openshell-supervisor-network/src/l7/middleware.rs index 6305653f6a..18deac9ecc 100644 --- a/crates/openshell-supervisor-network/src/l7/middleware.rs +++ b/crates/openshell-supervisor-network/src/l7/middleware.rs @@ -14,6 +14,8 @@ use openshell_ocsf::{ use std::path::PathBuf; use tokio::io::{AsyncRead, AsyncWrite}; +const AGENT_ADMISSION_HANDLE_HEADER: &str = "x-openshell-agent-admission-handle"; + pub enum MiddlewareApplyResult { Allowed(crate::l7::provider::L7Request), Denied { @@ -456,6 +458,7 @@ pub async fn apply_middleware_chain_for_scheme, ) -> Result { + let (req, admission_handle) = take_agent_admission_handle(req)?; if chain.is_empty() { return Ok(MiddlewareApplyResult::Allowed(req)); } @@ -525,7 +528,26 @@ pub async fn apply_middleware_chain_for_scheme Result<(crate::l7::provider::L7Request, Option)> { + let header_end = request + .raw_header + .windows(4) + .position(|window| window == b"\r\n\r\n") + .map(|position| position + 4) + .ok_or_else(|| miette!("HTTP request headers are incomplete"))?; + let header_bytes = &request.raw_header[..header_end]; + let header_text = std::str::from_utf8(header_bytes) + .map_err(|_| miette!("HTTP headers contain invalid UTF-8"))?; + let values = header_text + .split("\r\n") + .skip(1) + .filter_map(|line| line.split_once(':')) + .filter(|(name, _)| { + name.trim() + .eq_ignore_ascii_case(AGENT_ADMISSION_HANDLE_HEADER) + }) + .map(|(_, value)| value.trim().to_string()) + .collect::>(); + if values.len() > 1 || values.first().is_some_and(String::is_empty) { + return Err(miette!("invalid agent admission handle header")); + } + let mut stripped = crate::l7::rest::strip_header(header_bytes, AGENT_ADMISSION_HANDLE_HEADER)?; + stripped.extend_from_slice(&request.raw_header[header_end..]); + request.raw_header = stripped; + Ok((request, values.into_iter().next())) +} + pub(super) fn raw_query_from_request_headers(headers: &[u8]) -> Result { let header_str = std::str::from_utf8(headers).map_err(|_| miette!("HTTP headers contain invalid UTF-8"))?; @@ -893,13 +948,47 @@ fn emit_middleware_events( #[cfg(test)] mod tests { use super::{ - middleware_admission_exhausted_event, safe_middleware_headers, - send_middleware_rejection_response, websocket_coverage_events, - websocket_message_finding_events, websocket_preflight_finding_events, + AGENT_ADMISSION_HANDLE_HEADER, middleware_admission_exhausted_event, + safe_middleware_headers, send_middleware_rejection_response, take_agent_admission_handle, + websocket_coverage_events, websocket_message_finding_events, + websocket_preflight_finding_events, }; use crate::l7::relay::L7EvalContext; use tokio::io::AsyncReadExt; + #[test] + fn agent_admission_handle_is_removed_without_losing_body_overflow() { + let request = crate::l7::provider::L7Request { + action: "POST".into(), + target: "/v1/messages".into(), + query_params: std::collections::HashMap::new(), + raw_header: b"POST /v1/messages HTTP/1.1\r\nHost: api.example.com\r\nX-OpenShell-Agent-Admission-Handle: opaque\r\nContent-Length: 4\r\n\r\nbody".to_vec(), + body_length: crate::l7::provider::BodyLength::ContentLength(4), + }; + + let (request, handle) = take_agent_admission_handle(request).expect("valid headers"); + assert_eq!(handle.as_deref(), Some("opaque")); + assert!( + !String::from_utf8_lossy(&request.raw_header) + .to_ascii_lowercase() + .contains(AGENT_ADMISSION_HANDLE_HEADER) + ); + assert!(request.raw_header.ends_with(b"\r\n\r\nbody")); + } + + #[test] + fn duplicate_agent_admission_handles_are_rejected() { + let request = crate::l7::provider::L7Request { + action: "POST".into(), + target: "/v1/messages".into(), + query_params: std::collections::HashMap::new(), + raw_header: b"POST /v1/messages HTTP/1.1\r\nX-OpenShell-Agent-Admission-Handle: one\r\nx-openshell-agent-admission-handle: two\r\n\r\n".to_vec(), + body_length: crate::l7::provider::BodyLength::None, + }; + + assert!(take_agent_admission_handle(request).is_err()); + } + #[test] fn admission_exhaustion_event_contains_only_platform_context() { let ctx = L7EvalContext { diff --git a/crates/openshell-supervisor-network/src/l7/rest.rs b/crates/openshell-supervisor-network/src/l7/rest.rs index 93315a671a..d8a38f49d0 100644 --- a/crates/openshell-supervisor-network/src/l7/rest.rs +++ b/crates/openshell-supervisor-network/src/l7/rest.rs @@ -2105,7 +2105,7 @@ fn set_content_length(headers: &[u8], len: usize) -> Result> { Ok(out.into_bytes()) } -fn strip_header(headers: &[u8], strip_name: &str) -> Result> { +pub(crate) fn strip_header(headers: &[u8], strip_name: &str) -> Result> { let header_str = std::str::from_utf8(headers).map_err(|_| miette!("HTTP headers contain invalid UTF-8"))?; let mut out = String::with_capacity(header_str.len()); diff --git a/proto/supervisor_middleware.proto b/proto/supervisor_middleware.proto index b388eec856..a51a57ba69 100644 --- a/proto/supervisor_middleware.proto +++ b/proto/supervisor_middleware.proto @@ -122,6 +122,9 @@ message HttpRequestEvaluation { bytes body = 6; // Built-in middleware name or operator-owned registration name. string middleware_name = 7; + // Supervisor-resolved agent attestation for this middleware stage. The + // workload cannot set or observe these bytes. Limited to 8 KiB. + bytes agent_attestation = 8; } // HttpHeader is one request header line. From 8332c459c89124499e76da0a4095af9661aec10f Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Mon, 31 Aug 2026 15:16:17 -0400 Subject: [PATCH 07/18] feat(providers): support proxy-delivered static header credentials Signed-off-by: Johnny Greco --- .../src/provider_credentials.rs | 45 +- crates/openshell-providers/src/discovery.rs | 2 + crates/openshell-providers/src/profiles.rs | 299 ++++- crates/openshell-server/src/grpc/provider.rs | 238 +++- .../src/l7/relay.rs | 37 +- .../src/l7/token_grant_injection.rs | 189 ++- .../src/l7/websocket.rs | 1 + .../openshell-supervisor-network/src/proxy.rs | 70 +- docs/sandboxes/providers-v2.mdx | 33 +- proto/openshell.proto | 13 + sdk/go/proto/openshellv1/openshell.pb.go | 1191 +++++++++-------- 11 files changed, 1482 insertions(+), 636 deletions(-) diff --git a/crates/openshell-core/src/provider_credentials.rs b/crates/openshell-core/src/provider_credentials.rs index 2b1537a21b..4f70e02d30 100644 --- a/crates/openshell-core/src/provider_credentials.rs +++ b/crates/openshell-core/src/provider_credentials.rs @@ -46,6 +46,7 @@ struct CompiledStaticCredentialBinding { endpoints: Vec, credential_identity: String, workload_credential_handle: String, + delivery: crate::proto::ProviderCredentialDelivery, } #[derive(Debug, Clone)] @@ -124,13 +125,14 @@ impl ProviderCredentialState { &non_secret_environment_keys, )?; let stable_handles = static_credential_stable_handles(&static_credential_bindings); - let (child_env, generation_resolver, current_resolver) = + let (mut child_env, generation_resolver, current_resolver) = SecretResolver::from_provider_env_for_current_revision_with_stable_handles( env, credential_expires_at_ms, revision, &stable_handles, ); + suppress_proxy_delivered_credentials(&mut child_env, &static_credential_bindings); let snapshot = Arc::new(ProviderCredentialSnapshot { revision, child_env, @@ -532,6 +534,7 @@ impl ProviderCredentialState { revision, &stable_handles, ); + suppress_proxy_delivered_credentials(&mut child_env, &static_credential_bindings); let mut inner = self .inner .write() @@ -684,12 +687,25 @@ fn compile_static_credential_bindings( endpoints, credential_identity: binding.credential_identity, workload_credential_handle: binding.workload_credential_handle, + delivery: crate::proto::ProviderCredentialDelivery::try_from(binding.delivery) + .unwrap_or(crate::proto::ProviderCredentialDelivery::Environment), }, )) }) .collect() } +fn suppress_proxy_delivered_credentials( + child_env: &mut HashMap, + bindings: &HashMap, +) { + for (key, binding) in bindings { + if binding.delivery == crate::proto::ProviderCredentialDelivery::Proxy { + child_env.remove(key); + } + } +} + fn compile_static_credential_endpoint( endpoint: StaticCredentialEndpointBinding, ) -> Result { @@ -808,6 +824,7 @@ mod tests { }], credential_identity: "provider-a:API_KEY".to_string(), workload_credential_handle: String::new(), + delivery: 0, } } @@ -942,6 +959,32 @@ mod tests { } } + #[test] + fn proxy_delivered_credential_is_not_in_child_environment() { + let mut proxy_binding = binding("api.example.com", 443, "/v1/**"); + proxy_binding.delivery = crate::proto::ProviderCredentialDelivery::Proxy as i32; + let state = ProviderCredentialState::from_bound_environment( + 7, + HashMap::from([("API_KEY".to_string(), "secret".to_string())]), + HashMap::new(), + HashMap::new(), + HashMap::from([("API_KEY".to_string(), proxy_binding)]), + Vec::new(), + ) + .expect("valid proxy delivery binding"); + + assert!(!state.snapshot().child_env.contains_key("API_KEY")); + let resolver = state + .resolver_for_endpoint("api.example.com", 443, "/v1/messages") + .expect("endpoint resolver"); + assert_eq!( + resolver + .resolve_current_env_key_checked("API_KEY", "test") + .expect("authorized endpoint"), + Some("secret") + ); + } + #[test] fn multiple_credentials_resolve_only_at_their_own_endpoints() { let mut binding_a = binding("a.example.com", 443, "/a/**"); diff --git a/crates/openshell-providers/src/discovery.rs b/crates/openshell-providers/src/discovery.rs index f1af8fbfa3..561eb3ba45 100644 --- a/crates/openshell-providers/src/discovery.rs +++ b/crates/openshell-providers/src/discovery.rs @@ -93,6 +93,7 @@ mod tests { name: "api_key".to_string(), env_vars: vec!["CUSTOM_API_KEY".to_string(), "CUSTOM_API_TOKEN".to_string()], required: true, + delivery: openshell_core::proto::ProviderCredentialDelivery::Environment, description: String::new(), auth_style: String::new(), header_name: String::new(), @@ -105,6 +106,7 @@ mod tests { name: "secondary".to_string(), env_vars: vec!["CUSTOM_API_KEY".to_string()], required: false, + delivery: openshell_core::proto::ProviderCredentialDelivery::Environment, description: String::new(), auth_style: String::new(), header_name: String::new(), diff --git a/crates/openshell-providers/src/profiles.rs b/crates/openshell-providers/src/profiles.rs index 6e87826c4d..9a8fe55838 100644 --- a/crates/openshell-providers/src/profiles.rs +++ b/crates/openshell-providers/src/profiles.rs @@ -7,7 +7,7 @@ use openshell_core::proto::{ GraphqlOperation, L7Allow, L7DenyRule, L7QueryMatcher, L7Rule, McpOptions, NetworkBinary, - NetworkEndpoint, NetworkPolicyRule, ProviderCredentialRefresh, + NetworkEndpoint, NetworkPolicyRule, ProviderCredentialDelivery, ProviderCredentialRefresh, ProviderCredentialRefreshMaterial, ProviderCredentialRefreshOutput, ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrantSubjectToken, ProviderCredentialTokenGrantType, ProviderProfile, ProviderProfileCategory, @@ -98,6 +98,13 @@ pub struct CredentialProfile { pub env_vars: Vec, #[serde(default)] pub required: bool, + #[serde( + default = "default_credential_delivery", + deserialize_with = "deserialize_credential_delivery", + serialize_with = "serialize_credential_delivery", + skip_serializing_if = "is_environment_delivery" + )] + pub delivery: ProviderCredentialDelivery, #[serde(default)] pub auth_style: String, #[serde(default)] @@ -453,6 +460,10 @@ impl ProviderTypeProfile { description: credential.description.clone(), env_vars: credential.env_vars.clone(), required: credential.required, + delivery: effective_credential_delivery( + ProviderCredentialDelivery::try_from(credential.delivery) + .unwrap_or(ProviderCredentialDelivery::Unspecified), + ), auth_style: credential.auth_style.clone(), header_name: credential.header_name.clone(), query_param: credential.query_param.clone(), @@ -596,6 +607,7 @@ impl ProviderTypeProfile { description: credential.description.clone(), env_vars: credential.env_vars.clone(), required: credential.required, + delivery: credential.delivery as i32, auth_style: credential.auth_style.clone(), header_name: credential.header_name.clone(), query_param: credential.query_param.clone(), @@ -863,6 +875,24 @@ fn default_token_grant_type() -> ProviderCredentialTokenGrantType { ProviderCredentialTokenGrantType::ClientCredentials } +fn default_credential_delivery() -> ProviderCredentialDelivery { + ProviderCredentialDelivery::Environment +} + +fn effective_credential_delivery( + delivery: ProviderCredentialDelivery, +) -> ProviderCredentialDelivery { + match delivery { + ProviderCredentialDelivery::Unspecified => ProviderCredentialDelivery::Environment, + other => other, + } +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn is_environment_delivery(value: &ProviderCredentialDelivery) -> bool { + effective_credential_delivery(*value) == ProviderCredentialDelivery::Environment +} + fn effective_token_grant_type( grant_type: ProviderCredentialTokenGrantType, ) -> ProviderCredentialTokenGrantType { @@ -932,6 +962,29 @@ where .ok_or_else(|| de::Error::custom(format!("unsupported provider token grant type: {raw}"))) } +fn deserialize_credential_delivery<'de, D>( + deserializer: D, +) -> Result +where + D: Deserializer<'de>, +{ + let raw = String::deserialize(deserializer)?; + provider_credential_delivery_from_yaml(&raw).ok_or_else(|| { + de::Error::custom(format!("unsupported provider credential delivery: {raw}")) + }) +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn serialize_credential_delivery( + delivery: &ProviderCredentialDelivery, + serializer: S, +) -> Result +where + S: Serializer, +{ + serializer.serialize_str(provider_credential_delivery_to_yaml(*delivery)) +} + #[allow(clippy::trivially_copy_pass_by_ref)] fn serialize_token_grant_type( grant_type: &ProviderCredentialTokenGrantType, @@ -988,6 +1041,25 @@ pub fn provider_refresh_strategy_from_yaml(raw: &str) -> Option Option { + match raw.trim().to_ascii_lowercase().replace('-', "_").as_str() { + "" | "environment" => Some(ProviderCredentialDelivery::Environment), + "proxy" => Some(ProviderCredentialDelivery::Proxy), + _ => None, + } +} + +#[must_use] +pub fn provider_credential_delivery_to_yaml(delivery: ProviderCredentialDelivery) -> &'static str { + match delivery { + ProviderCredentialDelivery::Proxy => "proxy", + ProviderCredentialDelivery::Environment | ProviderCredentialDelivery::Unspecified => { + "environment" + } + } +} + #[must_use] pub fn provider_refresh_strategy_to_yaml( strategy: ProviderCredentialRefreshStrategy, @@ -1693,6 +1765,7 @@ pub fn validate_profile_set( )); let mut env_vars = HashSet::new(); + let mut proxy_delivery_credentials = 0; for credential in &profile.credentials { for env_var in &credential.env_vars { if env_var.trim().is_empty() { @@ -1724,6 +1797,9 @@ pub fn validate_profile_set( let auth_style = credential.auth_style.trim().to_ascii_lowercase(); match auth_style.as_str() { "" | "basic" => {} + "bearer" + if effective_credential_delivery(credential.delivery) + == ProviderCredentialDelivery::Proxy => {} "bearer" | "header" => { if credential.header_name.trim().is_empty() { diagnostics.push(ProfileValidationDiagnostic::error( @@ -1777,6 +1853,71 @@ pub fn validate_profile_set( )), } + if effective_credential_delivery(credential.delivery) + == ProviderCredentialDelivery::Proxy + { + proxy_delivery_credentials += 1; + if credential.env_vars.is_empty() { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.env_vars", + "proxy-delivered credentials must declare at least one env var", + )); + } + if profile.endpoints.is_empty() { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.delivery", + "proxy-delivered credentials require a profile endpoint", + )); + } + if credential.token_grant.is_some() { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.delivery", + "proxy delivery is only valid for static credentials", + )); + } + if !matches!(auth_style.as_str(), "bearer" | "header") { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.auth_style", + "proxy-delivered credentials support auth_style bearer or header", + )); + } else if let Err(message) = validate_proxy_delivery_header_name(credential) { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.header_name", + message, + )); + } + + for (index, endpoint) in profile.endpoints.iter().enumerate() { + let protocol = endpoint.protocol.trim().to_ascii_lowercase(); + if protocol != "rest" { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + format!("endpoints[{index}].protocol"), + "proxy-delivered credentials require protocol: rest", + )); + } + if endpoint.tls.trim().eq_ignore_ascii_case("skip") { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + format!("endpoints[{index}].tls"), + "proxy-delivered credentials do not support tls: skip", + )); + } + } + } + if let Some(refresh) = credential.refresh.as_ref() { if refresh.strategy == ProviderCredentialRefreshStrategy::Unspecified { diagnostics.push(ProfileValidationDiagnostic::error( @@ -1997,6 +2138,28 @@ pub fn validate_profile_set( } } + if proxy_delivery_credentials > 1 { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.delivery", + "provider profiles support only one proxy-delivered credential", + )); + } + if proxy_delivery_credentials > 0 + && profile + .credentials + .iter() + .any(|credential| credential.token_grant.is_some()) + { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.delivery", + "provider profiles cannot combine proxy-delivered credentials with token grants", + )); + } + for (index, endpoint) in profile.endpoints.iter().enumerate() { if !endpoint_is_valid(endpoint) { diagnostics.push(ProfileValidationDiagnostic::error( @@ -2821,13 +2984,24 @@ fn validate_token_grant_auth_style(credential: &CredentialProfile) -> Result<(), } fn validate_token_grant_header_name(credential: &CredentialProfile) -> Result<(), String> { + validate_injected_header_name(credential, "token_grant") +} + +fn validate_proxy_delivery_header_name(credential: &CredentialProfile) -> Result<(), String> { + validate_injected_header_name(credential, "proxy delivery") +} + +fn validate_injected_header_name( + credential: &CredentialProfile, + context: &str, +) -> Result<(), String> { let header_name = match credential.auth_style.trim().to_ascii_lowercase().as_str() { "" | "bearer" if credential.header_name.trim().is_empty() => "Authorization", "" | "bearer" | "header" => credential.header_name.trim(), _ => return Ok(()), }; if header_name.is_empty() { - return Ok(()); + return Err(format!("{context} auth_style header requires header_name")); } let valid = header_name.bytes().all(|byte| { byte.is_ascii_alphanumeric() @@ -2850,13 +3024,14 @@ fn validate_token_grant_header_name(credential: &CredentialProfile) -> Result<() ) }); if !valid { - return Err("token_grant header_name is not a valid HTTP header name".to_string()); + return Err(format!( + "{context} header_name is not a valid HTTP header name" + )); } match header_name.to_ascii_lowercase().as_str() { - "host" | "content-length" | "transfer-encoding" | "connection" => Err( - "token_grant header_name may not override HTTP framing or connection headers" - .to_string(), - ), + "host" | "content-length" | "transfer-encoding" | "connection" => Err(format!( + "{context} header_name may not override HTTP framing or connection headers" + )), _ => Ok(()), } } @@ -2911,7 +3086,9 @@ pub fn builtin_profiles() -> &'static [ProviderTypeProfile] { mod tests { use std::collections::HashMap; - use openshell_core::proto::{ProviderCredentialTokenGrantType, ProviderProfileCategory}; + use openshell_core::proto::{ + ProviderCredentialDelivery, ProviderCredentialTokenGrantType, ProviderProfileCategory, + }; use super::{ DiscoveryProfile, L7AllowProfile, L7QueryMatcherProfile, ProfileError, ProviderTypeProfile, @@ -3434,6 +3611,112 @@ credentials: ); } + #[test] + fn proxy_credential_delivery_round_trips_and_environment_remains_default() { + let profile = parse_profile_yaml( + r" +id: proxy-auth +display_name: Proxy Auth +credentials: + - name: environment_key + env_vars: [ENVIRONMENT_API_KEY] + auth_style: bearer + header_name: authorization + - name: proxy_header + env_vars: [PROXY_HEADER_KEY, PROXY_HEADER_KEY_FALLBACK] + delivery: proxy + auth_style: header + header_name: x-api-key +endpoints: + - host: api.example.com + port: 443 + protocol: rest + access: full +", + ) + .expect("profile should parse"); + + let diagnostics = validate_profile_set(&[("proxy-auth.yaml".to_string(), profile.clone())]); + assert!( + diagnostics.is_empty(), + "unexpected diagnostics: {diagnostics:?}" + ); + assert_eq!( + profile.credentials[1].delivery, + ProviderCredentialDelivery::Proxy + ); + assert_eq!(profile.credentials[1].env_vars.len(), 2); + assert_eq!( + profile.credentials[0].delivery, + ProviderCredentialDelivery::Environment + ); + + let exported = profile_to_yaml(&ProviderTypeProfile::from_proto(&profile.to_proto())) + .expect("serialize YAML"); + assert!(exported.contains("delivery: proxy")); + assert_eq!(exported.matches("delivery: proxy").count(), 1); + } + + #[test] + fn proxy_delivery_requires_an_env_var_and_one_credential_per_profile() { + let profile = parse_profile_yaml( + r" +id: invalid-proxy-auth +display_name: Invalid Proxy Auth +credentials: + - name: missing_env + delivery: proxy + auth_style: bearer + - name: second_proxy + env_vars: [SECOND_KEY] + delivery: proxy + auth_style: header + header_name: x-api-key +endpoints: + - host: api.example.com + port: 443 + protocol: rest + access: full +", + ) + .expect("profile should parse"); + + let diagnostics = validate_profile_set(&[("invalid.yaml".to_string(), profile)]); + assert!(diagnostics.iter().any(|diagnostic| diagnostic.message + == "proxy-delivered credentials must declare at least one env var")); + assert!(diagnostics.iter().any(|diagnostic| diagnostic.message + == "provider profiles support only one proxy-delivered credential")); + } + + #[test] + fn proxy_delivery_cannot_share_a_profile_with_token_grants() { + let profile = parse_profile_yaml( + r" +id: mixed-runtime-auth +display_name: Mixed Runtime Auth +credentials: + - name: api_key + env_vars: [API_KEY] + delivery: proxy + auth_style: bearer + - name: access_token + auth_style: bearer + token_grant: + token_endpoint: https://login.example.com/oauth2/token +endpoints: + - host: api.example.com + port: 443 + protocol: rest + access: full +", + ) + .expect("profile should parse"); + + let diagnostics = validate_profile_set(&[("mixed.yaml".to_string(), profile)]); + assert!(diagnostics.iter().any(|diagnostic| diagnostic.message + == "provider profiles cannot combine proxy-delivered credentials with token grants")); + } + #[test] fn token_grant_audience_overrides_round_trip_through_proto() { let profile = parse_profile_yaml( diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index c8c8149a82..d14760a47e 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -16,7 +16,7 @@ use crate::provider_profile_sources::{ }; use openshell_core::metadata::ObjectWorkspace; use openshell_core::proto::{ - CredentialHandle, Provider, ProviderCredentialRefreshStrategy, + CredentialHandle, Provider, ProviderCredentialDelivery, ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrantAudienceOverride, ProviderCredentialTokenGrantType, ProviderProfile, ProviderProfileCredential, Sandbox, StaticCredentialBinding, StaticCredentialEndpointBinding, StoredProviderCredentialRefreshState, @@ -64,6 +64,7 @@ fn redact_provider_credentials(mut provider: Provider) -> Provider { pub(super) struct ProviderEnvironment { pub environment: HashMap, pub credential_expires_at_ms: HashMap, + /// Endpoint metadata for token grants and proxy-delivered static credentials. pub dynamic_credentials: HashMap, pub static_credential_bindings: HashMap, pub static_credential_keys: HashSet, @@ -1199,6 +1200,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin key, endpoints, refresh_epochs.get(key).map(String::as_str), + profile_credential_delivery_for_key(profile_proto.as_ref(), key), ), ); } @@ -1258,6 +1260,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin &key, endpoints, refresh_epochs.get(&key).map(String::as_str), + profile_credential_delivery_for_key(profile_proto.as_ref(), &key), ), ); } @@ -1283,7 +1286,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin Ok(ProviderEnvironment { environment: env, credential_expires_at_ms: expires, - dynamic_credentials: resolve_dynamic_credentials_from_records(catalog, records), + dynamic_credentials: resolve_runtime_credentials_from_records(catalog, records), static_credential_bindings, static_credential_keys, }) @@ -1328,6 +1331,7 @@ fn static_credential_binding( key: &str, endpoints: &[StaticCredentialEndpointBinding], authorization_epoch: Option<&str>, + delivery: ProviderCredentialDelivery, ) -> StaticCredentialBinding { let workload_credential_handle = sandbox_id .zip(authorization_epoch) @@ -1344,9 +1348,26 @@ fn static_credential_binding( endpoints: endpoints.to_vec(), credential_identity, workload_credential_handle, + delivery: delivery as i32, } } +fn profile_credential_delivery_for_key( + profile: Option<&ProviderProfile>, + key: &str, +) -> ProviderCredentialDelivery { + profile + .and_then(|profile| { + profile + .credentials + .iter() + .find(|credential| credential.env_vars.iter().any(|env_var| env_var == key)) + }) + .and_then(|credential| ProviderCredentialDelivery::try_from(credential.delivery).ok()) + .filter(|delivery| *delivery == ProviderCredentialDelivery::Proxy) + .unwrap_or(ProviderCredentialDelivery::Environment) +} + fn derive_workload_credential_handle( sandbox_id: &str, provider_id: &str, @@ -1391,9 +1412,9 @@ fn hash_handle_component(hasher: &mut Sha256, value: &[u8]) { hasher.update(value); } -/// Resolve dynamic credentials (token grants) from the same records used for -/// the provider-environment revision and static credential bindings. -fn resolve_dynamic_credentials_from_records( +/// Resolve runtime-injected credentials from the same records used for the +/// provider-environment revision and static credential bindings. +fn resolve_runtime_credentials_from_records( catalog: &EffectiveProviderProfileCatalog, records: &[ProviderEnvironmentRecord], ) -> HashMap { @@ -1407,7 +1428,7 @@ fn resolve_dynamic_credentials_from_records( else { continue; }; - insert_dynamic_credentials_for_profile( + insert_runtime_credentials_for_profile( &mut dynamic_creds, &profile.to_proto(), &record.name, @@ -1416,13 +1437,16 @@ fn resolve_dynamic_credentials_from_records( dynamic_creds } -fn insert_dynamic_credentials_for_profile( +fn insert_runtime_credentials_for_profile( dynamic_creds: &mut HashMap, profile: &ProviderProfile, provider_name: &str, ) { for credential in &profile.credentials { - if credential.token_grant.is_none() { + if credential.token_grant.is_none() + && ProviderCredentialDelivery::try_from(credential.delivery).unwrap_or_default() + != ProviderCredentialDelivery::Proxy + { continue; } for endpoint in &profile.endpoints { @@ -1789,7 +1813,7 @@ async fn validate_provider_environment_keys_unique_at( ) -> Result<(), Status> { let mut seen_credentials = HashMap::::new(); let mut seen_plugin_config = HashMap::::new(); - let mut dynamic_bindings = Vec::new(); + let mut runtime_bindings = Vec::new(); for name in provider_names { let provider = match candidate_provider { Some(candidate) if candidate.object_name() == name.as_str() => candidate.clone(), @@ -1809,11 +1833,11 @@ async fn validate_provider_environment_keys_unique_at( active_provider_environment_keys(store, catalog, &provider, now_ms).await?, provider_plugin_environment_keys(&provider), )?; - dynamic_bindings.extend(dynamic_token_grant_bindings_for_provider_with_catalog( + runtime_bindings.extend(runtime_credential_bindings_for_provider_with_catalog( catalog, &provider, )); } - validate_dynamic_token_grant_bindings_unambiguous(&dynamic_bindings)?; + validate_runtime_credential_bindings_unambiguous(&runtime_bindings)?; Ok(()) } @@ -1825,7 +1849,7 @@ async fn validate_provider_environment_records_unique_at( ) -> Result<(), Status> { let mut seen_credentials = HashMap::::new(); let mut seen_plugin_config = HashMap::::new(); - let mut dynamic_bindings = Vec::new(); + let mut runtime_bindings = Vec::new(); for record in records { let provider = &record.provider; validate_provider_environment_key_ownership( @@ -1842,11 +1866,11 @@ async fn validate_provider_environment_records_unique_at( .await?, provider_plugin_environment_keys(provider), )?; - dynamic_bindings.extend(dynamic_token_grant_bindings_for_provider_with_catalog( + runtime_bindings.extend(runtime_credential_bindings_for_provider_with_catalog( catalog, provider, )); } - validate_dynamic_token_grant_bindings_unambiguous(&dynamic_bindings)?; + validate_runtime_credential_bindings_unambiguous(&runtime_bindings)?; Ok(()) } @@ -1912,19 +1936,20 @@ fn provider_credential_config_key_collision( } #[derive(Debug, Clone, PartialEq, Eq)] -struct DynamicTokenGrantBinding { +struct RuntimeCredentialBinding { provider_name: String, credential_name: String, host: String, port: u32, path: String, score: u32, + proxy_delivery: bool, } -fn dynamic_token_grant_bindings_for_provider_with_catalog( +fn runtime_credential_bindings_for_provider_with_catalog( catalog: &EffectiveProviderProfileCatalog, provider: &Provider, -) -> Vec { +) -> Vec { let provider_name = provider.object_name().to_string(); let profile_id = normalize_provider_type(&provider.r#type).unwrap_or(provider.r#type.as_str()); let Some(profile) = @@ -1932,16 +1957,19 @@ fn dynamic_token_grant_bindings_for_provider_with_catalog( else { return Vec::new(); }; - dynamic_token_grant_bindings_for_profile(&provider_name, &profile.to_proto()) + runtime_credential_bindings_for_profile(&provider_name, &profile.to_proto()) } -fn dynamic_token_grant_bindings_for_profile( +fn runtime_credential_bindings_for_profile( provider_name: &str, profile: &ProviderProfile, -) -> Vec { +) -> Vec { let mut bindings = Vec::new(); for credential in &profile.credentials { - if credential.token_grant.is_none() { + if credential.token_grant.is_none() + && ProviderCredentialDelivery::try_from(credential.delivery).unwrap_or_default() + != ProviderCredentialDelivery::Proxy + { continue; } for endpoint in &profile.endpoints { @@ -1961,20 +1989,22 @@ fn dynamic_token_grant_bindings_for_profile( } fn push_dynamic_token_grant_bindings_for_endpoint( - bindings: &mut Vec, + bindings: &mut Vec, provider_name: &str, credential: &ProviderProfileCredential, endpoint_host: &str, endpoint_port: u32, endpoint_path: &str, ) { - push_dynamic_token_grant_binding( + push_runtime_credential_binding( bindings, provider_name, &credential.name, endpoint_host, endpoint_port, endpoint_path, + ProviderCredentialDelivery::try_from(credential.delivery).unwrap_or_default() + == ProviderCredentialDelivery::Proxy, ); let Some(token_grant) = credential.token_grant.as_ref() else { @@ -2000,40 +2030,43 @@ fn push_dynamic_token_grant_bindings_for_endpoint( } else { override_config.path.as_str() }; - push_dynamic_token_grant_binding( + push_runtime_credential_binding( bindings, provider_name, &credential.name, override_host, override_port, override_path, + false, ); } } -fn push_dynamic_token_grant_binding( - bindings: &mut Vec, +fn push_runtime_credential_binding( + bindings: &mut Vec, provider_name: &str, credential_name: &str, host: &str, port: u32, path: &str, + proxy_delivery: bool, ) { - let candidate = DynamicTokenGrantBinding { + let candidate = RuntimeCredentialBinding { provider_name: provider_name.to_string(), credential_name: credential_name.to_string(), host: host.to_ascii_lowercase(), port, path: path.to_string(), score: dynamic_token_grant_match_score(host, path), + proxy_delivery, }; if !bindings.iter().any(|binding| binding == &candidate) { bindings.push(candidate); } } -fn validate_dynamic_token_grant_bindings_unambiguous( - bindings: &[DynamicTokenGrantBinding], +fn validate_runtime_credential_bindings_unambiguous( + bindings: &[RuntimeCredentialBinding], ) -> Result<(), Status> { for (index, first) in bindings.iter().enumerate() { for second in bindings.iter().skip(index + 1) { @@ -2042,13 +2075,14 @@ fn validate_dynamic_token_grant_bindings_unambiguous( { continue; } - if first.port == second.port - && first.score == second.score - && host_patterns_can_overlap(&first.host, &second.host) - && path_patterns_can_overlap(&first.path, &second.path) - { + if runtime_credential_bindings_are_ambiguous(first, second) { + let guidance = if first.proxy_delivery || second.proxy_delivery { + "attach only one matching provider" + } else { + "make one host/path selector more specific or attach only one matching provider" + }; return Err(Status::failed_precondition(format!( - "dynamic token grants for '{}:{}' and '{}:{}' are ambiguous for {}:{} path selectors '{}' and '{}'; make one host/path selector more specific or attach only one matching provider", + "runtime-injected credentials for '{}:{}' and '{}:{}' are ambiguous for {}:{} path selectors '{}' and '{}'; {guidance}", first.provider_name, first.credential_name, second.provider_name, @@ -2064,6 +2098,16 @@ fn validate_dynamic_token_grant_bindings_unambiguous( Ok(()) } +fn runtime_credential_bindings_are_ambiguous( + first: &RuntimeCredentialBinding, + second: &RuntimeCredentialBinding, +) -> bool { + first.port == second.port + && (first.proxy_delivery || second.proxy_delivery || first.score == second.score) + && host_patterns_can_overlap(&first.host, &second.host) + && path_patterns_can_overlap(&first.path, &second.path) +} + async fn active_provider_environment_keys( store: &Store, catalog: &EffectiveProviderProfileCatalog, @@ -3266,7 +3310,7 @@ async fn profile_attached_sandbox_diagnostics( let scope_mismatch = (is_platform_scope && !provider.profile_workspace.is_empty()) || (!is_platform_scope && provider.profile_workspace.is_empty()); if scope_mismatch { - bindings.extend(dynamic_token_grant_bindings_for_provider_with_catalog( + bindings.extend(runtime_credential_bindings_for_provider_with_catalog( catalog, &provider, )); if validate_policy_composition @@ -3319,7 +3363,7 @@ async fn profile_attached_sandbox_diagnostics( severity: "error".to_string(), }); } - bindings.extend(dynamic_token_grant_bindings_for_profile( + bindings.extend(runtime_credential_bindings_for_profile( provider.object_name(), &profile.to_proto(), )); @@ -3335,7 +3379,7 @@ async fn profile_attached_sandbox_diagnostics( imported_profiles_used.push(used); } } else { - bindings.extend(dynamic_token_grant_bindings_for_provider_with_catalog( + bindings.extend(runtime_credential_bindings_for_provider_with_catalog( catalog, &provider, )); if validate_policy_composition @@ -3357,14 +3401,14 @@ async fn profile_attached_sandbox_diagnostics( if imported_profiles_used.is_empty() { continue; } - if let Err(err) = validate_dynamic_token_grant_bindings_unambiguous(&bindings) { + if let Err(err) = validate_runtime_credential_bindings_unambiguous(&bindings) { for (source, profile_id) in &imported_profiles_used { diagnostics.push(ProfileValidationDiagnostic { source: source.clone(), profile_id: profile_id.clone(), - field: "credentials.token_grant.audience_overrides".to_string(), + field: "credentials".to_string(), message: format!( - "{operation} would create ambiguous dynamic token grants on sandbox '{sandbox_name}': {}", + "{operation} would create ambiguous runtime-injected credentials on sandbox '{sandbox_name}': {}", err.message() ), severity: "error".to_string(), @@ -4864,6 +4908,7 @@ mod tests { ) .collect(), }), + delivery: 0, }; let profile = ProviderProfile { id: "keycloak-sso".to_string(), @@ -4889,7 +4934,7 @@ mod tests { }; let mut dynamic_creds = HashMap::new(); - insert_dynamic_credentials_for_profile(&mut dynamic_creds, &profile, "keycloak"); + insert_runtime_credentials_for_profile(&mut dynamic_creds, &profile, "keycloak"); assert_eq!(dynamic_creds.len(), 4); for (host, audience) in service_audiences { @@ -4981,10 +5026,31 @@ mod tests { .expect_err("equal-specificity dynamic grants should be ambiguous"); assert_eq!(err.code(), Code::FailedPrecondition); - assert!(err.message().contains("dynamic token grants")); + assert!(err.message().contains("runtime-injected credentials")); assert!(err.message().contains("ambiguous")); } + #[test] + fn proxy_delivery_rejects_overlapping_bindings_at_different_specificity() { + let binding = |provider: &str, path: &str| RuntimeCredentialBinding { + provider_name: provider.to_string(), + credential_name: "api_key".to_string(), + host: "api.example.com".to_string(), + port: 443, + path: path.to_string(), + score: dynamic_token_grant_match_score("api.example.com", path), + proxy_delivery: true, + }; + let error = validate_runtime_credential_bindings_unambiguous(&[ + binding("provider-a", "/v1/**"), + binding("provider-b", "/v1/chat/**"), + ]) + .expect_err("overlapping proxy delivery must fail before request handling"); + + assert!(error.message().contains("runtime-injected credentials")); + assert!(error.message().contains("ambiguous")); + } + #[tokio::test] async fn dynamic_token_grants_allow_more_specific_path_overlap() { let state = test_server_state().await; @@ -5076,7 +5142,7 @@ mod tests { assert!(response.diagnostics.iter().any(|diagnostic| { diagnostic .message - .contains("import would create ambiguous dynamic token grants") + .contains("import would create ambiguous runtime-injected credentials") })); } @@ -5414,7 +5480,7 @@ mod tests { assert!(response.diagnostics.iter().any(|diagnostic| { diagnostic .message - .contains("update would create ambiguous dynamic token grants") + .contains("update would create ambiguous runtime-injected credentials") })); } @@ -5565,6 +5631,7 @@ mod tests { ], }), token_grant: None, + delivery: 0, } } @@ -5604,6 +5671,7 @@ mod tests { refresh: None, path_template: String::new(), token_grant: None, + delivery: 0, } } @@ -5631,9 +5699,88 @@ mod tests { cache_ttl_seconds: 300, audience_overrides: Vec::new(), }), + delivery: 0, } } + #[tokio::test] + async fn resolved_proxy_delivery_keeps_credential_out_of_workload_environment() { + let state = test_server_state().await; + let mut credential = static_credential("api_key", "API_KEY", true); + credential.delivery = ProviderCredentialDelivery::Proxy as i32; + let mut profile = custom_profile("proxy-auth-provider"); + profile.credentials = vec![credential]; + profile.endpoints = vec![NetworkEndpoint { + host: "api.example.com".to_string(), + port: 443, + path: "/v1/**".to_string(), + protocol: "rest".to_string(), + access: "full".to_string(), + ..Default::default() + }]; + handle_import_provider_profiles( + &state, + authed_request(ImportProviderProfilesRequest { + profiles: vec![ProviderProfileImportItem { + profile: Some(profile), + source: "proxy-auth-provider.yaml".to_string(), + }], + workspace: "default".to_string(), + }), + ) + .await + .expect("import profile"); + create_provider_record( + state.store.as_ref(), + "default", + provider_with_credential_value( + "proxy-auth", + "proxy-auth-provider", + "API_KEY", + "secret", + ), + ) + .await + .expect("create provider"); + + let names = vec!["proxy-auth".to_string()]; + let catalog = ProviderProfileSources::with_default_sources() + .snapshot_catalog(state.store.as_ref(), "default") + .await + .expect("catalog"); + let records = load_provider_environment_records(state.store.as_ref(), "default", &names) + .await + .expect("records"); + let environment = resolve_provider_environment_from_records_with_policy_bindings( + state.store.as_ref(), + &catalog, + &records, + &HashMap::new(), + ) + .await + .expect("provider environment"); + let credentials = + openshell_core::provider_credentials::ProviderCredentialState::from_bound_environment( + 1, + environment.environment, + environment.credential_expires_at_ms, + environment.dynamic_credentials, + environment.static_credential_bindings, + Vec::new(), + ) + .expect("credential state"); + + assert!(!credentials.snapshot().child_env.contains_key("API_KEY")); + assert_eq!( + credentials + .resolver_for_endpoint("api.example.com", 443, "/v1/messages") + .expect("endpoint resolver") + .resolve_current_env_key_checked("API_KEY", "test") + .expect("authorized endpoint"), + Some("secret") + ); + } + #[tokio::test] async fn list_provider_profiles_returns_built_in_profile_categories() { let state = test_server_state().await; @@ -8498,6 +8645,7 @@ mod tests { ], }), token_grant: None, + delivery: 0, }], endpoints: vec![], binaries: vec![], diff --git a/crates/openshell-supervisor-network/src/l7/relay.rs b/crates/openshell-supervisor-network/src/l7/relay.rs index 82284730a8..da017e6513 100644 --- a/crates/openshell-supervisor-network/src/l7/relay.rs +++ b/crates/openshell-supervisor-network/src/l7/relay.rs @@ -66,7 +66,8 @@ pub struct L7EvalContext { pub(crate) provider_credential_revision: Option, /// Anonymous activity counter channel. pub(crate) activity_tx: Option, - /// Dynamic credentials (token grants) keyed by endpoint-bound provider metadata. + /// Runtime-injected credentials (token grants and proxy-delivered static + /// credentials) keyed by endpoint-bound provider metadata. pub(crate) dynamic_credentials: Option< Arc< std::sync::RwLock< @@ -1537,6 +1538,21 @@ where }; let scoped_ctx = scoped_context_for_request(ctx, &req_with_auth); let ctx = scoped_ctx.as_ref().unwrap_or(ctx); + let req_with_auth = + match crate::l7::token_grant_injection::inject_static_if_needed(req_with_auth, ctx) + { + Ok(req) => req, + Err(error) => { + warn!( + host = %ctx.host, + port = ctx.port, + error = %error, + "Static provider credential injection failed in L7 relay" + ); + write_bad_gateway_response(client).await?; + return Ok(()); + } + }; // Forward request to upstream and relay response let outcome_result = relay_http_request_with_credential_rejection( @@ -2785,6 +2801,20 @@ where }; let scoped_ctx = scoped_context_for_request(ctx, &req_with_auth); let ctx = scoped_ctx.as_ref().unwrap_or(ctx); + let req_with_auth = + match crate::l7::token_grant_injection::inject_static_if_needed(req_with_auth, ctx) { + Ok(req) => req, + Err(error) => { + warn!( + host = %ctx.host, + port = ctx.port, + error = %error, + "Static provider credential injection failed in passthrough relay" + ); + write_bad_gateway_response(client).await?; + return Ok(()); + } + }; let resolver = ctx.secret_resolver.as_deref(); // Forward request with credential rewriting and relay the response. @@ -2865,6 +2895,7 @@ mod tests { }], credential_identity: identity.to_string(), workload_credential_handle: String::new(), + delivery: 0, } } @@ -4666,6 +4697,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -4770,6 +4802,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -5420,6 +5453,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -7793,6 +7827,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), diff --git a/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs b/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs index fc440c6547..5665937fc1 100644 --- a/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs +++ b/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs @@ -8,7 +8,9 @@ use std::pin::Pin; use std::sync::Arc; use miette::{Result, miette}; -use openshell_core::proto::{ProviderCredentialTokenGrant, ProviderProfileCredential}; +use openshell_core::proto::{ + ProviderCredentialDelivery, ProviderCredentialTokenGrant, ProviderProfileCredential, +}; use openshell_ocsf::{ ActionId, ActivityId, DispositionId, Endpoint, HttpActivityBuilder, HttpRequest, SeverityId, StatusId, Url as OcsfUrl, ctx::ctx as ocsf_ctx, ocsf_emit, @@ -162,6 +164,55 @@ pub async fn inject_if_needed(req: L7Request, ctx: &L7EvalContext) -> Result Result { + let request_path = req.target.split('?').next().unwrap_or(req.target.as_str()); + let credential = ctx.dynamic_credentials.as_ref().and_then(|credentials| { + credentials.read().map_or(None, |credentials| { + credentials + .iter() + .filter_map(|(key, credential)| { + let score = + dynamic_credential_key_match_score(key, &ctx.host, ctx.port, request_path)?; + (ProviderCredentialDelivery::try_from(credential.delivery).unwrap_or_default() + == ProviderCredentialDelivery::Proxy) + .then(|| (score, key.clone(), credential.clone())) + }) + .max_by_key(|(score, key, _)| (*score, key.clone())) + .map(|(_, key, credential)| (key, credential)) + }) + }); + + let Some((provider_key, credential)) = credential else { + return Ok(req); + }; + let resolver = ctx + .secret_resolver + .as_deref() + .ok_or_else(|| miette!("proxy-delivered credential unavailable for {provider_key}"))?; + let value = credential + .env_vars + .iter() + .find_map(|key| { + resolver + .resolve_current_env_key_checked(key, "proxy-delivered credential") + .transpose() + }) + .transpose()? + .ok_or_else(|| miette!("proxy-delivered credential unavailable for {provider_key}"))?; + let (header_name, header_value) = + injected_credential_header(&credential, value, "proxy delivery")?; + let raw_header = inject_header(&req.raw_header, &header_name, &header_value)?; + + Ok(L7Request { + action: req.action, + target: req.target, + query_params: req.query_params, + raw_header, + body_length: req.body_length, + }) +} + fn ocsf_message_field(value: &str) -> String { value .chars() @@ -245,42 +296,55 @@ fn inject_token_grant_header( credential: &ProviderProfileCredential, access_token: &str, ) -> Result> { - crate::token_grant::validate_access_token(access_token)?; - let (header_name, header_value) = token_grant_header(credential, access_token)?; + let (header_name, header_value) = + injected_credential_header(credential, access_token, "token grant")?; inject_header(raw_header, &header_name, &header_value) } -fn token_grant_header( +fn injected_credential_header( credential: &ProviderProfileCredential, access_token: &str, + context: &str, ) -> Result<(String, String)> { match credential.auth_style.trim().to_ascii_lowercase().as_str() { "" | "bearer" => { + crate::token_grant::validate_access_token(access_token)?; let header_name = if credential.header_name.trim().is_empty() { "Authorization" } else { credential.header_name.trim() }; - validate_header_name(header_name)?; + validate_header_name(header_name, context)?; Ok((header_name.to_string(), format!("Bearer {access_token}"))) } "header" => { let header_name = credential.header_name.trim(); if header_name.is_empty() { - return Err(miette!( - "token grant auth_style header requires header_name" - )); + return Err(miette!("{context} auth_style header requires header_name")); } - validate_header_name(header_name)?; + validate_header_name(header_name, context)?; + validate_header_value(access_token, context)?; Ok((header_name.to_string(), access_token.to_string())) } other => Err(miette!( - "token grant auth_style '{other}' is not supported; use bearer or header" + "{context} auth_style '{other}' is not supported; use bearer or header" )), } } -fn validate_header_name(header_name: &str) -> Result<()> { +fn validate_header_value(value: &str, context: &str) -> Result<()> { + if value + .bytes() + .any(|byte| (byte < b' ' && byte != b'\t') || byte == 0x7f) + { + return Err(miette!( + "{context} credential contains invalid HTTP header value characters" + )); + } + Ok(()) +} + +fn validate_header_name(header_name: &str, context: &str) -> Result<()> { let valid = !header_name.is_empty() && header_name.bytes().all(|byte| { byte.is_ascii_alphanumeric() @@ -304,12 +368,12 @@ fn validate_header_name(header_name: &str) -> Result<()> { }); if !valid { return Err(miette!( - "token grant header_name is not a valid HTTP header name" + "{context} header_name is not a valid HTTP header name" )); } match header_name.to_ascii_lowercase().as_str() { "host" | "content-length" | "transfer-encoding" | "connection" => Err(miette!( - "token grant header_name may not override HTTP framing or connection headers" + "{context} header_name may not override HTTP framing or connection headers" )), _ => Ok(()), } @@ -573,6 +637,8 @@ mod tests { use super::*; use crate::l7::provider::{BodyLength, L7Request}; use crate::l7::token_grant_injection::test_support::TokenGrantTestFixture; + use openshell_core::proto::{StaticCredentialBinding, StaticCredentialEndpointBinding}; + use openshell_core::provider_credentials::ProviderCredentialState; fn credential(auth_style: &str, header_name: &str) -> ProviderProfileCredential { ProviderProfileCredential { @@ -749,8 +815,12 @@ mod tests { #[test] fn token_grant_header_rejects_framing_and_connection_headers() { for header_name in ["Host", "Content-Length", "Transfer-Encoding", "Connection"] { - let err = token_grant_header(&credential("header", header_name), "grant-token") - .expect_err("framing header override should be rejected"); + let err = injected_credential_header( + &credential("header", header_name), + "grant-token", + "token grant", + ) + .expect_err("framing header override should be rejected"); assert_eq!( err.to_string(), "token grant header_name may not override HTTP framing or connection headers" @@ -758,6 +828,34 @@ mod tests { } } + #[test] + fn proxy_delivery_named_header_accepts_safe_non_token_value() { + let header = injected_credential_header( + &credential("header", "x-api-key"), + "key with spaces = allowed", + "proxy delivery", + ) + .expect("safe HTTP field value"); + assert_eq!( + header, + ( + "x-api-key".to_string(), + "key with spaces = allowed".to_string() + ) + ); + + let error = injected_credential_header( + &credential("header", "x-api-key"), + "safe\r\ninjected: value", + "proxy delivery", + ) + .expect_err("CRLF injection must be rejected"); + assert_eq!( + error.to_string(), + "proxy delivery credential contains invalid HTTP header value characters" + ); + } + #[test] fn inject_token_grant_header_preserves_header_terminator_before_body() { let raw = b"POST /v1 HTTP/1.1\r\nHost: api.example.com\r\nContent-Length: 2\r\n\r\nOK"; @@ -802,6 +900,67 @@ mod tests { ); } + #[test] + fn proxy_delivery_replaces_header_without_changing_body() { + let dynamic_credentials = + Arc::new(std::sync::RwLock::new(std::collections::HashMap::from([( + "api.example.com\t443\t/v1/**\tprovider:api_key".to_string(), + ProviderProfileCredential { + name: "api_key".to_string(), + env_vars: vec!["API_KEY".to_string()], + auth_style: "bearer".to_string(), + delivery: ProviderCredentialDelivery::Proxy as i32, + ..Default::default() + }, + )]))); + let state = ProviderCredentialState::from_bound_environment( + 7, + std::collections::HashMap::from([("API_KEY".to_string(), "real-secret".to_string())]), + std::collections::HashMap::new(), + std::collections::HashMap::new(), + std::collections::HashMap::from([( + "API_KEY".to_string(), + StaticCredentialBinding { + endpoints: vec![StaticCredentialEndpointBinding { + host: "api.example.com".to_string(), + port: 443, + path: "/v1/**".to_string(), + }], + credential_identity: "provider:API_KEY".to_string(), + workload_credential_handle: String::new(), + delivery: ProviderCredentialDelivery::Proxy as i32, + }, + )]), + Vec::new(), + ) + .expect("valid provider state"); + let ctx = L7EvalContext { + host: "api.example.com".to_string(), + port: 443, + secret_resolver: state.resolver_for_endpoint("api.example.com", 443, "/v1/chat"), + provider_credentials: Some(state), + provider_credential_revision: Some(7), + dynamic_credentials: Some(dynamic_credentials), + ..Default::default() + }; + let body = br#"{"messages":[{"content":"ordinary environment output"}]}"#; + let mut raw_header = b"POST /v1/chat HTTP/1.1\r\nHost: api.example.com\r\nAuthorization: Bearer openshell-managed\r\n\r\n".to_vec(); + raw_header.extend_from_slice(body); + let request = L7Request { + action: "POST".to_string(), + target: "/v1/chat".to_string(), + query_params: std::collections::HashMap::new(), + raw_header, + body_length: BodyLength::ContentLength(body.len() as u64), + }; + + let injected = inject_static_if_needed(request, &ctx).expect("credential injection"); + let text = String::from_utf8(injected.raw_header).expect("HTTP request is UTF-8"); + assert!(text.contains("Authorization: Bearer real-secret\r\n")); + assert!(!text.contains("openshell-managed")); + assert!(text.ends_with(std::str::from_utf8(body).expect("body is UTF-8"))); + } + #[tokio::test] async fn inject_if_needed_uses_configured_resolver() { let fixture = TokenGrantTestFixture::success( diff --git a/crates/openshell-supervisor-network/src/l7/websocket.rs b/crates/openshell-supervisor-network/src/l7/websocket.rs index 60ebfcc725..2ecc740413 100644 --- a/crates/openshell-supervisor-network/src/l7/websocket.rs +++ b/crates/openshell-supervisor-network/src/l7/websocket.rs @@ -2682,6 +2682,7 @@ network_policies: }], credential_identity: "provider-a:DISCORD_BOT_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), diff --git a/crates/openshell-supervisor-network/src/proxy.rs b/crates/openshell-supervisor-network/src/proxy.rs index 505c5555d4..a2248f97b4 100644 --- a/crates/openshell-supervisor-network/src/proxy.rs +++ b/crates/openshell-supervisor-network/src/proxy.rs @@ -4783,6 +4783,33 @@ async fn inject_token_grant_for_forward_request( forward_request_bytes: Vec, l7_ctx: &crate::l7::relay::L7EvalContext, ) -> Result> { + let request = forward_request_for_injection(method, upstream_target, forward_request_bytes)?; + crate::l7::token_grant_injection::inject_if_needed(request, l7_ctx) + .await + .map(|req| req.raw_header) +} + +fn inject_static_credential_for_forward_request( + method: &str, + upstream_target: &str, + forward_request_bytes: Vec, + l7_ctx: &crate::l7::relay::L7EvalContext, + secret_resolver: Option>, + revision: Option, +) -> Result> { + let request = forward_request_for_injection(method, upstream_target, forward_request_bytes)?; + let mut scoped_ctx = l7_ctx.clone(); + scoped_ctx.secret_resolver = secret_resolver; + scoped_ctx.provider_credential_revision = revision; + crate::l7::token_grant_injection::inject_static_if_needed(request, &scoped_ctx) + .map(|req| req.raw_header) +} + +fn forward_request_for_injection( + method: &str, + upstream_target: &str, + forward_request_bytes: Vec, +) -> Result { let header_end = forward_request_bytes .windows(4) .position(|w| w == b"\r\n\r\n") @@ -4791,17 +4818,13 @@ async fn inject_token_grant_for_forward_request( .into_diagnostic() .map_err(|_| miette::miette!("Forward HTTP headers contain invalid UTF-8"))?; let body_length = crate::l7::rest::parse_body_length(header_str)?; - let forward_request_for_token_grant = crate::l7::provider::L7Request { + Ok(crate::l7::provider::L7Request { action: method.to_string(), target: upstream_target.to_string(), query_params: std::collections::HashMap::new(), raw_header: forward_request_bytes, body_length, - }; - - crate::l7::token_grant_injection::inject_if_needed(forward_request_for_token_grant, l7_ctx) - .await - .map(|req| req.raw_header) + }) } /// Handle a plain HTTP forward proxy request (non-CONNECT). @@ -5849,6 +5872,35 @@ async fn handle_forward_proxy( if let Some(guard) = credential_generation { guard.ensure_current()?; } + forward_request_bytes = match inject_static_credential_for_forward_request( + method, + &upstream_target, + forward_request_bytes, + &l7_ctx, + secret_resolver.clone(), + endpoint_credentials.revision, + ) { + Ok(bytes) => bytes, + Err(error) => { + warn!( + dst_host = %host_lc, + dst_port = port, + error = %error, + "static provider credential injection failed in forward proxy" + ); + respond( + client, + &build_json_error_response( + 502, + "Bad Gateway", + "provider_authentication_failed", + "provider credential unavailable", + ), + ) + .await?; + return Ok(()); + } + }; // 9. Rewrite request and forward to upstream let rewritten = match rewrite_forward_request( @@ -7744,6 +7796,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -10523,6 +10576,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -10565,6 +10619,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -10611,6 +10666,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -11364,6 +11420,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -11453,6 +11510,7 @@ network_policies: }], credential_identity: format!("provider-a:{key}"), workload_credential_handle: String::new(), + delivery: 0, }, ) }) diff --git a/docs/sandboxes/providers-v2.mdx b/docs/sandboxes/providers-v2.mdx index 7a0f96b5ac..d9f9795706 100644 --- a/docs/sandboxes/providers-v2.mdx +++ b/docs/sandboxes/providers-v2.mdx @@ -213,7 +213,7 @@ The following Providers v2 design items are not part of the current behavior: | Roadmap item | Current behavior | |---|---| -| General profile-driven credential placement | Static `auth_style`, `header_name`, `query_param`, and `path_template` placement metadata is stored and validated, but static credential injection still depends on environment placeholders generated from provider credentials. Dynamic `token_grant` credentials support `bearer` and `header` placement for matching HTTP endpoints. | +| General profile-driven credential placement | Static credentials can opt into proxy-delivered `bearer` or named `header` placement for inspected REST endpoints. Other static placement styles still depend on environment placeholders. Dynamic `token_grant` credentials support `bearer` and `header` placement for matching HTTP endpoints. | | Binary-scoped credential injection | Provider profile binaries affect policy composition but do not yet restrict placeholder resolution by calling binary. Static and dynamic credentials are endpoint-scoped. | | Credential verification on create | `openshell provider create` does not yet probe provider verification endpoints or expose `--no-verify`. | | Automatic credential scope extraction | OpenShell does not yet inspect upstream provider responses to discover credential scopes. | @@ -288,7 +288,7 @@ openshell provider profile export github-profile -o yaml > github-profile.yaml openshell provider profile update github-profile -f github-profile.yaml ``` -Exported custom profiles include `resource_version`. OpenShell requires that version during update so stale files cannot silently overwrite newer profile definitions. The target ID in the command must match the profile ID in the file. Update accepts one file at a time. If an update would make dynamic token grants ambiguous for an attached sandbox, OpenShell rejects it before changing the profile. +Exported custom profiles include `resource_version`. OpenShell requires that version during update so stale files cannot silently overwrite newer profile definitions. The target ID in the command must match the profile ID in the file. Update accepts one file at a time. If an update would make runtime-injected credentials ambiguous for an attached sandbox, OpenShell rejects it before changing the profile. Custom profile IDs must use lowercase kebab-case with `a-z`, `0-9`, and `-`. Built-in profile IDs and legacy provider aliases are reserved. Built-in and interceptor-managed profiles are read-only through the profile APIs. OpenShell also rejects deleting a custom profile while a sandbox-attached provider uses it. @@ -332,9 +332,13 @@ credentials: env_vars: [CUSTOM_API_TOKEN] required: true + # Accepted values: environment (default), proxy. + delivery: environment + # Accepted values: basic, bearer, header, query, path. # These fields describe static credential placement. - # Static runtime injection still uses env placeholder resolution. + # Environment delivery uses placeholder resolution; proxy delivery sets a + # bearer or named header at the final outbound proxy step. auth_style: bearer header_name: authorization query_param: api_key @@ -432,13 +436,34 @@ binaries: - /usr/local/bin/custom-cli ``` +### Proxy-Delivered Static Credential + +This minimal profile keeps `MODEL_API_KEY` and its OpenShell placeholder out of the sandbox. The application may send any public placeholder value in `Authorization`; OpenShell replaces the complete header before forwarding the allowed request. + +The initial proxy-delivery implementation supports one proxy-delivered static credential per profile. That credential may list multiple `env_vars` aliases. A profile using proxy delivery cannot also declare a `token_grant` credential. + +```yaml +id: custom-model-api +display_name: Custom Model API +credentials: + - name: api_key + env_vars: [MODEL_API_KEY] + delivery: proxy + auth_style: bearer +endpoints: + - host: models.example.com + port: 443 + protocol: rest + access: full +``` + ### Profile Sections `id`, `display_name`, and `description` identify the profile. `id` is the value passed to `openshell provider create --type`. `category` groups profiles in `openshell provider list-profiles`. Use one of the values in the category enum. -`credentials` declares the credential names, environment variables, auth metadata, optional refresh metadata, and optional dynamic token grant metadata for the provider type. The `auth_style` field accepts `basic`, `bearer`, `header`, `query`, or `path`. When `auth_style` is `path`, set `path_template` to a URL path containing the `{credential}` placeholder exactly once (for example, `/v1/{credential}/resources`). Static credentials are exposed as placeholder environment variables and resolved in outbound HTTP requests only at their binding endpoints. Every static credential environment key receives the full profile endpoint set when the profile defines endpoints. An endpointless profile requires explicit sandbox policy bindings for each attached provider instance. Dynamic token grants are resolved by the sandbox proxy on demand for matching profile endpoints and support `bearer` or `header` placement. Credential environment variable names must not use the reserved `v_` prefix, such as `v10_GITHUB_TOKEN`, because OpenShell uses that namespace for revision-scoped placeholders. +`credentials` declares the credential names, environment variables, auth metadata, optional refresh metadata, and optional dynamic token grant metadata for the provider type. The `auth_style` field accepts `basic`, `bearer`, `header`, `query`, or `path`. When `auth_style` is `path`, set `path_template` to a URL path containing the `{credential}` placeholder exactly once (for example, `/v1/{credential}/resources`). Static credentials use `delivery: environment` by default: OpenShell exposes placeholder environment variables and resolves them in outbound requests only at their binding endpoints. Set `delivery: proxy` on a static `bearer` or named `header` credential to omit its environment placeholder and have the proxy set the complete header immediately before forwarding an allowed request. Proxy delivery is limited to inspected REST endpoints and does not support `tls: skip`. `env_vars` still names the host variables used to discover or supply the credential when creating the provider. Every static credential environment key receives the full profile endpoint set when the profile defines endpoints. An endpointless profile requires explicit sandbox policy bindings for each attached provider instance. Dynamic token grants are resolved by the sandbox proxy on demand for matching profile endpoints and support `bearer` or `header` placement. Credential environment variable names must not use the reserved `v_` prefix, such as `v10_GITHUB_TOKEN`, because OpenShell uses that namespace for revision-scoped placeholders. `discovery` controls what `--from-existing` scans when `providers_v2_enabled=true`. Each entry in `discovery.credentials` must name a diff --git a/proto/openshell.proto b/proto/openshell.proto index 246fe0626f..d3dd515247 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -1575,6 +1575,9 @@ message ProviderProfileCredential { ProviderCredentialRefresh refresh = 8; string path_template = 9; ProviderCredentialTokenGrant token_grant = 10; + // Controls how a static credential reaches the workload. Unspecified and + // environment preserve the existing environment-placeholder behavior. + ProviderCredentialDelivery delivery = 11; } enum ProviderCredentialRefreshStrategy { @@ -1904,6 +1907,8 @@ message StaticCredentialBinding { // handle changes when the sandbox, provider, credential key, refresh // authorization epoch, or endpoint authorization boundary changes. string workload_credential_handle = 3; + // Delivery mode copied from the provider profile credential declaration. + ProviderCredentialDelivery delivery = 4; } // Get sandbox provider environment response. @@ -2820,6 +2825,14 @@ enum ProviderCredentialRefreshRecoveryAction { PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE = 4; } +// Kept after the pre-existing enums so adding it does not renumber their +// generated descriptors. +enum ProviderCredentialDelivery { + PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED = 0; + PROVIDER_CREDENTIAL_DELIVERY_ENVIRONMENT = 1; + PROVIDER_CREDENTIAL_DELIVERY_PROXY = 2; +} + // Workspace membership record. message WorkspaceMember { openshell.datamodel.v1.ObjectMeta metadata = 1; diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 6b3b740a0b..7c1fcbafe9 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -498,6 +498,57 @@ func (ProviderCredentialRefreshRecoveryAction) EnumDescriptor() ([]byte, []int) return file_openshell_proto_rawDescGZIP(), []int{7} } +// Kept after the pre-existing enums so adding it does not renumber their +// generated descriptors. +type ProviderCredentialDelivery int32 + +const ( + ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED ProviderCredentialDelivery = 0 + ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_ENVIRONMENT ProviderCredentialDelivery = 1 + ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_PROXY ProviderCredentialDelivery = 2 +) + +// Enum value maps for ProviderCredentialDelivery. +var ( + ProviderCredentialDelivery_name = map[int32]string{ + 0: "PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED", + 1: "PROVIDER_CREDENTIAL_DELIVERY_ENVIRONMENT", + 2: "PROVIDER_CREDENTIAL_DELIVERY_PROXY", + } + ProviderCredentialDelivery_value = map[string]int32{ + "PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED": 0, + "PROVIDER_CREDENTIAL_DELIVERY_ENVIRONMENT": 1, + "PROVIDER_CREDENTIAL_DELIVERY_PROXY": 2, + } +) + +func (x ProviderCredentialDelivery) Enum() *ProviderCredentialDelivery { + p := new(ProviderCredentialDelivery) + *p = x + return p +} + +func (x ProviderCredentialDelivery) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderCredentialDelivery) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[8].Descriptor() +} + +func (ProviderCredentialDelivery) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[8] +} + +func (x ProviderCredentialDelivery) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderCredentialDelivery.Descriptor instead. +func (ProviderCredentialDelivery) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{8} +} + // IssueSandboxToken request. Empty body; identity is established by the // authentication credentials carried in the request headers (a projected // Kubernetes ServiceAccount JWT in the K8s driver path). @@ -5588,17 +5639,20 @@ func (x *ProviderCredentialTokenGrant) GetRequestedTokenType() string { // Provider credential declaration. type ProviderProfileCredential struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - EnvVars []string `protobuf:"bytes,3,rep,name=env_vars,json=envVars,proto3" json:"env_vars,omitempty"` - Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` - AuthStyle string `protobuf:"bytes,5,opt,name=auth_style,json=authStyle,proto3" json:"auth_style,omitempty"` - HeaderName string `protobuf:"bytes,6,opt,name=header_name,json=headerName,proto3" json:"header_name,omitempty"` - QueryParam string `protobuf:"bytes,7,opt,name=query_param,json=queryParam,proto3" json:"query_param,omitempty"` - Refresh *ProviderCredentialRefresh `protobuf:"bytes,8,opt,name=refresh,proto3" json:"refresh,omitempty"` - PathTemplate string `protobuf:"bytes,9,opt,name=path_template,json=pathTemplate,proto3" json:"path_template,omitempty"` - TokenGrant *ProviderCredentialTokenGrant `protobuf:"bytes,10,opt,name=token_grant,json=tokenGrant,proto3" json:"token_grant,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + EnvVars []string `protobuf:"bytes,3,rep,name=env_vars,json=envVars,proto3" json:"env_vars,omitempty"` + Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` + AuthStyle string `protobuf:"bytes,5,opt,name=auth_style,json=authStyle,proto3" json:"auth_style,omitempty"` + HeaderName string `protobuf:"bytes,6,opt,name=header_name,json=headerName,proto3" json:"header_name,omitempty"` + QueryParam string `protobuf:"bytes,7,opt,name=query_param,json=queryParam,proto3" json:"query_param,omitempty"` + Refresh *ProviderCredentialRefresh `protobuf:"bytes,8,opt,name=refresh,proto3" json:"refresh,omitempty"` + PathTemplate string `protobuf:"bytes,9,opt,name=path_template,json=pathTemplate,proto3" json:"path_template,omitempty"` + TokenGrant *ProviderCredentialTokenGrant `protobuf:"bytes,10,opt,name=token_grant,json=tokenGrant,proto3" json:"token_grant,omitempty"` + // Controls how a static credential reaches the workload. Unspecified and + // environment preserve the existing environment-placeholder behavior. + Delivery ProviderCredentialDelivery `protobuf:"varint,11,opt,name=delivery,proto3,enum=openshell.v1.ProviderCredentialDelivery" json:"delivery,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5703,6 +5757,13 @@ func (x *ProviderProfileCredential) GetTokenGrant() *ProviderCredentialTokenGran return nil } +func (x *ProviderProfileCredential) GetDelivery() ProviderCredentialDelivery { + if x != nil { + return x.Delivery + } + return ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED +} + type ProviderCredentialRefreshMaterial struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -7804,8 +7865,10 @@ type StaticCredentialBinding struct { // handle changes when the sandbox, provider, credential key, refresh // authorization epoch, or endpoint authorization boundary changes. WorkloadCredentialHandle string `protobuf:"bytes,3,opt,name=workload_credential_handle,json=workloadCredentialHandle,proto3" json:"workload_credential_handle,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Delivery mode copied from the provider profile credential declaration. + Delivery ProviderCredentialDelivery `protobuf:"varint,4,opt,name=delivery,proto3,enum=openshell.v1.ProviderCredentialDelivery" json:"delivery,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StaticCredentialBinding) Reset() { @@ -7859,6 +7922,13 @@ func (x *StaticCredentialBinding) GetWorkloadCredentialHandle() string { return "" } +func (x *StaticCredentialBinding) GetDelivery() ProviderCredentialDelivery { + if x != nil { + return x.Delivery + } + return ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED +} + // Get sandbox provider environment response. type GetSandboxProviderEnvironmentResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -14335,7 +14405,7 @@ const file_openshell_proto_rawDesc = "" + "grant_type\x18\b \x01(\x0e2..openshell.v1.ProviderCredentialTokenGrantTypeR\tgrantType\x12[\n" + "\rsubject_token\x18\t \x01(\v26.openshell.v1.ProviderCredentialTokenGrantSubjectTokenR\fsubjectToken\x120\n" + "\x14requested_token_type\x18\n" + - " \x01(\tR\x12requestedTokenType\"\x9e\x03\n" + + " \x01(\tR\x12requestedTokenType\"\xe4\x03\n" + "\x19ProviderProfileCredential\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + @@ -14351,7 +14421,8 @@ const file_openshell_proto_rawDesc = "" + "\rpath_template\x18\t \x01(\tR\fpathTemplate\x12K\n" + "\vtoken_grant\x18\n" + " \x01(\v2*.openshell.v1.ProviderCredentialTokenGrantR\n" + - "tokenGrant\"\x8d\x01\n" + + "tokenGrant\x12D\n" + + "\bdelivery\x18\v \x01(\x0e2(.openshell.v1.ProviderCredentialDeliveryR\bdelivery\"\x8d\x01\n" + "!ProviderCredentialRefreshMaterial\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x1a\n" + @@ -14522,11 +14593,12 @@ const file_openshell_proto_rawDesc = "" + "\x1fStaticCredentialEndpointBinding\x12\x12\n" + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + "\x04port\x18\x02 \x01(\rR\x04port\x12\x12\n" + - "\x04path\x18\x03 \x01(\tR\x04path\"\xd5\x01\n" + + "\x04path\x18\x03 \x01(\tR\x04path\"\x9b\x02\n" + "\x17StaticCredentialBinding\x12K\n" + "\tendpoints\x18\x01 \x03(\v2-.openshell.v1.StaticCredentialEndpointBindingR\tendpoints\x12/\n" + "\x13credential_identity\x18\x02 \x01(\tR\x12credentialIdentity\x12<\n" + - "\x1aworkload_credential_handle\x18\x03 \x01(\tR\x18workloadCredentialHandle\"\x90\b\n" + + "\x1aworkload_credential_handle\x18\x03 \x01(\tR\x18workloadCredentialHandle\x12D\n" + + "\bdelivery\x18\x04 \x01(\x0e2(.openshell.v1.ProviderCredentialDeliveryR\bdelivery\"\x90\b\n" + "%GetSandboxProviderEnvironmentResponse\x12l\n" + "\venvironment\x18\x01 \x03(\v2D.openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntryB\x04\x88\xb5\x18\x01R\venvironment\x122\n" + "\x15provider_env_revision\x18\x02 \x01(\x04R\x13providerEnvRevision\x12\x87\x01\n" + @@ -15081,7 +15153,11 @@ const file_openshell_proto_rawDesc = "" + "1PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY\x10\x01\x12;\n" + "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE\x10\x02\x12A\n" + "=PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION\x10\x03\x12;\n" + - "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE\x10\x042\xacF\n" + + "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE\x10\x04*\xa0\x01\n" + + "\x1aProviderCredentialDelivery\x12,\n" + + "(PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED\x10\x00\x12,\n" + + "(PROVIDER_CREDENTIAL_DELIVERY_ENVIRONMENT\x10\x01\x12&\n" + + "\"PROVIDER_CREDENTIAL_DELIVERY_PROXY\x10\x022\xacF\n" + "\tOpenShell\x12Z\n" + "\x06Health\x12\x1b.openshell.v1.HealthRequest\x1a\x1c.openshell.v1.HealthResponse\"\x15\x82\xb5\x18\x11\n" + "\x0funauthenticated\x12i\n" + @@ -15235,7 +15311,7 @@ func file_openshell_proto_rawDescGZIP() []byte { return file_openshell_proto_rawDescData } -var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 8) +var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 9) var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 217) var file_openshell_proto_goTypes = []any{ (SandboxPhase)(0), // 0: openshell.v1.SandboxPhase @@ -15246,548 +15322,551 @@ var file_openshell_proto_goTypes = []any{ (ServiceStatus)(0), // 5: openshell.v1.ServiceStatus (WorkspaceRole)(0), // 6: openshell.v1.WorkspaceRole (ProviderCredentialRefreshRecoveryAction)(0), // 7: openshell.v1.ProviderCredentialRefreshRecoveryAction - (*IssueSandboxTokenRequest)(nil), // 8: openshell.v1.IssueSandboxTokenRequest - (*IssueSandboxTokenResponse)(nil), // 9: openshell.v1.IssueSandboxTokenResponse - (*RefreshSandboxTokenRequest)(nil), // 10: openshell.v1.RefreshSandboxTokenRequest - (*RefreshSandboxTokenResponse)(nil), // 11: openshell.v1.RefreshSandboxTokenResponse - (*HealthRequest)(nil), // 12: openshell.v1.HealthRequest - (*HealthResponse)(nil), // 13: openshell.v1.HealthResponse - (*GetCurrentUserRequest)(nil), // 14: openshell.v1.GetCurrentUserRequest - (*GetCurrentUserResponse)(nil), // 15: openshell.v1.GetCurrentUserResponse - (*GetGatewayInfoRequest)(nil), // 16: openshell.v1.GetGatewayInfoRequest - (*GetGatewayInfoResponse)(nil), // 17: openshell.v1.GetGatewayInfoResponse - (*ComputeDriverInfo)(nil), // 18: openshell.v1.ComputeDriverInfo - (*ComputeDriverCapabilities)(nil), // 19: openshell.v1.ComputeDriverCapabilities - (*Sandbox)(nil), // 20: openshell.v1.Sandbox - (*SandboxSpec)(nil), // 21: openshell.v1.SandboxSpec - (*ResourceRequirements)(nil), // 22: openshell.v1.ResourceRequirements - (*GpuResourceRequirements)(nil), // 23: openshell.v1.GpuResourceRequirements - (*SandboxTemplate)(nil), // 24: openshell.v1.SandboxTemplate - (*SandboxStatus)(nil), // 25: openshell.v1.SandboxStatus - (*SandboxCondition)(nil), // 26: openshell.v1.SandboxCondition - (*PlatformEvent)(nil), // 27: openshell.v1.PlatformEvent - (*CreateSandboxRequest)(nil), // 28: openshell.v1.CreateSandboxRequest - (*GetSandboxRequest)(nil), // 29: openshell.v1.GetSandboxRequest - (*ListSandboxesRequest)(nil), // 30: openshell.v1.ListSandboxesRequest - (*ListSandboxProvidersRequest)(nil), // 31: openshell.v1.ListSandboxProvidersRequest - (*AttachSandboxProviderRequest)(nil), // 32: openshell.v1.AttachSandboxProviderRequest - (*DetachSandboxProviderRequest)(nil), // 33: openshell.v1.DetachSandboxProviderRequest - (*DeleteSandboxRequest)(nil), // 34: openshell.v1.DeleteSandboxRequest - (*StopSandboxRequest)(nil), // 35: openshell.v1.StopSandboxRequest - (*StartSandboxRequest)(nil), // 36: openshell.v1.StartSandboxRequest - (*SandboxResponse)(nil), // 37: openshell.v1.SandboxResponse - (*ListSandboxesResponse)(nil), // 38: openshell.v1.ListSandboxesResponse - (*ListSandboxProvidersResponse)(nil), // 39: openshell.v1.ListSandboxProvidersResponse - (*AttachSandboxProviderResponse)(nil), // 40: openshell.v1.AttachSandboxProviderResponse - (*DetachSandboxProviderResponse)(nil), // 41: openshell.v1.DetachSandboxProviderResponse - (*DeleteSandboxResponse)(nil), // 42: openshell.v1.DeleteSandboxResponse - (*CreateSshSessionRequest)(nil), // 43: openshell.v1.CreateSshSessionRequest - (*CreateSshSessionResponse)(nil), // 44: openshell.v1.CreateSshSessionResponse - (*ExposeServiceRequest)(nil), // 45: openshell.v1.ExposeServiceRequest - (*GetServiceRequest)(nil), // 46: openshell.v1.GetServiceRequest - (*ListServicesRequest)(nil), // 47: openshell.v1.ListServicesRequest - (*ListServicesResponse)(nil), // 48: openshell.v1.ListServicesResponse - (*DeleteServiceRequest)(nil), // 49: openshell.v1.DeleteServiceRequest - (*DeleteServiceResponse)(nil), // 50: openshell.v1.DeleteServiceResponse - (*ServiceEndpoint)(nil), // 51: openshell.v1.ServiceEndpoint - (*ServiceEndpointResponse)(nil), // 52: openshell.v1.ServiceEndpointResponse - (*RevokeSshSessionRequest)(nil), // 53: openshell.v1.RevokeSshSessionRequest - (*RevokeSshSessionResponse)(nil), // 54: openshell.v1.RevokeSshSessionResponse - (*ExecSandboxRequest)(nil), // 55: openshell.v1.ExecSandboxRequest - (*ExecSandboxStdout)(nil), // 56: openshell.v1.ExecSandboxStdout - (*ExecSandboxStderr)(nil), // 57: openshell.v1.ExecSandboxStderr - (*ExecSandboxExit)(nil), // 58: openshell.v1.ExecSandboxExit - (*ExecSandboxEvent)(nil), // 59: openshell.v1.ExecSandboxEvent - (*TcpForwardInit)(nil), // 60: openshell.v1.TcpForwardInit - (*TcpForwardFrame)(nil), // 61: openshell.v1.TcpForwardFrame - (*ExecSandboxInput)(nil), // 62: openshell.v1.ExecSandboxInput - (*ExecSandboxWindowResize)(nil), // 63: openshell.v1.ExecSandboxWindowResize - (*SshSession)(nil), // 64: openshell.v1.SshSession - (*WatchSandboxRequest)(nil), // 65: openshell.v1.WatchSandboxRequest - (*SandboxStreamEvent)(nil), // 66: openshell.v1.SandboxStreamEvent - (*SandboxLogLine)(nil), // 67: openshell.v1.SandboxLogLine - (*SandboxStreamWarning)(nil), // 68: openshell.v1.SandboxStreamWarning - (*CreateProviderRequest)(nil), // 69: openshell.v1.CreateProviderRequest - (*GetProviderRequest)(nil), // 70: openshell.v1.GetProviderRequest - (*ListProvidersRequest)(nil), // 71: openshell.v1.ListProvidersRequest - (*UpdateProviderRequest)(nil), // 72: openshell.v1.UpdateProviderRequest - (*DeleteProviderRequest)(nil), // 73: openshell.v1.DeleteProviderRequest - (*ProviderResponse)(nil), // 74: openshell.v1.ProviderResponse - (*ListProvidersResponse)(nil), // 75: openshell.v1.ListProvidersResponse - (*ListProviderProfilesRequest)(nil), // 76: openshell.v1.ListProviderProfilesRequest - (*GetProviderProfileRequest)(nil), // 77: openshell.v1.GetProviderProfileRequest - (*ProviderProfileImportItem)(nil), // 78: openshell.v1.ProviderProfileImportItem - (*ProviderProfileDiagnostic)(nil), // 79: openshell.v1.ProviderProfileDiagnostic - (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 80: openshell.v1.ProviderCredentialTokenGrantAudienceOverride - (*ProviderCredentialTokenGrantSubjectToken)(nil), // 81: openshell.v1.ProviderCredentialTokenGrantSubjectToken - (*ProviderCredentialTokenGrant)(nil), // 82: openshell.v1.ProviderCredentialTokenGrant - (*ProviderProfileCredential)(nil), // 83: openshell.v1.ProviderProfileCredential - (*ProviderCredentialRefreshMaterial)(nil), // 84: openshell.v1.ProviderCredentialRefreshMaterial - (*ProviderCredentialRefreshOutput)(nil), // 85: openshell.v1.ProviderCredentialRefreshOutput - (*ProviderCredentialRefresh)(nil), // 86: openshell.v1.ProviderCredentialRefresh - (*ProviderCredentialRefreshStatus)(nil), // 87: openshell.v1.ProviderCredentialRefreshStatus - (*ProviderProfileDiscovery)(nil), // 88: openshell.v1.ProviderProfileDiscovery - (*StoredProviderCredentialRefreshState)(nil), // 89: openshell.v1.StoredProviderCredentialRefreshState - (*StoredRefreshMaterialDeletion)(nil), // 90: openshell.v1.StoredRefreshMaterialDeletion - (*GetProviderRefreshStatusRequest)(nil), // 91: openshell.v1.GetProviderRefreshStatusRequest - (*GetProviderRefreshStatusResponse)(nil), // 92: openshell.v1.GetProviderRefreshStatusResponse - (*ConfigureProviderRefreshRequest)(nil), // 93: openshell.v1.ConfigureProviderRefreshRequest - (*ConfigureProviderRefreshResponse)(nil), // 94: openshell.v1.ConfigureProviderRefreshResponse - (*RotateProviderCredentialRequest)(nil), // 95: openshell.v1.RotateProviderCredentialRequest - (*RotateProviderCredentialResponse)(nil), // 96: openshell.v1.RotateProviderCredentialResponse - (*DeleteProviderRefreshRequest)(nil), // 97: openshell.v1.DeleteProviderRefreshRequest - (*DeleteProviderRefreshResponse)(nil), // 98: openshell.v1.DeleteProviderRefreshResponse - (*ProviderProfile)(nil), // 99: openshell.v1.ProviderProfile - (*StoredProviderProfile)(nil), // 100: openshell.v1.StoredProviderProfile - (*ProviderProfileResponse)(nil), // 101: openshell.v1.ProviderProfileResponse - (*ListProviderProfilesResponse)(nil), // 102: openshell.v1.ListProviderProfilesResponse - (*ImportProviderProfilesRequest)(nil), // 103: openshell.v1.ImportProviderProfilesRequest - (*ImportProviderProfilesResponse)(nil), // 104: openshell.v1.ImportProviderProfilesResponse - (*UpdateProviderProfilesRequest)(nil), // 105: openshell.v1.UpdateProviderProfilesRequest - (*UpdateProviderProfilesResponse)(nil), // 106: openshell.v1.UpdateProviderProfilesResponse - (*LintProviderProfilesRequest)(nil), // 107: openshell.v1.LintProviderProfilesRequest - (*LintProviderProfilesResponse)(nil), // 108: openshell.v1.LintProviderProfilesResponse - (*DeleteProviderResponse)(nil), // 109: openshell.v1.DeleteProviderResponse - (*DeleteProviderProfileRequest)(nil), // 110: openshell.v1.DeleteProviderProfileRequest - (*DeleteProviderProfileResponse)(nil), // 111: openshell.v1.DeleteProviderProfileResponse - (*GetSandboxProviderEnvironmentRequest)(nil), // 112: openshell.v1.GetSandboxProviderEnvironmentRequest - (*StaticCredentialEndpointBinding)(nil), // 113: openshell.v1.StaticCredentialEndpointBinding - (*StaticCredentialBinding)(nil), // 114: openshell.v1.StaticCredentialBinding - (*GetSandboxProviderEnvironmentResponse)(nil), // 115: openshell.v1.GetSandboxProviderEnvironmentResponse - (*ExchangeProviderSubjectTokenRequest)(nil), // 116: openshell.v1.ExchangeProviderSubjectTokenRequest - (*ExchangeProviderSubjectTokenResponse)(nil), // 117: openshell.v1.ExchangeProviderSubjectTokenResponse - (*UpdateConfigRequest)(nil), // 118: openshell.v1.UpdateConfigRequest - (*PolicyMergeOperation)(nil), // 119: openshell.v1.PolicyMergeOperation - (*AddNetworkRule)(nil), // 120: openshell.v1.AddNetworkRule - (*RemoveNetworkEndpoint)(nil), // 121: openshell.v1.RemoveNetworkEndpoint - (*RemoveNetworkRule)(nil), // 122: openshell.v1.RemoveNetworkRule - (*AddDenyRules)(nil), // 123: openshell.v1.AddDenyRules - (*AddAllowRules)(nil), // 124: openshell.v1.AddAllowRules - (*RemoveNetworkBinary)(nil), // 125: openshell.v1.RemoveNetworkBinary - (*UpdateConfigResponse)(nil), // 126: openshell.v1.UpdateConfigResponse - (*GetSandboxPolicyStatusRequest)(nil), // 127: openshell.v1.GetSandboxPolicyStatusRequest - (*GetSandboxPolicyStatusResponse)(nil), // 128: openshell.v1.GetSandboxPolicyStatusResponse - (*ListSandboxPoliciesRequest)(nil), // 129: openshell.v1.ListSandboxPoliciesRequest - (*ListSandboxPoliciesResponse)(nil), // 130: openshell.v1.ListSandboxPoliciesResponse - (*ReportPolicyStatusRequest)(nil), // 131: openshell.v1.ReportPolicyStatusRequest - (*ReportPolicyStatusResponse)(nil), // 132: openshell.v1.ReportPolicyStatusResponse - (*SandboxPolicyRevision)(nil), // 133: openshell.v1.SandboxPolicyRevision - (*GetSandboxLogsRequest)(nil), // 134: openshell.v1.GetSandboxLogsRequest - (*PushSandboxLogsRequest)(nil), // 135: openshell.v1.PushSandboxLogsRequest - (*PushSandboxLogsResponse)(nil), // 136: openshell.v1.PushSandboxLogsResponse - (*GetSandboxLogsResponse)(nil), // 137: openshell.v1.GetSandboxLogsResponse - (*SupervisorMessage)(nil), // 138: openshell.v1.SupervisorMessage - (*GatewayMessage)(nil), // 139: openshell.v1.GatewayMessage - (*SupervisorHello)(nil), // 140: openshell.v1.SupervisorHello - (*SessionAccepted)(nil), // 141: openshell.v1.SessionAccepted - (*SessionRejected)(nil), // 142: openshell.v1.SessionRejected - (*SupervisorHeartbeat)(nil), // 143: openshell.v1.SupervisorHeartbeat - (*GatewayHeartbeat)(nil), // 144: openshell.v1.GatewayHeartbeat - (*ReportMainProcessExitRequest)(nil), // 145: openshell.v1.ReportMainProcessExitRequest - (*ReportMainProcessExitResponse)(nil), // 146: openshell.v1.ReportMainProcessExitResponse - (*RelayOpen)(nil), // 147: openshell.v1.RelayOpen - (*SshRelayTarget)(nil), // 148: openshell.v1.SshRelayTarget - (*TcpRelayTarget)(nil), // 149: openshell.v1.TcpRelayTarget - (*RelayInit)(nil), // 150: openshell.v1.RelayInit - (*RelayFrame)(nil), // 151: openshell.v1.RelayFrame - (*RelayOpenResult)(nil), // 152: openshell.v1.RelayOpenResult - (*RelayClose)(nil), // 153: openshell.v1.RelayClose - (*L7RequestSample)(nil), // 154: openshell.v1.L7RequestSample - (*DenialSummary)(nil), // 155: openshell.v1.DenialSummary - (*DenialGroupCount)(nil), // 156: openshell.v1.DenialGroupCount - (*NetworkActivitySummary)(nil), // 157: openshell.v1.NetworkActivitySummary - (*PolicyChunk)(nil), // 158: openshell.v1.PolicyChunk - (*DraftPolicyUpdate)(nil), // 159: openshell.v1.DraftPolicyUpdate - (*SubmitPolicyAnalysisRequest)(nil), // 160: openshell.v1.SubmitPolicyAnalysisRequest - (*SubmitPolicyAnalysisResponse)(nil), // 161: openshell.v1.SubmitPolicyAnalysisResponse - (*GetDraftPolicyRequest)(nil), // 162: openshell.v1.GetDraftPolicyRequest - (*GetDraftPolicyResponse)(nil), // 163: openshell.v1.GetDraftPolicyResponse - (*ApproveDraftChunkRequest)(nil), // 164: openshell.v1.ApproveDraftChunkRequest - (*ApproveDraftChunkResponse)(nil), // 165: openshell.v1.ApproveDraftChunkResponse - (*RejectDraftChunkRequest)(nil), // 166: openshell.v1.RejectDraftChunkRequest - (*RejectDraftChunkResponse)(nil), // 167: openshell.v1.RejectDraftChunkResponse - (*DraftChunkApproval)(nil), // 168: openshell.v1.DraftChunkApproval - (*ApproveAllDraftChunksRequest)(nil), // 169: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 170: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 171: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 172: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 173: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 174: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 175: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 176: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 177: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 178: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 179: openshell.v1.GetDraftHistoryResponse - (*PolicyRevisionPayload)(nil), // 180: openshell.v1.PolicyRevisionPayload - (*DraftChunkPayload)(nil), // 181: openshell.v1.DraftChunkPayload - (*StoredPolicyRevision)(nil), // 182: openshell.v1.StoredPolicyRevision - (*StoredDraftChunk)(nil), // 183: openshell.v1.StoredDraftChunk - (*CreateWorkspaceRequest)(nil), // 184: openshell.v1.CreateWorkspaceRequest - (*CreateWorkspaceResponse)(nil), // 185: openshell.v1.CreateWorkspaceResponse - (*GetWorkspaceRequest)(nil), // 186: openshell.v1.GetWorkspaceRequest - (*GetWorkspaceResponse)(nil), // 187: openshell.v1.GetWorkspaceResponse - (*ListWorkspacesRequest)(nil), // 188: openshell.v1.ListWorkspacesRequest - (*ListWorkspacesResponse)(nil), // 189: openshell.v1.ListWorkspacesResponse - (*DeleteWorkspaceRequest)(nil), // 190: openshell.v1.DeleteWorkspaceRequest - (*DeleteWorkspaceResponse)(nil), // 191: openshell.v1.DeleteWorkspaceResponse - (*WorkspaceMember)(nil), // 192: openshell.v1.WorkspaceMember - (*AddWorkspaceMemberRequest)(nil), // 193: openshell.v1.AddWorkspaceMemberRequest - (*AddWorkspaceMemberResponse)(nil), // 194: openshell.v1.AddWorkspaceMemberResponse - (*RemoveWorkspaceMemberRequest)(nil), // 195: openshell.v1.RemoveWorkspaceMemberRequest - (*RemoveWorkspaceMemberResponse)(nil), // 196: openshell.v1.RemoveWorkspaceMemberResponse - (*ListWorkspaceMembersRequest)(nil), // 197: openshell.v1.ListWorkspaceMembersRequest - (*ListWorkspaceMembersResponse)(nil), // 198: openshell.v1.ListWorkspaceMembersResponse - (*ExtensionServiceCredential)(nil), // 199: openshell.v1.ExtensionServiceCredential - nil, // 200: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 201: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 202: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 203: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 204: openshell.v1.PlatformEvent.MetadataEntry - nil, // 205: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 206: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 207: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 208: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 209: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - nil, // 210: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - nil, // 211: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - nil, // 212: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry - nil, // 213: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 214: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 215: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 216: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - nil, // 217: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 218: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 219: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 220: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 221: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 222: openshell.v1.PolicyRevisionPayload.ProvenanceEntry - nil, // 223: openshell.v1.StoredPolicyRevision.ProvenanceEntry - nil, // 224: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*datamodelv1.ObjectMeta)(nil), // 225: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 226: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 227: google.protobuf.Struct - (*datamodelv1.Provider)(nil), // 228: openshell.datamodel.v1.Provider - (*datamodelv1.CredentialHandle)(nil), // 229: openshell.datamodel.v1.CredentialHandle - (*sandboxv1.NetworkEndpoint)(nil), // 230: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 231: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 232: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 233: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 234: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 235: openshell.sandbox.v1.L7Rule - (*datamodelv1.Workspace)(nil), // 236: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 237: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 238: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 239: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 240: openshell.sandbox.v1.GetGatewayConfigResponse + (ProviderCredentialDelivery)(0), // 8: openshell.v1.ProviderCredentialDelivery + (*IssueSandboxTokenRequest)(nil), // 9: openshell.v1.IssueSandboxTokenRequest + (*IssueSandboxTokenResponse)(nil), // 10: openshell.v1.IssueSandboxTokenResponse + (*RefreshSandboxTokenRequest)(nil), // 11: openshell.v1.RefreshSandboxTokenRequest + (*RefreshSandboxTokenResponse)(nil), // 12: openshell.v1.RefreshSandboxTokenResponse + (*HealthRequest)(nil), // 13: openshell.v1.HealthRequest + (*HealthResponse)(nil), // 14: openshell.v1.HealthResponse + (*GetCurrentUserRequest)(nil), // 15: openshell.v1.GetCurrentUserRequest + (*GetCurrentUserResponse)(nil), // 16: openshell.v1.GetCurrentUserResponse + (*GetGatewayInfoRequest)(nil), // 17: openshell.v1.GetGatewayInfoRequest + (*GetGatewayInfoResponse)(nil), // 18: openshell.v1.GetGatewayInfoResponse + (*ComputeDriverInfo)(nil), // 19: openshell.v1.ComputeDriverInfo + (*ComputeDriverCapabilities)(nil), // 20: openshell.v1.ComputeDriverCapabilities + (*Sandbox)(nil), // 21: openshell.v1.Sandbox + (*SandboxSpec)(nil), // 22: openshell.v1.SandboxSpec + (*ResourceRequirements)(nil), // 23: openshell.v1.ResourceRequirements + (*GpuResourceRequirements)(nil), // 24: openshell.v1.GpuResourceRequirements + (*SandboxTemplate)(nil), // 25: openshell.v1.SandboxTemplate + (*SandboxStatus)(nil), // 26: openshell.v1.SandboxStatus + (*SandboxCondition)(nil), // 27: openshell.v1.SandboxCondition + (*PlatformEvent)(nil), // 28: openshell.v1.PlatformEvent + (*CreateSandboxRequest)(nil), // 29: openshell.v1.CreateSandboxRequest + (*GetSandboxRequest)(nil), // 30: openshell.v1.GetSandboxRequest + (*ListSandboxesRequest)(nil), // 31: openshell.v1.ListSandboxesRequest + (*ListSandboxProvidersRequest)(nil), // 32: openshell.v1.ListSandboxProvidersRequest + (*AttachSandboxProviderRequest)(nil), // 33: openshell.v1.AttachSandboxProviderRequest + (*DetachSandboxProviderRequest)(nil), // 34: openshell.v1.DetachSandboxProviderRequest + (*DeleteSandboxRequest)(nil), // 35: openshell.v1.DeleteSandboxRequest + (*StopSandboxRequest)(nil), // 36: openshell.v1.StopSandboxRequest + (*StartSandboxRequest)(nil), // 37: openshell.v1.StartSandboxRequest + (*SandboxResponse)(nil), // 38: openshell.v1.SandboxResponse + (*ListSandboxesResponse)(nil), // 39: openshell.v1.ListSandboxesResponse + (*ListSandboxProvidersResponse)(nil), // 40: openshell.v1.ListSandboxProvidersResponse + (*AttachSandboxProviderResponse)(nil), // 41: openshell.v1.AttachSandboxProviderResponse + (*DetachSandboxProviderResponse)(nil), // 42: openshell.v1.DetachSandboxProviderResponse + (*DeleteSandboxResponse)(nil), // 43: openshell.v1.DeleteSandboxResponse + (*CreateSshSessionRequest)(nil), // 44: openshell.v1.CreateSshSessionRequest + (*CreateSshSessionResponse)(nil), // 45: openshell.v1.CreateSshSessionResponse + (*ExposeServiceRequest)(nil), // 46: openshell.v1.ExposeServiceRequest + (*GetServiceRequest)(nil), // 47: openshell.v1.GetServiceRequest + (*ListServicesRequest)(nil), // 48: openshell.v1.ListServicesRequest + (*ListServicesResponse)(nil), // 49: openshell.v1.ListServicesResponse + (*DeleteServiceRequest)(nil), // 50: openshell.v1.DeleteServiceRequest + (*DeleteServiceResponse)(nil), // 51: openshell.v1.DeleteServiceResponse + (*ServiceEndpoint)(nil), // 52: openshell.v1.ServiceEndpoint + (*ServiceEndpointResponse)(nil), // 53: openshell.v1.ServiceEndpointResponse + (*RevokeSshSessionRequest)(nil), // 54: openshell.v1.RevokeSshSessionRequest + (*RevokeSshSessionResponse)(nil), // 55: openshell.v1.RevokeSshSessionResponse + (*ExecSandboxRequest)(nil), // 56: openshell.v1.ExecSandboxRequest + (*ExecSandboxStdout)(nil), // 57: openshell.v1.ExecSandboxStdout + (*ExecSandboxStderr)(nil), // 58: openshell.v1.ExecSandboxStderr + (*ExecSandboxExit)(nil), // 59: openshell.v1.ExecSandboxExit + (*ExecSandboxEvent)(nil), // 60: openshell.v1.ExecSandboxEvent + (*TcpForwardInit)(nil), // 61: openshell.v1.TcpForwardInit + (*TcpForwardFrame)(nil), // 62: openshell.v1.TcpForwardFrame + (*ExecSandboxInput)(nil), // 63: openshell.v1.ExecSandboxInput + (*ExecSandboxWindowResize)(nil), // 64: openshell.v1.ExecSandboxWindowResize + (*SshSession)(nil), // 65: openshell.v1.SshSession + (*WatchSandboxRequest)(nil), // 66: openshell.v1.WatchSandboxRequest + (*SandboxStreamEvent)(nil), // 67: openshell.v1.SandboxStreamEvent + (*SandboxLogLine)(nil), // 68: openshell.v1.SandboxLogLine + (*SandboxStreamWarning)(nil), // 69: openshell.v1.SandboxStreamWarning + (*CreateProviderRequest)(nil), // 70: openshell.v1.CreateProviderRequest + (*GetProviderRequest)(nil), // 71: openshell.v1.GetProviderRequest + (*ListProvidersRequest)(nil), // 72: openshell.v1.ListProvidersRequest + (*UpdateProviderRequest)(nil), // 73: openshell.v1.UpdateProviderRequest + (*DeleteProviderRequest)(nil), // 74: openshell.v1.DeleteProviderRequest + (*ProviderResponse)(nil), // 75: openshell.v1.ProviderResponse + (*ListProvidersResponse)(nil), // 76: openshell.v1.ListProvidersResponse + (*ListProviderProfilesRequest)(nil), // 77: openshell.v1.ListProviderProfilesRequest + (*GetProviderProfileRequest)(nil), // 78: openshell.v1.GetProviderProfileRequest + (*ProviderProfileImportItem)(nil), // 79: openshell.v1.ProviderProfileImportItem + (*ProviderProfileDiagnostic)(nil), // 80: openshell.v1.ProviderProfileDiagnostic + (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 81: openshell.v1.ProviderCredentialTokenGrantAudienceOverride + (*ProviderCredentialTokenGrantSubjectToken)(nil), // 82: openshell.v1.ProviderCredentialTokenGrantSubjectToken + (*ProviderCredentialTokenGrant)(nil), // 83: openshell.v1.ProviderCredentialTokenGrant + (*ProviderProfileCredential)(nil), // 84: openshell.v1.ProviderProfileCredential + (*ProviderCredentialRefreshMaterial)(nil), // 85: openshell.v1.ProviderCredentialRefreshMaterial + (*ProviderCredentialRefreshOutput)(nil), // 86: openshell.v1.ProviderCredentialRefreshOutput + (*ProviderCredentialRefresh)(nil), // 87: openshell.v1.ProviderCredentialRefresh + (*ProviderCredentialRefreshStatus)(nil), // 88: openshell.v1.ProviderCredentialRefreshStatus + (*ProviderProfileDiscovery)(nil), // 89: openshell.v1.ProviderProfileDiscovery + (*StoredProviderCredentialRefreshState)(nil), // 90: openshell.v1.StoredProviderCredentialRefreshState + (*StoredRefreshMaterialDeletion)(nil), // 91: openshell.v1.StoredRefreshMaterialDeletion + (*GetProviderRefreshStatusRequest)(nil), // 92: openshell.v1.GetProviderRefreshStatusRequest + (*GetProviderRefreshStatusResponse)(nil), // 93: openshell.v1.GetProviderRefreshStatusResponse + (*ConfigureProviderRefreshRequest)(nil), // 94: openshell.v1.ConfigureProviderRefreshRequest + (*ConfigureProviderRefreshResponse)(nil), // 95: openshell.v1.ConfigureProviderRefreshResponse + (*RotateProviderCredentialRequest)(nil), // 96: openshell.v1.RotateProviderCredentialRequest + (*RotateProviderCredentialResponse)(nil), // 97: openshell.v1.RotateProviderCredentialResponse + (*DeleteProviderRefreshRequest)(nil), // 98: openshell.v1.DeleteProviderRefreshRequest + (*DeleteProviderRefreshResponse)(nil), // 99: openshell.v1.DeleteProviderRefreshResponse + (*ProviderProfile)(nil), // 100: openshell.v1.ProviderProfile + (*StoredProviderProfile)(nil), // 101: openshell.v1.StoredProviderProfile + (*ProviderProfileResponse)(nil), // 102: openshell.v1.ProviderProfileResponse + (*ListProviderProfilesResponse)(nil), // 103: openshell.v1.ListProviderProfilesResponse + (*ImportProviderProfilesRequest)(nil), // 104: openshell.v1.ImportProviderProfilesRequest + (*ImportProviderProfilesResponse)(nil), // 105: openshell.v1.ImportProviderProfilesResponse + (*UpdateProviderProfilesRequest)(nil), // 106: openshell.v1.UpdateProviderProfilesRequest + (*UpdateProviderProfilesResponse)(nil), // 107: openshell.v1.UpdateProviderProfilesResponse + (*LintProviderProfilesRequest)(nil), // 108: openshell.v1.LintProviderProfilesRequest + (*LintProviderProfilesResponse)(nil), // 109: openshell.v1.LintProviderProfilesResponse + (*DeleteProviderResponse)(nil), // 110: openshell.v1.DeleteProviderResponse + (*DeleteProviderProfileRequest)(nil), // 111: openshell.v1.DeleteProviderProfileRequest + (*DeleteProviderProfileResponse)(nil), // 112: openshell.v1.DeleteProviderProfileResponse + (*GetSandboxProviderEnvironmentRequest)(nil), // 113: openshell.v1.GetSandboxProviderEnvironmentRequest + (*StaticCredentialEndpointBinding)(nil), // 114: openshell.v1.StaticCredentialEndpointBinding + (*StaticCredentialBinding)(nil), // 115: openshell.v1.StaticCredentialBinding + (*GetSandboxProviderEnvironmentResponse)(nil), // 116: openshell.v1.GetSandboxProviderEnvironmentResponse + (*ExchangeProviderSubjectTokenRequest)(nil), // 117: openshell.v1.ExchangeProviderSubjectTokenRequest + (*ExchangeProviderSubjectTokenResponse)(nil), // 118: openshell.v1.ExchangeProviderSubjectTokenResponse + (*UpdateConfigRequest)(nil), // 119: openshell.v1.UpdateConfigRequest + (*PolicyMergeOperation)(nil), // 120: openshell.v1.PolicyMergeOperation + (*AddNetworkRule)(nil), // 121: openshell.v1.AddNetworkRule + (*RemoveNetworkEndpoint)(nil), // 122: openshell.v1.RemoveNetworkEndpoint + (*RemoveNetworkRule)(nil), // 123: openshell.v1.RemoveNetworkRule + (*AddDenyRules)(nil), // 124: openshell.v1.AddDenyRules + (*AddAllowRules)(nil), // 125: openshell.v1.AddAllowRules + (*RemoveNetworkBinary)(nil), // 126: openshell.v1.RemoveNetworkBinary + (*UpdateConfigResponse)(nil), // 127: openshell.v1.UpdateConfigResponse + (*GetSandboxPolicyStatusRequest)(nil), // 128: openshell.v1.GetSandboxPolicyStatusRequest + (*GetSandboxPolicyStatusResponse)(nil), // 129: openshell.v1.GetSandboxPolicyStatusResponse + (*ListSandboxPoliciesRequest)(nil), // 130: openshell.v1.ListSandboxPoliciesRequest + (*ListSandboxPoliciesResponse)(nil), // 131: openshell.v1.ListSandboxPoliciesResponse + (*ReportPolicyStatusRequest)(nil), // 132: openshell.v1.ReportPolicyStatusRequest + (*ReportPolicyStatusResponse)(nil), // 133: openshell.v1.ReportPolicyStatusResponse + (*SandboxPolicyRevision)(nil), // 134: openshell.v1.SandboxPolicyRevision + (*GetSandboxLogsRequest)(nil), // 135: openshell.v1.GetSandboxLogsRequest + (*PushSandboxLogsRequest)(nil), // 136: openshell.v1.PushSandboxLogsRequest + (*PushSandboxLogsResponse)(nil), // 137: openshell.v1.PushSandboxLogsResponse + (*GetSandboxLogsResponse)(nil), // 138: openshell.v1.GetSandboxLogsResponse + (*SupervisorMessage)(nil), // 139: openshell.v1.SupervisorMessage + (*GatewayMessage)(nil), // 140: openshell.v1.GatewayMessage + (*SupervisorHello)(nil), // 141: openshell.v1.SupervisorHello + (*SessionAccepted)(nil), // 142: openshell.v1.SessionAccepted + (*SessionRejected)(nil), // 143: openshell.v1.SessionRejected + (*SupervisorHeartbeat)(nil), // 144: openshell.v1.SupervisorHeartbeat + (*GatewayHeartbeat)(nil), // 145: openshell.v1.GatewayHeartbeat + (*ReportMainProcessExitRequest)(nil), // 146: openshell.v1.ReportMainProcessExitRequest + (*ReportMainProcessExitResponse)(nil), // 147: openshell.v1.ReportMainProcessExitResponse + (*RelayOpen)(nil), // 148: openshell.v1.RelayOpen + (*SshRelayTarget)(nil), // 149: openshell.v1.SshRelayTarget + (*TcpRelayTarget)(nil), // 150: openshell.v1.TcpRelayTarget + (*RelayInit)(nil), // 151: openshell.v1.RelayInit + (*RelayFrame)(nil), // 152: openshell.v1.RelayFrame + (*RelayOpenResult)(nil), // 153: openshell.v1.RelayOpenResult + (*RelayClose)(nil), // 154: openshell.v1.RelayClose + (*L7RequestSample)(nil), // 155: openshell.v1.L7RequestSample + (*DenialSummary)(nil), // 156: openshell.v1.DenialSummary + (*DenialGroupCount)(nil), // 157: openshell.v1.DenialGroupCount + (*NetworkActivitySummary)(nil), // 158: openshell.v1.NetworkActivitySummary + (*PolicyChunk)(nil), // 159: openshell.v1.PolicyChunk + (*DraftPolicyUpdate)(nil), // 160: openshell.v1.DraftPolicyUpdate + (*SubmitPolicyAnalysisRequest)(nil), // 161: openshell.v1.SubmitPolicyAnalysisRequest + (*SubmitPolicyAnalysisResponse)(nil), // 162: openshell.v1.SubmitPolicyAnalysisResponse + (*GetDraftPolicyRequest)(nil), // 163: openshell.v1.GetDraftPolicyRequest + (*GetDraftPolicyResponse)(nil), // 164: openshell.v1.GetDraftPolicyResponse + (*ApproveDraftChunkRequest)(nil), // 165: openshell.v1.ApproveDraftChunkRequest + (*ApproveDraftChunkResponse)(nil), // 166: openshell.v1.ApproveDraftChunkResponse + (*RejectDraftChunkRequest)(nil), // 167: openshell.v1.RejectDraftChunkRequest + (*RejectDraftChunkResponse)(nil), // 168: openshell.v1.RejectDraftChunkResponse + (*DraftChunkApproval)(nil), // 169: openshell.v1.DraftChunkApproval + (*ApproveAllDraftChunksRequest)(nil), // 170: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 171: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 172: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 173: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 174: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 175: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 176: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 177: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 178: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 179: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 180: openshell.v1.GetDraftHistoryResponse + (*PolicyRevisionPayload)(nil), // 181: openshell.v1.PolicyRevisionPayload + (*DraftChunkPayload)(nil), // 182: openshell.v1.DraftChunkPayload + (*StoredPolicyRevision)(nil), // 183: openshell.v1.StoredPolicyRevision + (*StoredDraftChunk)(nil), // 184: openshell.v1.StoredDraftChunk + (*CreateWorkspaceRequest)(nil), // 185: openshell.v1.CreateWorkspaceRequest + (*CreateWorkspaceResponse)(nil), // 186: openshell.v1.CreateWorkspaceResponse + (*GetWorkspaceRequest)(nil), // 187: openshell.v1.GetWorkspaceRequest + (*GetWorkspaceResponse)(nil), // 188: openshell.v1.GetWorkspaceResponse + (*ListWorkspacesRequest)(nil), // 189: openshell.v1.ListWorkspacesRequest + (*ListWorkspacesResponse)(nil), // 190: openshell.v1.ListWorkspacesResponse + (*DeleteWorkspaceRequest)(nil), // 191: openshell.v1.DeleteWorkspaceRequest + (*DeleteWorkspaceResponse)(nil), // 192: openshell.v1.DeleteWorkspaceResponse + (*WorkspaceMember)(nil), // 193: openshell.v1.WorkspaceMember + (*AddWorkspaceMemberRequest)(nil), // 194: openshell.v1.AddWorkspaceMemberRequest + (*AddWorkspaceMemberResponse)(nil), // 195: openshell.v1.AddWorkspaceMemberResponse + (*RemoveWorkspaceMemberRequest)(nil), // 196: openshell.v1.RemoveWorkspaceMemberRequest + (*RemoveWorkspaceMemberResponse)(nil), // 197: openshell.v1.RemoveWorkspaceMemberResponse + (*ListWorkspaceMembersRequest)(nil), // 198: openshell.v1.ListWorkspaceMembersRequest + (*ListWorkspaceMembersResponse)(nil), // 199: openshell.v1.ListWorkspaceMembersResponse + (*ExtensionServiceCredential)(nil), // 200: openshell.v1.ExtensionServiceCredential + nil, // 201: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 202: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 203: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 204: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 205: openshell.v1.PlatformEvent.MetadataEntry + nil, // 206: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 207: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 208: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 209: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 210: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + nil, // 211: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + nil, // 212: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + nil, // 213: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry + nil, // 214: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 215: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 216: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 217: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + nil, // 218: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 219: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 220: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 221: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 222: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 223: openshell.v1.PolicyRevisionPayload.ProvenanceEntry + nil, // 224: openshell.v1.StoredPolicyRevision.ProvenanceEntry + nil, // 225: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*datamodelv1.ObjectMeta)(nil), // 226: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 227: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 228: google.protobuf.Struct + (*datamodelv1.Provider)(nil), // 229: openshell.datamodel.v1.Provider + (*datamodelv1.CredentialHandle)(nil), // 230: openshell.datamodel.v1.CredentialHandle + (*sandboxv1.NetworkEndpoint)(nil), // 231: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 232: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 233: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 234: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 235: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 236: openshell.sandbox.v1.L7Rule + (*datamodelv1.Workspace)(nil), // 237: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 238: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 239: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 240: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 241: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 199, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential + 200, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential 5, // 1: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus 5, // 2: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus - 18, // 3: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo - 19, // 4: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities - 225, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 21, // 6: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec - 25, // 7: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus - 200, // 8: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry - 24, // 9: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 226, // 10: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 22, // 11: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements - 23, // 12: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 201, // 13: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 202, // 14: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 203, // 15: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 227, // 16: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 227, // 17: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 26, // 18: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition + 19, // 3: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo + 20, // 4: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities + 226, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 22, // 6: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec + 26, // 7: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus + 201, // 8: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 25, // 9: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate + 227, // 10: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 23, // 11: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements + 24, // 12: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements + 202, // 13: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 203, // 14: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 204, // 15: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 228, // 16: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 228, // 17: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 27, // 18: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition 0, // 19: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 204, // 20: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 21, // 21: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 205, // 22: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 206, // 23: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 20, // 24: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 20, // 25: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 228, // 26: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 20, // 27: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 20, // 28: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 52, // 29: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 225, // 30: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 51, // 31: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 207, // 32: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 56, // 33: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 57, // 34: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 58, // 35: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 148, // 36: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 149, // 37: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 60, // 38: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 55, // 39: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 63, // 40: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 225, // 41: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 20, // 42: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 67, // 43: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 27, // 44: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 68, // 45: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 159, // 46: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 208, // 47: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 228, // 48: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 228, // 49: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 209, // 50: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - 228, // 51: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 228, // 52: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 99, // 53: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 80, // 54: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 205, // 20: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 22, // 21: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec + 206, // 22: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 207, // 23: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 21, // 24: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 21, // 25: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 229, // 26: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 21, // 27: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 21, // 28: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 53, // 29: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 226, // 30: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 52, // 31: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 208, // 32: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 57, // 33: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 58, // 34: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 59, // 35: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 149, // 36: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 150, // 37: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 61, // 38: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 56, // 39: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 64, // 40: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 226, // 41: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 21, // 42: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 68, // 43: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 28, // 44: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 69, // 45: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 160, // 46: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 209, // 47: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 229, // 48: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 229, // 49: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 210, // 50: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + 229, // 51: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 229, // 52: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 100, // 53: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 81, // 54: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride 1, // 55: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType - 81, // 56: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken - 86, // 57: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 82, // 58: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant - 2, // 59: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 84, // 60: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 85, // 61: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput - 2, // 62: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 7, // 63: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 225, // 64: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 2, // 65: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 210, // 66: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - 211, // 67: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - 212, // 68: openshell.v1.StoredProviderCredentialRefreshState.secret_material_handles:type_name -> openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry - 90, // 69: openshell.v1.StoredProviderCredentialRefreshState.pending_secret_deletions:type_name -> openshell.v1.StoredRefreshMaterialDeletion - 7, // 70: openshell.v1.StoredProviderCredentialRefreshState.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 229, // 71: openshell.v1.StoredRefreshMaterialDeletion.handle:type_name -> openshell.datamodel.v1.CredentialHandle - 87, // 72: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 2, // 73: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 213, // 74: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 87, // 75: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 87, // 76: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 3, // 77: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 83, // 78: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 230, // 79: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 231, // 80: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 88, // 81: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 214, // 82: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 225, // 83: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 99, // 84: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile - 99, // 85: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 99, // 86: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 78, // 87: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 79, // 88: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 99, // 89: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 78, // 90: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 79, // 91: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 99, // 92: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 78, // 93: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 79, // 94: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 113, // 95: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 215, // 96: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 216, // 97: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - 217, // 98: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 218, // 99: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 226, // 100: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 232, // 101: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 119, // 102: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 219, // 103: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 120, // 104: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 121, // 105: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 122, // 106: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 123, // 107: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 124, // 108: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 125, // 109: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 233, // 110: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 234, // 111: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 235, // 112: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 220, // 113: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 133, // 114: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 133, // 115: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 4, // 116: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 4, // 117: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 226, // 118: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 221, // 119: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 67, // 120: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 67, // 121: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 140, // 122: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 143, // 123: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 152, // 124: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 153, // 125: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 141, // 126: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 142, // 127: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 144, // 128: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 147, // 129: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 153, // 130: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 148, // 131: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 149, // 132: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 150, // 133: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 154, // 134: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 156, // 135: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 233, // 136: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 226, // 137: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 226, // 138: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 155, // 139: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 158, // 140: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 157, // 141: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 158, // 142: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 168, // 143: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 233, // 144: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 178, // 145: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 226, // 146: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 222, // 147: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry - 233, // 148: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 226, // 149: openshell.v1.DraftChunkPayload.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 226, // 150: openshell.v1.DraftChunkPayload.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 223, // 151: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry - 226, // 152: openshell.v1.StoredDraftChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 226, // 153: openshell.v1.StoredDraftChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 224, // 154: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 236, // 155: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 236, // 156: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 236, // 157: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 225, // 158: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 6, // 159: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 6, // 160: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 192, // 161: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 192, // 162: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 229, // 163: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle - 83, // 164: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 114, // 165: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 12, // 166: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 14, // 167: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 16, // 168: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 28, // 169: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 29, // 170: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 30, // 171: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 31, // 172: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 32, // 173: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 33, // 174: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 34, // 175: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 35, // 176: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 36, // 177: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 43, // 178: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 45, // 179: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 46, // 180: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 47, // 181: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 49, // 182: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 53, // 183: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 55, // 184: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 61, // 185: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 62, // 186: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 69, // 187: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 70, // 188: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 71, // 189: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 76, // 190: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 77, // 191: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 103, // 192: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 105, // 193: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 107, // 194: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 72, // 195: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 91, // 196: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 93, // 197: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 95, // 198: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 97, // 199: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 73, // 200: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 110, // 201: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 237, // 202: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 238, // 203: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 118, // 204: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 127, // 205: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 129, // 206: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 131, // 207: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 112, // 208: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 116, // 209: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 134, // 210: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 135, // 211: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 138, // 212: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 145, // 213: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 151, // 214: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 65, // 215: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 160, // 216: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 162, // 217: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 164, // 218: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 166, // 219: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 169, // 220: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 171, // 221: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 173, // 222: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 175, // 223: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 177, // 224: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 8, // 225: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 10, // 226: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 184, // 227: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 186, // 228: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 188, // 229: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 190, // 230: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 193, // 231: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 195, // 232: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 197, // 233: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 13, // 234: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 15, // 235: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 17, // 236: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 37, // 237: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 37, // 238: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 38, // 239: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 39, // 240: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 40, // 241: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 41, // 242: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 42, // 243: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 37, // 244: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 37, // 245: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 44, // 246: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 52, // 247: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 52, // 248: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 48, // 249: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 50, // 250: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 54, // 251: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 59, // 252: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 61, // 253: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 59, // 254: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 74, // 255: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 74, // 256: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 75, // 257: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 102, // 258: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 101, // 259: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 104, // 260: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 106, // 261: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 108, // 262: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 74, // 263: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 92, // 264: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 94, // 265: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 96, // 266: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 98, // 267: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 109, // 268: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 111, // 269: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 239, // 270: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 240, // 271: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 126, // 272: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 128, // 273: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 130, // 274: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 132, // 275: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 115, // 276: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 117, // 277: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 137, // 278: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 136, // 279: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 139, // 280: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 146, // 281: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 151, // 282: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 66, // 283: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 161, // 284: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 163, // 285: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 165, // 286: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 167, // 287: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 170, // 288: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 172, // 289: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 174, // 290: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 176, // 291: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 179, // 292: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 9, // 293: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 11, // 294: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 185, // 295: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 187, // 296: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 189, // 297: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 191, // 298: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 194, // 299: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 196, // 300: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 198, // 301: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 234, // [234:302] is the sub-list for method output_type - 166, // [166:234] is the sub-list for method input_type - 166, // [166:166] is the sub-list for extension type_name - 166, // [166:166] is the sub-list for extension extendee - 0, // [0:166] is the sub-list for field type_name + 82, // 56: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 87, // 57: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 83, // 58: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 8, // 59: openshell.v1.ProviderProfileCredential.delivery:type_name -> openshell.v1.ProviderCredentialDelivery + 2, // 60: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 85, // 61: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 86, // 62: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 2, // 63: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 7, // 64: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 226, // 65: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 2, // 66: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 211, // 67: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + 212, // 68: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + 213, // 69: openshell.v1.StoredProviderCredentialRefreshState.secret_material_handles:type_name -> openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry + 91, // 70: openshell.v1.StoredProviderCredentialRefreshState.pending_secret_deletions:type_name -> openshell.v1.StoredRefreshMaterialDeletion + 7, // 71: openshell.v1.StoredProviderCredentialRefreshState.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 230, // 72: openshell.v1.StoredRefreshMaterialDeletion.handle:type_name -> openshell.datamodel.v1.CredentialHandle + 88, // 73: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 2, // 74: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 214, // 75: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 88, // 76: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 88, // 77: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 3, // 78: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 84, // 79: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 231, // 80: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 232, // 81: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 89, // 82: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 215, // 83: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 226, // 84: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 100, // 85: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile + 100, // 86: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 100, // 87: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 79, // 88: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 80, // 89: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 100, // 90: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 79, // 91: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 80, // 92: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 100, // 93: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 79, // 94: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 80, // 95: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 114, // 96: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 8, // 97: openshell.v1.StaticCredentialBinding.delivery:type_name -> openshell.v1.ProviderCredentialDelivery + 216, // 98: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 217, // 99: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + 218, // 100: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 219, // 101: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 227, // 102: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 233, // 103: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 120, // 104: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 220, // 105: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 121, // 106: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 122, // 107: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 123, // 108: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 124, // 109: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 125, // 110: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 126, // 111: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 234, // 112: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 235, // 113: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 236, // 114: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 221, // 115: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 134, // 116: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 134, // 117: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 4, // 118: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 4, // 119: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 227, // 120: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 222, // 121: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 68, // 122: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 68, // 123: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 141, // 124: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 144, // 125: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 153, // 126: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 154, // 127: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 142, // 128: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 143, // 129: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 145, // 130: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 148, // 131: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 154, // 132: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 149, // 133: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 150, // 134: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 151, // 135: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 155, // 136: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 157, // 137: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 234, // 138: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 227, // 139: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 227, // 140: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 156, // 141: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 159, // 142: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 158, // 143: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 159, // 144: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 169, // 145: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 234, // 146: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 179, // 147: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 227, // 148: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 223, // 149: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry + 234, // 150: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 227, // 151: openshell.v1.DraftChunkPayload.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 227, // 152: openshell.v1.DraftChunkPayload.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 224, // 153: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry + 227, // 154: openshell.v1.StoredDraftChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 227, // 155: openshell.v1.StoredDraftChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 225, // 156: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 237, // 157: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 237, // 158: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 237, // 159: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 226, // 160: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 6, // 161: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 6, // 162: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 193, // 163: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 193, // 164: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 230, // 165: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle + 84, // 166: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 115, // 167: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 13, // 168: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 15, // 169: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 17, // 170: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 29, // 171: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 30, // 172: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 31, // 173: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 32, // 174: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 33, // 175: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 34, // 176: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 35, // 177: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 36, // 178: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 37, // 179: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 44, // 180: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 46, // 181: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 47, // 182: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 48, // 183: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 50, // 184: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 54, // 185: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 56, // 186: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 62, // 187: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 63, // 188: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 70, // 189: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 71, // 190: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 72, // 191: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 77, // 192: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 78, // 193: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 104, // 194: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 106, // 195: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 108, // 196: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 73, // 197: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 92, // 198: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 94, // 199: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 96, // 200: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 98, // 201: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 74, // 202: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 111, // 203: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 238, // 204: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 239, // 205: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 119, // 206: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 128, // 207: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 130, // 208: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 132, // 209: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 113, // 210: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 117, // 211: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 135, // 212: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 136, // 213: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 139, // 214: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 146, // 215: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 152, // 216: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 66, // 217: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 161, // 218: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 163, // 219: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 165, // 220: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 167, // 221: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 170, // 222: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 172, // 223: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 174, // 224: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 176, // 225: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 178, // 226: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 9, // 227: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 11, // 228: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 185, // 229: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 187, // 230: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 189, // 231: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 191, // 232: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 194, // 233: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 196, // 234: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 198, // 235: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 14, // 236: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 16, // 237: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 18, // 238: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 38, // 239: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 38, // 240: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 39, // 241: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 40, // 242: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 41, // 243: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 42, // 244: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 43, // 245: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 38, // 246: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 38, // 247: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 45, // 248: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 53, // 249: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 53, // 250: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 49, // 251: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 51, // 252: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 55, // 253: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 60, // 254: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 62, // 255: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 60, // 256: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 75, // 257: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 75, // 258: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 76, // 259: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 103, // 260: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 102, // 261: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 105, // 262: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 107, // 263: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 109, // 264: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 75, // 265: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 93, // 266: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 95, // 267: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 97, // 268: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 99, // 269: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 110, // 270: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 112, // 271: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 240, // 272: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 241, // 273: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 127, // 274: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 129, // 275: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 131, // 276: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 133, // 277: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 116, // 278: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 118, // 279: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 138, // 280: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 137, // 281: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 140, // 282: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 147, // 283: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 152, // 284: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 67, // 285: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 162, // 286: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 164, // 287: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 166, // 288: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 168, // 289: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 171, // 290: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 173, // 291: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 175, // 292: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 177, // 293: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 180, // 294: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 10, // 295: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 12, // 296: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 186, // 297: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 188, // 298: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 190, // 299: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 192, // 300: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 195, // 301: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 197, // 302: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 199, // 303: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 236, // [236:304] is the sub-list for method output_type + 168, // [168:236] is the sub-list for method input_type + 168, // [168:168] is the sub-list for extension type_name + 168, // [168:168] is the sub-list for extension extendee + 0, // [0:168] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -15860,7 +15939,7 @@ func file_openshell_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_openshell_proto_rawDesc), len(file_openshell_proto_rawDesc)), - NumEnums: 8, + NumEnums: 9, NumMessages: 217, NumExtensions: 0, NumServices: 1, From 9ac8f8dbdd7aa7c3f86faf2ceed5a298b5b50f6b Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Wed, 2 Sep 2026 16:59:10 +0000 Subject: [PATCH 08/18] refactor(sandbox): remove legacy Pi bridge alias Signed-off-by: Johnny Greco --- crates/openshell-sandbox/src/agent_bridge.rs | 1 - crates/openshell-sandbox/src/lib.rs | 4 ---- docs/extensibility/supervisor-middleware.mdx | 5 ++--- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/openshell-sandbox/src/agent_bridge.rs b/crates/openshell-sandbox/src/agent_bridge.rs index 828d780d8e..c86c266159 100644 --- a/crates/openshell-sandbox/src/agent_bridge.rs +++ b/crates/openshell-sandbox/src/agent_bridge.rs @@ -24,7 +24,6 @@ pub const BRIDGE_ADDR: &str = "127.0.0.1:8193"; pub const BRIDGE_PATH: &str = "/v1/agent/conversation"; pub const BRIDGE_URL: &str = "http://127.0.0.1:8193/v1/agent/conversation"; pub const BRIDGE_URL_ENV: &str = "OPENSHELL_AGENT_CONVERSATION_URL"; -pub const LEGACY_PI_BRIDGE_URL_ENV: &str = "OPENSHELL_PI_CONVERSATION_URL"; // Vec is encoded as a JSON number array by the existing bridge contract, // so its transport envelope can be roughly five times the logical payload. diff --git a/crates/openshell-sandbox/src/lib.rs b/crates/openshell-sandbox/src/lib.rs index be5b5bf62b..fe602fb7e8 100644 --- a/crates/openshell-sandbox/src/lib.rs +++ b/crates/openshell-sandbox/src/lib.rs @@ -707,10 +707,6 @@ pub async fn run_sandbox( agent_bridge::BRIDGE_URL_ENV.into(), agent_bridge::BRIDGE_URL.into(), ); - provider_env.insert( - agent_bridge::LEGACY_PI_BRIDGE_URL_ENV.into(), - agent_bridge::BRIDGE_URL.into(), - ); info!( url = agent_bridge::BRIDGE_URL, "agent admission bridge ready" diff --git a/docs/extensibility/supervisor-middleware.mdx b/docs/extensibility/supervisor-middleware.mdx index 559360500e..c569b0a675 100644 --- a/docs/extensibility/supervisor-middleware.mdx +++ b/docs/extensibility/supervisor-middleware.mdx @@ -20,9 +20,8 @@ and returns only the structured decision, optional replacement, and opaque receipt. It shares the policy and middleware registry generation used for provider egress, so an incomplete reload makes admission unavailable. -Managed Pi is the first client of this general contract. For compatibility, the -supervisor also sets `OPENSHELL_PI_CONVERSATION_URL`. A Pi extension submits a -rendered, idle, text-only user prompt before Pi stores it; denied prompts never +Managed Pi is the first client of this general contract. A Pi extension submits +a rendered, idle, text-only user prompt before Pi stores it; denied prompts never enter chat history, while replacement bodies support redaction. Pi remains unaware of OpenShell-specific behavior. Admission bodies are limited to 32 KiB and to the advertised binding limit. Images, queued input, retries, compaction, From db5a3bbd3457bb93590c4e4ae23748fbac86f8e8 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Mon, 31 Aug 2026 15:16:17 -0400 Subject: [PATCH 09/18] feat(providers): support proxy-delivered static header credentials Signed-off-by: Johnny Greco --- .../src/provider_credentials.rs | 45 +- crates/openshell-providers/src/discovery.rs | 2 + crates/openshell-providers/src/profiles.rs | 299 +++- crates/openshell-server/src/grpc/provider.rs | 238 ++- .../src/l7/relay.rs | 37 +- .../src/l7/token_grant_injection.rs | 189 ++- .../src/l7/websocket.rs | 1 + .../openshell-supervisor-network/src/proxy.rs | 70 +- docs/providers/profiles.mdx | 33 +- proto/openshell.proto | 13 + sdk/go/proto/openshellv1/openshell.pb.go | 1275 +++++++++-------- 11 files changed, 1524 insertions(+), 678 deletions(-) diff --git a/crates/openshell-core/src/provider_credentials.rs b/crates/openshell-core/src/provider_credentials.rs index 2b1537a21b..4f70e02d30 100644 --- a/crates/openshell-core/src/provider_credentials.rs +++ b/crates/openshell-core/src/provider_credentials.rs @@ -46,6 +46,7 @@ struct CompiledStaticCredentialBinding { endpoints: Vec, credential_identity: String, workload_credential_handle: String, + delivery: crate::proto::ProviderCredentialDelivery, } #[derive(Debug, Clone)] @@ -124,13 +125,14 @@ impl ProviderCredentialState { &non_secret_environment_keys, )?; let stable_handles = static_credential_stable_handles(&static_credential_bindings); - let (child_env, generation_resolver, current_resolver) = + let (mut child_env, generation_resolver, current_resolver) = SecretResolver::from_provider_env_for_current_revision_with_stable_handles( env, credential_expires_at_ms, revision, &stable_handles, ); + suppress_proxy_delivered_credentials(&mut child_env, &static_credential_bindings); let snapshot = Arc::new(ProviderCredentialSnapshot { revision, child_env, @@ -532,6 +534,7 @@ impl ProviderCredentialState { revision, &stable_handles, ); + suppress_proxy_delivered_credentials(&mut child_env, &static_credential_bindings); let mut inner = self .inner .write() @@ -684,12 +687,25 @@ fn compile_static_credential_bindings( endpoints, credential_identity: binding.credential_identity, workload_credential_handle: binding.workload_credential_handle, + delivery: crate::proto::ProviderCredentialDelivery::try_from(binding.delivery) + .unwrap_or(crate::proto::ProviderCredentialDelivery::Environment), }, )) }) .collect() } +fn suppress_proxy_delivered_credentials( + child_env: &mut HashMap, + bindings: &HashMap, +) { + for (key, binding) in bindings { + if binding.delivery == crate::proto::ProviderCredentialDelivery::Proxy { + child_env.remove(key); + } + } +} + fn compile_static_credential_endpoint( endpoint: StaticCredentialEndpointBinding, ) -> Result { @@ -808,6 +824,7 @@ mod tests { }], credential_identity: "provider-a:API_KEY".to_string(), workload_credential_handle: String::new(), + delivery: 0, } } @@ -942,6 +959,32 @@ mod tests { } } + #[test] + fn proxy_delivered_credential_is_not_in_child_environment() { + let mut proxy_binding = binding("api.example.com", 443, "/v1/**"); + proxy_binding.delivery = crate::proto::ProviderCredentialDelivery::Proxy as i32; + let state = ProviderCredentialState::from_bound_environment( + 7, + HashMap::from([("API_KEY".to_string(), "secret".to_string())]), + HashMap::new(), + HashMap::new(), + HashMap::from([("API_KEY".to_string(), proxy_binding)]), + Vec::new(), + ) + .expect("valid proxy delivery binding"); + + assert!(!state.snapshot().child_env.contains_key("API_KEY")); + let resolver = state + .resolver_for_endpoint("api.example.com", 443, "/v1/messages") + .expect("endpoint resolver"); + assert_eq!( + resolver + .resolve_current_env_key_checked("API_KEY", "test") + .expect("authorized endpoint"), + Some("secret") + ); + } + #[test] fn multiple_credentials_resolve_only_at_their_own_endpoints() { let mut binding_a = binding("a.example.com", 443, "/a/**"); diff --git a/crates/openshell-providers/src/discovery.rs b/crates/openshell-providers/src/discovery.rs index 16cf497e92..2b2f0c4c8e 100644 --- a/crates/openshell-providers/src/discovery.rs +++ b/crates/openshell-providers/src/discovery.rs @@ -103,6 +103,7 @@ mod tests { name: "api_key".to_string(), env_vars: vec!["CUSTOM_API_KEY".to_string(), "CUSTOM_API_TOKEN".to_string()], required: true, + delivery: openshell_core::proto::ProviderCredentialDelivery::Environment, description: String::new(), auth_style: String::new(), header_name: String::new(), @@ -115,6 +116,7 @@ mod tests { name: "secondary".to_string(), env_vars: vec!["CUSTOM_API_KEY".to_string()], required: false, + delivery: openshell_core::proto::ProviderCredentialDelivery::Environment, description: String::new(), auth_style: String::new(), header_name: String::new(), diff --git a/crates/openshell-providers/src/profiles.rs b/crates/openshell-providers/src/profiles.rs index a1f9f130a1..ed2fbb3ddc 100644 --- a/crates/openshell-providers/src/profiles.rs +++ b/crates/openshell-providers/src/profiles.rs @@ -7,7 +7,7 @@ use openshell_core::proto::{ GraphqlOperation, L7Allow, L7DenyRule, L7QueryMatcher, L7Rule, McpOptions, NetworkBinary, - NetworkEndpoint, NetworkPolicyRule, ProviderCredentialRefresh, + NetworkEndpoint, NetworkPolicyRule, ProviderCredentialDelivery, ProviderCredentialRefresh, ProviderCredentialRefreshMaterial, ProviderCredentialRefreshOutput, ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrantSubjectToken, ProviderCredentialTokenGrantType, ProviderProfile, ProviderProfileCategory, @@ -100,6 +100,13 @@ pub struct CredentialProfile { pub env_vars: Vec, #[serde(default)] pub required: bool, + #[serde( + default = "default_credential_delivery", + deserialize_with = "deserialize_credential_delivery", + serialize_with = "serialize_credential_delivery", + skip_serializing_if = "is_environment_delivery" + )] + pub delivery: ProviderCredentialDelivery, #[serde(default)] pub auth_style: String, #[serde(default)] @@ -455,6 +462,10 @@ impl ProviderTypeProfile { description: credential.description.clone(), env_vars: credential.env_vars.clone(), required: credential.required, + delivery: effective_credential_delivery( + ProviderCredentialDelivery::try_from(credential.delivery) + .unwrap_or(ProviderCredentialDelivery::Unspecified), + ), auth_style: credential.auth_style.clone(), header_name: credential.header_name.clone(), query_param: credential.query_param.clone(), @@ -624,6 +635,7 @@ impl ProviderTypeProfile { description: credential.description.clone(), env_vars: credential.env_vars.clone(), required: credential.required, + delivery: credential.delivery as i32, auth_style: credential.auth_style.clone(), header_name: credential.header_name.clone(), query_param: credential.query_param.clone(), @@ -906,6 +918,24 @@ fn default_token_grant_type() -> ProviderCredentialTokenGrantType { ProviderCredentialTokenGrantType::ClientCredentials } +fn default_credential_delivery() -> ProviderCredentialDelivery { + ProviderCredentialDelivery::Environment +} + +fn effective_credential_delivery( + delivery: ProviderCredentialDelivery, +) -> ProviderCredentialDelivery { + match delivery { + ProviderCredentialDelivery::Unspecified => ProviderCredentialDelivery::Environment, + other => other, + } +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn is_environment_delivery(value: &ProviderCredentialDelivery) -> bool { + effective_credential_delivery(*value) == ProviderCredentialDelivery::Environment +} + fn effective_token_grant_type( grant_type: ProviderCredentialTokenGrantType, ) -> ProviderCredentialTokenGrantType { @@ -975,6 +1005,29 @@ where .ok_or_else(|| de::Error::custom(format!("unsupported provider token grant type: {raw}"))) } +fn deserialize_credential_delivery<'de, D>( + deserializer: D, +) -> Result +where + D: Deserializer<'de>, +{ + let raw = String::deserialize(deserializer)?; + provider_credential_delivery_from_yaml(&raw).ok_or_else(|| { + de::Error::custom(format!("unsupported provider credential delivery: {raw}")) + }) +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn serialize_credential_delivery( + delivery: &ProviderCredentialDelivery, + serializer: S, +) -> Result +where + S: Serializer, +{ + serializer.serialize_str(provider_credential_delivery_to_yaml(*delivery)) +} + #[allow(clippy::trivially_copy_pass_by_ref)] fn serialize_token_grant_type( grant_type: &ProviderCredentialTokenGrantType, @@ -1031,6 +1084,25 @@ pub fn provider_refresh_strategy_from_yaml(raw: &str) -> Option Option { + match raw.trim().to_ascii_lowercase().replace('-', "_").as_str() { + "" | "environment" => Some(ProviderCredentialDelivery::Environment), + "proxy" => Some(ProviderCredentialDelivery::Proxy), + _ => None, + } +} + +#[must_use] +pub fn provider_credential_delivery_to_yaml(delivery: ProviderCredentialDelivery) -> &'static str { + match delivery { + ProviderCredentialDelivery::Proxy => "proxy", + ProviderCredentialDelivery::Environment | ProviderCredentialDelivery::Unspecified => { + "environment" + } + } +} + #[must_use] pub fn provider_refresh_strategy_to_yaml( strategy: ProviderCredentialRefreshStrategy, @@ -1736,6 +1808,7 @@ pub fn validate_profile_set( )); let mut env_vars = HashSet::new(); + let mut proxy_delivery_credentials = 0; for credential in &profile.credentials { for env_var in &credential.env_vars { if env_var.trim().is_empty() { @@ -1767,6 +1840,9 @@ pub fn validate_profile_set( let auth_style = credential.auth_style.trim().to_ascii_lowercase(); match auth_style.as_str() { "" | "basic" => {} + "bearer" + if effective_credential_delivery(credential.delivery) + == ProviderCredentialDelivery::Proxy => {} "bearer" | "header" => { if credential.header_name.trim().is_empty() { diagnostics.push(ProfileValidationDiagnostic::error( @@ -1820,6 +1896,71 @@ pub fn validate_profile_set( )), } + if effective_credential_delivery(credential.delivery) + == ProviderCredentialDelivery::Proxy + { + proxy_delivery_credentials += 1; + if credential.env_vars.is_empty() { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.env_vars", + "proxy-delivered credentials must declare at least one env var", + )); + } + if profile.endpoints.is_empty() { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.delivery", + "proxy-delivered credentials require a profile endpoint", + )); + } + if credential.token_grant.is_some() { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.delivery", + "proxy delivery is only valid for static credentials", + )); + } + if !matches!(auth_style.as_str(), "bearer" | "header") { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.auth_style", + "proxy-delivered credentials support auth_style bearer or header", + )); + } else if let Err(message) = validate_proxy_delivery_header_name(credential) { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.header_name", + message, + )); + } + + for (index, endpoint) in profile.endpoints.iter().enumerate() { + let protocol = endpoint.protocol.trim().to_ascii_lowercase(); + if protocol != "rest" { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + format!("endpoints[{index}].protocol"), + "proxy-delivered credentials require protocol: rest", + )); + } + if endpoint.tls.trim().eq_ignore_ascii_case("skip") { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + format!("endpoints[{index}].tls"), + "proxy-delivered credentials do not support tls: skip", + )); + } + } + } + if let Some(refresh) = credential.refresh.as_ref() { if refresh.strategy == ProviderCredentialRefreshStrategy::Unspecified { diagnostics.push(ProfileValidationDiagnostic::error( @@ -2040,6 +2181,28 @@ pub fn validate_profile_set( } } + if proxy_delivery_credentials > 1 { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.delivery", + "provider profiles support only one proxy-delivered credential", + )); + } + if proxy_delivery_credentials > 0 + && profile + .credentials + .iter() + .any(|credential| credential.token_grant.is_some()) + { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.delivery", + "provider profiles cannot combine proxy-delivered credentials with token grants", + )); + } + for (index, endpoint) in profile.endpoints.iter().enumerate() { if !endpoint_is_valid(endpoint) { diagnostics.push(ProfileValidationDiagnostic::error( @@ -2864,13 +3027,24 @@ fn validate_token_grant_auth_style(credential: &CredentialProfile) -> Result<(), } fn validate_token_grant_header_name(credential: &CredentialProfile) -> Result<(), String> { + validate_injected_header_name(credential, "token_grant") +} + +fn validate_proxy_delivery_header_name(credential: &CredentialProfile) -> Result<(), String> { + validate_injected_header_name(credential, "proxy delivery") +} + +fn validate_injected_header_name( + credential: &CredentialProfile, + context: &str, +) -> Result<(), String> { let header_name = match credential.auth_style.trim().to_ascii_lowercase().as_str() { "" | "bearer" if credential.header_name.trim().is_empty() => "Authorization", "" | "bearer" | "header" => credential.header_name.trim(), _ => return Ok(()), }; if header_name.is_empty() { - return Ok(()); + return Err(format!("{context} auth_style header requires header_name")); } let valid = header_name.bytes().all(|byte| { byte.is_ascii_alphanumeric() @@ -2893,13 +3067,14 @@ fn validate_token_grant_header_name(credential: &CredentialProfile) -> Result<() ) }); if !valid { - return Err("token_grant header_name is not a valid HTTP header name".to_string()); + return Err(format!( + "{context} header_name is not a valid HTTP header name" + )); } match header_name.to_ascii_lowercase().as_str() { - "host" | "content-length" | "transfer-encoding" | "connection" => Err( - "token_grant header_name may not override HTTP framing or connection headers" - .to_string(), - ), + "host" | "content-length" | "transfer-encoding" | "connection" => Err(format!( + "{context} header_name may not override HTTP framing or connection headers" + )), _ => Ok(()), } } @@ -2954,7 +3129,9 @@ pub fn builtin_profiles() -> &'static [ProviderTypeProfile] { mod tests { use std::collections::HashMap; - use openshell_core::proto::{ProviderCredentialTokenGrantType, ProviderProfileCategory}; + use openshell_core::proto::{ + ProviderCredentialDelivery, ProviderCredentialTokenGrantType, ProviderProfileCategory, + }; use super::{ DiscoveryProfile, L7AllowProfile, L7QueryMatcherProfile, ProfileError, ProviderTypeProfile, @@ -3518,6 +3695,112 @@ credentials: ); } + #[test] + fn proxy_credential_delivery_round_trips_and_environment_remains_default() { + let profile = parse_profile_yaml( + r" +id: proxy-auth +display_name: Proxy Auth +credentials: + - name: environment_key + env_vars: [ENVIRONMENT_API_KEY] + auth_style: bearer + header_name: authorization + - name: proxy_header + env_vars: [PROXY_HEADER_KEY, PROXY_HEADER_KEY_FALLBACK] + delivery: proxy + auth_style: header + header_name: x-api-key +endpoints: + - host: api.example.com + port: 443 + protocol: rest + access: full +", + ) + .expect("profile should parse"); + + let diagnostics = validate_profile_set(&[("proxy-auth.yaml".to_string(), profile.clone())]); + assert!( + diagnostics.is_empty(), + "unexpected diagnostics: {diagnostics:?}" + ); + assert_eq!( + profile.credentials[1].delivery, + ProviderCredentialDelivery::Proxy + ); + assert_eq!(profile.credentials[1].env_vars.len(), 2); + assert_eq!( + profile.credentials[0].delivery, + ProviderCredentialDelivery::Environment + ); + + let exported = profile_to_yaml(&ProviderTypeProfile::from_proto(&profile.to_proto())) + .expect("serialize YAML"); + assert!(exported.contains("delivery: proxy")); + assert_eq!(exported.matches("delivery: proxy").count(), 1); + } + + #[test] + fn proxy_delivery_requires_an_env_var_and_one_credential_per_profile() { + let profile = parse_profile_yaml( + r" +id: invalid-proxy-auth +display_name: Invalid Proxy Auth +credentials: + - name: missing_env + delivery: proxy + auth_style: bearer + - name: second_proxy + env_vars: [SECOND_KEY] + delivery: proxy + auth_style: header + header_name: x-api-key +endpoints: + - host: api.example.com + port: 443 + protocol: rest + access: full +", + ) + .expect("profile should parse"); + + let diagnostics = validate_profile_set(&[("invalid.yaml".to_string(), profile)]); + assert!(diagnostics.iter().any(|diagnostic| diagnostic.message + == "proxy-delivered credentials must declare at least one env var")); + assert!(diagnostics.iter().any(|diagnostic| diagnostic.message + == "provider profiles support only one proxy-delivered credential")); + } + + #[test] + fn proxy_delivery_cannot_share_a_profile_with_token_grants() { + let profile = parse_profile_yaml( + r" +id: mixed-runtime-auth +display_name: Mixed Runtime Auth +credentials: + - name: api_key + env_vars: [API_KEY] + delivery: proxy + auth_style: bearer + - name: access_token + auth_style: bearer + token_grant: + token_endpoint: https://login.example.com/oauth2/token +endpoints: + - host: api.example.com + port: 443 + protocol: rest + access: full +", + ) + .expect("profile should parse"); + + let diagnostics = validate_profile_set(&[("mixed.yaml".to_string(), profile)]); + assert!(diagnostics.iter().any(|diagnostic| diagnostic.message + == "provider profiles cannot combine proxy-delivered credentials with token grants")); + } + #[test] fn token_grant_audience_overrides_round_trip_through_proto() { let profile = parse_profile_yaml( diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index 764c0bbfde..d06919fdbf 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -16,7 +16,7 @@ use crate::provider_profile_sources::{ }; use openshell_core::metadata::ObjectWorkspace; use openshell_core::proto::{ - CredentialHandle, Provider, ProviderCredentialRefreshStrategy, + CredentialHandle, Provider, ProviderCredentialDelivery, ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrantAudienceOverride, ProviderCredentialTokenGrantType, ProviderProfile, ProviderProfileCredential, Sandbox, StaticCredentialBinding, StaticCredentialEndpointBinding, StoredProviderCredentialRefreshState, @@ -64,6 +64,7 @@ fn redact_provider_credentials(mut provider: Provider) -> Provider { pub(super) struct ProviderEnvironment { pub environment: HashMap, pub credential_expires_at_ms: HashMap, + /// Endpoint metadata for token grants and proxy-delivered static credentials. pub dynamic_credentials: HashMap, pub static_credential_bindings: HashMap, pub static_credential_keys: HashSet, @@ -1286,6 +1287,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin key, endpoints, refresh_epochs.get(key).map(String::as_str), + profile_credential_delivery_for_key(profile_proto.as_ref(), key), ), ); } @@ -1356,6 +1358,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin &key, endpoints, refresh_epochs.get(&key).map(String::as_str), + profile_credential_delivery_for_key(profile_proto.as_ref(), &key), ), ); } @@ -1381,7 +1384,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin Ok(ProviderEnvironment { environment: env, credential_expires_at_ms: expires, - dynamic_credentials: resolve_dynamic_credentials_from_records(catalog, records), + dynamic_credentials: resolve_runtime_credentials_from_records(catalog, records), static_credential_bindings, static_credential_keys, }) @@ -1426,6 +1429,7 @@ fn static_credential_binding( key: &str, endpoints: &[StaticCredentialEndpointBinding], authorization_epoch: Option<&str>, + delivery: ProviderCredentialDelivery, ) -> StaticCredentialBinding { let workload_credential_handle = sandbox_id .zip(authorization_epoch) @@ -1442,9 +1446,26 @@ fn static_credential_binding( endpoints: endpoints.to_vec(), credential_identity, workload_credential_handle, + delivery: delivery as i32, } } +fn profile_credential_delivery_for_key( + profile: Option<&ProviderProfile>, + key: &str, +) -> ProviderCredentialDelivery { + profile + .and_then(|profile| { + profile + .credentials + .iter() + .find(|credential| credential.env_vars.iter().any(|env_var| env_var == key)) + }) + .and_then(|credential| ProviderCredentialDelivery::try_from(credential.delivery).ok()) + .filter(|delivery| *delivery == ProviderCredentialDelivery::Proxy) + .unwrap_or(ProviderCredentialDelivery::Environment) +} + fn derive_workload_credential_handle( sandbox_id: &str, provider_id: &str, @@ -1489,9 +1510,9 @@ fn hash_handle_component(hasher: &mut Sha256, value: &[u8]) { hasher.update(value); } -/// Resolve dynamic credentials (token grants) from the same records used for -/// the provider-environment revision and static credential bindings. -fn resolve_dynamic_credentials_from_records( +/// Resolve runtime-injected credentials from the same records used for the +/// provider-environment revision and static credential bindings. +fn resolve_runtime_credentials_from_records( catalog: &EffectiveProviderProfileCatalog, records: &[ProviderEnvironmentRecord], ) -> HashMap { @@ -1505,7 +1526,7 @@ fn resolve_dynamic_credentials_from_records( ) else { continue; }; - insert_dynamic_credentials_for_profile( + insert_runtime_credentials_for_profile( &mut dynamic_creds, &profile.to_proto(), &record.name, @@ -1514,13 +1535,16 @@ fn resolve_dynamic_credentials_from_records( dynamic_creds } -fn insert_dynamic_credentials_for_profile( +fn insert_runtime_credentials_for_profile( dynamic_creds: &mut HashMap, profile: &ProviderProfile, provider_name: &str, ) { for credential in &profile.credentials { - if credential.token_grant.is_none() { + if credential.token_grant.is_none() + && ProviderCredentialDelivery::try_from(credential.delivery).unwrap_or_default() + != ProviderCredentialDelivery::Proxy + { continue; } for endpoint in &profile.endpoints { @@ -1887,7 +1911,7 @@ async fn validate_provider_environment_keys_unique_at( ) -> Result<(), Status> { let mut seen_credentials = HashMap::::new(); let mut seen_plugin_config = HashMap::::new(); - let mut dynamic_bindings = Vec::new(); + let mut runtime_bindings = Vec::new(); for name in provider_names { let provider = match candidate_provider { Some(candidate) if candidate.object_name() == name.as_str() => candidate.clone(), @@ -1907,11 +1931,11 @@ async fn validate_provider_environment_keys_unique_at( active_provider_environment_keys(store, catalog, &provider, now_ms).await?, provider_plugin_environment_keys(catalog, &provider), )?; - dynamic_bindings.extend(dynamic_token_grant_bindings_for_provider_with_catalog( + runtime_bindings.extend(runtime_credential_bindings_for_provider_with_catalog( catalog, &provider, )); } - validate_dynamic_token_grant_bindings_unambiguous(&dynamic_bindings)?; + validate_runtime_credential_bindings_unambiguous(&runtime_bindings)?; Ok(()) } @@ -1923,7 +1947,7 @@ async fn validate_provider_environment_records_unique_at( ) -> Result<(), Status> { let mut seen_credentials = HashMap::::new(); let mut seen_plugin_config = HashMap::::new(); - let mut dynamic_bindings = Vec::new(); + let mut runtime_bindings = Vec::new(); for record in records { let provider = &record.provider; validate_provider_environment_key_ownership( @@ -1940,11 +1964,11 @@ async fn validate_provider_environment_records_unique_at( .await?, provider_plugin_environment_keys(catalog, provider), )?; - dynamic_bindings.extend(dynamic_token_grant_bindings_for_provider_with_catalog( + runtime_bindings.extend(runtime_credential_bindings_for_provider_with_catalog( catalog, provider, )); } - validate_dynamic_token_grant_bindings_unambiguous(&dynamic_bindings)?; + validate_runtime_credential_bindings_unambiguous(&runtime_bindings)?; Ok(()) } @@ -2031,35 +2055,39 @@ fn provider_credential_config_key_collision( } #[derive(Debug, Clone, PartialEq, Eq)] -struct DynamicTokenGrantBinding { +struct RuntimeCredentialBinding { provider_name: String, credential_name: String, host: String, port: u32, path: String, score: u32, + proxy_delivery: bool, } -fn dynamic_token_grant_bindings_for_provider_with_catalog( +fn runtime_credential_bindings_for_provider_with_catalog( catalog: &EffectiveProviderProfileCatalog, provider: &Provider, -) -> Vec { +) -> Vec { let provider_name = provider.object_name().to_string(); let Some(profile) = get_provider_type_profile_for_scope(catalog, &provider.r#type, &provider.profile_workspace) else { return Vec::new(); }; - dynamic_token_grant_bindings_for_profile(&provider_name, &profile.to_proto()) + runtime_credential_bindings_for_profile(&provider_name, &profile.to_proto()) } -fn dynamic_token_grant_bindings_for_profile( +fn runtime_credential_bindings_for_profile( provider_name: &str, profile: &ProviderProfile, -) -> Vec { +) -> Vec { let mut bindings = Vec::new(); for credential in &profile.credentials { - if credential.token_grant.is_none() { + if credential.token_grant.is_none() + && ProviderCredentialDelivery::try_from(credential.delivery).unwrap_or_default() + != ProviderCredentialDelivery::Proxy + { continue; } for endpoint in &profile.endpoints { @@ -2079,20 +2107,22 @@ fn dynamic_token_grant_bindings_for_profile( } fn push_dynamic_token_grant_bindings_for_endpoint( - bindings: &mut Vec, + bindings: &mut Vec, provider_name: &str, credential: &ProviderProfileCredential, endpoint_host: &str, endpoint_port: u32, endpoint_path: &str, ) { - push_dynamic_token_grant_binding( + push_runtime_credential_binding( bindings, provider_name, &credential.name, endpoint_host, endpoint_port, endpoint_path, + ProviderCredentialDelivery::try_from(credential.delivery).unwrap_or_default() + == ProviderCredentialDelivery::Proxy, ); let Some(token_grant) = credential.token_grant.as_ref() else { @@ -2118,40 +2148,43 @@ fn push_dynamic_token_grant_bindings_for_endpoint( } else { override_config.path.as_str() }; - push_dynamic_token_grant_binding( + push_runtime_credential_binding( bindings, provider_name, &credential.name, override_host, override_port, override_path, + false, ); } } -fn push_dynamic_token_grant_binding( - bindings: &mut Vec, +fn push_runtime_credential_binding( + bindings: &mut Vec, provider_name: &str, credential_name: &str, host: &str, port: u32, path: &str, + proxy_delivery: bool, ) { - let candidate = DynamicTokenGrantBinding { + let candidate = RuntimeCredentialBinding { provider_name: provider_name.to_string(), credential_name: credential_name.to_string(), host: host.to_ascii_lowercase(), port, path: path.to_string(), score: dynamic_token_grant_match_score(host, path), + proxy_delivery, }; if !bindings.iter().any(|binding| binding == &candidate) { bindings.push(candidate); } } -fn validate_dynamic_token_grant_bindings_unambiguous( - bindings: &[DynamicTokenGrantBinding], +fn validate_runtime_credential_bindings_unambiguous( + bindings: &[RuntimeCredentialBinding], ) -> Result<(), Status> { for (index, first) in bindings.iter().enumerate() { for second in bindings.iter().skip(index + 1) { @@ -2160,13 +2193,14 @@ fn validate_dynamic_token_grant_bindings_unambiguous( { continue; } - if first.port == second.port - && first.score == second.score - && host_patterns_can_overlap(&first.host, &second.host) - && path_patterns_can_overlap(&first.path, &second.path) - { + if runtime_credential_bindings_are_ambiguous(first, second) { + let guidance = if first.proxy_delivery || second.proxy_delivery { + "attach only one matching provider" + } else { + "make one host/path selector more specific or attach only one matching provider" + }; return Err(Status::failed_precondition(format!( - "dynamic token grants for '{}:{}' and '{}:{}' are ambiguous for {}:{} path selectors '{}' and '{}'; make one host/path selector more specific or attach only one matching provider", + "runtime-injected credentials for '{}:{}' and '{}:{}' are ambiguous for {}:{} path selectors '{}' and '{}'; {guidance}", first.provider_name, first.credential_name, second.provider_name, @@ -2182,6 +2216,16 @@ fn validate_dynamic_token_grant_bindings_unambiguous( Ok(()) } +fn runtime_credential_bindings_are_ambiguous( + first: &RuntimeCredentialBinding, + second: &RuntimeCredentialBinding, +) -> bool { + first.port == second.port + && (first.proxy_delivery || second.proxy_delivery || first.score == second.score) + && host_patterns_can_overlap(&first.host, &second.host) + && path_patterns_can_overlap(&first.path, &second.path) +} + async fn active_provider_environment_keys( store: &Store, catalog: &EffectiveProviderProfileCatalog, @@ -3517,7 +3561,7 @@ async fn profile_attached_sandbox_diagnostics( let scope_mismatch = (is_platform_scope && !provider.profile_workspace.is_empty()) || (!is_platform_scope && provider.profile_workspace.is_empty()); if scope_mismatch { - bindings.extend(dynamic_token_grant_bindings_for_provider_with_catalog( + bindings.extend(runtime_credential_bindings_for_provider_with_catalog( catalog, &provider, )); if validate_policy_composition @@ -3570,7 +3614,7 @@ async fn profile_attached_sandbox_diagnostics( severity: "error".to_string(), }); } - bindings.extend(dynamic_token_grant_bindings_for_profile( + bindings.extend(runtime_credential_bindings_for_profile( provider.object_name(), &profile.to_proto(), )); @@ -3586,7 +3630,7 @@ async fn profile_attached_sandbox_diagnostics( imported_profiles_used.push(used); } } else { - bindings.extend(dynamic_token_grant_bindings_for_provider_with_catalog( + bindings.extend(runtime_credential_bindings_for_provider_with_catalog( catalog, &provider, )); if validate_policy_composition @@ -3608,14 +3652,14 @@ async fn profile_attached_sandbox_diagnostics( if imported_profiles_used.is_empty() { continue; } - if let Err(err) = validate_dynamic_token_grant_bindings_unambiguous(&bindings) { + if let Err(err) = validate_runtime_credential_bindings_unambiguous(&bindings) { for (source, profile_id) in &imported_profiles_used { diagnostics.push(ProfileValidationDiagnostic { source: source.clone(), profile_id: profile_id.clone(), - field: "credentials.token_grant.audience_overrides".to_string(), + field: "credentials".to_string(), message: format!( - "{operation} would create ambiguous dynamic token grants on sandbox '{sandbox_name}': {}", + "{operation} would create ambiguous runtime-injected credentials on sandbox '{sandbox_name}': {}", err.message() ), severity: "error".to_string(), @@ -5074,6 +5118,7 @@ mod tests { ) .collect(), }), + delivery: 0, }; let profile = ProviderProfile { id: "keycloak-sso".to_string(), @@ -5099,7 +5144,7 @@ mod tests { }; let mut dynamic_creds = HashMap::new(); - insert_dynamic_credentials_for_profile(&mut dynamic_creds, &profile, "keycloak"); + insert_runtime_credentials_for_profile(&mut dynamic_creds, &profile, "keycloak"); assert_eq!(dynamic_creds.len(), 4); for (host, audience) in service_audiences { @@ -5191,10 +5236,31 @@ mod tests { .expect_err("equal-specificity dynamic grants should be ambiguous"); assert_eq!(err.code(), Code::FailedPrecondition); - assert!(err.message().contains("dynamic token grants")); + assert!(err.message().contains("runtime-injected credentials")); assert!(err.message().contains("ambiguous")); } + #[test] + fn proxy_delivery_rejects_overlapping_bindings_at_different_specificity() { + let binding = |provider: &str, path: &str| RuntimeCredentialBinding { + provider_name: provider.to_string(), + credential_name: "api_key".to_string(), + host: "api.example.com".to_string(), + port: 443, + path: path.to_string(), + score: dynamic_token_grant_match_score("api.example.com", path), + proxy_delivery: true, + }; + let error = validate_runtime_credential_bindings_unambiguous(&[ + binding("provider-a", "/v1/**"), + binding("provider-b", "/v1/chat/**"), + ]) + .expect_err("overlapping proxy delivery must fail before request handling"); + + assert!(error.message().contains("runtime-injected credentials")); + assert!(error.message().contains("ambiguous")); + } + #[tokio::test] async fn dynamic_token_grants_allow_more_specific_path_overlap() { let state = test_server_state().await; @@ -5286,7 +5352,7 @@ mod tests { assert!(response.diagnostics.iter().any(|diagnostic| { diagnostic .message - .contains("import would create ambiguous dynamic token grants") + .contains("import would create ambiguous runtime-injected credentials") })); } @@ -5624,7 +5690,7 @@ mod tests { assert!(response.diagnostics.iter().any(|diagnostic| { diagnostic .message - .contains("update would create ambiguous dynamic token grants") + .contains("update would create ambiguous runtime-injected credentials") })); } @@ -5775,6 +5841,7 @@ mod tests { ], }), token_grant: None, + delivery: 0, } } @@ -5814,6 +5881,7 @@ mod tests { refresh: None, path_template: String::new(), token_grant: None, + delivery: 0, } } @@ -5841,9 +5909,88 @@ mod tests { cache_ttl_seconds: 300, audience_overrides: Vec::new(), }), + delivery: 0, } } + #[tokio::test] + async fn resolved_proxy_delivery_keeps_credential_out_of_workload_environment() { + let state = test_server_state().await; + let mut credential = static_credential("api_key", "API_KEY", true); + credential.delivery = ProviderCredentialDelivery::Proxy as i32; + let mut profile = custom_profile("proxy-auth-provider"); + profile.credentials = vec![credential]; + profile.endpoints = vec![NetworkEndpoint { + host: "api.example.com".to_string(), + port: 443, + path: "/v1/**".to_string(), + protocol: "rest".to_string(), + access: "full".to_string(), + ..Default::default() + }]; + handle_import_provider_profiles( + &state, + authed_request(ImportProviderProfilesRequest { + profiles: vec![ProviderProfileImportItem { + profile: Some(profile), + source: "proxy-auth-provider.yaml".to_string(), + }], + workspace: "default".to_string(), + }), + ) + .await + .expect("import profile"); + create_provider_record( + state.store.as_ref(), + "default", + provider_with_credential_value( + "proxy-auth", + "proxy-auth-provider", + "API_KEY", + "secret", + ), + ) + .await + .expect("create provider"); + + let names = vec!["proxy-auth".to_string()]; + let catalog = ProviderProfileSources::with_default_sources() + .snapshot_catalog(state.store.as_ref(), "default") + .await + .expect("catalog"); + let records = load_provider_environment_records(state.store.as_ref(), "default", &names) + .await + .expect("records"); + let environment = resolve_provider_environment_from_records_with_policy_bindings( + state.store.as_ref(), + &catalog, + &records, + &HashMap::new(), + ) + .await + .expect("provider environment"); + let credentials = + openshell_core::provider_credentials::ProviderCredentialState::from_bound_environment( + 1, + environment.environment, + environment.credential_expires_at_ms, + environment.dynamic_credentials, + environment.static_credential_bindings, + Vec::new(), + ) + .expect("credential state"); + + assert!(!credentials.snapshot().child_env.contains_key("API_KEY")); + assert_eq!( + credentials + .resolver_for_endpoint("api.example.com", 443, "/v1/messages") + .expect("endpoint resolver") + .resolve_current_env_key_checked("API_KEY", "test") + .expect("authorized endpoint"), + Some("secret") + ); + } + #[tokio::test] async fn list_provider_profiles_returns_built_in_profile_categories() { let state = test_server_state().await; @@ -9034,6 +9181,7 @@ mod tests { ], }), token_grant: None, + delivery: 0, }], endpoints: vec![], binaries: vec![], diff --git a/crates/openshell-supervisor-network/src/l7/relay.rs b/crates/openshell-supervisor-network/src/l7/relay.rs index 2697fedb3c..9c6a295c5d 100644 --- a/crates/openshell-supervisor-network/src/l7/relay.rs +++ b/crates/openshell-supervisor-network/src/l7/relay.rs @@ -66,7 +66,8 @@ pub struct L7EvalContext { pub(crate) provider_credential_revision: Option, /// Anonymous activity counter channel. pub(crate) activity_tx: Option, - /// Dynamic credentials (token grants) keyed by endpoint-bound provider metadata. + /// Runtime-injected credentials (token grants and proxy-delivered static + /// credentials) keyed by endpoint-bound provider metadata. pub(crate) dynamic_credentials: Option< Arc< std::sync::RwLock< @@ -1562,6 +1563,21 @@ where }; let scoped_ctx = scoped_context_for_request(ctx, &req_with_auth); let ctx = scoped_ctx.as_ref().unwrap_or(ctx); + let req_with_auth = + match crate::l7::token_grant_injection::inject_static_if_needed(req_with_auth, ctx) + { + Ok(req) => req, + Err(error) => { + warn!( + host = %ctx.host, + port = ctx.port, + error = %error, + "Static provider credential injection failed in L7 relay" + ); + write_bad_gateway_response(client).await?; + return Ok(()); + } + }; // Forward request to upstream and relay response let outcome_result = relay_http_request_with_credential_rejection( @@ -2810,6 +2826,20 @@ where }; let scoped_ctx = scoped_context_for_request(ctx, &req_with_auth); let ctx = scoped_ctx.as_ref().unwrap_or(ctx); + let req_with_auth = + match crate::l7::token_grant_injection::inject_static_if_needed(req_with_auth, ctx) { + Ok(req) => req, + Err(error) => { + warn!( + host = %ctx.host, + port = ctx.port, + error = %error, + "Static provider credential injection failed in passthrough relay" + ); + write_bad_gateway_response(client).await?; + return Ok(()); + } + }; let resolver = ctx.secret_resolver.as_deref(); // Forward request with credential rewriting and relay the response. @@ -2890,6 +2920,7 @@ mod tests { }], credential_identity: identity.to_string(), workload_credential_handle: String::new(), + delivery: 0, } } @@ -4680,6 +4711,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -4784,6 +4816,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -5433,6 +5466,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -7893,6 +7927,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), diff --git a/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs b/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs index fc440c6547..5665937fc1 100644 --- a/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs +++ b/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs @@ -8,7 +8,9 @@ use std::pin::Pin; use std::sync::Arc; use miette::{Result, miette}; -use openshell_core::proto::{ProviderCredentialTokenGrant, ProviderProfileCredential}; +use openshell_core::proto::{ + ProviderCredentialDelivery, ProviderCredentialTokenGrant, ProviderProfileCredential, +}; use openshell_ocsf::{ ActionId, ActivityId, DispositionId, Endpoint, HttpActivityBuilder, HttpRequest, SeverityId, StatusId, Url as OcsfUrl, ctx::ctx as ocsf_ctx, ocsf_emit, @@ -162,6 +164,55 @@ pub async fn inject_if_needed(req: L7Request, ctx: &L7EvalContext) -> Result Result { + let request_path = req.target.split('?').next().unwrap_or(req.target.as_str()); + let credential = ctx.dynamic_credentials.as_ref().and_then(|credentials| { + credentials.read().map_or(None, |credentials| { + credentials + .iter() + .filter_map(|(key, credential)| { + let score = + dynamic_credential_key_match_score(key, &ctx.host, ctx.port, request_path)?; + (ProviderCredentialDelivery::try_from(credential.delivery).unwrap_or_default() + == ProviderCredentialDelivery::Proxy) + .then(|| (score, key.clone(), credential.clone())) + }) + .max_by_key(|(score, key, _)| (*score, key.clone())) + .map(|(_, key, credential)| (key, credential)) + }) + }); + + let Some((provider_key, credential)) = credential else { + return Ok(req); + }; + let resolver = ctx + .secret_resolver + .as_deref() + .ok_or_else(|| miette!("proxy-delivered credential unavailable for {provider_key}"))?; + let value = credential + .env_vars + .iter() + .find_map(|key| { + resolver + .resolve_current_env_key_checked(key, "proxy-delivered credential") + .transpose() + }) + .transpose()? + .ok_or_else(|| miette!("proxy-delivered credential unavailable for {provider_key}"))?; + let (header_name, header_value) = + injected_credential_header(&credential, value, "proxy delivery")?; + let raw_header = inject_header(&req.raw_header, &header_name, &header_value)?; + + Ok(L7Request { + action: req.action, + target: req.target, + query_params: req.query_params, + raw_header, + body_length: req.body_length, + }) +} + fn ocsf_message_field(value: &str) -> String { value .chars() @@ -245,42 +296,55 @@ fn inject_token_grant_header( credential: &ProviderProfileCredential, access_token: &str, ) -> Result> { - crate::token_grant::validate_access_token(access_token)?; - let (header_name, header_value) = token_grant_header(credential, access_token)?; + let (header_name, header_value) = + injected_credential_header(credential, access_token, "token grant")?; inject_header(raw_header, &header_name, &header_value) } -fn token_grant_header( +fn injected_credential_header( credential: &ProviderProfileCredential, access_token: &str, + context: &str, ) -> Result<(String, String)> { match credential.auth_style.trim().to_ascii_lowercase().as_str() { "" | "bearer" => { + crate::token_grant::validate_access_token(access_token)?; let header_name = if credential.header_name.trim().is_empty() { "Authorization" } else { credential.header_name.trim() }; - validate_header_name(header_name)?; + validate_header_name(header_name, context)?; Ok((header_name.to_string(), format!("Bearer {access_token}"))) } "header" => { let header_name = credential.header_name.trim(); if header_name.is_empty() { - return Err(miette!( - "token grant auth_style header requires header_name" - )); + return Err(miette!("{context} auth_style header requires header_name")); } - validate_header_name(header_name)?; + validate_header_name(header_name, context)?; + validate_header_value(access_token, context)?; Ok((header_name.to_string(), access_token.to_string())) } other => Err(miette!( - "token grant auth_style '{other}' is not supported; use bearer or header" + "{context} auth_style '{other}' is not supported; use bearer or header" )), } } -fn validate_header_name(header_name: &str) -> Result<()> { +fn validate_header_value(value: &str, context: &str) -> Result<()> { + if value + .bytes() + .any(|byte| (byte < b' ' && byte != b'\t') || byte == 0x7f) + { + return Err(miette!( + "{context} credential contains invalid HTTP header value characters" + )); + } + Ok(()) +} + +fn validate_header_name(header_name: &str, context: &str) -> Result<()> { let valid = !header_name.is_empty() && header_name.bytes().all(|byte| { byte.is_ascii_alphanumeric() @@ -304,12 +368,12 @@ fn validate_header_name(header_name: &str) -> Result<()> { }); if !valid { return Err(miette!( - "token grant header_name is not a valid HTTP header name" + "{context} header_name is not a valid HTTP header name" )); } match header_name.to_ascii_lowercase().as_str() { "host" | "content-length" | "transfer-encoding" | "connection" => Err(miette!( - "token grant header_name may not override HTTP framing or connection headers" + "{context} header_name may not override HTTP framing or connection headers" )), _ => Ok(()), } @@ -573,6 +637,8 @@ mod tests { use super::*; use crate::l7::provider::{BodyLength, L7Request}; use crate::l7::token_grant_injection::test_support::TokenGrantTestFixture; + use openshell_core::proto::{StaticCredentialBinding, StaticCredentialEndpointBinding}; + use openshell_core::provider_credentials::ProviderCredentialState; fn credential(auth_style: &str, header_name: &str) -> ProviderProfileCredential { ProviderProfileCredential { @@ -749,8 +815,12 @@ mod tests { #[test] fn token_grant_header_rejects_framing_and_connection_headers() { for header_name in ["Host", "Content-Length", "Transfer-Encoding", "Connection"] { - let err = token_grant_header(&credential("header", header_name), "grant-token") - .expect_err("framing header override should be rejected"); + let err = injected_credential_header( + &credential("header", header_name), + "grant-token", + "token grant", + ) + .expect_err("framing header override should be rejected"); assert_eq!( err.to_string(), "token grant header_name may not override HTTP framing or connection headers" @@ -758,6 +828,34 @@ mod tests { } } + #[test] + fn proxy_delivery_named_header_accepts_safe_non_token_value() { + let header = injected_credential_header( + &credential("header", "x-api-key"), + "key with spaces = allowed", + "proxy delivery", + ) + .expect("safe HTTP field value"); + assert_eq!( + header, + ( + "x-api-key".to_string(), + "key with spaces = allowed".to_string() + ) + ); + + let error = injected_credential_header( + &credential("header", "x-api-key"), + "safe\r\ninjected: value", + "proxy delivery", + ) + .expect_err("CRLF injection must be rejected"); + assert_eq!( + error.to_string(), + "proxy delivery credential contains invalid HTTP header value characters" + ); + } + #[test] fn inject_token_grant_header_preserves_header_terminator_before_body() { let raw = b"POST /v1 HTTP/1.1\r\nHost: api.example.com\r\nContent-Length: 2\r\n\r\nOK"; @@ -802,6 +900,67 @@ mod tests { ); } + #[test] + fn proxy_delivery_replaces_header_without_changing_body() { + let dynamic_credentials = + Arc::new(std::sync::RwLock::new(std::collections::HashMap::from([( + "api.example.com\t443\t/v1/**\tprovider:api_key".to_string(), + ProviderProfileCredential { + name: "api_key".to_string(), + env_vars: vec!["API_KEY".to_string()], + auth_style: "bearer".to_string(), + delivery: ProviderCredentialDelivery::Proxy as i32, + ..Default::default() + }, + )]))); + let state = ProviderCredentialState::from_bound_environment( + 7, + std::collections::HashMap::from([("API_KEY".to_string(), "real-secret".to_string())]), + std::collections::HashMap::new(), + std::collections::HashMap::new(), + std::collections::HashMap::from([( + "API_KEY".to_string(), + StaticCredentialBinding { + endpoints: vec![StaticCredentialEndpointBinding { + host: "api.example.com".to_string(), + port: 443, + path: "/v1/**".to_string(), + }], + credential_identity: "provider:API_KEY".to_string(), + workload_credential_handle: String::new(), + delivery: ProviderCredentialDelivery::Proxy as i32, + }, + )]), + Vec::new(), + ) + .expect("valid provider state"); + let ctx = L7EvalContext { + host: "api.example.com".to_string(), + port: 443, + secret_resolver: state.resolver_for_endpoint("api.example.com", 443, "/v1/chat"), + provider_credentials: Some(state), + provider_credential_revision: Some(7), + dynamic_credentials: Some(dynamic_credentials), + ..Default::default() + }; + let body = br#"{"messages":[{"content":"ordinary environment output"}]}"#; + let mut raw_header = b"POST /v1/chat HTTP/1.1\r\nHost: api.example.com\r\nAuthorization: Bearer openshell-managed\r\n\r\n".to_vec(); + raw_header.extend_from_slice(body); + let request = L7Request { + action: "POST".to_string(), + target: "/v1/chat".to_string(), + query_params: std::collections::HashMap::new(), + raw_header, + body_length: BodyLength::ContentLength(body.len() as u64), + }; + + let injected = inject_static_if_needed(request, &ctx).expect("credential injection"); + let text = String::from_utf8(injected.raw_header).expect("HTTP request is UTF-8"); + assert!(text.contains("Authorization: Bearer real-secret\r\n")); + assert!(!text.contains("openshell-managed")); + assert!(text.ends_with(std::str::from_utf8(body).expect("body is UTF-8"))); + } + #[tokio::test] async fn inject_if_needed_uses_configured_resolver() { let fixture = TokenGrantTestFixture::success( diff --git a/crates/openshell-supervisor-network/src/l7/websocket.rs b/crates/openshell-supervisor-network/src/l7/websocket.rs index 6cd8aa4818..21aca41eaa 100644 --- a/crates/openshell-supervisor-network/src/l7/websocket.rs +++ b/crates/openshell-supervisor-network/src/l7/websocket.rs @@ -2682,6 +2682,7 @@ network_policies: }], credential_identity: "provider-a:DISCORD_BOT_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), diff --git a/crates/openshell-supervisor-network/src/proxy.rs b/crates/openshell-supervisor-network/src/proxy.rs index dc2736a4ea..ee0275b0db 100644 --- a/crates/openshell-supervisor-network/src/proxy.rs +++ b/crates/openshell-supervisor-network/src/proxy.rs @@ -4783,6 +4783,33 @@ async fn inject_token_grant_for_forward_request( forward_request_bytes: Vec, l7_ctx: &crate::l7::relay::L7EvalContext, ) -> Result> { + let request = forward_request_for_injection(method, upstream_target, forward_request_bytes)?; + crate::l7::token_grant_injection::inject_if_needed(request, l7_ctx) + .await + .map(|req| req.raw_header) +} + +fn inject_static_credential_for_forward_request( + method: &str, + upstream_target: &str, + forward_request_bytes: Vec, + l7_ctx: &crate::l7::relay::L7EvalContext, + secret_resolver: Option>, + revision: Option, +) -> Result> { + let request = forward_request_for_injection(method, upstream_target, forward_request_bytes)?; + let mut scoped_ctx = l7_ctx.clone(); + scoped_ctx.secret_resolver = secret_resolver; + scoped_ctx.provider_credential_revision = revision; + crate::l7::token_grant_injection::inject_static_if_needed(request, &scoped_ctx) + .map(|req| req.raw_header) +} + +fn forward_request_for_injection( + method: &str, + upstream_target: &str, + forward_request_bytes: Vec, +) -> Result { let header_end = forward_request_bytes .windows(4) .position(|w| w == b"\r\n\r\n") @@ -4791,17 +4818,13 @@ async fn inject_token_grant_for_forward_request( .into_diagnostic() .map_err(|_| miette::miette!("Forward HTTP headers contain invalid UTF-8"))?; let body_length = crate::l7::rest::parse_body_length(header_str)?; - let forward_request_for_token_grant = crate::l7::provider::L7Request { + Ok(crate::l7::provider::L7Request { action: method.to_string(), target: upstream_target.to_string(), query_params: std::collections::HashMap::new(), raw_header: forward_request_bytes, body_length, - }; - - crate::l7::token_grant_injection::inject_if_needed(forward_request_for_token_grant, l7_ctx) - .await - .map(|req| req.raw_header) + }) } /// Handle a plain HTTP forward proxy request (non-CONNECT). @@ -5849,6 +5872,35 @@ async fn handle_forward_proxy( if let Some(guard) = credential_generation { guard.ensure_current()?; } + forward_request_bytes = match inject_static_credential_for_forward_request( + method, + &upstream_target, + forward_request_bytes, + &l7_ctx, + secret_resolver.clone(), + endpoint_credentials.revision, + ) { + Ok(bytes) => bytes, + Err(error) => { + warn!( + dst_host = %host_lc, + dst_port = port, + error = %error, + "static provider credential injection failed in forward proxy" + ); + respond( + client, + &build_json_error_response( + 502, + "Bad Gateway", + "provider_authentication_failed", + "provider credential unavailable", + ), + ) + .await?; + return Ok(()); + } + }; // 9. Rewrite request and forward to upstream let rewritten = match rewrite_forward_request( @@ -7732,6 +7784,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -10511,6 +10564,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -10553,6 +10607,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -10599,6 +10654,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -11352,6 +11408,7 @@ network_policies: }], credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), + delivery: 0, }, )]), Vec::new(), @@ -11441,6 +11498,7 @@ network_policies: }], credential_identity: format!("provider-a:{key}"), workload_credential_handle: String::new(), + delivery: 0, }, ) }) diff --git a/docs/providers/profiles.mdx b/docs/providers/profiles.mdx index c310c02b15..69de34526a 100644 --- a/docs/providers/profiles.mdx +++ b/docs/providers/profiles.mdx @@ -200,7 +200,7 @@ The following provider profile design items are not part of the current behavior | Roadmap item | Current behavior | |---|---| -| General profile-driven credential placement | Static `auth_style`, `header_name`, `query_param`, and `path_template` placement metadata is stored and validated, but static credential injection still depends on environment placeholders generated from provider credentials. Dynamic `token_grant` credentials support `bearer` and `header` placement for matching HTTP endpoints. | +| General profile-driven credential placement | Static credentials can opt into proxy-delivered `bearer` or named `header` placement for inspected REST endpoints. Other static placement styles still depend on environment placeholders. Dynamic `token_grant` credentials support `bearer` and `header` placement for matching HTTP endpoints. | | Binary-scoped credential injection | Provider profile binaries affect policy composition but do not yet restrict placeholder resolution by calling binary. Static and dynamic credentials are endpoint-scoped. | | Credential verification on create | `openshell provider create` does not yet probe provider verification endpoints or expose `--no-verify`. | | Automatic credential scope extraction | OpenShell does not yet inspect upstream provider responses to discover credential scopes. | @@ -277,7 +277,7 @@ openshell provider profile export github-profile -o yaml > github-profile.yaml openshell provider profile update github-profile -f github-profile.yaml ``` -Exported custom profiles include `resource_version`. OpenShell requires that version during update so stale files cannot silently overwrite newer profile definitions. The target ID in the command must match the profile ID in the file. Update accepts one file at a time. If an update would make dynamic token grants ambiguous for an attached sandbox, OpenShell rejects it before changing the profile. +Exported custom profiles include `resource_version`. OpenShell requires that version during update so stale files cannot silently overwrite newer profile definitions. The target ID in the command must match the profile ID in the file. Update accepts one file at a time. If an update would make runtime-injected credentials ambiguous for an attached sandbox, OpenShell rejects it before changing the profile. Custom profile IDs must use lowercase kebab-case with `a-z`, `0-9`, and `-`. Built-in profile IDs and legacy provider aliases are reserved. Built-in and interceptor-managed profiles are read-only through the profile APIs. OpenShell also rejects deleting a custom profile while a sandbox-attached provider uses it. @@ -321,9 +321,13 @@ credentials: env_vars: [CUSTOM_API_TOKEN] required: true + # Accepted values: environment (default), proxy. + delivery: environment + # Accepted values: basic, bearer, header, query, path. # These fields describe static credential placement. - # Static runtime injection still uses env placeholder resolution. + # Environment delivery uses placeholder resolution; proxy delivery sets a + # bearer or named header at the final outbound proxy step. auth_style: bearer header_name: authorization query_param: api_key @@ -421,13 +425,34 @@ binaries: - /usr/local/bin/custom-cli ``` +### Proxy-Delivered Static Credential + +This minimal profile keeps `MODEL_API_KEY` and its OpenShell placeholder out of the sandbox. The application may send any public placeholder value in `Authorization`; OpenShell replaces the complete header before forwarding the allowed request. + +The initial proxy-delivery implementation supports one proxy-delivered static credential per profile. That credential may list multiple `env_vars` aliases. A profile using proxy delivery cannot also declare a `token_grant` credential. + +```yaml +id: custom-model-api +display_name: Custom Model API +credentials: + - name: api_key + env_vars: [MODEL_API_KEY] + delivery: proxy + auth_style: bearer +endpoints: + - host: models.example.com + port: 443 + protocol: rest + access: full +``` + ### Profile Sections `id`, `display_name`, and `description` identify the profile. `id` is the value passed to `openshell provider create --type`. `category` groups profiles in `openshell provider list-profiles`. Use one of the values in the category enum. -`credentials` declares the credential names, environment variables, auth metadata, optional refresh metadata, and optional dynamic token grant metadata for the provider type. The `auth_style` field accepts `basic`, `bearer`, `header`, `query`, or `path`. When `auth_style` is `path`, set `path_template` to a URL path containing the `{credential}` placeholder exactly once (for example, `/v1/{credential}/resources`). Static credentials are exposed as placeholder environment variables and resolved in outbound HTTP requests only at their binding endpoints. Every static credential environment key receives the full profile endpoint set when the profile defines endpoints. An endpointless profile requires explicit sandbox policy bindings for each attached provider instance. Dynamic token grants are resolved by the sandbox proxy on demand for matching profile endpoints and support `bearer` or `header` placement. Credential environment variable names must not use the reserved `v_` prefix, such as `v10_GITHUB_TOKEN`, because OpenShell uses that namespace for revision-scoped placeholders. +`credentials` declares the credential names, environment variables, auth metadata, optional refresh metadata, and optional dynamic token grant metadata for the provider type. The `auth_style` field accepts `basic`, `bearer`, `header`, `query`, or `path`. When `auth_style` is `path`, set `path_template` to a URL path containing the `{credential}` placeholder exactly once (for example, `/v1/{credential}/resources`). Static credentials use `delivery: environment` by default: OpenShell exposes placeholder environment variables and resolves them in outbound requests only at their binding endpoints. Set `delivery: proxy` on a static `bearer` or named `header` credential to omit its environment placeholder and have the proxy set the complete header immediately before forwarding an allowed request. Proxy delivery is limited to inspected REST endpoints and does not support `tls: skip`. `env_vars` still names the host variables used to discover or supply the credential when creating the provider. Every static credential environment key receives the full profile endpoint set when the profile defines endpoints. An endpointless profile requires explicit sandbox policy bindings for each attached provider instance. Dynamic token grants are resolved by the sandbox proxy on demand for matching profile endpoints and support `bearer` or `header` placement. Credential environment variable names must not use the reserved `v_` prefix, such as `v10_GITHUB_TOKEN`, because OpenShell uses that namespace for revision-scoped placeholders. `discovery` controls what `--from-existing` scans. Each entry in `discovery.credentials` must name a diff --git a/proto/openshell.proto b/proto/openshell.proto index 138b973474..28c49065e7 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -1744,6 +1744,9 @@ message ProviderProfileCredential { ProviderCredentialRefresh refresh = 8; string path_template = 9; ProviderCredentialTokenGrant token_grant = 10; + // Controls how a static credential reaches the workload. Unspecified and + // environment preserve the existing environment-placeholder behavior. + ProviderCredentialDelivery delivery = 11; } enum ProviderCredentialRefreshStrategy { @@ -2073,6 +2076,8 @@ message StaticCredentialBinding { // handle changes when the sandbox, provider, credential key, refresh // authorization epoch, or endpoint authorization boundary changes. string workload_credential_handle = 3; + // Delivery mode copied from the provider profile credential declaration. + ProviderCredentialDelivery delivery = 4; } // Get sandbox provider environment response. @@ -2998,6 +3003,14 @@ enum ProviderCredentialRefreshRecoveryAction { PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE = 4; } +// Kept after the pre-existing enums so adding it does not renumber their +// generated descriptors. +enum ProviderCredentialDelivery { + PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED = 0; + PROVIDER_CREDENTIAL_DELIVERY_ENVIRONMENT = 1; + PROVIDER_CREDENTIAL_DELIVERY_PROXY = 2; +} + // Workspace membership record. message WorkspaceMember { openshell.datamodel.v1.ObjectMeta metadata = 1; diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index f9e7f39030..527f44d343 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -503,6 +503,57 @@ func (ProviderCredentialRefreshRecoveryAction) EnumDescriptor() ([]byte, []int) return file_openshell_proto_rawDescGZIP(), []int{7} } +// Kept after the pre-existing enums so adding it does not renumber their +// generated descriptors. +type ProviderCredentialDelivery int32 + +const ( + ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED ProviderCredentialDelivery = 0 + ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_ENVIRONMENT ProviderCredentialDelivery = 1 + ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_PROXY ProviderCredentialDelivery = 2 +) + +// Enum value maps for ProviderCredentialDelivery. +var ( + ProviderCredentialDelivery_name = map[int32]string{ + 0: "PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED", + 1: "PROVIDER_CREDENTIAL_DELIVERY_ENVIRONMENT", + 2: "PROVIDER_CREDENTIAL_DELIVERY_PROXY", + } + ProviderCredentialDelivery_value = map[string]int32{ + "PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED": 0, + "PROVIDER_CREDENTIAL_DELIVERY_ENVIRONMENT": 1, + "PROVIDER_CREDENTIAL_DELIVERY_PROXY": 2, + } +) + +func (x ProviderCredentialDelivery) Enum() *ProviderCredentialDelivery { + p := new(ProviderCredentialDelivery) + *p = x + return p +} + +func (x ProviderCredentialDelivery) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderCredentialDelivery) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[8].Descriptor() +} + +func (ProviderCredentialDelivery) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[8] +} + +func (x ProviderCredentialDelivery) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderCredentialDelivery.Descriptor instead. +func (ProviderCredentialDelivery) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{8} +} + // IssueSandboxToken request. Empty body; identity is established by the // authentication credentials carried in the request headers (a projected // Kubernetes ServiceAccount JWT in the K8s driver path). @@ -6411,17 +6462,20 @@ func (x *ProviderCredentialTokenGrant) GetRequestedTokenType() string { // Provider credential declaration. type ProviderProfileCredential struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - EnvVars []string `protobuf:"bytes,3,rep,name=env_vars,json=envVars,proto3" json:"env_vars,omitempty"` - Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` - AuthStyle string `protobuf:"bytes,5,opt,name=auth_style,json=authStyle,proto3" json:"auth_style,omitempty"` - HeaderName string `protobuf:"bytes,6,opt,name=header_name,json=headerName,proto3" json:"header_name,omitempty"` - QueryParam string `protobuf:"bytes,7,opt,name=query_param,json=queryParam,proto3" json:"query_param,omitempty"` - Refresh *ProviderCredentialRefresh `protobuf:"bytes,8,opt,name=refresh,proto3" json:"refresh,omitempty"` - PathTemplate string `protobuf:"bytes,9,opt,name=path_template,json=pathTemplate,proto3" json:"path_template,omitempty"` - TokenGrant *ProviderCredentialTokenGrant `protobuf:"bytes,10,opt,name=token_grant,json=tokenGrant,proto3" json:"token_grant,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + EnvVars []string `protobuf:"bytes,3,rep,name=env_vars,json=envVars,proto3" json:"env_vars,omitempty"` + Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` + AuthStyle string `protobuf:"bytes,5,opt,name=auth_style,json=authStyle,proto3" json:"auth_style,omitempty"` + HeaderName string `protobuf:"bytes,6,opt,name=header_name,json=headerName,proto3" json:"header_name,omitempty"` + QueryParam string `protobuf:"bytes,7,opt,name=query_param,json=queryParam,proto3" json:"query_param,omitempty"` + Refresh *ProviderCredentialRefresh `protobuf:"bytes,8,opt,name=refresh,proto3" json:"refresh,omitempty"` + PathTemplate string `protobuf:"bytes,9,opt,name=path_template,json=pathTemplate,proto3" json:"path_template,omitempty"` + TokenGrant *ProviderCredentialTokenGrant `protobuf:"bytes,10,opt,name=token_grant,json=tokenGrant,proto3" json:"token_grant,omitempty"` + // Controls how a static credential reaches the workload. Unspecified and + // environment preserve the existing environment-placeholder behavior. + Delivery ProviderCredentialDelivery `protobuf:"varint,11,opt,name=delivery,proto3,enum=openshell.v1.ProviderCredentialDelivery" json:"delivery,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -6526,6 +6580,13 @@ func (x *ProviderProfileCredential) GetTokenGrant() *ProviderCredentialTokenGran return nil } +func (x *ProviderProfileCredential) GetDelivery() ProviderCredentialDelivery { + if x != nil { + return x.Delivery + } + return ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED +} + type ProviderCredentialRefreshMaterial struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -8627,8 +8688,10 @@ type StaticCredentialBinding struct { // handle changes when the sandbox, provider, credential key, refresh // authorization epoch, or endpoint authorization boundary changes. WorkloadCredentialHandle string `protobuf:"bytes,3,opt,name=workload_credential_handle,json=workloadCredentialHandle,proto3" json:"workload_credential_handle,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Delivery mode copied from the provider profile credential declaration. + Delivery ProviderCredentialDelivery `protobuf:"varint,4,opt,name=delivery,proto3,enum=openshell.v1.ProviderCredentialDelivery" json:"delivery,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StaticCredentialBinding) Reset() { @@ -8682,6 +8745,13 @@ func (x *StaticCredentialBinding) GetWorkloadCredentialHandle() string { return "" } +func (x *StaticCredentialBinding) GetDelivery() ProviderCredentialDelivery { + if x != nil { + return x.Delivery + } + return ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED +} + // Get sandbox provider environment response. type GetSandboxProviderEnvironmentResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -15300,7 +15370,7 @@ const file_openshell_proto_rawDesc = "" + "grant_type\x18\b \x01(\x0e2..openshell.v1.ProviderCredentialTokenGrantTypeR\tgrantType\x12[\n" + "\rsubject_token\x18\t \x01(\v26.openshell.v1.ProviderCredentialTokenGrantSubjectTokenR\fsubjectToken\x120\n" + "\x14requested_token_type\x18\n" + - " \x01(\tR\x12requestedTokenType\"\x9e\x03\n" + + " \x01(\tR\x12requestedTokenType\"\xe4\x03\n" + "\x19ProviderProfileCredential\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + @@ -15316,7 +15386,8 @@ const file_openshell_proto_rawDesc = "" + "\rpath_template\x18\t \x01(\tR\fpathTemplate\x12K\n" + "\vtoken_grant\x18\n" + " \x01(\v2*.openshell.v1.ProviderCredentialTokenGrantR\n" + - "tokenGrant\"\x8d\x01\n" + + "tokenGrant\x12D\n" + + "\bdelivery\x18\v \x01(\x0e2(.openshell.v1.ProviderCredentialDeliveryR\bdelivery\"\x8d\x01\n" + "!ProviderCredentialRefreshMaterial\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x1a\n" + @@ -15487,11 +15558,12 @@ const file_openshell_proto_rawDesc = "" + "\x1fStaticCredentialEndpointBinding\x12\x12\n" + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + "\x04port\x18\x02 \x01(\rR\x04port\x12\x12\n" + - "\x04path\x18\x03 \x01(\tR\x04path\"\xd5\x01\n" + + "\x04path\x18\x03 \x01(\tR\x04path\"\x9b\x02\n" + "\x17StaticCredentialBinding\x12K\n" + "\tendpoints\x18\x01 \x03(\v2-.openshell.v1.StaticCredentialEndpointBindingR\tendpoints\x12/\n" + "\x13credential_identity\x18\x02 \x01(\tR\x12credentialIdentity\x12<\n" + - "\x1aworkload_credential_handle\x18\x03 \x01(\tR\x18workloadCredentialHandle\"\x90\b\n" + + "\x1aworkload_credential_handle\x18\x03 \x01(\tR\x18workloadCredentialHandle\x12D\n" + + "\bdelivery\x18\x04 \x01(\x0e2(.openshell.v1.ProviderCredentialDeliveryR\bdelivery\"\x90\b\n" + "%GetSandboxProviderEnvironmentResponse\x12l\n" + "\venvironment\x18\x01 \x03(\v2D.openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntryB\x04\x88\xb5\x18\x01R\venvironment\x122\n" + "\x15provider_env_revision\x18\x02 \x01(\x04R\x13providerEnvRevision\x12\x87\x01\n" + @@ -16053,7 +16125,11 @@ const file_openshell_proto_rawDesc = "" + "1PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY\x10\x01\x12;\n" + "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE\x10\x02\x12A\n" + "=PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION\x10\x03\x12;\n" + - "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE\x10\x042\xf7K\n" + + "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE\x10\x04*\xa0\x01\n" + + "\x1aProviderCredentialDelivery\x12,\n" + + "(PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED\x10\x00\x12,\n" + + "(PROVIDER_CREDENTIAL_DELIVERY_ENVIRONMENT\x10\x01\x12&\n" + + "\"PROVIDER_CREDENTIAL_DELIVERY_PROXY\x10\x022\xf7K\n" + "\tOpenShell\x12Z\n" + "\x06Health\x12\x1b.openshell.v1.HealthRequest\x1a\x1c.openshell.v1.HealthResponse\"\x15\x82\xb5\x18\x11\n" + "\x0funauthenticated\x12i\n" + @@ -16217,7 +16293,7 @@ func file_openshell_proto_rawDescGZIP() []byte { return file_openshell_proto_rawDescData } -var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 8) +var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 9) var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 234) var file_openshell_proto_goTypes = []any{ (SandboxPhase)(0), // 0: openshell.v1.SandboxPhase @@ -16228,590 +16304,593 @@ var file_openshell_proto_goTypes = []any{ (ServiceStatus)(0), // 5: openshell.v1.ServiceStatus (WorkspaceRole)(0), // 6: openshell.v1.WorkspaceRole (ProviderCredentialRefreshRecoveryAction)(0), // 7: openshell.v1.ProviderCredentialRefreshRecoveryAction - (*IssueSandboxTokenRequest)(nil), // 8: openshell.v1.IssueSandboxTokenRequest - (*IssueSandboxTokenResponse)(nil), // 9: openshell.v1.IssueSandboxTokenResponse - (*RefreshSandboxTokenRequest)(nil), // 10: openshell.v1.RefreshSandboxTokenRequest - (*RefreshSandboxTokenResponse)(nil), // 11: openshell.v1.RefreshSandboxTokenResponse - (*HealthRequest)(nil), // 12: openshell.v1.HealthRequest - (*HealthResponse)(nil), // 13: openshell.v1.HealthResponse - (*GetCurrentUserRequest)(nil), // 14: openshell.v1.GetCurrentUserRequest - (*GetCurrentUserResponse)(nil), // 15: openshell.v1.GetCurrentUserResponse - (*GetGatewayInfoRequest)(nil), // 16: openshell.v1.GetGatewayInfoRequest - (*GetGatewayInfoResponse)(nil), // 17: openshell.v1.GetGatewayInfoResponse - (*ComputeDriverInfo)(nil), // 18: openshell.v1.ComputeDriverInfo - (*ComputeDriverCapabilities)(nil), // 19: openshell.v1.ComputeDriverCapabilities - (*Sandbox)(nil), // 20: openshell.v1.Sandbox - (*SandboxSpec)(nil), // 21: openshell.v1.SandboxSpec - (*ResourceRequirements)(nil), // 22: openshell.v1.ResourceRequirements - (*GpuResourceRequirements)(nil), // 23: openshell.v1.GpuResourceRequirements - (*SandboxTemplate)(nil), // 24: openshell.v1.SandboxTemplate - (*SandboxWorkloadTemplate)(nil), // 25: openshell.v1.SandboxWorkloadTemplate - (*SandboxWorkloadTemplateSpec)(nil), // 26: openshell.v1.SandboxWorkloadTemplateSpec - (*SandboxWorkloadConfig)(nil), // 27: openshell.v1.SandboxWorkloadConfig - (*SandboxResources)(nil), // 28: openshell.v1.SandboxResources - (*SandboxServiceLevel)(nil), // 29: openshell.v1.SandboxServiceLevel - (*SandboxStartup)(nil), // 30: openshell.v1.SandboxStartup - (*SandboxWorkloadTemplateProvenance)(nil), // 31: openshell.v1.SandboxWorkloadTemplateProvenance - (*SandboxStatus)(nil), // 32: openshell.v1.SandboxStatus - (*SandboxCondition)(nil), // 33: openshell.v1.SandboxCondition - (*PlatformEvent)(nil), // 34: openshell.v1.PlatformEvent - (*CreateSandboxRequest)(nil), // 35: openshell.v1.CreateSandboxRequest - (*CreateSandboxTemplateRequest)(nil), // 36: openshell.v1.CreateSandboxTemplateRequest - (*GetSandboxTemplateRequest)(nil), // 37: openshell.v1.GetSandboxTemplateRequest - (*ListSandboxTemplatesRequest)(nil), // 38: openshell.v1.ListSandboxTemplatesRequest - (*DeleteSandboxTemplateRequest)(nil), // 39: openshell.v1.DeleteSandboxTemplateRequest - (*SandboxTemplateResponse)(nil), // 40: openshell.v1.SandboxTemplateResponse - (*ListSandboxTemplatesResponse)(nil), // 41: openshell.v1.ListSandboxTemplatesResponse - (*DeleteSandboxTemplateResponse)(nil), // 42: openshell.v1.DeleteSandboxTemplateResponse - (*GetSandboxRequest)(nil), // 43: openshell.v1.GetSandboxRequest - (*ListSandboxesRequest)(nil), // 44: openshell.v1.ListSandboxesRequest - (*ListSandboxProvidersRequest)(nil), // 45: openshell.v1.ListSandboxProvidersRequest - (*AttachSandboxProviderRequest)(nil), // 46: openshell.v1.AttachSandboxProviderRequest - (*DetachSandboxProviderRequest)(nil), // 47: openshell.v1.DetachSandboxProviderRequest - (*DeleteSandboxRequest)(nil), // 48: openshell.v1.DeleteSandboxRequest - (*StopSandboxRequest)(nil), // 49: openshell.v1.StopSandboxRequest - (*StartSandboxRequest)(nil), // 50: openshell.v1.StartSandboxRequest - (*SandboxResponse)(nil), // 51: openshell.v1.SandboxResponse - (*ListSandboxesResponse)(nil), // 52: openshell.v1.ListSandboxesResponse - (*ListSandboxProvidersResponse)(nil), // 53: openshell.v1.ListSandboxProvidersResponse - (*AttachSandboxProviderResponse)(nil), // 54: openshell.v1.AttachSandboxProviderResponse - (*DetachSandboxProviderResponse)(nil), // 55: openshell.v1.DetachSandboxProviderResponse - (*DeleteSandboxResponse)(nil), // 56: openshell.v1.DeleteSandboxResponse - (*CreateSshSessionRequest)(nil), // 57: openshell.v1.CreateSshSessionRequest - (*CreateSshSessionResponse)(nil), // 58: openshell.v1.CreateSshSessionResponse - (*ExposeServiceRequest)(nil), // 59: openshell.v1.ExposeServiceRequest - (*GetServiceRequest)(nil), // 60: openshell.v1.GetServiceRequest - (*ListServicesRequest)(nil), // 61: openshell.v1.ListServicesRequest - (*ListServicesResponse)(nil), // 62: openshell.v1.ListServicesResponse - (*DeleteServiceRequest)(nil), // 63: openshell.v1.DeleteServiceRequest - (*DeleteServiceResponse)(nil), // 64: openshell.v1.DeleteServiceResponse - (*ServiceEndpoint)(nil), // 65: openshell.v1.ServiceEndpoint - (*ServiceEndpointResponse)(nil), // 66: openshell.v1.ServiceEndpointResponse - (*RevokeSshSessionRequest)(nil), // 67: openshell.v1.RevokeSshSessionRequest - (*RevokeSshSessionResponse)(nil), // 68: openshell.v1.RevokeSshSessionResponse - (*ExecSandboxRequest)(nil), // 69: openshell.v1.ExecSandboxRequest - (*ExecSandboxStdout)(nil), // 70: openshell.v1.ExecSandboxStdout - (*ExecSandboxStderr)(nil), // 71: openshell.v1.ExecSandboxStderr - (*ExecSandboxExit)(nil), // 72: openshell.v1.ExecSandboxExit - (*ExecSandboxEvent)(nil), // 73: openshell.v1.ExecSandboxEvent - (*TcpForwardInit)(nil), // 74: openshell.v1.TcpForwardInit - (*TcpForwardFrame)(nil), // 75: openshell.v1.TcpForwardFrame - (*ExecSandboxInput)(nil), // 76: openshell.v1.ExecSandboxInput - (*ExecSandboxWindowResize)(nil), // 77: openshell.v1.ExecSandboxWindowResize - (*SshSession)(nil), // 78: openshell.v1.SshSession - (*WatchSandboxRequest)(nil), // 79: openshell.v1.WatchSandboxRequest - (*SandboxStreamEvent)(nil), // 80: openshell.v1.SandboxStreamEvent - (*SandboxLogLine)(nil), // 81: openshell.v1.SandboxLogLine - (*SandboxStreamWarning)(nil), // 82: openshell.v1.SandboxStreamWarning - (*CreateProviderRequest)(nil), // 83: openshell.v1.CreateProviderRequest - (*GetProviderRequest)(nil), // 84: openshell.v1.GetProviderRequest - (*ListProvidersRequest)(nil), // 85: openshell.v1.ListProvidersRequest - (*UpdateProviderRequest)(nil), // 86: openshell.v1.UpdateProviderRequest - (*DeleteProviderRequest)(nil), // 87: openshell.v1.DeleteProviderRequest - (*ProviderResponse)(nil), // 88: openshell.v1.ProviderResponse - (*ListProvidersResponse)(nil), // 89: openshell.v1.ListProvidersResponse - (*ListProviderProfilesRequest)(nil), // 90: openshell.v1.ListProviderProfilesRequest - (*GetProviderProfileRequest)(nil), // 91: openshell.v1.GetProviderProfileRequest - (*ProviderProfileImportItem)(nil), // 92: openshell.v1.ProviderProfileImportItem - (*ProviderProfileDiagnostic)(nil), // 93: openshell.v1.ProviderProfileDiagnostic - (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 94: openshell.v1.ProviderCredentialTokenGrantAudienceOverride - (*ProviderCredentialTokenGrantSubjectToken)(nil), // 95: openshell.v1.ProviderCredentialTokenGrantSubjectToken - (*ProviderCredentialTokenGrant)(nil), // 96: openshell.v1.ProviderCredentialTokenGrant - (*ProviderProfileCredential)(nil), // 97: openshell.v1.ProviderProfileCredential - (*ProviderCredentialRefreshMaterial)(nil), // 98: openshell.v1.ProviderCredentialRefreshMaterial - (*ProviderCredentialRefreshOutput)(nil), // 99: openshell.v1.ProviderCredentialRefreshOutput - (*ProviderCredentialRefresh)(nil), // 100: openshell.v1.ProviderCredentialRefresh - (*ProviderCredentialRefreshStatus)(nil), // 101: openshell.v1.ProviderCredentialRefreshStatus - (*ProviderProfileDiscovery)(nil), // 102: openshell.v1.ProviderProfileDiscovery - (*StoredProviderCredentialRefreshState)(nil), // 103: openshell.v1.StoredProviderCredentialRefreshState - (*StoredRefreshMaterialDeletion)(nil), // 104: openshell.v1.StoredRefreshMaterialDeletion - (*GetProviderRefreshStatusRequest)(nil), // 105: openshell.v1.GetProviderRefreshStatusRequest - (*GetProviderRefreshStatusResponse)(nil), // 106: openshell.v1.GetProviderRefreshStatusResponse - (*ConfigureProviderRefreshRequest)(nil), // 107: openshell.v1.ConfigureProviderRefreshRequest - (*ConfigureProviderRefreshResponse)(nil), // 108: openshell.v1.ConfigureProviderRefreshResponse - (*RotateProviderCredentialRequest)(nil), // 109: openshell.v1.RotateProviderCredentialRequest - (*RotateProviderCredentialResponse)(nil), // 110: openshell.v1.RotateProviderCredentialResponse - (*DeleteProviderRefreshRequest)(nil), // 111: openshell.v1.DeleteProviderRefreshRequest - (*DeleteProviderRefreshResponse)(nil), // 112: openshell.v1.DeleteProviderRefreshResponse - (*ProviderProfile)(nil), // 113: openshell.v1.ProviderProfile - (*StoredProviderProfile)(nil), // 114: openshell.v1.StoredProviderProfile - (*ProviderProfileResponse)(nil), // 115: openshell.v1.ProviderProfileResponse - (*ListProviderProfilesResponse)(nil), // 116: openshell.v1.ListProviderProfilesResponse - (*ImportProviderProfilesRequest)(nil), // 117: openshell.v1.ImportProviderProfilesRequest - (*ImportProviderProfilesResponse)(nil), // 118: openshell.v1.ImportProviderProfilesResponse - (*UpdateProviderProfilesRequest)(nil), // 119: openshell.v1.UpdateProviderProfilesRequest - (*UpdateProviderProfilesResponse)(nil), // 120: openshell.v1.UpdateProviderProfilesResponse - (*LintProviderProfilesRequest)(nil), // 121: openshell.v1.LintProviderProfilesRequest - (*LintProviderProfilesResponse)(nil), // 122: openshell.v1.LintProviderProfilesResponse - (*DeleteProviderResponse)(nil), // 123: openshell.v1.DeleteProviderResponse - (*DeleteProviderProfileRequest)(nil), // 124: openshell.v1.DeleteProviderProfileRequest - (*DeleteProviderProfileResponse)(nil), // 125: openshell.v1.DeleteProviderProfileResponse - (*GetSandboxProviderEnvironmentRequest)(nil), // 126: openshell.v1.GetSandboxProviderEnvironmentRequest - (*StaticCredentialEndpointBinding)(nil), // 127: openshell.v1.StaticCredentialEndpointBinding - (*StaticCredentialBinding)(nil), // 128: openshell.v1.StaticCredentialBinding - (*GetSandboxProviderEnvironmentResponse)(nil), // 129: openshell.v1.GetSandboxProviderEnvironmentResponse - (*ExchangeProviderSubjectTokenRequest)(nil), // 130: openshell.v1.ExchangeProviderSubjectTokenRequest - (*ExchangeProviderSubjectTokenResponse)(nil), // 131: openshell.v1.ExchangeProviderSubjectTokenResponse - (*UpdateConfigRequest)(nil), // 132: openshell.v1.UpdateConfigRequest - (*PolicyMergeOperation)(nil), // 133: openshell.v1.PolicyMergeOperation - (*AddNetworkRule)(nil), // 134: openshell.v1.AddNetworkRule - (*RemoveNetworkEndpoint)(nil), // 135: openshell.v1.RemoveNetworkEndpoint - (*RemoveNetworkRule)(nil), // 136: openshell.v1.RemoveNetworkRule - (*AddDenyRules)(nil), // 137: openshell.v1.AddDenyRules - (*AddAllowRules)(nil), // 138: openshell.v1.AddAllowRules - (*RemoveNetworkBinary)(nil), // 139: openshell.v1.RemoveNetworkBinary - (*UpdateConfigResponse)(nil), // 140: openshell.v1.UpdateConfigResponse - (*GetSandboxPolicyStatusRequest)(nil), // 141: openshell.v1.GetSandboxPolicyStatusRequest - (*GetSandboxPolicyStatusResponse)(nil), // 142: openshell.v1.GetSandboxPolicyStatusResponse - (*ListSandboxPoliciesRequest)(nil), // 143: openshell.v1.ListSandboxPoliciesRequest - (*ListSandboxPoliciesResponse)(nil), // 144: openshell.v1.ListSandboxPoliciesResponse - (*ReportPolicyStatusRequest)(nil), // 145: openshell.v1.ReportPolicyStatusRequest - (*ReportPolicyStatusResponse)(nil), // 146: openshell.v1.ReportPolicyStatusResponse - (*SandboxPolicyRevision)(nil), // 147: openshell.v1.SandboxPolicyRevision - (*GetSandboxLogsRequest)(nil), // 148: openshell.v1.GetSandboxLogsRequest - (*PushSandboxLogsRequest)(nil), // 149: openshell.v1.PushSandboxLogsRequest - (*PushSandboxLogsResponse)(nil), // 150: openshell.v1.PushSandboxLogsResponse - (*GetSandboxLogsResponse)(nil), // 151: openshell.v1.GetSandboxLogsResponse - (*SupervisorMessage)(nil), // 152: openshell.v1.SupervisorMessage - (*GatewayMessage)(nil), // 153: openshell.v1.GatewayMessage - (*SupervisorHello)(nil), // 154: openshell.v1.SupervisorHello - (*SessionAccepted)(nil), // 155: openshell.v1.SessionAccepted - (*SessionRejected)(nil), // 156: openshell.v1.SessionRejected - (*SupervisorHeartbeat)(nil), // 157: openshell.v1.SupervisorHeartbeat - (*GatewayHeartbeat)(nil), // 158: openshell.v1.GatewayHeartbeat - (*ReportMainProcessExitRequest)(nil), // 159: openshell.v1.ReportMainProcessExitRequest - (*ReportMainProcessExitResponse)(nil), // 160: openshell.v1.ReportMainProcessExitResponse - (*FinalizeMainProcessExitRequest)(nil), // 161: openshell.v1.FinalizeMainProcessExitRequest - (*FinalizeMainProcessExitResponse)(nil), // 162: openshell.v1.FinalizeMainProcessExitResponse - (*RelayOpen)(nil), // 163: openshell.v1.RelayOpen - (*SshRelayTarget)(nil), // 164: openshell.v1.SshRelayTarget - (*TcpRelayTarget)(nil), // 165: openshell.v1.TcpRelayTarget - (*RelayInit)(nil), // 166: openshell.v1.RelayInit - (*RelayFrame)(nil), // 167: openshell.v1.RelayFrame - (*RelayOpenResult)(nil), // 168: openshell.v1.RelayOpenResult - (*RelayClose)(nil), // 169: openshell.v1.RelayClose - (*L7RequestSample)(nil), // 170: openshell.v1.L7RequestSample - (*DenialSummary)(nil), // 171: openshell.v1.DenialSummary - (*DenialGroupCount)(nil), // 172: openshell.v1.DenialGroupCount - (*NetworkActivitySummary)(nil), // 173: openshell.v1.NetworkActivitySummary - (*PolicyChunk)(nil), // 174: openshell.v1.PolicyChunk - (*DraftPolicyUpdate)(nil), // 175: openshell.v1.DraftPolicyUpdate - (*SubmitPolicyAnalysisRequest)(nil), // 176: openshell.v1.SubmitPolicyAnalysisRequest - (*SubmitPolicyAnalysisResponse)(nil), // 177: openshell.v1.SubmitPolicyAnalysisResponse - (*GetDraftPolicyRequest)(nil), // 178: openshell.v1.GetDraftPolicyRequest - (*GetDraftPolicyResponse)(nil), // 179: openshell.v1.GetDraftPolicyResponse - (*ApproveDraftChunkRequest)(nil), // 180: openshell.v1.ApproveDraftChunkRequest - (*ApproveDraftChunkResponse)(nil), // 181: openshell.v1.ApproveDraftChunkResponse - (*RejectDraftChunkRequest)(nil), // 182: openshell.v1.RejectDraftChunkRequest - (*RejectDraftChunkResponse)(nil), // 183: openshell.v1.RejectDraftChunkResponse - (*DraftChunkApproval)(nil), // 184: openshell.v1.DraftChunkApproval - (*ApproveAllDraftChunksRequest)(nil), // 185: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 186: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 187: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 188: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 189: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 190: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 191: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 192: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 193: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 194: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 195: openshell.v1.GetDraftHistoryResponse - (*PolicyRevisionPayload)(nil), // 196: openshell.v1.PolicyRevisionPayload - (*DraftChunkPayload)(nil), // 197: openshell.v1.DraftChunkPayload - (*StoredPolicyRevision)(nil), // 198: openshell.v1.StoredPolicyRevision - (*StoredDraftChunk)(nil), // 199: openshell.v1.StoredDraftChunk - (*CreateWorkspaceRequest)(nil), // 200: openshell.v1.CreateWorkspaceRequest - (*CreateWorkspaceResponse)(nil), // 201: openshell.v1.CreateWorkspaceResponse - (*GetWorkspaceRequest)(nil), // 202: openshell.v1.GetWorkspaceRequest - (*GetWorkspaceResponse)(nil), // 203: openshell.v1.GetWorkspaceResponse - (*ListWorkspacesRequest)(nil), // 204: openshell.v1.ListWorkspacesRequest - (*ListWorkspacesResponse)(nil), // 205: openshell.v1.ListWorkspacesResponse - (*DeleteWorkspaceRequest)(nil), // 206: openshell.v1.DeleteWorkspaceRequest - (*DeleteWorkspaceResponse)(nil), // 207: openshell.v1.DeleteWorkspaceResponse - (*WorkspaceMember)(nil), // 208: openshell.v1.WorkspaceMember - (*AddWorkspaceMemberRequest)(nil), // 209: openshell.v1.AddWorkspaceMemberRequest - (*AddWorkspaceMemberResponse)(nil), // 210: openshell.v1.AddWorkspaceMemberResponse - (*RemoveWorkspaceMemberRequest)(nil), // 211: openshell.v1.RemoveWorkspaceMemberRequest - (*RemoveWorkspaceMemberResponse)(nil), // 212: openshell.v1.RemoveWorkspaceMemberResponse - (*ListWorkspaceMembersRequest)(nil), // 213: openshell.v1.ListWorkspaceMembersRequest - (*ListWorkspaceMembersResponse)(nil), // 214: openshell.v1.ListWorkspaceMembersResponse - (*ExtensionServiceCredential)(nil), // 215: openshell.v1.ExtensionServiceCredential - nil, // 216: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 217: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 218: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 219: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 220: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry - nil, // 221: openshell.v1.PlatformEvent.MetadataEntry - nil, // 222: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 223: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 224: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 225: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 226: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - nil, // 227: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - nil, // 228: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - nil, // 229: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry - nil, // 230: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 231: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 232: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 233: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - nil, // 234: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 235: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 236: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 237: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 238: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 239: openshell.v1.PolicyRevisionPayload.ProvenanceEntry - nil, // 240: openshell.v1.StoredPolicyRevision.ProvenanceEntry - nil, // 241: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*datamodelv1.ObjectMeta)(nil), // 242: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 243: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 244: google.protobuf.Struct - (*durationpb.Duration)(nil), // 245: google.protobuf.Duration - (*datamodelv1.Provider)(nil), // 246: openshell.datamodel.v1.Provider - (*datamodelv1.CredentialHandle)(nil), // 247: openshell.datamodel.v1.CredentialHandle - (*sandboxv1.NetworkEndpoint)(nil), // 248: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 249: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 250: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 251: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 252: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 253: openshell.sandbox.v1.L7Rule - (*datamodelv1.Workspace)(nil), // 254: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 255: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 256: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 257: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 258: openshell.sandbox.v1.GetGatewayConfigResponse + (ProviderCredentialDelivery)(0), // 8: openshell.v1.ProviderCredentialDelivery + (*IssueSandboxTokenRequest)(nil), // 9: openshell.v1.IssueSandboxTokenRequest + (*IssueSandboxTokenResponse)(nil), // 10: openshell.v1.IssueSandboxTokenResponse + (*RefreshSandboxTokenRequest)(nil), // 11: openshell.v1.RefreshSandboxTokenRequest + (*RefreshSandboxTokenResponse)(nil), // 12: openshell.v1.RefreshSandboxTokenResponse + (*HealthRequest)(nil), // 13: openshell.v1.HealthRequest + (*HealthResponse)(nil), // 14: openshell.v1.HealthResponse + (*GetCurrentUserRequest)(nil), // 15: openshell.v1.GetCurrentUserRequest + (*GetCurrentUserResponse)(nil), // 16: openshell.v1.GetCurrentUserResponse + (*GetGatewayInfoRequest)(nil), // 17: openshell.v1.GetGatewayInfoRequest + (*GetGatewayInfoResponse)(nil), // 18: openshell.v1.GetGatewayInfoResponse + (*ComputeDriverInfo)(nil), // 19: openshell.v1.ComputeDriverInfo + (*ComputeDriverCapabilities)(nil), // 20: openshell.v1.ComputeDriverCapabilities + (*Sandbox)(nil), // 21: openshell.v1.Sandbox + (*SandboxSpec)(nil), // 22: openshell.v1.SandboxSpec + (*ResourceRequirements)(nil), // 23: openshell.v1.ResourceRequirements + (*GpuResourceRequirements)(nil), // 24: openshell.v1.GpuResourceRequirements + (*SandboxTemplate)(nil), // 25: openshell.v1.SandboxTemplate + (*SandboxWorkloadTemplate)(nil), // 26: openshell.v1.SandboxWorkloadTemplate + (*SandboxWorkloadTemplateSpec)(nil), // 27: openshell.v1.SandboxWorkloadTemplateSpec + (*SandboxWorkloadConfig)(nil), // 28: openshell.v1.SandboxWorkloadConfig + (*SandboxResources)(nil), // 29: openshell.v1.SandboxResources + (*SandboxServiceLevel)(nil), // 30: openshell.v1.SandboxServiceLevel + (*SandboxStartup)(nil), // 31: openshell.v1.SandboxStartup + (*SandboxWorkloadTemplateProvenance)(nil), // 32: openshell.v1.SandboxWorkloadTemplateProvenance + (*SandboxStatus)(nil), // 33: openshell.v1.SandboxStatus + (*SandboxCondition)(nil), // 34: openshell.v1.SandboxCondition + (*PlatformEvent)(nil), // 35: openshell.v1.PlatformEvent + (*CreateSandboxRequest)(nil), // 36: openshell.v1.CreateSandboxRequest + (*CreateSandboxTemplateRequest)(nil), // 37: openshell.v1.CreateSandboxTemplateRequest + (*GetSandboxTemplateRequest)(nil), // 38: openshell.v1.GetSandboxTemplateRequest + (*ListSandboxTemplatesRequest)(nil), // 39: openshell.v1.ListSandboxTemplatesRequest + (*DeleteSandboxTemplateRequest)(nil), // 40: openshell.v1.DeleteSandboxTemplateRequest + (*SandboxTemplateResponse)(nil), // 41: openshell.v1.SandboxTemplateResponse + (*ListSandboxTemplatesResponse)(nil), // 42: openshell.v1.ListSandboxTemplatesResponse + (*DeleteSandboxTemplateResponse)(nil), // 43: openshell.v1.DeleteSandboxTemplateResponse + (*GetSandboxRequest)(nil), // 44: openshell.v1.GetSandboxRequest + (*ListSandboxesRequest)(nil), // 45: openshell.v1.ListSandboxesRequest + (*ListSandboxProvidersRequest)(nil), // 46: openshell.v1.ListSandboxProvidersRequest + (*AttachSandboxProviderRequest)(nil), // 47: openshell.v1.AttachSandboxProviderRequest + (*DetachSandboxProviderRequest)(nil), // 48: openshell.v1.DetachSandboxProviderRequest + (*DeleteSandboxRequest)(nil), // 49: openshell.v1.DeleteSandboxRequest + (*StopSandboxRequest)(nil), // 50: openshell.v1.StopSandboxRequest + (*StartSandboxRequest)(nil), // 51: openshell.v1.StartSandboxRequest + (*SandboxResponse)(nil), // 52: openshell.v1.SandboxResponse + (*ListSandboxesResponse)(nil), // 53: openshell.v1.ListSandboxesResponse + (*ListSandboxProvidersResponse)(nil), // 54: openshell.v1.ListSandboxProvidersResponse + (*AttachSandboxProviderResponse)(nil), // 55: openshell.v1.AttachSandboxProviderResponse + (*DetachSandboxProviderResponse)(nil), // 56: openshell.v1.DetachSandboxProviderResponse + (*DeleteSandboxResponse)(nil), // 57: openshell.v1.DeleteSandboxResponse + (*CreateSshSessionRequest)(nil), // 58: openshell.v1.CreateSshSessionRequest + (*CreateSshSessionResponse)(nil), // 59: openshell.v1.CreateSshSessionResponse + (*ExposeServiceRequest)(nil), // 60: openshell.v1.ExposeServiceRequest + (*GetServiceRequest)(nil), // 61: openshell.v1.GetServiceRequest + (*ListServicesRequest)(nil), // 62: openshell.v1.ListServicesRequest + (*ListServicesResponse)(nil), // 63: openshell.v1.ListServicesResponse + (*DeleteServiceRequest)(nil), // 64: openshell.v1.DeleteServiceRequest + (*DeleteServiceResponse)(nil), // 65: openshell.v1.DeleteServiceResponse + (*ServiceEndpoint)(nil), // 66: openshell.v1.ServiceEndpoint + (*ServiceEndpointResponse)(nil), // 67: openshell.v1.ServiceEndpointResponse + (*RevokeSshSessionRequest)(nil), // 68: openshell.v1.RevokeSshSessionRequest + (*RevokeSshSessionResponse)(nil), // 69: openshell.v1.RevokeSshSessionResponse + (*ExecSandboxRequest)(nil), // 70: openshell.v1.ExecSandboxRequest + (*ExecSandboxStdout)(nil), // 71: openshell.v1.ExecSandboxStdout + (*ExecSandboxStderr)(nil), // 72: openshell.v1.ExecSandboxStderr + (*ExecSandboxExit)(nil), // 73: openshell.v1.ExecSandboxExit + (*ExecSandboxEvent)(nil), // 74: openshell.v1.ExecSandboxEvent + (*TcpForwardInit)(nil), // 75: openshell.v1.TcpForwardInit + (*TcpForwardFrame)(nil), // 76: openshell.v1.TcpForwardFrame + (*ExecSandboxInput)(nil), // 77: openshell.v1.ExecSandboxInput + (*ExecSandboxWindowResize)(nil), // 78: openshell.v1.ExecSandboxWindowResize + (*SshSession)(nil), // 79: openshell.v1.SshSession + (*WatchSandboxRequest)(nil), // 80: openshell.v1.WatchSandboxRequest + (*SandboxStreamEvent)(nil), // 81: openshell.v1.SandboxStreamEvent + (*SandboxLogLine)(nil), // 82: openshell.v1.SandboxLogLine + (*SandboxStreamWarning)(nil), // 83: openshell.v1.SandboxStreamWarning + (*CreateProviderRequest)(nil), // 84: openshell.v1.CreateProviderRequest + (*GetProviderRequest)(nil), // 85: openshell.v1.GetProviderRequest + (*ListProvidersRequest)(nil), // 86: openshell.v1.ListProvidersRequest + (*UpdateProviderRequest)(nil), // 87: openshell.v1.UpdateProviderRequest + (*DeleteProviderRequest)(nil), // 88: openshell.v1.DeleteProviderRequest + (*ProviderResponse)(nil), // 89: openshell.v1.ProviderResponse + (*ListProvidersResponse)(nil), // 90: openshell.v1.ListProvidersResponse + (*ListProviderProfilesRequest)(nil), // 91: openshell.v1.ListProviderProfilesRequest + (*GetProviderProfileRequest)(nil), // 92: openshell.v1.GetProviderProfileRequest + (*ProviderProfileImportItem)(nil), // 93: openshell.v1.ProviderProfileImportItem + (*ProviderProfileDiagnostic)(nil), // 94: openshell.v1.ProviderProfileDiagnostic + (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 95: openshell.v1.ProviderCredentialTokenGrantAudienceOverride + (*ProviderCredentialTokenGrantSubjectToken)(nil), // 96: openshell.v1.ProviderCredentialTokenGrantSubjectToken + (*ProviderCredentialTokenGrant)(nil), // 97: openshell.v1.ProviderCredentialTokenGrant + (*ProviderProfileCredential)(nil), // 98: openshell.v1.ProviderProfileCredential + (*ProviderCredentialRefreshMaterial)(nil), // 99: openshell.v1.ProviderCredentialRefreshMaterial + (*ProviderCredentialRefreshOutput)(nil), // 100: openshell.v1.ProviderCredentialRefreshOutput + (*ProviderCredentialRefresh)(nil), // 101: openshell.v1.ProviderCredentialRefresh + (*ProviderCredentialRefreshStatus)(nil), // 102: openshell.v1.ProviderCredentialRefreshStatus + (*ProviderProfileDiscovery)(nil), // 103: openshell.v1.ProviderProfileDiscovery + (*StoredProviderCredentialRefreshState)(nil), // 104: openshell.v1.StoredProviderCredentialRefreshState + (*StoredRefreshMaterialDeletion)(nil), // 105: openshell.v1.StoredRefreshMaterialDeletion + (*GetProviderRefreshStatusRequest)(nil), // 106: openshell.v1.GetProviderRefreshStatusRequest + (*GetProviderRefreshStatusResponse)(nil), // 107: openshell.v1.GetProviderRefreshStatusResponse + (*ConfigureProviderRefreshRequest)(nil), // 108: openshell.v1.ConfigureProviderRefreshRequest + (*ConfigureProviderRefreshResponse)(nil), // 109: openshell.v1.ConfigureProviderRefreshResponse + (*RotateProviderCredentialRequest)(nil), // 110: openshell.v1.RotateProviderCredentialRequest + (*RotateProviderCredentialResponse)(nil), // 111: openshell.v1.RotateProviderCredentialResponse + (*DeleteProviderRefreshRequest)(nil), // 112: openshell.v1.DeleteProviderRefreshRequest + (*DeleteProviderRefreshResponse)(nil), // 113: openshell.v1.DeleteProviderRefreshResponse + (*ProviderProfile)(nil), // 114: openshell.v1.ProviderProfile + (*StoredProviderProfile)(nil), // 115: openshell.v1.StoredProviderProfile + (*ProviderProfileResponse)(nil), // 116: openshell.v1.ProviderProfileResponse + (*ListProviderProfilesResponse)(nil), // 117: openshell.v1.ListProviderProfilesResponse + (*ImportProviderProfilesRequest)(nil), // 118: openshell.v1.ImportProviderProfilesRequest + (*ImportProviderProfilesResponse)(nil), // 119: openshell.v1.ImportProviderProfilesResponse + (*UpdateProviderProfilesRequest)(nil), // 120: openshell.v1.UpdateProviderProfilesRequest + (*UpdateProviderProfilesResponse)(nil), // 121: openshell.v1.UpdateProviderProfilesResponse + (*LintProviderProfilesRequest)(nil), // 122: openshell.v1.LintProviderProfilesRequest + (*LintProviderProfilesResponse)(nil), // 123: openshell.v1.LintProviderProfilesResponse + (*DeleteProviderResponse)(nil), // 124: openshell.v1.DeleteProviderResponse + (*DeleteProviderProfileRequest)(nil), // 125: openshell.v1.DeleteProviderProfileRequest + (*DeleteProviderProfileResponse)(nil), // 126: openshell.v1.DeleteProviderProfileResponse + (*GetSandboxProviderEnvironmentRequest)(nil), // 127: openshell.v1.GetSandboxProviderEnvironmentRequest + (*StaticCredentialEndpointBinding)(nil), // 128: openshell.v1.StaticCredentialEndpointBinding + (*StaticCredentialBinding)(nil), // 129: openshell.v1.StaticCredentialBinding + (*GetSandboxProviderEnvironmentResponse)(nil), // 130: openshell.v1.GetSandboxProviderEnvironmentResponse + (*ExchangeProviderSubjectTokenRequest)(nil), // 131: openshell.v1.ExchangeProviderSubjectTokenRequest + (*ExchangeProviderSubjectTokenResponse)(nil), // 132: openshell.v1.ExchangeProviderSubjectTokenResponse + (*UpdateConfigRequest)(nil), // 133: openshell.v1.UpdateConfigRequest + (*PolicyMergeOperation)(nil), // 134: openshell.v1.PolicyMergeOperation + (*AddNetworkRule)(nil), // 135: openshell.v1.AddNetworkRule + (*RemoveNetworkEndpoint)(nil), // 136: openshell.v1.RemoveNetworkEndpoint + (*RemoveNetworkRule)(nil), // 137: openshell.v1.RemoveNetworkRule + (*AddDenyRules)(nil), // 138: openshell.v1.AddDenyRules + (*AddAllowRules)(nil), // 139: openshell.v1.AddAllowRules + (*RemoveNetworkBinary)(nil), // 140: openshell.v1.RemoveNetworkBinary + (*UpdateConfigResponse)(nil), // 141: openshell.v1.UpdateConfigResponse + (*GetSandboxPolicyStatusRequest)(nil), // 142: openshell.v1.GetSandboxPolicyStatusRequest + (*GetSandboxPolicyStatusResponse)(nil), // 143: openshell.v1.GetSandboxPolicyStatusResponse + (*ListSandboxPoliciesRequest)(nil), // 144: openshell.v1.ListSandboxPoliciesRequest + (*ListSandboxPoliciesResponse)(nil), // 145: openshell.v1.ListSandboxPoliciesResponse + (*ReportPolicyStatusRequest)(nil), // 146: openshell.v1.ReportPolicyStatusRequest + (*ReportPolicyStatusResponse)(nil), // 147: openshell.v1.ReportPolicyStatusResponse + (*SandboxPolicyRevision)(nil), // 148: openshell.v1.SandboxPolicyRevision + (*GetSandboxLogsRequest)(nil), // 149: openshell.v1.GetSandboxLogsRequest + (*PushSandboxLogsRequest)(nil), // 150: openshell.v1.PushSandboxLogsRequest + (*PushSandboxLogsResponse)(nil), // 151: openshell.v1.PushSandboxLogsResponse + (*GetSandboxLogsResponse)(nil), // 152: openshell.v1.GetSandboxLogsResponse + (*SupervisorMessage)(nil), // 153: openshell.v1.SupervisorMessage + (*GatewayMessage)(nil), // 154: openshell.v1.GatewayMessage + (*SupervisorHello)(nil), // 155: openshell.v1.SupervisorHello + (*SessionAccepted)(nil), // 156: openshell.v1.SessionAccepted + (*SessionRejected)(nil), // 157: openshell.v1.SessionRejected + (*SupervisorHeartbeat)(nil), // 158: openshell.v1.SupervisorHeartbeat + (*GatewayHeartbeat)(nil), // 159: openshell.v1.GatewayHeartbeat + (*ReportMainProcessExitRequest)(nil), // 160: openshell.v1.ReportMainProcessExitRequest + (*ReportMainProcessExitResponse)(nil), // 161: openshell.v1.ReportMainProcessExitResponse + (*FinalizeMainProcessExitRequest)(nil), // 162: openshell.v1.FinalizeMainProcessExitRequest + (*FinalizeMainProcessExitResponse)(nil), // 163: openshell.v1.FinalizeMainProcessExitResponse + (*RelayOpen)(nil), // 164: openshell.v1.RelayOpen + (*SshRelayTarget)(nil), // 165: openshell.v1.SshRelayTarget + (*TcpRelayTarget)(nil), // 166: openshell.v1.TcpRelayTarget + (*RelayInit)(nil), // 167: openshell.v1.RelayInit + (*RelayFrame)(nil), // 168: openshell.v1.RelayFrame + (*RelayOpenResult)(nil), // 169: openshell.v1.RelayOpenResult + (*RelayClose)(nil), // 170: openshell.v1.RelayClose + (*L7RequestSample)(nil), // 171: openshell.v1.L7RequestSample + (*DenialSummary)(nil), // 172: openshell.v1.DenialSummary + (*DenialGroupCount)(nil), // 173: openshell.v1.DenialGroupCount + (*NetworkActivitySummary)(nil), // 174: openshell.v1.NetworkActivitySummary + (*PolicyChunk)(nil), // 175: openshell.v1.PolicyChunk + (*DraftPolicyUpdate)(nil), // 176: openshell.v1.DraftPolicyUpdate + (*SubmitPolicyAnalysisRequest)(nil), // 177: openshell.v1.SubmitPolicyAnalysisRequest + (*SubmitPolicyAnalysisResponse)(nil), // 178: openshell.v1.SubmitPolicyAnalysisResponse + (*GetDraftPolicyRequest)(nil), // 179: openshell.v1.GetDraftPolicyRequest + (*GetDraftPolicyResponse)(nil), // 180: openshell.v1.GetDraftPolicyResponse + (*ApproveDraftChunkRequest)(nil), // 181: openshell.v1.ApproveDraftChunkRequest + (*ApproveDraftChunkResponse)(nil), // 182: openshell.v1.ApproveDraftChunkResponse + (*RejectDraftChunkRequest)(nil), // 183: openshell.v1.RejectDraftChunkRequest + (*RejectDraftChunkResponse)(nil), // 184: openshell.v1.RejectDraftChunkResponse + (*DraftChunkApproval)(nil), // 185: openshell.v1.DraftChunkApproval + (*ApproveAllDraftChunksRequest)(nil), // 186: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 187: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 188: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 189: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 190: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 191: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 192: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 193: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 194: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 195: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 196: openshell.v1.GetDraftHistoryResponse + (*PolicyRevisionPayload)(nil), // 197: openshell.v1.PolicyRevisionPayload + (*DraftChunkPayload)(nil), // 198: openshell.v1.DraftChunkPayload + (*StoredPolicyRevision)(nil), // 199: openshell.v1.StoredPolicyRevision + (*StoredDraftChunk)(nil), // 200: openshell.v1.StoredDraftChunk + (*CreateWorkspaceRequest)(nil), // 201: openshell.v1.CreateWorkspaceRequest + (*CreateWorkspaceResponse)(nil), // 202: openshell.v1.CreateWorkspaceResponse + (*GetWorkspaceRequest)(nil), // 203: openshell.v1.GetWorkspaceRequest + (*GetWorkspaceResponse)(nil), // 204: openshell.v1.GetWorkspaceResponse + (*ListWorkspacesRequest)(nil), // 205: openshell.v1.ListWorkspacesRequest + (*ListWorkspacesResponse)(nil), // 206: openshell.v1.ListWorkspacesResponse + (*DeleteWorkspaceRequest)(nil), // 207: openshell.v1.DeleteWorkspaceRequest + (*DeleteWorkspaceResponse)(nil), // 208: openshell.v1.DeleteWorkspaceResponse + (*WorkspaceMember)(nil), // 209: openshell.v1.WorkspaceMember + (*AddWorkspaceMemberRequest)(nil), // 210: openshell.v1.AddWorkspaceMemberRequest + (*AddWorkspaceMemberResponse)(nil), // 211: openshell.v1.AddWorkspaceMemberResponse + (*RemoveWorkspaceMemberRequest)(nil), // 212: openshell.v1.RemoveWorkspaceMemberRequest + (*RemoveWorkspaceMemberResponse)(nil), // 213: openshell.v1.RemoveWorkspaceMemberResponse + (*ListWorkspaceMembersRequest)(nil), // 214: openshell.v1.ListWorkspaceMembersRequest + (*ListWorkspaceMembersResponse)(nil), // 215: openshell.v1.ListWorkspaceMembersResponse + (*ExtensionServiceCredential)(nil), // 216: openshell.v1.ExtensionServiceCredential + nil, // 217: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 218: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 219: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 220: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 221: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + nil, // 222: openshell.v1.PlatformEvent.MetadataEntry + nil, // 223: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 224: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 225: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 226: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 227: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + nil, // 228: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + nil, // 229: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + nil, // 230: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry + nil, // 231: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 232: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 233: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 234: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + nil, // 235: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 236: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 237: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 238: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 239: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 240: openshell.v1.PolicyRevisionPayload.ProvenanceEntry + nil, // 241: openshell.v1.StoredPolicyRevision.ProvenanceEntry + nil, // 242: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*datamodelv1.ObjectMeta)(nil), // 243: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 244: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 245: google.protobuf.Struct + (*durationpb.Duration)(nil), // 246: google.protobuf.Duration + (*datamodelv1.Provider)(nil), // 247: openshell.datamodel.v1.Provider + (*datamodelv1.CredentialHandle)(nil), // 248: openshell.datamodel.v1.CredentialHandle + (*sandboxv1.NetworkEndpoint)(nil), // 249: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 250: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 251: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 252: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 253: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 254: openshell.sandbox.v1.L7Rule + (*datamodelv1.Workspace)(nil), // 255: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 256: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 257: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 258: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 259: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 215, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential + 216, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential 5, // 1: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus 5, // 2: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus - 18, // 3: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo - 19, // 4: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities - 242, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 21, // 6: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec - 32, // 7: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus - 31, // 8: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance - 216, // 9: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry - 24, // 10: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 243, // 11: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 22, // 12: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements - 23, // 13: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 217, // 14: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 218, // 15: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 219, // 16: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 244, // 17: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 244, // 18: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 242, // 19: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 26, // 20: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec - 27, // 21: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig - 244, // 22: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct - 29, // 23: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel - 220, // 24: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry - 28, // 25: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources - 23, // 26: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements - 30, // 27: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup - 245, // 28: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration - 33, // 29: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition + 19, // 3: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo + 20, // 4: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities + 243, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 22, // 6: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec + 33, // 7: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus + 32, // 8: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance + 217, // 9: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 25, // 10: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate + 244, // 11: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 23, // 12: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements + 24, // 13: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements + 218, // 14: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 219, // 15: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 220, // 16: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 245, // 17: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 245, // 18: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 243, // 19: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 27, // 20: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec + 28, // 21: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig + 245, // 22: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct + 30, // 23: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel + 221, // 24: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + 29, // 25: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources + 24, // 26: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements + 31, // 27: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup + 246, // 28: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration + 34, // 29: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition 0, // 30: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 221, // 31: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 21, // 32: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 222, // 33: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 223, // 34: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 25, // 35: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 25, // 36: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 25, // 37: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate - 20, // 38: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 20, // 39: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 246, // 40: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 20, // 41: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 20, // 42: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 66, // 43: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 242, // 44: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 65, // 45: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 224, // 46: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 70, // 47: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 71, // 48: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 72, // 49: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 164, // 50: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 165, // 51: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 74, // 52: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 69, // 53: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 77, // 54: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 242, // 55: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 20, // 56: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 81, // 57: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 34, // 58: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 82, // 59: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 175, // 60: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 225, // 61: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 246, // 62: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 246, // 63: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 226, // 64: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - 246, // 65: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 246, // 66: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 113, // 67: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 94, // 68: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 222, // 31: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 22, // 32: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec + 223, // 33: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 224, // 34: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 26, // 35: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 26, // 36: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 26, // 37: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate + 21, // 38: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 21, // 39: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 247, // 40: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 21, // 41: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 21, // 42: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 67, // 43: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 243, // 44: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 66, // 45: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 225, // 46: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 71, // 47: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 72, // 48: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 73, // 49: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 165, // 50: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 166, // 51: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 75, // 52: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 70, // 53: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 78, // 54: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 243, // 55: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 21, // 56: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 82, // 57: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 35, // 58: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 83, // 59: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 176, // 60: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 226, // 61: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 247, // 62: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 247, // 63: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 227, // 64: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + 247, // 65: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 247, // 66: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 114, // 67: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 95, // 68: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride 1, // 69: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType - 95, // 70: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken - 100, // 71: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 96, // 72: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant - 2, // 73: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 98, // 74: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 99, // 75: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput - 2, // 76: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 7, // 77: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 242, // 78: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 2, // 79: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 227, // 80: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - 228, // 81: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - 229, // 82: openshell.v1.StoredProviderCredentialRefreshState.secret_material_handles:type_name -> openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry - 104, // 83: openshell.v1.StoredProviderCredentialRefreshState.pending_secret_deletions:type_name -> openshell.v1.StoredRefreshMaterialDeletion - 7, // 84: openshell.v1.StoredProviderCredentialRefreshState.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 247, // 85: openshell.v1.StoredRefreshMaterialDeletion.handle:type_name -> openshell.datamodel.v1.CredentialHandle - 101, // 86: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 2, // 87: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 230, // 88: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 101, // 89: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 101, // 90: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 3, // 91: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 97, // 92: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 248, // 93: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 249, // 94: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 102, // 95: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 231, // 96: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 242, // 97: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 113, // 98: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile - 113, // 99: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 113, // 100: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 92, // 101: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 93, // 102: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 113, // 103: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 92, // 104: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 93, // 105: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 113, // 106: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 92, // 107: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 93, // 108: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 127, // 109: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 232, // 110: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 233, // 111: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - 234, // 112: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 235, // 113: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 243, // 114: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 250, // 115: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 133, // 116: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 236, // 117: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 134, // 118: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 135, // 119: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 136, // 120: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 137, // 121: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 138, // 122: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 139, // 123: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 251, // 124: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 252, // 125: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 253, // 126: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 237, // 127: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 147, // 128: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 147, // 129: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 4, // 130: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 4, // 131: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 243, // 132: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 238, // 133: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 81, // 134: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 81, // 135: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 154, // 136: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 157, // 137: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 168, // 138: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 169, // 139: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 155, // 140: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 156, // 141: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 158, // 142: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 163, // 143: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 169, // 144: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 164, // 145: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 165, // 146: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 166, // 147: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 170, // 148: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 172, // 149: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 251, // 150: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 243, // 151: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 243, // 152: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 171, // 153: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 174, // 154: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 173, // 155: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 174, // 156: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 184, // 157: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 251, // 158: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 194, // 159: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 243, // 160: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 239, // 161: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry - 251, // 162: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 243, // 163: openshell.v1.DraftChunkPayload.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 243, // 164: openshell.v1.DraftChunkPayload.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 240, // 165: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry - 243, // 166: openshell.v1.StoredDraftChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 243, // 167: openshell.v1.StoredDraftChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 241, // 168: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 254, // 169: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 254, // 170: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 254, // 171: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 242, // 172: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 6, // 173: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 6, // 174: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 208, // 175: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 208, // 176: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 247, // 177: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle - 97, // 178: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 128, // 179: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 12, // 180: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 14, // 181: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 16, // 182: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 35, // 183: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 43, // 184: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 44, // 185: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 36, // 186: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest - 37, // 187: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest - 38, // 188: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest - 39, // 189: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest - 45, // 190: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 46, // 191: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 47, // 192: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 48, // 193: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 49, // 194: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 50, // 195: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 57, // 196: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 59, // 197: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 60, // 198: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 61, // 199: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 63, // 200: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 67, // 201: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 69, // 202: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 75, // 203: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 76, // 204: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 83, // 205: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 84, // 206: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 85, // 207: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 90, // 208: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 91, // 209: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 117, // 210: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 119, // 211: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 121, // 212: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 86, // 213: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 105, // 214: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 107, // 215: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 109, // 216: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 111, // 217: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 87, // 218: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 124, // 219: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 255, // 220: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 256, // 221: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 132, // 222: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 141, // 223: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 143, // 224: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 145, // 225: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 126, // 226: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 130, // 227: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 148, // 228: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 149, // 229: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 152, // 230: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 159, // 231: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 161, // 232: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest - 167, // 233: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 79, // 234: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 176, // 235: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 178, // 236: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 180, // 237: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 182, // 238: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 185, // 239: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 187, // 240: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 189, // 241: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 191, // 242: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 193, // 243: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 8, // 244: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 10, // 245: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 200, // 246: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 202, // 247: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 204, // 248: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 206, // 249: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 209, // 250: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 211, // 251: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 213, // 252: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 13, // 253: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 15, // 254: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 17, // 255: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 51, // 256: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 51, // 257: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 52, // 258: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 40, // 259: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 40, // 260: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 41, // 261: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse - 42, // 262: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse - 53, // 263: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 54, // 264: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 55, // 265: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 56, // 266: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 51, // 267: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 51, // 268: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 58, // 269: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 66, // 270: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 66, // 271: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 62, // 272: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 64, // 273: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 68, // 274: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 73, // 275: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 75, // 276: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 73, // 277: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 88, // 278: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 88, // 279: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 89, // 280: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 116, // 281: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 115, // 282: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 118, // 283: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 120, // 284: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 122, // 285: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 88, // 286: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 106, // 287: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 108, // 288: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 110, // 289: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 112, // 290: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 123, // 291: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 125, // 292: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 257, // 293: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 258, // 294: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 140, // 295: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 142, // 296: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 144, // 297: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 146, // 298: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 129, // 299: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 131, // 300: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 151, // 301: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 150, // 302: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 153, // 303: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 160, // 304: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 162, // 305: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse - 167, // 306: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 80, // 307: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 177, // 308: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 179, // 309: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 181, // 310: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 183, // 311: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 186, // 312: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 188, // 313: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 190, // 314: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 192, // 315: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 195, // 316: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 9, // 317: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 11, // 318: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 201, // 319: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 203, // 320: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 205, // 321: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 207, // 322: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 210, // 323: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 212, // 324: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 214, // 325: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 253, // [253:326] is the sub-list for method output_type - 180, // [180:253] is the sub-list for method input_type - 180, // [180:180] is the sub-list for extension type_name - 180, // [180:180] is the sub-list for extension extendee - 0, // [0:180] is the sub-list for field type_name + 96, // 70: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 101, // 71: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 97, // 72: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 8, // 73: openshell.v1.ProviderProfileCredential.delivery:type_name -> openshell.v1.ProviderCredentialDelivery + 2, // 74: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 99, // 75: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 100, // 76: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 2, // 77: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 7, // 78: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 243, // 79: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 2, // 80: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 228, // 81: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + 229, // 82: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + 230, // 83: openshell.v1.StoredProviderCredentialRefreshState.secret_material_handles:type_name -> openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry + 105, // 84: openshell.v1.StoredProviderCredentialRefreshState.pending_secret_deletions:type_name -> openshell.v1.StoredRefreshMaterialDeletion + 7, // 85: openshell.v1.StoredProviderCredentialRefreshState.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 248, // 86: openshell.v1.StoredRefreshMaterialDeletion.handle:type_name -> openshell.datamodel.v1.CredentialHandle + 102, // 87: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 2, // 88: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 231, // 89: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 102, // 90: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 102, // 91: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 3, // 92: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 98, // 93: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 249, // 94: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 250, // 95: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 103, // 96: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 232, // 97: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 243, // 98: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 114, // 99: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile + 114, // 100: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 114, // 101: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 93, // 102: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 94, // 103: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 114, // 104: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 93, // 105: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 94, // 106: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 114, // 107: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 93, // 108: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 94, // 109: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 128, // 110: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 8, // 111: openshell.v1.StaticCredentialBinding.delivery:type_name -> openshell.v1.ProviderCredentialDelivery + 233, // 112: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 234, // 113: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + 235, // 114: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 236, // 115: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 244, // 116: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 251, // 117: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 134, // 118: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 237, // 119: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 135, // 120: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 136, // 121: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 137, // 122: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 138, // 123: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 139, // 124: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 140, // 125: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 252, // 126: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 253, // 127: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 254, // 128: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 238, // 129: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 148, // 130: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 148, // 131: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 4, // 132: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 4, // 133: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 244, // 134: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 239, // 135: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 82, // 136: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 82, // 137: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 155, // 138: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 158, // 139: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 169, // 140: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 170, // 141: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 156, // 142: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 157, // 143: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 159, // 144: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 164, // 145: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 170, // 146: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 165, // 147: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 166, // 148: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 167, // 149: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 171, // 150: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 173, // 151: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 252, // 152: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 244, // 153: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 244, // 154: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 172, // 155: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 175, // 156: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 174, // 157: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 175, // 158: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 185, // 159: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 252, // 160: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 195, // 161: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 244, // 162: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 240, // 163: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry + 252, // 164: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 244, // 165: openshell.v1.DraftChunkPayload.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 244, // 166: openshell.v1.DraftChunkPayload.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 241, // 167: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry + 244, // 168: openshell.v1.StoredDraftChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 244, // 169: openshell.v1.StoredDraftChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 242, // 170: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 255, // 171: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 255, // 172: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 255, // 173: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 243, // 174: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 6, // 175: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 6, // 176: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 209, // 177: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 209, // 178: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 248, // 179: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle + 98, // 180: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 129, // 181: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 13, // 182: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 15, // 183: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 17, // 184: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 36, // 185: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 44, // 186: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 45, // 187: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 37, // 188: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest + 38, // 189: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest + 39, // 190: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest + 40, // 191: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest + 46, // 192: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 47, // 193: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 48, // 194: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 49, // 195: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 50, // 196: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 51, // 197: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 58, // 198: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 60, // 199: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 61, // 200: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 62, // 201: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 64, // 202: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 68, // 203: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 70, // 204: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 76, // 205: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 77, // 206: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 84, // 207: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 85, // 208: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 86, // 209: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 91, // 210: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 92, // 211: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 118, // 212: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 120, // 213: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 122, // 214: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 87, // 215: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 106, // 216: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 108, // 217: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 110, // 218: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 112, // 219: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 88, // 220: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 125, // 221: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 256, // 222: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 257, // 223: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 133, // 224: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 142, // 225: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 144, // 226: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 146, // 227: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 127, // 228: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 131, // 229: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 149, // 230: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 150, // 231: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 153, // 232: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 160, // 233: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 162, // 234: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest + 168, // 235: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 80, // 236: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 177, // 237: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 179, // 238: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 181, // 239: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 183, // 240: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 186, // 241: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 188, // 242: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 190, // 243: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 192, // 244: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 194, // 245: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 9, // 246: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 11, // 247: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 201, // 248: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 203, // 249: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 205, // 250: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 207, // 251: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 210, // 252: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 212, // 253: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 214, // 254: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 14, // 255: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 16, // 256: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 18, // 257: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 52, // 258: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 52, // 259: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 53, // 260: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 41, // 261: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 41, // 262: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 42, // 263: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse + 43, // 264: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse + 54, // 265: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 55, // 266: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 56, // 267: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 57, // 268: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 52, // 269: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 52, // 270: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 59, // 271: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 67, // 272: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 67, // 273: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 63, // 274: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 65, // 275: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 69, // 276: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 74, // 277: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 76, // 278: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 74, // 279: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 89, // 280: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 89, // 281: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 90, // 282: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 117, // 283: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 116, // 284: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 119, // 285: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 121, // 286: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 123, // 287: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 89, // 288: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 107, // 289: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 109, // 290: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 111, // 291: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 113, // 292: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 124, // 293: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 126, // 294: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 258, // 295: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 259, // 296: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 141, // 297: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 143, // 298: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 145, // 299: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 147, // 300: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 130, // 301: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 132, // 302: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 152, // 303: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 151, // 304: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 154, // 305: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 161, // 306: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 163, // 307: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse + 168, // 308: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 81, // 309: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 178, // 310: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 180, // 311: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 182, // 312: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 184, // 313: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 187, // 314: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 189, // 315: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 191, // 316: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 193, // 317: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 196, // 318: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 10, // 319: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 12, // 320: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 202, // 321: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 204, // 322: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 206, // 323: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 208, // 324: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 211, // 325: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 213, // 326: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 215, // 327: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 255, // [255:328] is the sub-list for method output_type + 182, // [182:255] is the sub-list for method input_type + 182, // [182:182] is the sub-list for extension type_name + 182, // [182:182] is the sub-list for extension extendee + 0, // [0:182] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -16884,7 +16963,7 @@ func file_openshell_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_openshell_proto_rawDesc), len(file_openshell_proto_rawDesc)), - NumEnums: 8, + NumEnums: 9, NumMessages: 234, NumExtensions: 0, NumServices: 1, From 5f542b70931b3618f484c58ff77492e1d7ec12d7 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Wed, 2 Sep 2026 17:02:51 +0000 Subject: [PATCH 10/18] docs(agent): remove legacy bridge alias guidance Signed-off-by: Johnny Greco --- .agents/skills/debug-openshell-cluster/SKILL.md | 3 +-- .agents/skills/openshell-cli/SKILL.md | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.agents/skills/debug-openshell-cluster/SKILL.md b/.agents/skills/debug-openshell-cluster/SKILL.md index cbc4b6920f..2ab51fd62f 100644 --- a/.agents/skills/debug-openshell-cluster/SKILL.md +++ b/.agents/skills/debug-openshell-cluster/SKILL.md @@ -116,8 +116,7 @@ an agent-conversation binding, uses `fail_closed`, selects one exact provider host without exclusions, and that the host resolves to one admitted network endpoint. The loopback bridge is available only in combined topology and is started when the sandbox starts with an agent binding. Check the sandbox -environment for `OPENSHELL_AGENT_CONVERSATION_URL`; managed Pi may use the -compatibility alias `OPENSHELL_PI_CONVERSATION_URL`. A generation mismatch after +environment for `OPENSHELL_AGENT_CONVERSATION_URL`. A generation mismatch after a rejected or fail-closed policy reload intentionally returns admission unavailable instead of evaluating against stale middleware state. diff --git a/.agents/skills/openshell-cli/SKILL.md b/.agents/skills/openshell-cli/SKILL.md index 21bda4d8c0..841e14296a 100644 --- a/.agents/skills/openshell-cli/SKILL.md +++ b/.agents/skills/openshell-cli/SKILL.md @@ -449,9 +449,9 @@ An operator service can also advertise `AGENT_CONVERSATION/AGENT_CONTEXT`. Agent admission requires `fail_closed`, exactly one configured agent binding, and one exact provider host that resolves to one admitted network endpoint. The supervisor exposes `OPENSHELL_AGENT_CONVERSATION_URL` in combined topology and -keeps `OPENSHELL_PI_CONVERSATION_URL` as a compatibility alias. Adding the first -agent binding to a running sandbox requires recreating the sandbox so the -bridge listener and environment are installed. +uses that generic variable for every harness. Adding the first agent binding to +a running sandbox requires recreating the sandbox so OpenShell installs the +bridge listener and environment. ### Step 5: Push the updated policy From 4dc92be4648cd4db96cbf8e94660e64f4902ad6e Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Wed, 2 Sep 2026 17:20:40 +0000 Subject: [PATCH 11/18] docs(skills): explain proxy credential delivery Signed-off-by: Johnny Greco --- skills/openshell-cli/SKILL.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index 48f84fa653..6c39b296b6 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -141,6 +141,13 @@ when a placeholder is present but requests receive `credential_endpoint_mismatch`. A profileless static provider fails closed because the gateway cannot construct a binding. +Static credentials use `delivery: environment` by default, which exposes an +endpoint-bound placeholder to the sandbox. A custom profile may instead use +`delivery: proxy` with `auth_style: bearer` or a named header. Proxy delivery +keeps both the credential and its placeholder out of the workload environment; +the inspected HTTP proxy sets the complete header only after the request passes +network and middleware policy. Export the profile to confirm which mode applies. + When an inspected request receives `request_authority_mismatch`, compare its HTTP authority with the CONNECT tunnel endpoint. The host and effective port must match. For a tunnel to `api.example.com:8443`, send From 8f208f5c6beb1a4a4cbf21afe45732d08ee955ab Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Wed, 2 Sep 2026 17:53:00 +0000 Subject: [PATCH 12/18] feat(sandbox): allow admission without handle Signed-off-by: Johnny Greco --- crates/openshell-sandbox/src/agent_bridge.rs | 57 +++++++++++++------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/crates/openshell-sandbox/src/agent_bridge.rs b/crates/openshell-sandbox/src/agent_bridge.rs index c86c266159..18e5bbd441 100644 --- a/crates/openshell-sandbox/src/agent_bridge.rs +++ b/crates/openshell-sandbox/src/agent_bridge.rs @@ -225,30 +225,33 @@ fn bridge_result( ) -> Result { match Decision::try_from(result.decision).unwrap_or(Decision::Unspecified) { Decision::Allow => { - if result.attestation.is_empty() { - return Err("missing_admission_attestation"); - } - let port = - u16::try_from(selection.provider_port).map_err(|_| "invalid_provider_port")?; - let handle = runner - .issue_agent_admission_handle( - openshell_supervisor_middleware::AgentAdmissionGrantInput { - sandbox_id, - middleware_name: &selection.middleware_name, - scheme: &selection.provider_scheme, - host: &selection.provider_host, - port, - policy_generation, - attestation: result.attestation, - }, + let handle = if result.attestation.is_empty() { + None + } else { + let port = + u16::try_from(selection.provider_port).map_err(|_| "invalid_provider_port")?; + Some( + runner + .issue_agent_admission_handle( + openshell_supervisor_middleware::AgentAdmissionGrantInput { + sandbox_id, + middleware_name: &selection.middleware_name, + scheme: &selection.provider_scheme, + host: &selection.provider_host, + port, + policy_generation, + attestation: result.attestation, + }, + ) + .map_err(|_| "admission_store_unavailable")?, ) - .map_err(|_| "admission_store_unavailable")?; + }; Ok(BridgeResponse { decision: "allow", replacement_body: result .has_replacement_body .then_some(result.replacement_body), - handle: Some(handle), + handle, reason_code: None, metadata: (!result.metadata.is_empty()).then_some(result.metadata), }) @@ -441,4 +444,22 @@ mod tests { assert_eq!(deny.handle, None); assert_eq!(deny.replacement_body, None); } + + #[test] + fn result_mapping_allows_without_an_attestation_handle() { + let response = bridge_result( + &openshell_supervisor_middleware::ChainRunner::default(), + "sandbox-1", + 7, + &selection(1024), + AgentConversationResult { + decision: Decision::Allow as i32, + ..Default::default() + }, + ) + .expect("allow without attestation"); + + assert_eq!(response.decision, "allow"); + assert_eq!(response.handle, None); + } } From bd5d278fdfc5d8c0c43c1759916868eb61be8f12 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Wed, 2 Sep 2026 18:23:51 +0000 Subject: [PATCH 13/18] feat(providers): drive proxy delivery from credential bindings Make the static credential binding the sandbox's single source of truth for proxy-delivered credentials. The gateway copies the delivery mode, auth style, and header name onto the binding, and the proxy resolves matching proxy-delivered bindings through the request-scoped resolver instead of consulting the dynamic token grant map, which returns to carrying token grants only. Validate proxy-delivered credential values against their declared placement when a provider is created or updated, so a non-token68 bearer value fails with a message that names the credential rather than as a 502 on every request. Emit OCSF HTTP activity events for every successful and failed injection, treat a poisoned credential registry as an error instead of "no credential", and end any open middleware session when forward-proxy injection fails. Add request-level tests through relay_rest, the passthrough relay, and a real handle_forward_proxy round trip, profile validation tests for every proxy-delivery rejection, gateway tests for create/update value validation and binding metadata, and a Docker e2e test that verifies the variable is absent from the sandbox and the upstream receives the injected credential. Document the ambiguity rule, value constraints, and failure modes, and update the sandbox and gateway architecture docs. Signed-off-by: Johnny Greco --- architecture/gateway.md | 28 +- architecture/sandbox.md | 17 + crates/openshell-core/src/oauth.rs | 20 +- .../src/provider_credentials.rs | 253 ++++++++- crates/openshell-providers/src/profiles.rs | 201 +++++++ crates/openshell-server/src/grpc/provider.rs | 309 ++++++++-- .../src/l7/relay.rs | 246 ++++++++ .../src/l7/token_grant_injection.rs | 532 ++++++++++++++---- .../src/l7/websocket.rs | 2 + .../openshell-supervisor-network/src/proxy.rs | 351 ++++++++++++ docs/providers/profiles.mdx | 8 + e2e/rust/Cargo.toml | 5 + e2e/rust/tests/provider_proxy_delivery.rs | 429 ++++++++++++++ proto/openshell.proto | 5 + sdk/go/proto/openshellv1/openshell.pb.go | 29 +- skills/openshell-cli/SKILL.md | 6 + 16 files changed, 2254 insertions(+), 187 deletions(-) create mode 100644 e2e/rust/tests/provider_proxy_delivery.rs diff --git a/architecture/gateway.md b/architecture/gateway.md index f86411511b..59145a0c7d 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -177,8 +177,9 @@ their profile payloads. Each logical gateway request captures the selected sources into one validated, immutable effective catalog before deriving provider behavior. Policy layers, -credential scope, injected environment material, dynamic token grants, and -provider-environment revisions use that same catalog. Each configured source is +credential scope, injected environment material, dynamic token grants, +proxy-delivered static credential bindings, and provider-environment revisions +use that same catalog. Each configured source is therefore fetched at most once per request, and a source revision change becomes visible on the next request instead of partway through the current request. The capture emits debug diagnostics with the combined catalog revision, source fetch @@ -479,12 +480,23 @@ target so an edited export cannot overwrite a different profile. Database migrations backfill existing rows with version 1. Provider profile imports, updates, and deletes hold the sandbox synchronization -guard while checking attached-sandbox dynamic token grant ambiguity or in-use -state and writing the profile record. Sandbox creation with initial providers and -sandbox provider attach/detach use the same guard, so one gateway process cannot -interleave a profile mutation with a sandbox provider-set mutation that would -leave an ambiguous final dynamic-token state or a deleted custom profile that is -still referenced by a sandbox. +guard while checking attached-sandbox runtime-injected credential ambiguity or +in-use state and writing the profile record. Runtime-injected credentials are +dynamic token grants and proxy-delivered static credentials. Token grants on +overlapping selectors are ambiguous only at equal specificity; proxy-delivered +credentials are ambiguous on any overlap of the same port, because the workload +sends no credential the proxy could use to disambiguate. Sandbox creation with +initial providers and sandbox provider attach/detach use the same guard, so one +gateway process cannot interleave a profile mutation with a sandbox provider-set +mutation that would leave an ambiguous final runtime-credential state or a +deleted custom profile that is still referenced by a sandbox. + +Proxy-delivered credential values are validated against their declared +placement when a provider is created or updated: `bearer` values must be +`token68` and named `header` values must not contain control characters. The +gateway copies the delivery mode, auth style, and header name onto the static +credential binding it sends to the sandbox so the supervisor never needs profile +metadata to build the header. Policy and runtime settings are delivered together through the effective sandbox config path. A gateway-global policy can override sandbox-scoped policy. The diff --git a/architecture/sandbox.md b/architecture/sandbox.md index dd9621a09d..78c98cc338 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -394,6 +394,23 @@ subject, and gateway SPIFFE subject, and their cache lifetime is capped by the intermediate token response, stored subject-token expiry, and supervisor SVID expiry. +Static credentials may instead opt into proxy delivery (`delivery: proxy` on a +`bearer` or named `header` profile credential). The gateway marks the static +credential binding with the delivery mode and placement metadata, and the +supervisor removes the key from the child environment so the workload holds +neither the secret nor a placeholder. For an inspected REST request whose +endpoint matches a proxy-delivered binding, the proxy resolves the value through +the same request-scoped resolver used for placeholders and replaces the complete +header immediately before the upstream write, after network policy, L7 rules, +and middleware have admitted the request. The binding is the only source of +truth: the proxy does not consult profile metadata at request time. Aliases of +one credential collapse into a single header; distinct matching credentials fail +closed because the gateway already rejects that configuration. Injection +requires an inspected REST endpoint without `tls: skip`, so uninspected traffic +forwards whatever header the application sent. Success and failure both emit an +OCSF HTTP activity event naming the environment key and endpoint but never the +value. + For AWS endpoints that require request-level signing, the proxy supports SigV4 re-signing. When `credential_signing: sigv4` is set on an L7 endpoint, the proxy strips the client's placeholder-based AWS auth headers, re-signs with real diff --git a/crates/openshell-core/src/oauth.rs b/crates/openshell-core/src/oauth.rs index c68ecfa6b4..b3997cfd82 100644 --- a/crates/openshell-core/src/oauth.rs +++ b/crates/openshell-core/src/oauth.rs @@ -239,25 +239,7 @@ pub fn validate_access_token(token: &str) -> Result<()> { Ok(()) } -fn is_token68(token: &str) -> bool { - let mut padding_started = false; - let mut saw_value = false; - for byte in token.bytes() { - if byte == b'=' { - padding_started = true; - continue; - } - if padding_started || !is_token68_value_byte(byte) { - return false; - } - saw_value = true; - } - saw_value -} - -fn is_token68_value_byte(byte: u8) -> bool { - byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~' | b'+' | b'/') -} +pub use crate::provider_credentials::is_token68; fn failure_message(status: reqwest::StatusCode, body: &str) -> String { let Ok(error_response) = serde_json::from_str::(body) else { diff --git a/crates/openshell-core/src/provider_credentials.rs b/crates/openshell-core/src/provider_credentials.rs index 4f70e02d30..e98834beac 100644 --- a/crates/openshell-core/src/provider_credentials.rs +++ b/crates/openshell-core/src/provider_credentials.rs @@ -22,6 +22,80 @@ pub struct ProviderCredentialSnapshot { pub dynamic_credentials: HashMap, } +/// Placement metadata for a static credential that opted into proxy delivery. +/// +/// The sandbox proxy uses this to build the complete outbound header from the +/// endpoint-bound binding alone, without consulting profile metadata. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ProxyDeliveredCredential { + pub env_key: String, + pub auth_style: String, + pub header_name: String, +} + +/// Validate a static credential value against the header placement its +/// profile declares. +/// +/// Proxy-delivered credentials are written into an HTTP header verbatim, so a +/// value the proxy would reject at request time must be rejected when the +/// provider is created or updated instead. +pub fn validate_proxy_delivered_credential_value( + auth_style: &str, + value: &str, +) -> Result<(), String> { + if value.is_empty() { + return Err("credential value must not be empty".to_string()); + } + match auth_style.trim().to_ascii_lowercase().as_str() { + "" | "bearer" => { + if !is_token68(value) { + return Err( + "bearer credential values may only contain letters, digits, '-', '.', '_', '~', '+', '/', and trailing '=' padding" + .to_string(), + ); + } + Ok(()) + } + "header" => { + if value + .bytes() + .any(|byte| (byte < b' ' && byte != b'\t') || byte == 0x7f) + { + return Err( + "header credential values must not contain control characters".to_string(), + ); + } + Ok(()) + } + other => Err(format!( + "proxy delivery does not support auth_style '{other}'; use bearer or header" + )), + } +} + +/// Whether `token` is a non-empty RFC 7235 `token68` value, the character set +/// permitted in a `Bearer` authorization credential. +#[must_use] +pub fn is_token68(token: &str) -> bool { + let mut padding_started = false; + let mut saw_value = false; + for byte in token.bytes() { + if byte == b'=' { + padding_started = true; + continue; + } + if padding_started || !is_token68_value_byte(byte) { + return false; + } + saw_value = true; + } + saw_value +} + +fn is_token68_value_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~' | b'+' | b'/') +} + #[derive(Debug)] struct ProviderCredentialStateInner { current: Arc, @@ -47,6 +121,8 @@ struct CompiledStaticCredentialBinding { credential_identity: String, workload_credential_handle: String, delivery: crate::proto::ProviderCredentialDelivery, + auth_style: String, + header_name: String, } #[derive(Debug, Clone)] @@ -271,31 +347,17 @@ impl ProviderCredentialState { port: u16, path: &str, ) -> (Option>, u64) { - let request_path = path.split_once('?').map_or(path, |(path, _)| path); - let request_path = crate::secrets::redact_target_for_policy(request_path); - let normalized_host = host.to_ascii_lowercase(); - let host_labels = normalized_host.split('.').collect::>(); let inner = self .inner .read() .expect("provider credential state poisoned"); let revision = inner.current.revision; - let Ok(request_path) = request_path else { + let Some(allowed) = allowed_static_credential_keys(&inner, host, port, path) else { // Binding authorization must not depend on real credential // material. Malformed placeholder syntax cannot be normalized // safely, so expose no endpoint-scoped resolver. return (None, revision); }; - let allowed: HashSet = inner - .static_credential_bindings - .iter() - .filter(|(_, binding)| { - binding.endpoints.iter().any(|endpoint| { - static_credential_endpoint_matches(endpoint, &host_labels, port, &request_path) - }) - }) - .map(|(key, _)| key.clone()) - .collect(); let resolver = inner.combined_resolver.as_ref().map(|resolver| { let revision_fallback_allowed_revisions = inner .static_credential_identity_epochs @@ -318,6 +380,43 @@ impl ProviderCredentialState { (resolver, revision) } + /// Proxy-delivered static credentials whose bindings authorize this + /// endpoint, sorted by environment key. + /// + /// The proxy injects these into the outbound request instead of exposing + /// a placeholder to the workload. An unparseable request path yields no + /// credentials for the same reason it yields no endpoint-scoped resolver. + #[must_use] + pub fn proxy_delivered_credentials_for_endpoint( + &self, + host: &str, + port: u16, + path: &str, + ) -> Vec { + let inner = self + .inner + .read() + .expect("provider credential state poisoned"); + let Some(allowed) = allowed_static_credential_keys(&inner, host, port, path) else { + return Vec::new(); + }; + let mut credentials = inner + .static_credential_bindings + .iter() + .filter(|(key, binding)| { + allowed.contains(*key) + && binding.delivery == crate::proto::ProviderCredentialDelivery::Proxy + }) + .map(|(key, binding)| ProxyDeliveredCredential { + env_key: key.clone(), + auth_style: binding.auth_style.clone(), + header_name: binding.header_name.clone(), + }) + .collect::>(); + credentials.sort(); + credentials + } + #[must_use] pub fn revision(&self) -> u64 { self.inner @@ -689,6 +788,8 @@ fn compile_static_credential_bindings( workload_credential_handle: binding.workload_credential_handle, delivery: crate::proto::ProviderCredentialDelivery::try_from(binding.delivery) .unwrap_or(crate::proto::ProviderCredentialDelivery::Environment), + auth_style: binding.auth_style, + header_name: binding.header_name, }, )) }) @@ -786,6 +887,35 @@ fn binding_error(message: &str) -> StaticCredentialBindingError { } } +/// Static credential keys whose bindings authorize `host:port path`. +/// +/// Returns `None` when the request path cannot be normalized. Binding +/// authorization must not depend on real credential material, so malformed +/// placeholder syntax in the path authorizes nothing. +fn allowed_static_credential_keys( + inner: &ProviderCredentialStateInner, + host: &str, + port: u16, + path: &str, +) -> Option> { + let request_path = path.split_once('?').map_or(path, |(path, _)| path); + let request_path = crate::secrets::redact_target_for_policy(request_path).ok()?; + let normalized_host = host.to_ascii_lowercase(); + let host_labels = normalized_host.split('.').collect::>(); + Some( + inner + .static_credential_bindings + .iter() + .filter(|(_, binding)| { + binding.endpoints.iter().any(|endpoint| { + static_credential_endpoint_matches(endpoint, &host_labels, port, &request_path) + }) + }) + .map(|(key, _)| key.clone()) + .collect(), + ) +} + fn static_credential_endpoint_matches( endpoint: &CompiledStaticCredentialEndpointBinding, host_labels: &[&str], @@ -825,6 +955,8 @@ mod tests { credential_identity: "provider-a:API_KEY".to_string(), workload_credential_handle: String::new(), delivery: 0, + auth_style: String::new(), + header_name: String::new(), } } @@ -985,6 +1117,97 @@ mod tests { ); } + #[test] + fn proxy_delivered_credentials_are_listed_only_for_bound_endpoints() { + let mut proxy_binding = binding("api.example.com", 443, "/v1/**"); + proxy_binding.delivery = crate::proto::ProviderCredentialDelivery::Proxy as i32; + proxy_binding.auth_style = "header".to_string(); + proxy_binding.header_name = "x-api-key".to_string(); + let mut alias_binding = proxy_binding.clone(); + alias_binding.credential_identity = "provider-a:API_KEY_ALIAS".to_string(); + let environment_binding = binding("api.example.com", 443, "/v1/**"); + let state = ProviderCredentialState::from_bound_environment( + 7, + HashMap::from([ + ("API_KEY".to_string(), "secret".to_string()), + ("API_KEY_ALIAS".to_string(), "secret".to_string()), + ("OTHER_KEY".to_string(), "other".to_string()), + ]), + HashMap::new(), + HashMap::new(), + HashMap::from([ + ("API_KEY".to_string(), proxy_binding), + ("API_KEY_ALIAS".to_string(), alias_binding), + ("OTHER_KEY".to_string(), environment_binding), + ]), + Vec::new(), + ) + .expect("valid bindings"); + + let credentials = + state.proxy_delivered_credentials_for_endpoint("API.example.com", 443, "/v1/chat?x=1"); + assert_eq!( + credentials, + vec![ + ProxyDeliveredCredential { + env_key: "API_KEY".to_string(), + auth_style: "header".to_string(), + header_name: "x-api-key".to_string(), + }, + ProxyDeliveredCredential { + env_key: "API_KEY_ALIAS".to_string(), + auth_style: "header".to_string(), + header_name: "x-api-key".to_string(), + }, + ] + ); + assert!( + state + .proxy_delivered_credentials_for_endpoint("api.example.com", 443, "/v2/chat") + .is_empty() + ); + assert!( + state + .proxy_delivered_credentials_for_endpoint("api.example.com", 8443, "/v1/chat") + .is_empty() + ); + assert!( + state + .proxy_delivered_credentials_for_endpoint("other.example.com", 443, "/v1/chat") + .is_empty() + ); + } + + #[test] + fn proxy_delivered_credential_values_are_validated_per_auth_style() { + validate_proxy_delivered_credential_value("bearer", "sk-live_abc.DEF~123+/==") + .expect("token68 bearer value"); + validate_proxy_delivered_credential_value("", "plain-token").expect("default is bearer"); + validate_proxy_delivered_credential_value("header", "key with spaces = allowed") + .expect("header values may contain spaces"); + + assert!( + validate_proxy_delivered_credential_value("bearer", "has space") + .expect_err("space is not token68") + .contains("bearer credential values") + ); + assert!( + validate_proxy_delivered_credential_value("bearer", "") + .expect_err("empty value") + .contains("must not be empty") + ); + assert!( + validate_proxy_delivered_credential_value("header", "safe\r\ninjected: value") + .expect_err("CRLF is a control sequence") + .contains("control characters") + ); + assert!( + validate_proxy_delivered_credential_value("query", "value") + .expect_err("query placement is not proxy deliverable") + .contains("use bearer or header") + ); + } + #[test] fn multiple_credentials_resolve_only_at_their_own_endpoints() { let mut binding_a = binding("a.example.com", 443, "/a/**"); diff --git a/crates/openshell-providers/src/profiles.rs b/crates/openshell-providers/src/profiles.rs index ed2fbb3ddc..e0b6fac417 100644 --- a/crates/openshell-providers/src/profiles.rs +++ b/crates/openshell-providers/src/profiles.rs @@ -3044,6 +3044,9 @@ fn validate_injected_header_name( _ => return Ok(()), }; if header_name.is_empty() { + // `validate_profile_set` already reports a missing header_name for + // `header` auth before reaching this check. Kept so the function is + // self-contained for callers that validate a single credential. return Err(format!("{context} auth_style header requires header_name")); } let valid = header_name.bytes().all(|byte| { @@ -3801,6 +3804,204 @@ endpoints: == "provider profiles cannot combine proxy-delivered credentials with token grants")); } + fn proxy_delivery_diagnostics(yaml: &str) -> Vec { + let profile = parse_profile_yaml(yaml).expect("profile should parse"); + validate_profile_set(&[("proxy.yaml".to_string(), profile)]) + .into_iter() + .map(|diagnostic| diagnostic.message) + .collect() + } + + #[test] + fn proxy_delivery_requires_inspected_rest_endpoints() { + let messages = proxy_delivery_diagnostics( + r" +id: proxy-endpoints +display_name: Proxy Endpoints +credentials: + - name: api_key + env_vars: [API_KEY] + delivery: proxy + auth_style: bearer +endpoints: + - host: api.example.com + port: 443 + protocol: rest + access: full + tls: skip + - host: mcp.example.com + port: 443 + protocol: mcp + access: full + - host: ws.example.com + port: 443 + protocol: websocket + access: read-write +", + ); + assert!( + messages + .iter() + .any(|message| message == "proxy-delivered credentials do not support tls: skip"), + "{messages:?}" + ); + assert_eq!( + messages + .iter() + .filter(|message| *message == "proxy-delivered credentials require protocol: rest") + .count(), + 2, + "{messages:?}" + ); + + let messages = proxy_delivery_diagnostics( + r" +id: proxy-no-endpoints +display_name: Proxy No Endpoints +credentials: + - name: api_key + env_vars: [API_KEY] + delivery: proxy + auth_style: bearer +", + ); + assert!( + messages + .iter() + .any(|message| message == "proxy-delivered credentials require a profile endpoint"), + "{messages:?}" + ); + } + + #[test] + fn proxy_delivery_rejects_unsupported_placement() { + for auth_style in ["basic", "query", "path"] { + let extra = match auth_style { + "query" => " query_param: api_key\n", + "path" => " path_template: /v1/{credential}/x\n", + _ => "", + }; + let messages = proxy_delivery_diagnostics(&format!( + r" +id: proxy-style +display_name: Proxy Style +credentials: + - name: api_key + env_vars: [API_KEY] + delivery: proxy + auth_style: {auth_style} +{extra}endpoints: + - host: api.example.com + port: 443 + protocol: rest + access: full +" + )); + assert!( + messages.iter().any(|message| message + == "proxy-delivered credentials support auth_style bearer or header"), + "{auth_style}: {messages:?}" + ); + } + + let messages = proxy_delivery_diagnostics( + r" +id: proxy-framing-header +display_name: Proxy Framing Header +credentials: + - name: api_key + env_vars: [API_KEY] + delivery: proxy + auth_style: header + header_name: Content-Length +endpoints: + - host: api.example.com + port: 443 + protocol: rest + access: full +", + ); + assert!( + messages.iter().any(|message| message + == "proxy delivery header_name may not override HTTP framing or connection headers"), + "{messages:?}" + ); + + let messages = proxy_delivery_diagnostics( + r" +id: proxy-bad-header +display_name: Proxy Bad Header +credentials: + - name: api_key + env_vars: [API_KEY] + delivery: proxy + auth_style: bearer + header_name: 'x api key' +endpoints: + - host: api.example.com + port: 443 + protocol: rest + access: full +", + ); + assert!( + messages + .iter() + .any(|message| message + == "proxy delivery header_name is not a valid HTTP header name"), + "{messages:?}" + ); + } + + #[test] + fn proxy_delivery_rejects_token_grant_on_same_credential() { + let messages = proxy_delivery_diagnostics( + r" +id: proxy-token-grant +display_name: Proxy Token Grant +credentials: + - name: access_token + env_vars: [ACCESS_TOKEN] + delivery: proxy + auth_style: bearer + token_grant: + token_endpoint: https://login.example.com/oauth2/token +endpoints: + - host: api.example.com + port: 443 + protocol: rest + access: full +", + ); + assert!( + messages + .iter() + .any(|message| message == "proxy delivery is only valid for static credentials"), + "{messages:?}" + ); + } + + #[test] + fn proxy_delivery_rejects_unknown_delivery_values() { + let error = parse_profile_yaml( + r" +id: proxy-unknown +display_name: Proxy Unknown +credentials: + - name: api_key + env_vars: [API_KEY] + delivery: sidecar +", + ) + .expect_err("unknown delivery must fail to parse"); + assert!( + error + .to_string() + .contains("unsupported provider credential delivery: sidecar"), + "{error}" + ); + } + #[test] fn token_grant_audience_overrides_round_trip_through_proto() { let profile = parse_profile_yaml( diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index d06919fdbf..542b2fc9e9 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -64,7 +64,8 @@ fn redact_provider_credentials(mut provider: Provider) -> Provider { pub(super) struct ProviderEnvironment { pub environment: HashMap, pub credential_expires_at_ms: HashMap, - /// Endpoint metadata for token grants and proxy-delivered static credentials. + /// Endpoint metadata for dynamic token grants. Proxy-delivered static + /// credentials travel in `static_credential_bindings` instead. pub dynamic_credentials: HashMap, pub static_credential_bindings: HashMap, pub static_credential_keys: HashSet, @@ -1287,7 +1288,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin key, endpoints, refresh_epochs.get(key).map(String::as_str), - profile_credential_delivery_for_key(profile_proto.as_ref(), key), + profile_credential_for_key(profile_proto.as_ref(), key), ), ); } @@ -1358,7 +1359,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin &key, endpoints, refresh_epochs.get(&key).map(String::as_str), - profile_credential_delivery_for_key(profile_proto.as_ref(), &key), + profile_credential_for_key(profile_proto.as_ref(), &key), ), ); } @@ -1384,7 +1385,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin Ok(ProviderEnvironment { environment: env, credential_expires_at_ms: expires, - dynamic_credentials: resolve_runtime_credentials_from_records(catalog, records), + dynamic_credentials: resolve_dynamic_credentials_from_records(catalog, records), static_credential_bindings, static_credential_keys, }) @@ -1429,7 +1430,7 @@ fn static_credential_binding( key: &str, endpoints: &[StaticCredentialEndpointBinding], authorization_epoch: Option<&str>, - delivery: ProviderCredentialDelivery, + profile_credential: Option<&ProviderProfileCredential>, ) -> StaticCredentialBinding { let workload_credential_handle = sandbox_id .zip(authorization_epoch) @@ -1442,28 +1443,42 @@ fn static_credential_binding( } else { format!("refresh:{workload_credential_handle}") }; + // The binding is the sandbox's only source of truth for proxy delivery, so + // it carries the placement metadata the proxy needs to build the header. + // Environment-delivered credentials keep the wire fields empty. + let proxy_credential = profile_credential.filter(|credential| { + ProviderCredentialDelivery::try_from(credential.delivery).unwrap_or_default() + == ProviderCredentialDelivery::Proxy + }); + let delivery = if proxy_credential.is_some() { + ProviderCredentialDelivery::Proxy + } else { + ProviderCredentialDelivery::Environment + }; StaticCredentialBinding { endpoints: endpoints.to_vec(), credential_identity, workload_credential_handle, delivery: delivery as i32, + auth_style: proxy_credential + .map(|credential| credential.auth_style.trim().to_ascii_lowercase()) + .unwrap_or_default(), + header_name: proxy_credential + .map(|credential| credential.header_name.trim().to_string()) + .unwrap_or_default(), } } -fn profile_credential_delivery_for_key( - profile: Option<&ProviderProfile>, +fn profile_credential_for_key<'a>( + profile: Option<&'a ProviderProfile>, key: &str, -) -> ProviderCredentialDelivery { - profile - .and_then(|profile| { - profile - .credentials - .iter() - .find(|credential| credential.env_vars.iter().any(|env_var| env_var == key)) - }) - .and_then(|credential| ProviderCredentialDelivery::try_from(credential.delivery).ok()) - .filter(|delivery| *delivery == ProviderCredentialDelivery::Proxy) - .unwrap_or(ProviderCredentialDelivery::Environment) +) -> Option<&'a ProviderProfileCredential> { + profile.and_then(|profile| { + profile + .credentials + .iter() + .find(|credential| credential.env_vars.iter().any(|env_var| env_var == key)) + }) } fn derive_workload_credential_handle( @@ -1510,9 +1525,9 @@ fn hash_handle_component(hasher: &mut Sha256, value: &[u8]) { hasher.update(value); } -/// Resolve runtime-injected credentials from the same records used for the -/// provider-environment revision and static credential bindings. -fn resolve_runtime_credentials_from_records( +/// Resolve dynamic credentials (token grants) from the same records used for +/// the provider-environment revision and static credential bindings. +fn resolve_dynamic_credentials_from_records( catalog: &EffectiveProviderProfileCatalog, records: &[ProviderEnvironmentRecord], ) -> HashMap { @@ -1526,7 +1541,7 @@ fn resolve_runtime_credentials_from_records( ) else { continue; }; - insert_runtime_credentials_for_profile( + insert_dynamic_credentials_for_profile( &mut dynamic_creds, &profile.to_proto(), &record.name, @@ -1535,16 +1550,13 @@ fn resolve_runtime_credentials_from_records( dynamic_creds } -fn insert_runtime_credentials_for_profile( +fn insert_dynamic_credentials_for_profile( dynamic_creds: &mut HashMap, profile: &ProviderProfile, provider_name: &str, ) { for credential in &profile.credentials { - if credential.token_grant.is_none() - && ProviderCredentialDelivery::try_from(credential.delivery).unwrap_or_default() - != ProviderCredentialDelivery::Proxy - { + if credential.token_grant.is_none() { continue; } for endpoint in &profile.endpoints { @@ -1706,7 +1718,7 @@ fn host_pattern_labels_match(pattern: &[&str], host: &[&str]) -> bool { } } -fn dynamic_token_grant_match_score(host: &str, path: &str) -> u32 { +fn runtime_credential_match_score(host: &str, path: &str) -> u32 { host_pattern_specificity(host) + endpoint_path_specificity(path) } @@ -2092,7 +2104,7 @@ fn runtime_credential_bindings_for_profile( } for endpoint in &profile.endpoints { for port in endpoint_ports(endpoint.port, &endpoint.ports) { - push_dynamic_token_grant_bindings_for_endpoint( + push_runtime_credential_bindings_for_endpoint( &mut bindings, provider_name, credential, @@ -2106,7 +2118,7 @@ fn runtime_credential_bindings_for_profile( bindings } -fn push_dynamic_token_grant_bindings_for_endpoint( +fn push_runtime_credential_bindings_for_endpoint( bindings: &mut Vec, provider_name: &str, credential: &ProviderProfileCredential, @@ -2175,7 +2187,7 @@ fn push_runtime_credential_binding( host: host.to_ascii_lowercase(), port, path: path.to_string(), - score: dynamic_token_grant_match_score(host, path), + score: runtime_credential_match_score(host, path), proxy_delivery, }; if !bindings.iter().any(|binding| binding == &candidate) { @@ -3260,7 +3272,53 @@ fn validate_provider_credentials( ))); } - validate_required_static_credentials(profile, provider, pending_credentials) + validate_required_static_credentials(profile, provider, pending_credentials)?; + validate_proxy_delivered_credential_values(profile, provider, pending_credentials) +} + +/// Reject proxy-delivered credential values the sandbox proxy could not place +/// in an HTTP header. +/// +/// The proxy validates the value again immediately before injection, but a +/// value that can never be injected should fail at provider create or update +/// time with a message that names the credential, not as a 502 on every +/// request. Handle-backed credentials are opaque here and rely on the +/// request-time backstop. +fn validate_proxy_delivered_credential_values( + profile: &ProviderTypeProfile, + provider: &Provider, + pending_credentials: &HashMap, +) -> Result<(), Status> { + for credential in &profile.credentials { + if credential.delivery != ProviderCredentialDelivery::Proxy { + continue; + } + for key in credential.accepted_stored_keys() { + let value = pending_credentials + .get(key) + .or_else(|| provider.credentials.get(key)) + .filter(|value| !value.is_empty()); + let Some(value) = value else { + continue; + }; + if let Err(reason) = + openshell_core::provider_credentials::validate_proxy_delivered_credential_value( + &credential.auth_style, + value, + ) + { + return Err(Status::invalid_argument(format!( + "provider credential '{key}' cannot be proxy-delivered with auth_style '{}': {reason}", + if credential.auth_style.trim().is_empty() { + "bearer" + } else { + credential.auth_style.trim() + } + ))); + } + } + } + Ok(()) } fn validate_required_static_credentials( @@ -5144,7 +5202,7 @@ mod tests { }; let mut dynamic_creds = HashMap::new(); - insert_runtime_credentials_for_profile(&mut dynamic_creds, &profile, "keycloak"); + insert_dynamic_credentials_for_profile(&mut dynamic_creds, &profile, "keycloak"); assert_eq!(dynamic_creds.len(), 4); for (host, audience) in service_audiences { @@ -5248,7 +5306,7 @@ mod tests { host: "api.example.com".to_string(), port: 443, path: path.to_string(), - score: dynamic_token_grant_match_score("api.example.com", path), + score: runtime_credential_match_score("api.example.com", path), proxy_delivery: true, }; let error = validate_runtime_credential_bindings_unambiguous(&[ @@ -5969,6 +6027,17 @@ mod tests { ) .await .expect("provider environment"); + assert!( + environment.dynamic_credentials.is_empty(), + "proxy delivery must not be advertised as a dynamic credential" + ); + let binding = environment + .static_credential_bindings + .get("API_KEY") + .expect("static binding"); + assert_eq!(binding.delivery, ProviderCredentialDelivery::Proxy as i32); + assert_eq!(binding.auth_style, "bearer"); + assert_eq!(binding.header_name, "authorization"); let credentials = openshell_core::provider_credentials::ProviderCredentialState::from_bound_environment( 1, @@ -5989,6 +6058,178 @@ mod tests { .expect("authorized endpoint"), Some("secret") ); + assert_eq!( + credentials.proxy_delivered_credentials_for_endpoint("api.example.com", 443, "/v1/x"), + vec![ + openshell_core::provider_credentials::ProxyDeliveredCredential { + env_key: "API_KEY".to_string(), + auth_style: "bearer".to_string(), + header_name: "authorization".to_string(), + } + ] + ); + } + + #[tokio::test] + async fn environment_delivery_binding_carries_no_placement_metadata() { + let state = test_server_state().await; + let mut profile = custom_profile("env-auth-provider"); + profile.credentials = vec![static_credential("api_key", "API_KEY", true)]; + profile.endpoints = vec![NetworkEndpoint { + host: "api.example.com".to_string(), + port: 443, + path: "/v1/**".to_string(), + protocol: "rest".to_string(), + access: "full".to_string(), + ..Default::default() + }]; + handle_import_provider_profiles( + &state, + authed_request(ImportProviderProfilesRequest { + profiles: vec![ProviderProfileImportItem { + profile: Some(profile), + source: "env-auth-provider.yaml".to_string(), + }], + workspace: "default".to_string(), + }), + ) + .await + .expect("import profile"); + create_provider_record( + state.store.as_ref(), + "default", + provider_with_credential_value("env-auth", "env-auth-provider", "API_KEY", "secret"), + ) + .await + .expect("create provider"); + + let catalog = ProviderProfileSources::with_default_sources() + .snapshot_catalog(state.store.as_ref(), "default") + .await + .expect("catalog"); + let records = load_provider_environment_records( + state.store.as_ref(), + "default", + &["env-auth".to_string()], + ) + .await + .expect("records"); + let environment = resolve_provider_environment_from_records_with_policy_bindings( + state.store.as_ref(), + &catalog, + &records, + &HashMap::new(), + ) + .await + .expect("provider environment"); + + let binding = environment + .static_credential_bindings + .get("API_KEY") + .expect("static binding"); + assert_eq!( + binding.delivery, + ProviderCredentialDelivery::Environment as i32 + ); + assert!(binding.auth_style.is_empty()); + assert!(binding.header_name.is_empty()); + } + + #[tokio::test] + async fn proxy_delivered_credential_values_are_validated_on_create_and_update() { + let state = test_server_state().await; + let mut credential = static_credential("api_key", "API_KEY", true); + credential.delivery = ProviderCredentialDelivery::Proxy as i32; + let mut profile = custom_profile("proxy-auth-provider"); + profile.credentials = vec![credential]; + profile.endpoints = vec![NetworkEndpoint { + host: "api.example.com".to_string(), + port: 443, + path: "/v1/**".to_string(), + protocol: "rest".to_string(), + access: "full".to_string(), + ..Default::default() + }]; + handle_import_provider_profiles( + &state, + authed_request(ImportProviderProfilesRequest { + profiles: vec![ProviderProfileImportItem { + profile: Some(profile), + source: "proxy-auth-provider.yaml".to_string(), + }], + workspace: "default".to_string(), + }), + ) + .await + .expect("import profile"); + + let err = handle_create_provider( + &state, + authed_request(CreateProviderRequest { + provider: Some(provider_with_credential_value( + "proxy-auth", + "proxy-auth-provider", + "API_KEY", + "not a token68 value", + )), + workspace: "default".to_string(), + }), + ) + .await + .expect_err("bearer values must be token68 at create time"); + assert_eq!(err.code(), Code::InvalidArgument); + assert!( + err.message() + .contains("provider credential 'API_KEY' cannot be proxy-delivered"), + "{}", + err.message() + ); + assert!( + !err.message().contains("not a token68 value"), + "credential values must not be echoed: {}", + err.message() + ); + + handle_create_provider( + &state, + authed_request(CreateProviderRequest { + provider: Some(provider_with_credential_value( + "proxy-auth", + "proxy-auth-provider", + "API_KEY", + "sk-live_token.value~ok==", + )), + workspace: "default".to_string(), + }), + ) + .await + .expect("token68 bearer value is accepted"); + + let catalog = ProviderProfileSources::with_default_sources() + .snapshot_catalog(state.store.as_ref(), "default") + .await + .expect("catalog"); + let profile = + get_provider_type_profile_for_scope(&catalog, "proxy-auth-provider", "default") + .expect("imported profile"); + let stored = provider_with_credential_value( + "proxy-auth", + "proxy-auth-provider", + "API_KEY", + "sk-live_token.value~ok==", + ); + validate_provider_credentials( + &profile, + &stored, + &HashMap::from([("API_KEY".to_string(), "rotated with space".to_string())]), + ) + .expect_err("pending update values are validated too"); + validate_provider_credentials( + &profile, + &stored, + &HashMap::from([("API_KEY".to_string(), "rotated-ok".to_string())]), + ) + .expect("valid pending update value"); } #[tokio::test] diff --git a/crates/openshell-supervisor-network/src/l7/relay.rs b/crates/openshell-supervisor-network/src/l7/relay.rs index 9c6a295c5d..b2e5fbdd88 100644 --- a/crates/openshell-supervisor-network/src/l7/relay.rs +++ b/crates/openshell-supervisor-network/src/l7/relay.rs @@ -2921,6 +2921,8 @@ mod tests { credential_identity: identity.to_string(), workload_credential_handle: String::new(), delivery: 0, + auth_style: String::new(), + header_name: String::new(), } } @@ -4712,6 +4714,8 @@ network_policies: credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), delivery: 0, + auth_style: String::new(), + header_name: String::new(), }, )]), Vec::new(), @@ -4817,6 +4821,8 @@ network_policies: credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), delivery: 0, + auth_style: String::new(), + header_name: String::new(), }, )]), Vec::new(), @@ -4872,6 +4878,242 @@ network_policies: (response, forwarded) } + /// Bound provider state with one proxy-delivered bearer credential at + /// `api.example.test:{port}/v1/**`. + fn proxy_delivered_state(port: u16) -> ProviderCredentialState { + ProviderCredentialState::from_bound_environment( + 1, + TestHashMap::from([("API_TOKEN".to_string(), "real-secret".to_string())]), + TestHashMap::new(), + TestHashMap::new(), + TestHashMap::from([( + "API_TOKEN".to_string(), + StaticCredentialBinding { + endpoints: vec![StaticCredentialEndpointBinding { + host: "api.example.test".to_string(), + port: u32::from(port), + path: "/v1/**".to_string(), + }], + credential_identity: "provider-a:API_TOKEN".to_string(), + workload_credential_handle: String::new(), + delivery: openshell_core::proto::ProviderCredentialDelivery::Proxy as i32, + auth_style: "bearer".to_string(), + header_name: String::new(), + }, + )]), + Vec::new(), + ) + .expect("bound provider state") + } + + /// Read one HTTP head from `stream` with a timeout. + async fn read_http_head(stream: &mut S, what: &str) -> String + where + S: AsyncReadExt + Unpin, + { + let mut buffer = [0_u8; 2048]; + let n = tokio::time::timeout(std::time::Duration::from_secs(2), stream.read(&mut buffer)) + .await + .unwrap_or_else(|_| panic!("{what} should arrive")) + .unwrap(); + String::from_utf8_lossy(&buffer[..n]).into_owned() + } + + #[tokio::test] + async fn rest_relay_injects_proxy_delivered_credential_only_after_policy_allows() { + let data = r#" +network_policies: + rest_api: + name: rest_api + endpoints: + - host: api.example.test + port: 443 + protocol: rest + enforcement: enforce + rules: + - allow: + method: GET + path: "/v1/**" + binaries: + - { path: /usr/bin/node } +"#; + let engine = OpaEngine::from_strings(TEST_POLICY, data).unwrap(); + let input = NetworkInput { + host: "api.example.test".into(), + port: 443, + binary_path: PathBuf::from("/usr/bin/node"), + binary_sha256: "unused".into(), + ancestors: vec![], + cmdline_paths: vec![], + }; + let (endpoint, generation) = engine + .query_endpoint_config_with_generation(&input) + .expect("endpoint config"); + let config = crate::l7::parse_l7_config(&endpoint.expect("REST endpoint")) + .expect("parse REST config"); + + // Allowed path: the proxy-delivered header replaces the public value. + let tunnel_engine = engine.clone_engine_for_tunnel(generation).unwrap(); + let state = proxy_delivered_state(443); + assert!( + !state.snapshot().child_env.contains_key("API_TOKEN"), + "proxy delivery must not expose a placeholder" + ); + let ctx = L7EvalContext { + host: "api.example.test".into(), + port: 443, + request_default_port: Some(443), + policy_name: "rest_api".into(), + binary_path: "/usr/bin/node".into(), + provider_credentials: Some(state.clone()), + secret_resolver: state.resolver(), + ..Default::default() + }; + let allowed_config = config.clone(); + let (mut app, mut relay_client) = tokio::io::duplex(8192); + let (mut relay_upstream, mut upstream) = tokio::io::duplex(8192); + let relay = tokio::spawn(async move { + relay_rest( + &allowed_config, + &tunnel_engine, + &mut relay_client, + &mut relay_upstream, + &ctx, + ) + .await + }); + app.write_all( + b"GET /v1/messages HTTP/1.1\r\nHost: api.example.test\r\nAuthorization: Bearer public-value\r\nConnection: close\r\n\r\n", + ) + .await + .unwrap(); + let forwarded = read_http_head(&mut upstream, "allowed request").await; + assert!( + forwarded.starts_with("GET /v1/messages HTTP/1.1\r\n"), + "{forwarded}" + ); + assert!( + forwarded.contains("Authorization: Bearer real-secret\r\n"), + "{forwarded}" + ); + assert!(!forwarded.contains("public-value"), "{forwarded}"); + assert_eq!(authorization_header_count(&forwarded), 1, "{forwarded}"); + upstream + .write_all(b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") + .await + .unwrap(); + let response = read_http_head(&mut app, "allowed response").await; + assert!(response.contains("204 No Content"), "{response}"); + drop(app); + tokio::time::timeout(std::time::Duration::from_secs(2), relay) + .await + .expect("relay should finish") + .unwrap() + .unwrap(); + + // Denied path: policy rejects before any credential is resolved, and + // nothing reaches the upstream. + let tunnel_engine = engine.clone_engine_for_tunnel(generation).unwrap(); + let state = proxy_delivered_state(443); + let ctx = L7EvalContext { + host: "api.example.test".into(), + port: 443, + request_default_port: Some(443), + policy_name: "rest_api".into(), + binary_path: "/usr/bin/node".into(), + provider_credentials: Some(state.clone()), + secret_resolver: state.resolver(), + ..Default::default() + }; + let (mut app, mut relay_client) = tokio::io::duplex(8192); + let (mut relay_upstream, mut upstream) = tokio::io::duplex(8192); + let relay = tokio::spawn(async move { + relay_rest( + &config, + &tunnel_engine, + &mut relay_client, + &mut relay_upstream, + &ctx, + ) + .await + }); + app.write_all( + b"GET /v2/denied HTTP/1.1\r\nHost: api.example.test\r\nAuthorization: Bearer public-value\r\nConnection: close\r\n\r\n", + ) + .await + .unwrap(); + let response = read_http_head(&mut app, "denial response").await; + assert!(response.starts_with("HTTP/1.1 403"), "{response}"); + drop(app); + tokio::time::timeout(std::time::Duration::from_secs(2), relay) + .await + .expect("relay should finish") + .unwrap() + .unwrap(); + let mut forwarded = Vec::new(); + upstream.read_to_end(&mut forwarded).await.unwrap(); + assert!( + forwarded.is_empty(), + "denied request must not reach upstream: {}", + String::from_utf8_lossy(&forwarded) + ); + } + + #[tokio::test] + async fn passthrough_relay_injects_proxy_delivered_credential() { + let engine = OpaEngine::from_strings(TEST_POLICY, "network_policies: {}\n").unwrap(); + let generation_guard = engine + .generation_guard(engine.current_generation()) + .unwrap(); + let state = proxy_delivered_state(443); + let ctx = L7EvalContext { + host: "api.example.test".into(), + port: 443, + request_default_port: Some(443), + policy_name: "passthrough_api".into(), + binary_path: "/usr/bin/curl".into(), + provider_credentials: Some(state), + ..Default::default() + }; + + let (mut app, mut relay_client) = tokio::io::duplex(8192); + let (mut relay_upstream, mut upstream) = tokio::io::duplex(8192); + let relay = tokio::spawn(async move { + relay_passthrough_with_credentials( + &mut relay_client, + &mut relay_upstream, + &ctx, + &generation_guard, + None, + ) + .await + }); + + app.write_all( + b"GET /v1/messages HTTP/1.1\r\nHost: api.example.test\r\nConnection: close\r\n\r\n", + ) + .await + .unwrap(); + let forwarded = read_http_head(&mut upstream, "passthrough request").await; + assert!( + forwarded.contains("Authorization: Bearer real-secret\r\n"), + "passthrough relay must add the proxy-delivered header: {forwarded}" + ); + assert_eq!(authorization_header_count(&forwarded), 1, "{forwarded}"); + upstream + .write_all(b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") + .await + .unwrap(); + let response = read_http_head(&mut app, "passthrough response").await; + assert!(response.contains("204 No Content"), "{response}"); + drop(app); + tokio::time::timeout(std::time::Duration::from_secs(2), relay) + .await + .expect("relay should finish") + .unwrap() + .unwrap(); + } + #[tokio::test] async fn connect_http10_without_authority_cannot_resolve_static_credential() { let (response, forwarded) = run_bound_credential_request(80, 80, |placeholder| { @@ -5467,6 +5709,8 @@ network_policies: credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), delivery: 0, + auth_style: String::new(), + header_name: String::new(), }, )]), Vec::new(), @@ -7928,6 +8172,8 @@ network_policies: credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), delivery: 0, + auth_style: String::new(), + header_name: String::new(), }, )]), Vec::new(), diff --git a/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs b/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs index 5665937fc1..f99694edef 100644 --- a/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs +++ b/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs @@ -1,16 +1,16 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Endpoint-bound dynamic token grant injection for HTTP relay paths. +//! Endpoint-bound runtime credential injection for HTTP relay paths: dynamic +//! token grants and proxy-delivered static credentials. use std::future::Future; use std::pin::Pin; use std::sync::Arc; use miette::{Result, miette}; -use openshell_core::proto::{ - ProviderCredentialDelivery, ProviderCredentialTokenGrant, ProviderProfileCredential, -}; +use openshell_core::proto::{ProviderCredentialTokenGrant, ProviderProfileCredential}; +use openshell_core::provider_credentials::ProxyDeliveredCredential; use openshell_ocsf::{ ActionId, ActivityId, DispositionId, Endpoint, HttpActivityBuilder, HttpRequest, SeverityId, StatusId, Url as OcsfUrl, ctx::ctx as ocsf_ctx, ocsf_emit, @@ -74,8 +74,14 @@ pub fn default_resolver() -> Arc { /// Authorization header before forwarding the request upstream. pub async fn inject_if_needed(req: L7Request, ctx: &L7EvalContext) -> Result { let request_path = req.target.split('?').next().unwrap_or(req.target.as_str()); - let token_grant_credential = ctx.dynamic_credentials.as_ref().and_then(|dyn_creds| { - dyn_creds.read().map_or(None, |creds_guard| { + let token_grant_credential = match ctx.dynamic_credentials.as_ref() { + None => None, + Some(dyn_creds) => { + // A poisoned registry must fail closed. Treating it as "no + // credential" would forward the request unauthenticated. + let creds_guard = dyn_creds + .read() + .map_err(|_| miette!("dynamic credential registry is poisoned"))?; creds_guard .iter() .filter_map(|(key, cred)| { @@ -87,8 +93,8 @@ pub async fn inject_if_needed(req: L7Request, ctx: &L7EvalContext) -> Result Result Result { let request_path = req.target.split('?').next().unwrap_or(req.target.as_str()); - let credential = ctx.dynamic_credentials.as_ref().and_then(|credentials| { - credentials.read().map_or(None, |credentials| { - credentials - .iter() - .filter_map(|(key, credential)| { - let score = - dynamic_credential_key_match_score(key, &ctx.host, ctx.port, request_path)?; - (ProviderCredentialDelivery::try_from(credential.delivery).unwrap_or_default() - == ProviderCredentialDelivery::Proxy) - .then(|| (score, key.clone(), credential.clone())) - }) - .max_by_key(|(score, key, _)| (*score, key.clone())) - .map(|(_, key, credential)| (key, credential)) - }) - }); - - let Some((provider_key, credential)) = credential else { + let Some(state) = ctx.provider_credentials.as_ref() else { return Ok(req); }; + let bindings = + state.proxy_delivered_credentials_for_endpoint(&ctx.host, ctx.port, request_path); + if bindings.is_empty() { + return Ok(req); + } + let env_keys = bindings + .iter() + .map(|binding| ocsf_message_field(&binding.env_key)) + .collect::>() + .join(","); + + match inject_proxy_delivered_header(&req.raw_header, ctx, &bindings) { + Ok((header_name, raw_header)) => { + ocsf_emit!( + HttpActivityBuilder::new(ocsf_ctx()) + .activity(ActivityId::Other) + .action(ActionId::Allowed) + .disposition(DispositionId::Allowed) + .severity(SeverityId::Informational) + .http_request(HttpRequest::new( + &req.action, + OcsfUrl::new("http", &ctx.host, request_path, ctx.port), + )) + .dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port)) + .message(format!( + "Proxy-delivered credential {} injected as {} to {}:{}", + env_keys, + ocsf_message_field(&header_name), + ctx.host, + ctx.port + )) + .build() + ); + Ok(L7Request { + action: req.action, + target: req.target, + query_params: req.query_params, + raw_header, + body_length: req.body_length, + }) + } + Err(error) => { + warn!( + host = %ctx.host, + port = ctx.port, + env_keys = %env_keys, + error = %error, + "Proxy-delivered credential injection failed" + ); + ocsf_emit!( + HttpActivityBuilder::new(ocsf_ctx()) + .activity(ActivityId::Fail) + .action(ActionId::Denied) + .disposition(DispositionId::Blocked) + .severity(SeverityId::Medium) + .status(StatusId::Failure) + .http_request(HttpRequest::new( + &req.action, + OcsfUrl::new("http", &ctx.host, request_path, ctx.port), + )) + .dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port)) + .message(format!( + "Proxy-delivered credential {} injection failed for {}:{}: {}", + env_keys, ctx.host, ctx.port, error + )) + .build() + ); + Err(error) + } + } +} + +/// Resolve every matching proxy-delivered binding and build the header. +/// +/// Aliases of one credential resolve to the same header and collapse into a +/// single injection. Distinct headers mean two providers bound the same +/// endpoint, which the gateway rejects at attach time; the proxy fails closed +/// rather than choosing one. Returns the header name and the rewritten request. +fn inject_proxy_delivered_header( + raw_header: &[u8], + ctx: &L7EvalContext, + bindings: &[ProxyDeliveredCredential], +) -> Result<(String, Vec)> { let resolver = ctx .secret_resolver .as_deref() - .ok_or_else(|| miette!("proxy-delivered credential unavailable for {provider_key}"))?; - let value = credential - .env_vars - .iter() - .find_map(|key| { - resolver - .resolve_current_env_key_checked(key, "proxy-delivered credential") - .transpose() - }) - .transpose()? - .ok_or_else(|| miette!("proxy-delivered credential unavailable for {provider_key}"))?; + .ok_or_else(|| miette!("proxy-delivered credential resolver unavailable"))?; + let mut header: Option<(String, String)> = None; + for binding in bindings { + let value = resolver + .resolve_current_env_key_checked(&binding.env_key, "proxy-delivered credential") + .map_err(|error| miette!("proxy-delivered credential {}: {error}", binding.env_key))? + .ok_or_else(|| { + miette!( + "proxy-delivered credential {} is unavailable in the current provider revision", + binding.env_key + ) + })?; + let candidate = injected_credential_header( + &binding.auth_style, + &binding.header_name, + value, + "proxy delivery", + )?; + match &header { + None => header = Some(candidate), + Some(existing) if *existing == candidate => {} + Some(_) => { + return Err(miette!( + "multiple proxy-delivered credentials match this endpoint; attach only one matching provider" + )); + } + } + } let (header_name, header_value) = - injected_credential_header(&credential, value, "proxy delivery")?; - let raw_header = inject_header(&req.raw_header, &header_name, &header_value)?; - - Ok(L7Request { - action: req.action, - target: req.target, - query_params: req.query_params, - raw_header, - body_length: req.body_length, - }) + header.ok_or_else(|| miette!("no proxy-delivered credential binding matched"))?; + let raw_header = inject_header(raw_header, &header_name, &header_value)?; + Ok((header_name, raw_header)) } fn ocsf_message_field(value: &str) -> String { @@ -296,35 +388,45 @@ fn inject_token_grant_header( credential: &ProviderProfileCredential, access_token: &str, ) -> Result> { - let (header_name, header_value) = - injected_credential_header(credential, access_token, "token grant")?; + let (header_name, header_value) = injected_credential_header( + &credential.auth_style, + &credential.header_name, + access_token, + "token grant", + )?; inject_header(raw_header, &header_name, &header_value) } +/// Build the outbound header for a runtime-injected credential. +/// +/// `context` names the caller ("token grant" or "proxy delivery") in error +/// messages. Values are validated against the placement so a malformed +/// credential can never produce a malformed or header-injecting request. fn injected_credential_header( - credential: &ProviderProfileCredential, - access_token: &str, + auth_style: &str, + configured_header_name: &str, + value: &str, context: &str, ) -> Result<(String, String)> { - match credential.auth_style.trim().to_ascii_lowercase().as_str() { + match auth_style.trim().to_ascii_lowercase().as_str() { "" | "bearer" => { - crate::token_grant::validate_access_token(access_token)?; - let header_name = if credential.header_name.trim().is_empty() { + validate_bearer_value(value, context)?; + let header_name = if configured_header_name.trim().is_empty() { "Authorization" } else { - credential.header_name.trim() + configured_header_name.trim() }; validate_header_name(header_name, context)?; - Ok((header_name.to_string(), format!("Bearer {access_token}"))) + Ok((header_name.to_string(), format!("Bearer {value}"))) } "header" => { - let header_name = credential.header_name.trim(); + let header_name = configured_header_name.trim(); if header_name.is_empty() { return Err(miette!("{context} auth_style header requires header_name")); } validate_header_name(header_name, context)?; - validate_header_value(access_token, context)?; - Ok((header_name.to_string(), access_token.to_string())) + validate_header_value(value, context)?; + Ok((header_name.to_string(), value.to_string())) } other => Err(miette!( "{context} auth_style '{other}' is not supported; use bearer or header" @@ -332,6 +434,19 @@ fn injected_credential_header( } } +fn validate_bearer_value(value: &str, context: &str) -> Result<()> { + if context == "token grant" { + // Preserve the historical token grant wording. + return crate::token_grant::validate_access_token(value); + } + if !openshell_core::provider_credentials::is_token68(value) { + return Err(miette!( + "{context} bearer credential is not a valid token68 value; check the stored provider credential" + )); + } + Ok(()) +} + fn validate_header_value(value: &str, context: &str) -> Result<()> { if value .bytes() @@ -637,7 +752,9 @@ mod tests { use super::*; use crate::l7::provider::{BodyLength, L7Request}; use crate::l7::token_grant_injection::test_support::TokenGrantTestFixture; - use openshell_core::proto::{StaticCredentialBinding, StaticCredentialEndpointBinding}; + use openshell_core::proto::{ + ProviderCredentialDelivery, StaticCredentialBinding, StaticCredentialEndpointBinding, + }; use openshell_core::provider_credentials::ProviderCredentialState; fn credential(auth_style: &str, header_name: &str) -> ProviderProfileCredential { @@ -815,12 +932,9 @@ mod tests { #[test] fn token_grant_header_rejects_framing_and_connection_headers() { for header_name in ["Host", "Content-Length", "Transfer-Encoding", "Connection"] { - let err = injected_credential_header( - &credential("header", header_name), - "grant-token", - "token grant", - ) - .expect_err("framing header override should be rejected"); + let err = + injected_credential_header("header", header_name, "grant-token", "token grant") + .expect_err("framing header override should be rejected"); assert_eq!( err.to_string(), "token grant header_name may not override HTTP framing or connection headers" @@ -831,7 +945,8 @@ mod tests { #[test] fn proxy_delivery_named_header_accepts_safe_non_token_value() { let header = injected_credential_header( - &credential("header", "x-api-key"), + "header", + "x-api-key", "key with spaces = allowed", "proxy delivery", ) @@ -845,7 +960,8 @@ mod tests { ); let error = injected_credential_header( - &credential("header", "x-api-key"), + "header", + "x-api-key", "safe\r\ninjected: value", "proxy delivery", ) @@ -856,6 +972,23 @@ mod tests { ); } + #[test] + fn proxy_delivery_bearer_value_error_names_the_stored_credential() { + let error = injected_credential_header("bearer", "", "has space", "proxy delivery") + .expect_err("non-token68 bearer value must be rejected"); + assert_eq!( + error.to_string(), + "proxy delivery bearer credential is not a valid token68 value; check the stored provider credential" + ); + + let error = injected_credential_header("bearer", "", "has space", "token grant") + .expect_err("token grant keeps its own wording"); + assert_eq!( + error.to_string(), + "token grant returned a malformed access token" + ); + } + #[test] fn inject_token_grant_header_preserves_header_terminator_before_body() { let raw = b"POST /v1 HTTP/1.1\r\nHost: api.example.com\r\nContent-Length: 2\r\n\r\nOK"; @@ -900,67 +1033,250 @@ mod tests { ); } - #[test] - fn proxy_delivery_replaces_header_without_changing_body() { - let dynamic_credentials = - Arc::new(std::sync::RwLock::new(std::collections::HashMap::from([( - "api.example.com\t443\t/v1/**\tprovider:api_key".to_string(), - ProviderProfileCredential { - name: "api_key".to_string(), - env_vars: vec!["API_KEY".to_string()], - auth_style: "bearer".to_string(), - delivery: ProviderCredentialDelivery::Proxy as i32, - ..Default::default() - }, - )]))); - let state = ProviderCredentialState::from_bound_environment( + /// A proxy-delivered binding for `env_key` at `api.example.com:443/v1/**`. + fn proxy_binding( + identity: &str, + auth_style: &str, + header_name: &str, + ) -> StaticCredentialBinding { + StaticCredentialBinding { + endpoints: vec![StaticCredentialEndpointBinding { + host: "api.example.com".to_string(), + port: 443, + path: "/v1/**".to_string(), + }], + credential_identity: identity.to_string(), + workload_credential_handle: String::new(), + delivery: ProviderCredentialDelivery::Proxy as i32, + auth_style: auth_style.to_string(), + header_name: header_name.to_string(), + } + } + + fn proxy_state( + env: &[(&str, &str)], + bindings: Vec<(&str, StaticCredentialBinding)>, + ) -> ProviderCredentialState { + ProviderCredentialState::from_bound_environment( 7, - std::collections::HashMap::from([("API_KEY".to_string(), "real-secret".to_string())]), + env.iter() + .map(|(key, value)| ((*key).to_string(), (*value).to_string())) + .collect(), std::collections::HashMap::new(), std::collections::HashMap::new(), - std::collections::HashMap::from([( - "API_KEY".to_string(), - StaticCredentialBinding { - endpoints: vec![StaticCredentialEndpointBinding { - host: "api.example.com".to_string(), - port: 443, - path: "/v1/**".to_string(), - }], - credential_identity: "provider:API_KEY".to_string(), - workload_credential_handle: String::new(), - delivery: ProviderCredentialDelivery::Proxy as i32, - }, - )]), + bindings + .into_iter() + .map(|(key, binding)| (key.to_string(), binding)) + .collect(), Vec::new(), ) - .expect("valid provider state"); - let ctx = L7EvalContext { + .expect("valid provider state") + } + + fn proxy_ctx(state: &ProviderCredentialState, request_path: &str) -> L7EvalContext { + L7EvalContext { host: "api.example.com".to_string(), port: 443, - secret_resolver: state.resolver_for_endpoint("api.example.com", 443, "/v1/chat"), - provider_credentials: Some(state), + secret_resolver: state.resolver_for_endpoint("api.example.com", 443, request_path), + provider_credentials: Some(state.clone()), provider_credential_revision: Some(7), - dynamic_credentials: Some(dynamic_credentials), ..Default::default() - }; - let body = br#"{"messages":[{"content":"ordinary environment output"}]}"#; - let mut raw_header = b"POST /v1/chat HTTP/1.1\r\nHost: api.example.com\r\nAuthorization: Bearer openshell-managed\r\n\r\n".to_vec(); - raw_header.extend_from_slice(body); - let request = L7Request { + } + } + + fn proxy_request(target: &str, raw_header: &[u8], body: &[u8]) -> L7Request { + let mut raw = raw_header.to_vec(); + raw.extend_from_slice(body); + L7Request { action: "POST".to_string(), - target: "/v1/chat".to_string(), + target: target.to_string(), query_params: std::collections::HashMap::new(), - raw_header, + raw_header: raw, body_length: BodyLength::ContentLength(body.len() as u64), - }; + } + } + + #[test] + fn proxy_delivery_replaces_header_without_changing_body() { + let state = proxy_state( + &[("API_KEY", "real-secret")], + vec![("API_KEY", proxy_binding("provider:API_KEY", "bearer", ""))], + ); + let ctx = proxy_ctx(&state, "/v1/chat"); + let body = br#"{"messages":[{"content":"ordinary environment output"}]}"#; + let request = proxy_request( + "/v1/chat", + b"POST /v1/chat HTTP/1.1\r\nHost: api.example.com\r\nAuthorization: Bearer openshell-managed\r\n\r\n", + body, + ); let injected = inject_static_if_needed(request, &ctx).expect("credential injection"); let text = String::from_utf8(injected.raw_header).expect("HTTP request is UTF-8"); assert!(text.contains("Authorization: Bearer real-secret\r\n")); assert!(!text.contains("openshell-managed")); + assert_eq!(text.matches("Authorization:").count(), 1); assert!(text.ends_with(std::str::from_utf8(body).expect("body is UTF-8"))); } + #[test] + fn proxy_delivery_named_header_is_added_when_absent() { + let state = proxy_state( + &[("API_KEY", "key with spaces")], + vec![( + "API_KEY", + proxy_binding("provider:API_KEY", "header", "x-api-key"), + )], + ); + let ctx = proxy_ctx(&state, "/v1/chat"); + let request = proxy_request( + "/v1/chat", + b"POST /v1/chat HTTP/1.1\r\nHost: api.example.com\r\n\r\n", + b"{}", + ); + + let injected = inject_static_if_needed(request, &ctx).expect("credential injection"); + let text = String::from_utf8(injected.raw_header).expect("HTTP request is UTF-8"); + assert!(text.contains("x-api-key: key with spaces\r\n"), "{text}"); + assert!(text.ends_with("\r\n\r\n{}"), "{text}"); + } + + #[test] + fn proxy_delivery_passes_through_when_no_binding_matches() { + let state = proxy_state( + &[("API_KEY", "real-secret")], + vec![("API_KEY", proxy_binding("provider:API_KEY", "bearer", ""))], + ); + let ctx = proxy_ctx(&state, "/v2/other"); + let raw = b"POST /v2/other HTTP/1.1\r\nHost: api.example.com\r\nAuthorization: Bearer public\r\n\r\n"; + let request = proxy_request("/v2/other", raw, b""); + + let passed = inject_static_if_needed(request, &ctx).expect("no injection needed"); + assert_eq!(passed.raw_header, raw.to_vec()); + + // Environment-delivered bindings never trigger injection. + let mut environment_binding = proxy_binding("provider:API_KEY", "bearer", ""); + environment_binding.delivery = ProviderCredentialDelivery::Environment as i32; + let state = proxy_state( + &[("API_KEY", "real-secret")], + vec![("API_KEY", environment_binding)], + ); + let ctx = proxy_ctx(&state, "/v1/chat"); + let raw = b"POST /v1/chat HTTP/1.1\r\nHost: api.example.com\r\nAuthorization: Bearer public\r\n\r\n"; + let passed = inject_static_if_needed(proxy_request("/v1/chat", raw, b""), &ctx) + .expect("environment delivery is untouched"); + assert_eq!(passed.raw_header, raw.to_vec()); + } + + #[test] + fn proxy_delivery_collapses_aliases_and_rejects_competing_providers() { + let state = proxy_state( + &[("API_KEY", "real-secret"), ("API_KEY_ALIAS", "real-secret")], + vec![ + ("API_KEY", proxy_binding("provider:API_KEY", "bearer", "")), + ( + "API_KEY_ALIAS", + proxy_binding("provider:API_KEY_ALIAS", "bearer", ""), + ), + ], + ); + let ctx = proxy_ctx(&state, "/v1/chat"); + let injected = inject_static_if_needed( + proxy_request( + "/v1/chat", + b"POST /v1/chat HTTP/1.1\r\nHost: api.example.com\r\n\r\n", + b"", + ), + &ctx, + ) + .expect("aliases resolve to one header"); + let text = String::from_utf8(injected.raw_header).expect("UTF-8"); + assert_eq!( + text.matches("Authorization: Bearer real-secret\r\n") + .count(), + 1 + ); + + let state = proxy_state( + &[("A_KEY", "secret-a"), ("B_KEY", "secret-b")], + vec![ + ("A_KEY", proxy_binding("provider-a:A_KEY", "bearer", "")), + ("B_KEY", proxy_binding("provider-b:B_KEY", "bearer", "")), + ], + ); + let ctx = proxy_ctx(&state, "/v1/chat"); + let error = inject_static_if_needed( + proxy_request( + "/v1/chat", + b"POST /v1/chat HTTP/1.1\r\nHost: api.example.com\r\n\r\n", + b"", + ), + &ctx, + ) + .expect_err("two providers must fail closed"); + let message = error.to_string(); + assert!( + message.contains("attach only one matching provider"), + "{message}" + ); + assert!(!message.contains("secret-a") && !message.contains("secret-b")); + } + + #[test] + fn proxy_delivery_fails_closed_without_resolver_or_after_revocation() { + let state = proxy_state( + &[("API_KEY", "real-secret")], + vec![("API_KEY", proxy_binding("provider:API_KEY", "bearer", ""))], + ); + let mut ctx = proxy_ctx(&state, "/v1/chat"); + ctx.secret_resolver = None; + let error = inject_static_if_needed( + proxy_request( + "/v1/chat", + b"POST /v1/chat HTTP/1.1\r\nHost: api.example.com\r\n\r\n", + b"", + ), + &ctx, + ) + .expect_err("missing resolver must not forward unauthenticated"); + assert!(error.to_string().contains("resolver unavailable")); + + // Revocation clears the live bindings, so a request re-scoped after + // revocation has nothing to inject and passes through unchanged. The + // relay paths re-scope immediately before injection and guard the + // revision, so a stale pre-revocation resolver never reaches this call. + state.revoke_static_provider_environment(8); + let ctx = proxy_ctx(&state, "/v1/chat"); + assert!(ctx.secret_resolver.is_none()); + let raw = b"POST /v1/chat HTTP/1.1\r\nHost: api.example.com\r\nAuthorization: Bearer public\r\n\r\n"; + let passed = inject_static_if_needed(proxy_request("/v1/chat", raw, b""), &ctx) + .expect("revoked bindings leave the request untouched"); + assert_eq!(passed.raw_header, raw.to_vec()); + } + + #[test] + fn proxy_delivery_rejects_bearer_values_that_are_not_token68() { + let state = proxy_state( + &[("API_KEY", "has space")], + vec![("API_KEY", proxy_binding("provider:API_KEY", "bearer", ""))], + ); + let ctx = proxy_ctx(&state, "/v1/chat"); + let error = inject_static_if_needed( + proxy_request( + "/v1/chat", + b"POST /v1/chat HTTP/1.1\r\nHost: api.example.com\r\n\r\n", + b"", + ), + &ctx, + ) + .expect_err("malformed bearer value must not be injected"); + let message = error.to_string(); + assert!( + message.contains("proxy delivery bearer credential"), + "{message}" + ); + assert!(!message.contains("has space"), "{message}"); + } + #[tokio::test] async fn inject_if_needed_uses_configured_resolver() { let fixture = TokenGrantTestFixture::success( diff --git a/crates/openshell-supervisor-network/src/l7/websocket.rs b/crates/openshell-supervisor-network/src/l7/websocket.rs index 21aca41eaa..2c50f72065 100644 --- a/crates/openshell-supervisor-network/src/l7/websocket.rs +++ b/crates/openshell-supervisor-network/src/l7/websocket.rs @@ -2683,6 +2683,8 @@ network_policies: credential_identity: "provider-a:DISCORD_BOT_TOKEN".to_string(), workload_credential_handle: String::new(), delivery: 0, + auth_style: String::new(), + header_name: String::new(), }, )]), Vec::new(), diff --git a/crates/openshell-supervisor-network/src/proxy.rs b/crates/openshell-supervisor-network/src/proxy.rs index ee0275b0db..aa8c05c0c7 100644 --- a/crates/openshell-supervisor-network/src/proxy.rs +++ b/crates/openshell-supervisor-network/src/proxy.rs @@ -5888,6 +5888,11 @@ async fn handle_forward_proxy( error = %error, "static provider credential injection failed in forward proxy" ); + if let Some(session) = middleware_session.take() { + session + .end(openshell_core::proto::WebSocketSessionEndReason::Cancellation) + .await; + } respond( client, &build_json_error_response( @@ -7785,6 +7790,8 @@ network_policies: credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), delivery: 0, + auth_style: String::new(), + header_name: String::new(), }, )]), Vec::new(), @@ -10565,6 +10572,8 @@ network_policies: credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), delivery: 0, + auth_style: String::new(), + header_name: String::new(), }, )]), Vec::new(), @@ -10608,6 +10617,8 @@ network_policies: credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), delivery: 0, + auth_style: String::new(), + header_name: String::new(), }, )]), Vec::new(), @@ -10655,6 +10666,8 @@ network_policies: credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), delivery: 0, + auth_style: String::new(), + header_name: String::new(), }, )]), Vec::new(), @@ -10946,6 +10959,340 @@ network_policies: // --- rewrite_forward_request tests --- + /// Bound provider state with one proxy-delivered credential at + /// `{host}:{port}/v1/**`. + fn forward_proxy_delivered_state( + host: &str, + port: u16, + auth_style: &str, + header_name: &str, + value: &str, + ) -> ProviderCredentialState { + use openshell_core::proto::{StaticCredentialBinding, StaticCredentialEndpointBinding}; + ProviderCredentialState::from_bound_environment( + 3, + TestHashMap::from([("API_TOKEN".to_string(), value.to_string())]), + TestHashMap::new(), + TestHashMap::new(), + TestHashMap::from([( + "API_TOKEN".to_string(), + StaticCredentialBinding { + endpoints: vec![StaticCredentialEndpointBinding { + host: host.to_string(), + port: u32::from(port), + path: "/v1/**".to_string(), + }], + credential_identity: "provider-a:API_TOKEN".to_string(), + workload_credential_handle: String::new(), + delivery: openshell_core::proto::ProviderCredentialDelivery::Proxy as i32, + auth_style: auth_style.to_string(), + header_name: header_name.to_string(), + }, + )]), + Vec::new(), + ) + .expect("bound provider state") + } + + #[test] + fn forward_proxy_injects_proxy_delivered_credential_before_rewriting_request() { + let state = forward_proxy_delivered_state( + "api.example.test", + 8080, + "header", + "x-api-key", + "key with spaces", + ); + let ctx = crate::l7::relay::L7EvalContext { + host: "api.example.test".into(), + port: 8080, + request_default_port: Some(8080), + policy_name: "rest_api".into(), + binary_path: "/usr/bin/curl".into(), + provider_credentials: Some(state.clone()), + secret_resolver: state.resolver(), + ..Default::default() + }; + // Mirror the production sequence: acquire the endpoint-scoped resolver + // after admission, then inject from the live provider state. + let credentials = endpoint_credentials_for_request( + ctx.provider_credentials.as_ref(), + ctx.secret_resolver.clone(), + &ctx.host, + ctx.port, + "/v1/projects", + ); + let raw = b"GET http://api.example.test:8080/v1/projects HTTP/1.1\r\nHost: api.example.test:8080\r\nx-api-key: public-value\r\nConnection: close\r\n\r\n".to_vec(); + + let injected = inject_static_credential_for_forward_request( + "GET", + "/v1/projects", + raw, + &ctx, + credentials.resolver, + credentials.revision, + ) + .expect("proxy-delivered credential should inject"); + let rewritten = rewrite_forward_request( + &injected, + injected.len(), + "/v1/projects", + "api.example.test:8080", + None, + false, + ) + .expect("forward request should rewrite"); + let rewritten = String::from_utf8_lossy(&rewritten); + + assert!(rewritten.starts_with("GET /v1/projects HTTP/1.1\r\n")); + assert!( + rewritten.contains("x-api-key: key with spaces\r\n"), + "{rewritten}" + ); + assert!(!rewritten.contains("public-value"), "{rewritten}"); + assert_eq!( + rewritten.to_ascii_lowercase().matches("x-api-key:").count(), + 1 + ); + } + + #[test] + fn forward_proxy_proxy_delivered_credential_failure_stops_before_rewrite() { + // A live binding with no request-scoped resolver must fail closed + // instead of forwarding the workload's placeholder header upstream. + let state = + forward_proxy_delivered_state("api.example.test", 8080, "bearer", "", "real-secret"); + let ctx = crate::l7::relay::L7EvalContext { + host: "api.example.test".into(), + port: 8080, + request_default_port: Some(8080), + policy_name: "rest_api".into(), + binary_path: "/usr/bin/curl".into(), + provider_credentials: Some(state.clone()), + secret_resolver: state.resolver(), + ..Default::default() + }; + let raw = b"GET http://api.example.test:8080/v1/projects HTTP/1.1\r\nHost: api.example.test:8080\r\nAuthorization: Bearer public-value\r\nConnection: close\r\n\r\n".to_vec(); + + let err = inject_static_credential_for_forward_request( + "GET", + "/v1/projects", + raw.clone(), + &ctx, + None, + Some(3), + ) + .expect_err("proxy-delivered credential without a resolver must not inject"); + assert!(err.to_string().contains("resolver unavailable"), "{err}"); + assert!(!err.to_string().contains("real-secret")); + + // Revocation clears the live bindings. A request scoped after + // revocation has nothing to inject and is left for the placeholder + // rewriter, which has nothing to rewrite either. + state.revoke_static_provider_environment(4); + let credentials = endpoint_credentials_for_request( + ctx.provider_credentials.as_ref(), + ctx.secret_resolver.clone(), + &ctx.host, + ctx.port, + "/v1/projects", + ); + assert!(credentials.resolver.is_none()); + let untouched = inject_static_credential_for_forward_request( + "GET", + "/v1/projects", + raw.clone(), + &ctx, + credentials.resolver, + credentials.revision, + ) + .expect("revoked bindings leave the request untouched"); + assert_eq!(untouched, raw); + + // Requests outside the binding are left untouched as well. + let state = + forward_proxy_delivered_state("api.example.test", 8080, "bearer", "", "real-secret"); + let ctx = crate::l7::relay::L7EvalContext { + provider_credentials: Some(state.clone()), + secret_resolver: state.resolver(), + ..ctx + }; + let raw = b"GET http://api.example.test:8080/v2/other HTTP/1.1\r\nHost: api.example.test:8080\r\nAuthorization: Bearer public-value\r\nConnection: close\r\n\r\n".to_vec(); + let credentials = endpoint_credentials_for_request( + ctx.provider_credentials.as_ref(), + ctx.secret_resolver.clone(), + &ctx.host, + ctx.port, + "/v2/other", + ); + let untouched = inject_static_credential_for_forward_request( + "GET", + "/v2/other", + raw.clone(), + &ctx, + credentials.resolver, + credentials.revision, + ) + .expect("unbound path passes through"); + assert_eq!(untouched, raw); + } + + #[tokio::test] + async fn plaintext_forward_proxy_delivers_proxy_credential_end_to_end() { + if !cfg!(target_os = "linux") { + eprintln!("skipping: handler identity binding requires /proc (Linux)"); + return; + } + let Some(upstream_ip) = non_loopback_test_ipv4() else { + eprintln!("skipping: no routable non-loopback IPv4 test address"); + return; + }; + + let upstream_listener = TcpListener::bind((upstream_ip, 0)) + .await + .expect("bind upstream listener"); + let upstream_port = upstream_listener.local_addr().unwrap().port(); + let executable = std::env::current_exe().expect("current executable"); + let data = format!( + r#" +network_policies: + allow-upstream: + name: allow-upstream + endpoints: + - host: "{upstream_ip}" + port: {upstream_port} + protocol: rest + access: full + binaries: + - {{ path: "{executable}" }} +"#, + executable = executable.display(), + ); + let engine = Arc::new( + OpaEngine::from_strings(include_str!("../data/sandbox-policy.rego"), &data) + .expect("load policy"), + ); + let state = forward_proxy_delivered_state( + &upstream_ip.to_string(), + upstream_port, + "bearer", + "", + "real-secret", + ); + assert!( + !state.snapshot().child_env.contains_key("API_TOKEN"), + "proxy delivery must not expose a placeholder" + ); + + let upstream = tokio::spawn(async move { + let (mut socket, _) = upstream_listener.accept().await.expect("accept upstream"); + let mut received = Vec::new(); + let mut buffer = [0_u8; 1024]; + loop { + let read = socket + .read(&mut buffer) + .await + .expect("read upstream request"); + if read == 0 { + break; + } + received.extend_from_slice(&buffer[..read]); + if received.windows(4).any(|window| window == b"\r\n\r\n") { + break; + } + } + socket + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok") + .await + .expect("write upstream response"); + let _ = socket.shutdown().await; + received + }); + + let proxy_listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind proxy listener"); + let proxy_address = proxy_listener.local_addr().unwrap(); + let target = format!("http://{upstream_ip}:{upstream_port}/v1/projects"); + let request = format!( + "GET {target} HTTP/1.1\r\nHost: {upstream_ip}:{upstream_port}\r\nAuthorization: Bearer public-value\r\nConnection: close\r\n\r\n" + ); + let client = tokio::spawn(async move { + let mut socket = TcpStream::connect(proxy_address) + .await + .expect("connect proxy"); + socket + .write_all(request.as_bytes()) + .await + .expect("write proxy request"); + let mut response = Vec::new(); + socket + .read_to_end(&mut response) + .await + .expect("read proxy response"); + response + }); + let (mut proxy_connection, _) = proxy_listener.accept().await.unwrap(); + let mut buf = vec![0_u8; 8192]; + let used = proxy_connection + .read(&mut buf) + .await + .expect("read forward request"); + let head = String::from_utf8_lossy(&buf[..used]).into_owned(); + let mut parts = head.split_whitespace(); + let method = parts.next().expect("method").to_string(); + let target_uri = parts.next().expect("target").to_string(); + + tokio::time::timeout( + std::time::Duration::from_secs(30), + handle_forward_proxy( + &method, + &target_uri, + &buf, + used, + &mut proxy_connection, + engine, + Arc::new(BinaryIdentityCache::new()), + Arc::new(AtomicU32::new(std::process::id())), + None, + AgentProposals::default(), + Arc::new(None), + Some(state.clone()), + state.resolver(), + None, + None, + None, + ), + ) + .await + .expect("forward proxy must complete") + .expect("handle plaintext forward request"); + drop(proxy_connection); + + let response = String::from_utf8(client.await.unwrap()).expect("UTF-8 response"); + assert!( + response.starts_with("HTTP/1.1 200 OK"), + "upstream response must be relayed: {response}" + ); + let forwarded = String::from_utf8( + tokio::time::timeout(std::time::Duration::from_secs(5), upstream) + .await + .expect("upstream must receive the request") + .unwrap(), + ) + .expect("UTF-8 upstream request"); + assert!( + forwarded.contains("Authorization: Bearer real-secret\r\n"), + "forward proxy must inject the proxy-delivered credential: {forwarded}" + ); + assert!(!forwarded.contains("public-value"), "{forwarded}"); + assert_eq!( + forwarded.matches("Authorization:").count(), + 1, + "{forwarded}" + ); + } + #[tokio::test] async fn forward_proxy_injects_token_grant_before_rewriting_request() { let (ctx, fixture) = forward_token_grant_context(Ok("grant-token")); @@ -11409,6 +11756,8 @@ network_policies: credential_identity: "provider-a:API_TOKEN".to_string(), workload_credential_handle: String::new(), delivery: 0, + auth_style: String::new(), + header_name: String::new(), }, )]), Vec::new(), @@ -11499,6 +11848,8 @@ network_policies: credential_identity: format!("provider-a:{key}"), workload_credential_handle: String::new(), delivery: 0, + auth_style: String::new(), + header_name: String::new(), }, ) }) diff --git a/docs/providers/profiles.mdx b/docs/providers/profiles.mdx index 69de34526a..571bbac760 100644 --- a/docs/providers/profiles.mdx +++ b/docs/providers/profiles.mdx @@ -429,8 +429,16 @@ binaries: This minimal profile keeps `MODEL_API_KEY` and its OpenShell placeholder out of the sandbox. The application may send any public placeholder value in `Authorization`; OpenShell replaces the complete header before forwarding the allowed request. +Proxy delivery changes where the credential lives, not who can use it. Any sandbox process that reaches a bound endpoint through the inspected proxy sends an authenticated request, exactly as it could with an environment placeholder. What changes is that the workload never holds credential-shaped material, and network policy and middleware inspect the request before OpenShell authenticates it. + The initial proxy-delivery implementation supports one proxy-delivered static credential per profile. That credential may list multiple `env_vars` aliases. A profile using proxy delivery cannot also declare a `token_grant` credential. +Proxy-delivered credential values are validated when you create or update the provider. A `bearer` value must use the RFC 7235 `token68` character set: letters, digits, `-`, `.`, `_`, `~`, `+`, `/`, and trailing `=` padding. A named `header` value may contain any printable characters but no control characters. The proxy validates the value again immediately before injection. + +Two attached providers cannot bind proxy-delivered credentials to overlapping host and path selectors on the same port, even when one selector is more specific. This is stricter than the token grant rule, which lets the more specific selector win. Because the application sends no credential, the proxy cannot verify which provider the request intended, so OpenShell rejects the attach, profile import, or profile update instead of choosing one at request time. + +When a bound request cannot be authenticated, for example because the provider environment was revoked mid-request, the proxy returns `502 Bad Gateway` with error code `provider_authentication_failed` and never forwards the request. Every successful and failed injection emits an OCSF HTTP activity event that names the credential environment variable and target endpoint without the credential value. Requests to endpoints outside the binding, or through a policy endpoint that uses `tls: skip`, are forwarded with whatever header the application sent and the upstream rejects them. + ```yaml id: custom-model-api display_name: Custom Model API diff --git a/e2e/rust/Cargo.toml b/e2e/rust/Cargo.toml index 18556d0f7b..c3b98dc69a 100644 --- a/e2e/rust/Cargo.toml +++ b/e2e/rust/Cargo.toml @@ -118,6 +118,11 @@ name = "provider_token_exchange" path = "tests/provider_token_exchange.rs" required-features = ["e2e-podman"] +[[test]] +name = "provider_proxy_delivery" +path = "tests/provider_proxy_delivery.rs" +required-features = ["e2e-host-gateway"] + [[test]] name = "readyz_health" path = "tests/readyz_health.rs" diff --git a/e2e/rust/tests/provider_proxy_delivery.rs b/e2e/rust/tests/provider_proxy_delivery.rs new file mode 100644 index 0000000000..cac48bc913 --- /dev/null +++ b/e2e/rust/tests/provider_proxy_delivery.rs @@ -0,0 +1,429 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(feature = "e2e")] + +//! E2E coverage for proxy-delivered static provider credentials. +//! +//! A profile credential with `delivery: proxy` must keep both the secret and +//! the OpenShell placeholder out of the sandbox environment, and the inspected +//! proxy must replace the application's public `Authorization` value with the +//! real credential before the request reaches the upstream. + +use std::io::Write; +use std::process::Stdio; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use openshell_e2e::harness::binary::openshell_cmd; +use openshell_e2e::harness::sandbox::SandboxGuard; +use tempfile::NamedTempFile; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::task::JoinHandle; + +const PROFILE_ID: &str = "e2e-proxy-delivery"; +const PROVIDER_NAME: &str = "e2e-proxy-delivery"; +const TEST_HOST: &str = "host.openshell.internal"; +const TOKEN_ENV: &str = "E2E_PROXY_DELIVERED_KEY"; +const TEST_SECRET: &str = "e2e-proxy-delivered-secret-value"; +const PUBLIC_VALUE: &str = "public-placeholder-value"; +const PLACEHOLDER_PREFIX: &str = "openshell:resolve:env:"; + +async fn run_cli(args: &[&str]) -> (bool, String) { + let mut command = openshell_cmd(); + command + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let output = command.output().await.expect("spawn openshell CLI"); + ( + output.status.success(), + format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ), + ) +} + +/// Retry a delete until it takes effect; sandbox teardown drains +/// asynchronously and the gateway refuses to delete referenced resources. +async fn delete_until_gone(args: &[&str]) -> Result<(), String> { + const ATTEMPTS: u32 = 40; + let mut last_output = String::new(); + for _ in 0..ATTEMPTS { + let (deleted, output) = run_cli(args).await; + if deleted || output.to_lowercase().contains("not found") { + return Ok(()); + } + last_output = output; + tokio::time::sleep(Duration::from_millis(500)).await; + } + Err(format!( + "'{}' still failing after {ATTEMPTS} attempts:\n{last_output}", + args.join(" ") + )) +} + +async fn ensure_provider_resources_absent() -> Result<(), String> { + delete_until_gone(&["provider", "delete", PROVIDER_NAME]).await?; + delete_until_gone(&["provider", "profile", "delete", PROFILE_ID]).await +} + +async fn cleanup_provider_resources() { + if let Err(error) = ensure_provider_resources_absent().await { + eprintln!("provider cleanup did not settle: {error}"); + } +} + +fn write_provider_profile(port: u16) -> Result { + let mut file = tempfile::Builder::new() + .suffix(".yaml") + .tempfile() + .map_err(|error| format!("create profile: {error}"))?; + let profile = format!( + r"id: {PROFILE_ID} +display_name: E2E Proxy Delivery +category: other +credentials: + - name: api_key + env_vars: [{TOKEN_ENV}] + required: true + delivery: proxy + auth_style: bearer +endpoints: + - host: {TEST_HOST} + port: {port} + protocol: rest + access: full +binaries: + - path: /usr/bin/python* + - path: /usr/local/bin/python* + - path: /sandbox/.uv/python/*/bin/python* +", + ); + file.write_all(profile.as_bytes()) + .map_err(|error| format!("write profile: {error}"))?; + file.flush() + .map_err(|error| format!("flush profile: {error}"))?; + Ok(file) +} + +fn write_base_policy() -> Result { + let mut file = NamedTempFile::new().map_err(|error| format!("create policy: {error}"))?; + file.write_all( + br#"version: 1 +filesystem_policy: + include_workdir: true + read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] + read_write: [/sandbox, /tmp, /dev/null] +landlock: + compatibility: best_effort +process: + run_as_user: sandbox + run_as_group: sandbox +"#, + ) + .map_err(|error| format!("write policy: {error}"))?; + file.flush() + .map_err(|error| format!("flush policy: {error}"))?; + Ok(file) +} + +async fn import_profile(port: u16) -> Result<(), String> { + let profile = write_provider_profile(port)?; + let profile_path = profile + .path() + .to_str() + .ok_or_else(|| "profile path is not UTF-8".to_string())?; + let (imported, output) = + run_cli(&["provider", "profile", "import", "--file", profile_path]).await; + if !imported { + return Err(format!("profile import failed:\n{output}")); + } + Ok(()) +} + +async fn create_provider(value: &str) -> (bool, String) { + let credential = format!("{TOKEN_ENV}={value}"); + run_cli(&[ + "provider", + "create", + "--name", + PROVIDER_NAME, + "--type", + PROFILE_ID, + "--credential", + &credential, + ]) + .await +} + +#[derive(Debug, Clone, Default)] +struct AuthObservation { + authorization: Option, + saw_secret_anywhere: bool, + saw_placeholder: bool, +} + +struct HttpProbeServer { + port: u16, + observations: Arc>>, + task: JoinHandle<()>, +} + +impl HttpProbeServer { + async fn start() -> Result { + let listener = TcpListener::bind(("0.0.0.0", 0)) + .await + .map_err(|error| format!("bind HTTP probe: {error}"))?; + let port = listener + .local_addr() + .map_err(|error| format!("read HTTP probe address: {error}"))? + .port(); + let observations = Arc::new(Mutex::new(Vec::new())); + let task_observations = Arc::clone(&observations); + let task = tokio::spawn(async move { + loop { + let Ok((stream, _)) = listener.accept().await else { + break; + }; + let observations = Arc::clone(&task_observations); + tokio::spawn(async move { + let _ = handle_http_probe(stream, observations).await; + }); + } + }); + Ok(Self { + port, + observations, + task, + }) + } + + async fn wait_for_observations(&self, count: usize) -> Vec { + for _ in 0..100 { + let observations = self.observations.lock().unwrap().clone(); + if observations.len() >= count { + return observations; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + self.observations.lock().unwrap().clone() + } +} + +impl Drop for HttpProbeServer { + fn drop(&mut self) { + self.task.abort(); + } +} + +fn header_value(request: &str, name: &str) -> Option { + request.lines().find_map(|line| { + let (header, value) = line.split_once(':')?; + header + .trim() + .eq_ignore_ascii_case(name) + .then(|| value.trim().to_string()) + }) +} + +async fn handle_http_probe( + mut stream: TcpStream, + observations: Arc>>, +) -> std::io::Result<()> { + let mut received = Vec::new(); + let mut buffer = [0_u8; 4096]; + loop { + let read = tokio::time::timeout(Duration::from_secs(10), stream.read(&mut buffer)).await; + let Ok(Ok(read)) = read else { + break; + }; + if read == 0 { + break; + } + received.extend_from_slice(&buffer[..read]); + if received.windows(4).any(|window| window == b"\r\n\r\n") { + break; + } + } + + let request = String::from_utf8_lossy(&received).into_owned(); + let authorization = header_value(&request, "authorization"); + let observation = AuthObservation { + authorization: authorization.clone(), + saw_secret_anywhere: request.contains(TEST_SECRET), + saw_placeholder: request.contains(PLACEHOLDER_PREFIX), + }; + observations.lock().unwrap().push(observation); + + let result = match authorization.as_deref() { + Some(value) if value == format!("Bearer {TEST_SECRET}") => "AUTH_INJECTED", + Some(value) if value.contains(PUBLIC_VALUE) => "AUTH_PUBLIC", + Some(_) => "AUTH_OTHER", + None => "AUTH_MISSING", + }; + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{result}", + result.len() + ); + stream.write_all(response.as_bytes()).await?; + Ok(()) +} + +/// Reports whether the credential variable is visible, then sends one request +/// through the sandbox proxy with a public `Authorization` value and prints the +/// upstream's verdict. +fn client_script(port: u16) -> String { + format!( + r#" +import os +import socket +import urllib.parse + +host = {TEST_HOST:?} +port = {port} +token_env = {TOKEN_ENV:?} +if token_env not in os.environ: + print("TOKEN_ABSENT") +elif os.environ[token_env].startswith({PLACEHOLDER_PREFIX:?}): + print("TOKEN_PLACEHOLDER") +else: + print("TOKEN_UNSAFE") +proxy_url = next(os.environ[name] for name in + ("HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy") + if os.environ.get(name)) +proxy = urllib.parse.urlparse(proxy_url) + +with socket.create_connection((proxy.hostname, proxy.port or 80), timeout=10) as sock: + target = f"{{host}}:{{port}}" + sock.sendall(f"CONNECT {{target}} HTTP/1.1\r\nHost: {{target}}\r\n\r\n".encode("ascii")) + response = b"" + while b"\r\n\r\n" not in response: + chunk = sock.recv(4096) + if not chunk: + break + response += chunk + if not response.startswith(b"HTTP/1.1 200"): + raise RuntimeError("CONNECT failed") + request = ( + f"GET /v1/ping HTTP/1.1\r\nHost: {{target}}\r\n" + f"Authorization: Bearer {PUBLIC_VALUE}\r\nConnection: close\r\n\r\n" + ).encode("ascii") + sock.sendall(request) + sock.settimeout(5) + response = b"" + while True: + try: + chunk = sock.recv(4096) + except socket.timeout: + break + if not chunk: + break + response += chunk + text = response.decode("utf-8", "replace") + for marker in ("AUTH_INJECTED", "AUTH_PUBLIC", "AUTH_OTHER", "AUTH_MISSING"): + if marker in text: + print(marker) + break + else: + print("AUTH_NO_RESPONSE " + text.splitlines()[0] if text else "AUTH_NO_RESPONSE") +"# + ) +} + +async fn run_client_sandbox(port: u16) -> Result { + let policy = write_base_policy()?; + let policy_path = policy + .path() + .to_str() + .ok_or_else(|| "policy path is not UTF-8".to_string())?; + let script = client_script(port); + let mut sandbox = SandboxGuard::create(&[ + "--policy", + policy_path, + "--provider", + PROVIDER_NAME, + "--", + "python3", + "-c", + &script, + ]) + .await?; + let output = sandbox.create_output.clone(); + sandbox.cleanup().await; + Ok(output) +} + +#[tokio::test] +async fn proxy_delivered_credential_stays_out_of_sandbox_and_is_injected_upstream() { + let server = HttpProbeServer::start().await.expect("start HTTP probe"); + ensure_provider_resources_absent() + .await + .expect("clear stale provider resources"); + + let result = async { + import_profile(server.port).await?; + + // Create-time validation: a bearer value outside token68 is rejected + // before it can fail on every request. + let (created, output) = create_provider("not a token68 value").await; + if created { + return Err(format!( + "provider create accepted a non-token68 bearer value:\n{output}" + )); + } + if !output.contains("cannot be proxy-delivered") { + return Err(format!("unexpected create rejection:\n{output}")); + } + if output.contains("not a token68 value") { + return Err(format!("credential value echoed in error:\n{output}")); + } + + let (created, output) = create_provider(TEST_SECRET).await; + if !created { + return Err(format!("provider create failed:\n{output}")); + } + + let output = run_client_sandbox(server.port).await?; + if !output.contains("TOKEN_ABSENT") { + return Err(format!( + "proxy-delivered credential must not appear in the sandbox environment:\n{output}" + )); + } + if !output.contains("AUTH_INJECTED") { + return Err(format!( + "upstream did not receive the injected credential:\n{output}" + )); + } + if output.contains(TEST_SECRET) || output.contains(PLACEHOLDER_PREFIX) { + return Err(format!( + "sandbox output leaked credential material:\n{output}" + )); + } + + let observations = server.wait_for_observations(1).await; + if observations.len() != 1 { + return Err(format!("expected one upstream request: {observations:?}")); + } + let observation = &observations[0]; + if observation.authorization.as_deref() != Some(&format!("Bearer {TEST_SECRET}")) { + return Err(format!( + "upstream Authorization header was not replaced: {observation:?}" + )); + } + if observation.saw_placeholder { + return Err(format!("upstream saw a placeholder: {observation:?}")); + } + if !observation.saw_secret_anywhere { + return Err(format!("upstream did not see the secret: {observation:?}")); + } + Ok::<(), String>(()) + } + .await; + + cleanup_provider_resources().await; + result.expect("proxy delivery E2E"); +} diff --git a/proto/openshell.proto b/proto/openshell.proto index 28c49065e7..a73cd17568 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -2078,6 +2078,11 @@ message StaticCredentialBinding { string workload_credential_handle = 3; // Delivery mode copied from the provider profile credential declaration. ProviderCredentialDelivery delivery = 4; + // Placement metadata copied from the provider profile credential + // declaration. Populated only for proxy-delivered credentials so the + // sandbox proxy can build the outbound header from this binding alone. + string auth_style = 5; + string header_name = 6; } // Get sandbox provider environment response. diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 527f44d343..00d8f7de1b 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -8689,7 +8689,12 @@ type StaticCredentialBinding struct { // authorization epoch, or endpoint authorization boundary changes. WorkloadCredentialHandle string `protobuf:"bytes,3,opt,name=workload_credential_handle,json=workloadCredentialHandle,proto3" json:"workload_credential_handle,omitempty"` // Delivery mode copied from the provider profile credential declaration. - Delivery ProviderCredentialDelivery `protobuf:"varint,4,opt,name=delivery,proto3,enum=openshell.v1.ProviderCredentialDelivery" json:"delivery,omitempty"` + Delivery ProviderCredentialDelivery `protobuf:"varint,4,opt,name=delivery,proto3,enum=openshell.v1.ProviderCredentialDelivery" json:"delivery,omitempty"` + // Placement metadata copied from the provider profile credential + // declaration. Populated only for proxy-delivered credentials so the + // sandbox proxy can build the outbound header from this binding alone. + AuthStyle string `protobuf:"bytes,5,opt,name=auth_style,json=authStyle,proto3" json:"auth_style,omitempty"` + HeaderName string `protobuf:"bytes,6,opt,name=header_name,json=headerName,proto3" json:"header_name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -8752,6 +8757,20 @@ func (x *StaticCredentialBinding) GetDelivery() ProviderCredentialDelivery { return ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED } +func (x *StaticCredentialBinding) GetAuthStyle() string { + if x != nil { + return x.AuthStyle + } + return "" +} + +func (x *StaticCredentialBinding) GetHeaderName() string { + if x != nil { + return x.HeaderName + } + return "" +} + // Get sandbox provider environment response. type GetSandboxProviderEnvironmentResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -15558,12 +15577,16 @@ const file_openshell_proto_rawDesc = "" + "\x1fStaticCredentialEndpointBinding\x12\x12\n" + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + "\x04port\x18\x02 \x01(\rR\x04port\x12\x12\n" + - "\x04path\x18\x03 \x01(\tR\x04path\"\x9b\x02\n" + + "\x04path\x18\x03 \x01(\tR\x04path\"\xdb\x02\n" + "\x17StaticCredentialBinding\x12K\n" + "\tendpoints\x18\x01 \x03(\v2-.openshell.v1.StaticCredentialEndpointBindingR\tendpoints\x12/\n" + "\x13credential_identity\x18\x02 \x01(\tR\x12credentialIdentity\x12<\n" + "\x1aworkload_credential_handle\x18\x03 \x01(\tR\x18workloadCredentialHandle\x12D\n" + - "\bdelivery\x18\x04 \x01(\x0e2(.openshell.v1.ProviderCredentialDeliveryR\bdelivery\"\x90\b\n" + + "\bdelivery\x18\x04 \x01(\x0e2(.openshell.v1.ProviderCredentialDeliveryR\bdelivery\x12\x1d\n" + + "\n" + + "auth_style\x18\x05 \x01(\tR\tauthStyle\x12\x1f\n" + + "\vheader_name\x18\x06 \x01(\tR\n" + + "headerName\"\x90\b\n" + "%GetSandboxProviderEnvironmentResponse\x12l\n" + "\venvironment\x18\x01 \x03(\v2D.openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntryB\x04\x88\xb5\x18\x01R\venvironment\x122\n" + "\x15provider_env_revision\x18\x02 \x01(\x04R\x13providerEnvRevision\x12\x87\x01\n" + diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index 6c39b296b6..960bb8bcf4 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -147,6 +147,12 @@ endpoint-bound placeholder to the sandbox. A custom profile may instead use keeps both the credential and its placeholder out of the workload environment; the inspected HTTP proxy sets the complete header only after the request passes network and middleware policy. Export the profile to confirm which mode applies. +A `502` with error code `provider_authentication_failed` means a bound request +reached the proxy but the credential could not be injected; check the sandbox +OCSF log for the `Proxy-delivered credential` event. An upstream `401` on a +proxy-delivered endpoint usually means the request bypassed inspection, for +example through a `tls: skip` policy endpoint or a path outside the profile +endpoints, so no header was replaced. When an inspected request receives `request_authority_mismatch`, compare its HTTP authority with the CONNECT tunnel endpoint. The host and effective port From 8f7107e1739f4592bbbf620d5d117725e4cd083d Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Wed, 2 Sep 2026 18:42:42 +0000 Subject: [PATCH 14/18] feat(sandbox): encode admission bodies as base64 Signed-off-by: Johnny Greco --- Cargo.lock | 1 + architecture/sandbox.md | 4 +- crates/openshell-sandbox/Cargo.toml | 1 + crates/openshell-sandbox/src/agent_bridge.rs | 84 +++++++++++++++++++- docs/extensibility/supervisor-middleware.mdx | 19 +++-- 5 files changed, 97 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1d25b28696..b8e52ce0ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4249,6 +4249,7 @@ name = "openshell-sandbox" version = "0.0.0" dependencies = [ "axum", + "base64 0.22.1", "clap", "futures", "miette", diff --git a/architecture/sandbox.md b/architecture/sandbox.md index bf7b3a70e3..a8dc889b83 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -191,7 +191,9 @@ replacement. Provider egress strips and resolves that handle, exposing the attestation only to the matching middleware stage. Handles are scoped to the sandbox, middleware, provider target, and runtime generation and remain retryable only for their bounded lifetime, so partial policy or registry reloads -cannot mix admission and egress state. +cannot mix admission and egress state. An allowed admission result without an +attestation returns no handle; append-time checks need only a decision, while an +attested provider-context check supplies the handle used at provider egress. The supervisor installs policy and middleware registry changes as one runtime generation and preserves the last-known-good generation if preparation fails. diff --git a/crates/openshell-sandbox/Cargo.toml b/crates/openshell-sandbox/Cargo.toml index 5cb7ca8a84..929eec37d8 100644 --- a/crates/openshell-sandbox/Cargo.toml +++ b/crates/openshell-sandbox/Cargo.toml @@ -45,6 +45,7 @@ nix = { workspace = true } rustls = { workspace = true } # Serialization (serde_json::json! for OCSF unmapped fields) +base64 = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } prost = { workspace = true } diff --git a/crates/openshell-sandbox/src/agent_bridge.rs b/crates/openshell-sandbox/src/agent_bridge.rs index 18e5bbd441..6fe690232d 100644 --- a/crates/openshell-sandbox/src/agent_bridge.rs +++ b/crates/openshell-sandbox/src/agent_bridge.rs @@ -10,12 +10,13 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::routing::post; use axum::{Json, Router}; +use base64::Engine as _; use openshell_core::proto::{ AgentConversationEvaluation, AgentConversationResult, AgentConversationTarget, Decision, RequestContext, SupervisorMiddlewarePhase, }; use openshell_supervisor_network::opa::OpaEngine; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _}; use tokio::net::TcpListener; use tokio::sync::watch; use tracing::{debug, warn}; @@ -25,11 +26,9 @@ pub const BRIDGE_PATH: &str = "/v1/agent/conversation"; pub const BRIDGE_URL: &str = "http://127.0.0.1:8193/v1/agent/conversation"; pub const BRIDGE_URL_ENV: &str = "OPENSHELL_AGENT_CONVERSATION_URL"; -// Vec is encoded as a JSON number array by the existing bridge contract, -// so its transport envelope can be roughly five times the logical payload. const MAX_ADMISSION_BODY_BYTES: usize = openshell_supervisor_middleware::MAX_MIDDLEWARE_PAYLOAD_BYTES; -const MAX_BRIDGE_BODY_BYTES: usize = MAX_ADMISSION_BODY_BYTES * 5 + 64 * 1024; +const MAX_BRIDGE_BODY_BYTES: usize = MAX_ADMISSION_BODY_BYTES.div_ceil(3) * 4 + 64 * 1024; #[derive(Debug, Clone)] pub struct BridgeSelection { @@ -74,6 +73,7 @@ struct BridgeRequest { session_id: String, #[serde(default)] submission_id: String, + #[serde(rename = "request_body_b64", deserialize_with = "deserialize_base64")] request_body: Vec, } @@ -81,6 +81,10 @@ struct BridgeRequest { struct BridgeResponse { decision: &'static str, #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + rename = "replacement_body_b64", + serialize_with = "serialize_optional_base64" + )] replacement_body: Option>, #[serde(skip_serializing_if = "Option::is_none")] handle: Option, @@ -181,6 +185,25 @@ async fn evaluate(State(state): State, Json(input): Json(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let encoded = String::deserialize(deserializer)?; + base64::engine::general_purpose::STANDARD + .decode(encoded) + .map_err(D::Error::custom) +} + +fn serialize_optional_base64(body: &Option>, serializer: S) -> Result +where + S: Serializer, +{ + body.as_ref() + .map(|body| base64::engine::general_purpose::STANDARD.encode(body)) + .serialize(serializer) +} + fn build_evaluation( sandbox_id: &str, sandbox_name: &str, @@ -328,6 +351,38 @@ mod tests { assert!(selected_binding(&selection, &unadvertised).is_none()); } + #[test] + fn admission_request_json_requires_the_base64_field() { + let input: BridgeRequest = serde_json::from_value(serde_json::json!({ + "harness_version": "sdk-v1", + "hook": "user_input", + "schema_version": "example.input.v1", + "request_body_b64": "eHg=" + })) + .unwrap(); + assert_eq!(input.request_body, b"xx"); + + assert!( + serde_json::from_value::(serde_json::json!({ + "harness_version": "sdk-v1", + "hook": "user_input", + "schema_version": "example.input.v1", + "request_body_b64": "not base64" + })) + .is_err() + ); + + assert!( + serde_json::from_value::(serde_json::json!({ + "harness_version": "sdk-v1", + "hook": "user_input", + "schema_version": "example.input.v1", + "request_body": [120] + })) + .is_err() + ); + } + #[test] fn admission_request_enforces_the_logical_body_limit() { let max_selection = selection(MAX_ADMISSION_BODY_BYTES); @@ -403,6 +458,10 @@ mod tests { ) .expect("allow"); assert_eq!(allow.decision, "allow"); + assert_eq!( + serde_json::to_value(&allow).unwrap()["replacement_body_b64"], + "cmVkYWN0ZWQ=" + ); let handle = allow.handle.expect("handle"); assert_ne!(handle.as_bytes(), b"receipt"); let request = openshell_supervisor_middleware::AgentAdmissionRequest { @@ -427,6 +486,23 @@ mod tests { ); assert_eq!(allow.replacement_body, Some(b"redacted".to_vec())); + let empty_replacement = bridge_result( + &runner, + "sandbox-1", + 7, + &selection, + AgentConversationResult { + decision: Decision::Allow as i32, + has_replacement_body: true, + ..Default::default() + }, + ) + .expect("empty replacement"); + assert_eq!( + serde_json::to_value(&empty_replacement).unwrap()["replacement_body_b64"], + "" + ); + let deny = bridge_result( &runner, "sandbox-1", diff --git a/docs/extensibility/supervisor-middleware.mdx b/docs/extensibility/supervisor-middleware.mdx index c569b0a675..82adda57fa 100644 --- a/docs/extensibility/supervisor-middleware.mdx +++ b/docs/extensibility/supervisor-middleware.mdx @@ -17,15 +17,20 @@ the harness, hook, and schema identifiers. The selected policy config must use scheme and port from the corresponding admitted network endpoint. The bridge sets `OPENSHELL_AGENT_CONVERSATION_URL`, stamps sandbox and provider identity, and returns only the structured decision, optional replacement, and opaque -receipt. It shares the policy and middleware registry generation used for +handle. It shares the policy and middleware registry generation used for provider egress, so an incomplete reload makes admission unavailable. Managed Pi is the first client of this general contract. A Pi extension submits -a rendered, idle, text-only user prompt before Pi stores it; denied prompts never -enter chat history, while replacement bodies support redaction. Pi remains -unaware of OpenShell-specific behavior. Admission bodies are limited to 32 KiB -and to the advertised binding limit. Images, queued input, retries, compaction, -and automatic continuations after tool calls remain outside this initial hook. +each user, tool-result, assistant, summary, extension, and bash message before Pi +stores it; a denial prevents the original message from entering history, while +a replacement supports redaction. It separately submits the exact outbound text +context before provider serialization. The returned handle binds that context +to the provider request, where middleware checks it again before credentials are +injected. Queued input, retries, compaction, automatic continuations after tool +calls, and assistant output therefore pass through these checkpoints. Assistant +output is admitted at finalization after it has been displayed. Images remain +unsupported. Logical admission bodies are limited to 4 MiB and to the advertised +binding limit. Pi remains unaware of OpenShell-specific behavior. Middleware selection is independent of the network policy rule that admitted the request. OpenShell matches middleware by destination host, so the same middleware applies consistently across broad, specific, user-authored, and provider-derived network policies. @@ -184,7 +189,7 @@ Middleware decisions are enforced regardless of the endpoint's `enforcement` mod ## Set Payload Limits -Every middleware binding declares the largest logical payload or replacement it supports through `max_payload_bytes`. For `HTTP_REQUEST`, that payload is one request body. For `WEBSOCKET_MESSAGE`, it is one complete message rather than the whole session. For `AGENT_CONVERSATION`, it is one versioned hook body or replacement; the initial loopback bridge applies an additional 32 KiB limit. +Every middleware binding declares the largest logical payload or replacement it supports through `max_payload_bytes`. For `HTTP_REQUEST`, that payload is one request body. For `WEBSOCKET_MESSAGE`, it is one complete message rather than the whole session. For `AGENT_CONVERSATION`, it is one versioned hook body or replacement; the loopback bridge applies the 4 MiB platform maximum before the binding-specific limit. - Built-in middleware uses its OpenShell-defined limit. - Each operator-run registration sets one `max_payload_bytes` ceiling. OpenShell uses the smaller of that ceiling and each binding's advertised `max_payload_bytes` capability. From f5212b582890beed21fa3db345ead4a22205502d Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Wed, 2 Sep 2026 18:53:13 +0000 Subject: [PATCH 15/18] fix(sandbox): hide admission metadata Signed-off-by: Johnny Greco --- crates/openshell-sandbox/src/agent_bridge.rs | 12 ++++-------- docs/extensibility/supervisor-middleware.mdx | 2 +- docs/reference/gateway-config.mdx | 6 +++--- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/crates/openshell-sandbox/src/agent_bridge.rs b/crates/openshell-sandbox/src/agent_bridge.rs index 6fe690232d..0d0562a043 100644 --- a/crates/openshell-sandbox/src/agent_bridge.rs +++ b/crates/openshell-sandbox/src/agent_bridge.rs @@ -90,8 +90,6 @@ struct BridgeResponse { handle: Option, #[serde(skip_serializing_if = "Option::is_none")] reason_code: Option, - #[serde(skip_serializing_if = "Option::is_none")] - metadata: Option>, } #[derive(Debug, Serialize)] @@ -276,7 +274,6 @@ fn bridge_result( .then_some(result.replacement_body), handle, reason_code: None, - metadata: (!result.metadata.is_empty()).then_some(result.metadata), }) } Decision::Deny => Ok(BridgeResponse { @@ -284,7 +281,6 @@ fn bridge_result( replacement_body: None, handle: None, reason_code: (!result.reason_code.is_empty()).then_some(result.reason_code), - metadata: None, }), Decision::Unspecified => Err("invalid_admission_response"), } @@ -453,15 +449,15 @@ mod tests { attestation: b"receipt".to_vec(), replacement_body: b"redacted".to_vec(), has_replacement_body: true, + metadata: std::collections::HashMap::from([("internal".into(), "value".into())]), ..Default::default() }, ) .expect("allow"); assert_eq!(allow.decision, "allow"); - assert_eq!( - serde_json::to_value(&allow).unwrap()["replacement_body_b64"], - "cmVkYWN0ZWQ=" - ); + let response_json = serde_json::to_value(&allow).unwrap(); + assert_eq!(response_json["replacement_body_b64"], "cmVkYWN0ZWQ="); + assert!(response_json.get("metadata").is_none()); let handle = allow.handle.expect("handle"); assert_ne!(handle.as_bytes(), b"receipt"); let request = openshell_supervisor_middleware::AgentAdmissionRequest { diff --git a/docs/extensibility/supervisor-middleware.mdx b/docs/extensibility/supervisor-middleware.mdx index 82adda57fa..9630450158 100644 --- a/docs/extensibility/supervisor-middleware.mdx +++ b/docs/extensibility/supervisor-middleware.mdx @@ -247,7 +247,7 @@ See [Logging](/observability/logging) for log access and [OCSF JSON Export](/obs - Middleware applies only through operation bindings advertised by each implementation. For protocols that have no supported middleware operation at all, such as HTTP/2 prior knowledge or non-HTTP TCP, the existing uninspectable-traffic gate denies a host match containing `fail_closed` and relays an all-`fail_open` match with a detection finding. - The typed operation and phase pairs are `HTTP_REQUEST/PRE_CREDENTIALS`, `WEBSOCKET_MESSAGE/PRE_CREDENTIALS`, and `AGENT_CONVERSATION/AGENT_CONTEXT`. -- The initial agent bridge supports exactly one configured agent-conversation binding, one exact provider host, and combined sandbox topology. Adding the first agent binding to a running sandbox requires recreating that sandbox so the loopback listener and environment variable are present. +- The initial agent bridge selects one configured agent-conversation middleware and one exact provider host. That middleware may advertise multiple hook and schema bindings. Adding the first agent binding to a running sandbox requires recreating that sandbox so the loopback listener and environment variable are present. - A host match does not imply every advertised operation: an HTTP-only attachment can inspect the upgrade GET, then post-upgrade traffic passes with `binding_not_selected` coverage. - The V1 WebSocket binding inspects complete client text messages only. Binary messages pass with `unsupported_message_type` coverage for active stages; control frames and upstream-to-client messages remain outside the middleware operation. - Selection uses destination host include and exclude patterns. diff --git a/docs/reference/gateway-config.mdx b/docs/reference/gateway-config.mdx index 358d06fea2..a9d4d2577e 100644 --- a/docs/reference/gateway-config.mdx +++ b/docs/reference/gateway-config.mdx @@ -271,13 +271,13 @@ max_payload_bytes = 262144 timeout = "500ms" ``` -Each service implements the supervisor middleware gRPC contract and exposes bindings through `Describe`. Policies reference the operator-owned registration `name`, attaching the complete middleware and all of its bindings. Bindings are identified by operation and phase. A manifest may expose at most one binding for each operation and phase pair. V1 supports `HttpRequest/pre_credentials` and `WebSocketMessage/pre_credentials`, so a service can inspect HTTP, WebSocket, or both. Registration names must be unique, and operator-run registrations cannot claim the reserved `openshell/` namespace. The service-reported manifest name is diagnostic metadata and does not need to match the registration name. +Each service implements the supervisor middleware gRPC contract and exposes bindings through `Describe`. Policies reference the operator-owned registration `name`, attaching the complete middleware and all of its bindings. HTTP and WebSocket bindings are identified by operation and phase. Agent-conversation binding identity also includes the harness, hook, and schema version, so a manifest may advertise multiple `AgentConversation/agent_context` bindings with distinct identifiers. V1 also supports `HttpRequest/pre_credentials` and `WebSocketMessage/pre_credentials`. Registration names must be unique, and operator-run registrations cannot claim the reserved `openshell/` namespace. The service-reported manifest name is diagnostic metadata and does not need to match the registration name. The gateway connects to every registered service and validates `Describe` before it starts. The service must therefore be running before the gateway. Policy creation and full policy updates call `ValidateConfig`; an unavailable service or invalid middleware configuration rejects the policy before persistence. -`max_payload_bytes` is the shared operator ceiling for inspectable logical payloads across every binding exposed by the service. It caps HTTP request and replacement bodies as well as complete WebSocket text messages and replacements. The value must be greater than zero and no larger than the 4 MiB platform maximum. Each binding's effective limit is the smaller of this ceiling and the binding's advertised `max_payload_bytes` capability. Binary WebSocket messages are not exposed to V1 middleware, so this field does not limit binary pass-through. Middleware gRPC servers should allow messages of at least 4 MiB plus 293 KiB so a maximum-size payload and its protobuf envelope fit on the transport. +`max_payload_bytes` is the shared operator ceiling for inspectable logical payloads across every binding exposed by the service. It caps HTTP request and replacement bodies, complete WebSocket text messages and replacements, and agent-conversation request and replacement bodies. The value must be greater than zero and no larger than the 4 MiB platform maximum. Each binding's effective limit is the smaller of this ceiling and the binding's advertised `max_payload_bytes` capability. Binary WebSocket messages are not exposed to V1 middleware, so this field does not limit binary pass-through. Middleware gRPC servers should allow messages of at least 4 MiB plus 293 KiB so a maximum-size payload and its protobuf envelope fit on the transport. -`timeout` is the operator-configured service-wide RPC timeout. It accepts the same compact duration syntax as gateway interceptors: an integer followed by `ms` or `s`, such as `500ms` or `2s`. Values must be between `10ms` and `30s`, inclusive. Omit the field to use the 500 ms platform default. A binding may advertise a shorter `timeout` in the `Describe` manifest, but it cannot extend the operator-configured deadline; OpenShell uses the smaller value. OpenShell validates both levels before accepting the service. The operator-configured service timeout applies to `Describe` and `ValidateConfig`. The effective binding timeout applies only to `EvaluateHttpRequest`, WebSocket preflight, and each WebSocket message. An accepted WebSocket stream has no connection-wide RPC deadline. +`timeout` is the operator-configured service-wide RPC timeout. It accepts the same compact duration syntax as gateway interceptors: an integer followed by `ms` or `s`, such as `500ms` or `2s`. Values must be between `10ms` and `30s`, inclusive. Omit the field to use the 500 ms platform default. A binding may advertise a shorter `timeout` in the `Describe` manifest, but it cannot extend the operator-configured deadline; OpenShell uses the smaller value. OpenShell validates both levels before accepting the service. The operator-configured service timeout applies to `Describe` and `ValidateConfig`. The effective binding timeout applies only to `EvaluateHttpRequest`, `EvaluateAgentConversation`, WebSocket preflight, and each WebSocket message. An accepted WebSocket stream has no connection-wide RPC deadline. The service `grpc_endpoint` supports plaintext `http://` and TLS `https://`. HTTPS uses the platform trust store unless `tls_ca_cert_path` names a certificate-only PEM bundle. OpenShell rejects bundles containing private keys, loads the certificates at gateway startup, and distributes only public certificates to sandbox supervisors; normal TLS hostname verification still applies. `audience` sets the exact audience for gateway-minted service tokens and defaults to `urn:openshell:extension:middleware:`. After authenticated `Describe` succeeds, OpenShell treats a non-empty manifest `expected_audience` as a consistency assertion and refuses to start when it differs from the configured audience. A strict verifier may reject an incorrect audience before returning the manifest. From 1e2b1649a42db6dfa1ad688a57f9ec2d9071d6b0 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Wed, 2 Sep 2026 19:40:34 +0000 Subject: [PATCH 16/18] feat(sandbox): authenticate admission bridge callers Signed-off-by: Johnny Greco --- crates/openshell-sandbox/src/agent_bridge.rs | 77 ++++++- crates/openshell-sandbox/src/lib.rs | 35 ++- .../src/admission_token.rs | 208 ++++++++++++++++++ .../openshell-supervisor-process/src/lib.rs | 3 + .../src/process.rs | 1 + .../openshell-supervisor-process/src/run.rs | 3 + .../openshell-supervisor-process/src/ssh.rs | 26 +++ docs/extensibility/supervisor-middleware.mdx | 2 + 8 files changed, 352 insertions(+), 3 deletions(-) create mode 100644 crates/openshell-supervisor-process/src/admission_token.rs diff --git a/crates/openshell-sandbox/src/agent_bridge.rs b/crates/openshell-sandbox/src/agent_bridge.rs index 0d0562a043..5f522c7a61 100644 --- a/crates/openshell-sandbox/src/agent_bridge.rs +++ b/crates/openshell-sandbox/src/agent_bridge.rs @@ -6,7 +6,8 @@ use std::sync::Arc; use axum::extract::{DefaultBodyLimit, State}; -use axum::http::StatusCode; +use axum::http::{Request, StatusCode, header::AUTHORIZATION}; +use axum::middleware::{self, Next}; use axum::response::{IntoResponse, Response}; use axum::routing::post; use axum::{Json, Router}; @@ -38,6 +39,7 @@ pub struct BridgeSelection { pub provider_host: String, pub provider_port: u32, pub middleware_config: prost_types::Struct, + pub require_caller_token: bool, } #[derive(Debug, Clone)] @@ -61,6 +63,7 @@ struct BridgeState { sandbox_id: Arc, sandbox_name: Arc, workspace: watch::Receiver, + caller_tokens: openshell_supervisor_process::AdmissionTokenRegistry, } #[derive(Debug, Deserialize)] @@ -104,6 +107,7 @@ pub fn spawn( sandbox_id: String, sandbox_name: String, workspace: watch::Receiver, + caller_tokens: openshell_supervisor_process::AdmissionTokenRegistry, ) -> tokio::task::JoinHandle<()> { let state = BridgeState { engine, @@ -111,10 +115,17 @@ pub fn spawn( sandbox_id: Arc::from(sandbox_id), sandbox_name: Arc::from(sandbox_name), workspace, + caller_tokens, }; tokio::spawn(async move { let app = Router::new() - .route(BRIDGE_PATH, post(evaluate)) + .route( + BRIDGE_PATH, + post(evaluate).route_layer(middleware::from_fn_with_state( + state.clone(), + authorize_caller, + )), + ) .layer(DefaultBodyLimit::max(MAX_BRIDGE_BODY_BYTES)) .with_state(state); if let Err(error) = axum::serve(listener, app).await { @@ -123,6 +134,41 @@ pub fn spawn( }) } +async fn authorize_caller( + State(state): State, + request: Request, + next: Next, +) -> Response { + let runtime = state.runtime.borrow().clone(); + if runtime + .selection + .as_ref() + .is_some_and(|selection| selection.require_caller_token) + { + let authorized = caller_is_authorized(request.headers(), |token| { + state.caller_tokens.contains(token) + }); + if !authorized { + return caller_not_authorized(); + } + } + next.run(request).await +} + +fn bearer_token(headers: &axum::http::HeaderMap) -> Option<&str> { + headers + .get(AUTHORIZATION) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.strip_prefix("Bearer ")) +} + +fn caller_is_authorized( + headers: &axum::http::HeaderMap, + is_registered: impl FnOnce(&str) -> bool, +) -> bool { + bearer_token(headers).is_some_and(|token| !token.is_empty() && is_registered(token)) +} + async fn evaluate(State(state): State, Json(input): Json) -> Response { let runtime = state.runtime.borrow().clone(); if runtime.generation != state.engine.current_generation() { @@ -296,6 +342,16 @@ fn admission_unavailable() -> Response { .into_response() } +fn caller_not_authorized() -> Response { + ( + StatusCode::UNAUTHORIZED, + Json(BridgeError { + error: "caller_not_authorized", + }), + ) + .into_response() +} + fn selected_binding<'a>( selection: &'a BridgeSelection, input: &BridgeRequest, @@ -397,6 +453,22 @@ mod tests { assert!(selected_binding(&selection(1), &request("sdk-v1", 2)).is_none()); } + #[test] + fn caller_token_header_requires_exact_bearer_scheme() { + let mut headers = axum::http::HeaderMap::new(); + assert_eq!(bearer_token(&headers), None); + + headers.insert(AUTHORIZATION, "Basic token".parse().unwrap()); + assert_eq!(bearer_token(&headers), None); + assert!(!caller_is_authorized(&headers, |_| true)); + + headers.insert(AUTHORIZATION, "Bearer token".parse().unwrap()); + assert_eq!(bearer_token(&headers), Some("token")); + assert!(caller_is_authorized(&headers, |token| token == "token")); + assert!(!caller_is_authorized(&headers, |_| false)); + assert_eq!(caller_not_authorized().status(), StatusCode::UNAUTHORIZED); + } + fn selection(limit: usize) -> BridgeSelection { BridgeSelection { middleware_name: "operator/guard".into(), @@ -410,6 +482,7 @@ mod tests { provider_host: "api.example.com".into(), provider_port: 8443, middleware_config: prost_types::Struct::default(), + require_caller_token: true, } } diff --git a/crates/openshell-sandbox/src/lib.rs b/crates/openshell-sandbox/src/lib.rs index fe602fb7e8..729a04f69b 100644 --- a/crates/openshell-sandbox/src/lib.rs +++ b/crates/openshell-sandbox/src/lib.rs @@ -92,6 +92,11 @@ async fn select_agent_bridge( runner: &ChainRunner, policy: &openshell_core::proto::SandboxPolicy, ) -> Result> { + let require_caller_token = require_agent_bridge_caller_token( + std::env::var(openshell_supervisor_process::REQUIRE_CALLER_TOKEN_ENV) + .ok() + .as_deref(), + )?; let bindings = runner.agent_conversation_bindings().await?; let mut matches = policy.network_middlewares.iter().filter(|(_, config)| { bindings @@ -124,6 +129,7 @@ async fn select_agent_bridge( } let provider_host = &endpoints.include[0]; let (provider_scheme, provider_port) = exact_provider_endpoint(policy, provider_host)?; + let middleware_config = config.config.clone().unwrap_or_default(); let advertised = bindings .into_iter() .filter(|binding| binding.middleware_name == config.middleware) @@ -144,10 +150,22 @@ async fn select_agent_bridge( provider_scheme, provider_host: provider_host.clone(), provider_port, - middleware_config: config.config.clone().unwrap_or_default(), + middleware_config, + require_caller_token, })) } +fn require_agent_bridge_caller_token(value: Option<&str>) -> Result { + match value { + None | Some("true") => Ok(true), + Some("false") => Ok(false), + Some(_) => Err(miette::miette!( + "{} must be 'true' or 'false'", + openshell_supervisor_process::REQUIRE_CALLER_TOKEN_ENV + )), + } +} + fn exact_provider_endpoint( policy: &openshell_core::proto::SandboxPolicy, provider_host: &str, @@ -658,6 +676,9 @@ pub async fn run_sandbox( } _ => None, }; + let admission_tokens = agent_bridge + .as_ref() + .map(|_| openshell_supervisor_process::AdmissionTokenRegistry::default()); let agent_bridge_runtime = if let Some(selection) = agent_bridge { if sidecar_network_enforcement { return Err(miette::miette!( @@ -702,6 +723,9 @@ pub async fn run_sandbox( sandbox_id.clone().unwrap_or_default(), sandbox_name_for_agg.clone().unwrap_or_default(), workspace_rx.clone(), + admission_tokens + .clone() + .expect("selected agent bridge requires caller token registry"), ); provider_env.insert( agent_bridge::BRIDGE_URL_ENV.into(), @@ -1106,6 +1130,7 @@ pub async fn run_sandbox( main_env, ca_file_paths, agent_proposals.clone(), + admission_tokens, #[cfg(target_os = "linux")] netns.as_ref(), #[cfg(target_os = "linux")] @@ -4738,6 +4763,14 @@ mod tests { assert!(exact_provider_endpoint(&policy, "api.example.com").is_err()); } + #[test] + fn agent_bridge_caller_token_defaults_on_and_has_explicit_debug_opt_out() { + assert!(require_agent_bridge_caller_token(None).unwrap()); + assert!(require_agent_bridge_caller_token(Some("true")).unwrap()); + assert!(!require_agent_bridge_caller_token(Some("false")).unwrap()); + assert!(require_agent_bridge_caller_token(Some("0")).is_err()); + } + #[test] fn retained_policy_generation_keeps_agent_bridge_in_sync() { let (runtime, receiver) = diff --git a/crates/openshell-supervisor-process/src/admission_token.rs b/crates/openshell-supervisor-process/src/admission_token.rs new file mode 100644 index 0000000000..0c00fef9a0 --- /dev/null +++ b/crates/openshell-supervisor-process/src/admission_token.rs @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Per-exec bearer tokens for the sandbox-local agent admission bridge. + +use base64::Engine as _; +use rand::RngExt as _; +use std::collections::HashSet; +#[cfg(not(target_vendor = "apple"))] +use std::io::Write as _; +use std::os::fd::{AsRawFd as _, OwnedFd}; +use std::os::unix::process::CommandExt as _; +use std::process::Command; +use std::sync::{Arc, Mutex}; + +pub const TOKEN_FD_ENV: &str = "OPENSHELL_AGENT_ADMISSION_TOKEN_FD"; +pub const REQUIRE_CALLER_TOKEN_ENV: &str = "OPENSHELL_AGENT_ADMISSION_REQUIRE_CALLER_TOKEN"; + +#[derive(Clone, Debug, Default)] +pub struct AdmissionTokenRegistry(Arc>>); + +impl AdmissionTokenRegistry { + pub fn contains(&self, token: &str) -> bool { + self.0 + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .contains(token) + } + + pub(crate) fn prepare_child(&self, command: &mut Command) -> std::io::Result { + let mut random = [0_u8; 32]; + rand::rng().fill(&mut random); + let token = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(random); + + let read_fd = prepare_token_fd(command, &token)?; + + self.0 + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .insert(token.clone()); + command.env(TOKEN_FD_ENV, read_fd.as_raw_fd().to_string()); + + Ok(PreparedToken { + read_fd, + registration: TokenRegistration { + registry: self.clone(), + token, + }, + }) + } +} + +#[cfg(not(target_vendor = "apple"))] +fn prepare_token_fd(command: &mut Command, token: &str) -> std::io::Result { + let (read_fd, write_fd) = nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC)?; + let mut writer = std::fs::File::from(write_fd); + writer.write_all(token.as_bytes())?; + drop(writer); + + let read_fd_raw = read_fd.as_raw_fd(); + // Keep CLOEXEC set in the multithreaded parent. The intended child + // clears it after fork so concurrent spawns cannot inherit the token. + #[allow(unsafe_code)] + unsafe { + command.pre_exec(move || clear_close_on_exec(read_fd_raw)); + } + Ok(read_fd) +} + +#[cfg(target_vendor = "apple")] +fn prepare_token_fd(command: &mut Command, token: &str) -> std::io::Result { + use std::os::unix::fs::OpenOptionsExt as _; + + // Darwin lacks an atomic CLOEXEC pipe API on supported versions. Reserve + // the advertised fd atomically, then replace it in the post-fork child. + let reserved_fd: OwnedFd = std::fs::OpenOptions::new() + .read(true) + .custom_flags(libc::O_CLOEXEC) + .open("/dev/null")? + .into(); + let reserved_fd_raw = reserved_fd.as_raw_fd(); + let child_token = token.as_bytes().to_owned(); + #[allow(unsafe_code)] + unsafe { + command.pre_exec(move || install_child_pipe(reserved_fd_raw, &child_token)); + } + Ok(reserved_fd) +} + +#[cfg(target_vendor = "apple")] +#[allow(unsafe_code)] +fn install_child_pipe(target_fd: std::os::fd::RawFd, token: &[u8]) -> std::io::Result<()> { + let mut pipe_fds = [-1, -1]; + unsafe { + if libc::pipe(pipe_fds.as_mut_ptr()) != 0 { + return Err(std::io::Error::last_os_error()); + } + let written = libc::write(pipe_fds[1], token.as_ptr().cast(), token.len()); + if written < 0 { + return Err(std::io::Error::last_os_error()); + } + if written as usize != token.len() { + return Err(std::io::Error::from_raw_os_error(libc::EIO)); + } + if libc::close(pipe_fds[1]) != 0 + || libc::dup2(pipe_fds[0], target_fd) < 0 + || libc::close(pipe_fds[0]) != 0 + { + return Err(std::io::Error::last_os_error()); + } + } + Ok(()) +} + +pub struct PreparedToken { + read_fd: OwnedFd, + registration: TokenRegistration, +} + +impl PreparedToken { + pub(crate) fn child_spawned(self) -> TokenRegistration { + drop(self.read_fd); + self.registration + } +} + +pub struct TokenRegistration { + registry: AdmissionTokenRegistry, + token: String, +} + +impl Drop for TokenRegistration { + fn drop(&mut self) { + self.registry + .0 + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .remove(&self.token); + } +} + +#[cfg(not(target_vendor = "apple"))] +fn clear_close_on_exec(fd: std::os::fd::RawFd) -> std::io::Result<()> { + use nix::fcntl::{FcntlArg, FdFlag, fcntl}; + + let flags = FdFlag::from_bits_truncate(fcntl(fd, FcntlArg::F_GETFD)?); + fcntl(fd, FcntlArg::F_SETFD(flags & !FdFlag::FD_CLOEXEC))?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn child_receives_token_only_through_inherited_fd_and_exit_revokes_it() { + let registry = AdmissionTokenRegistry::default(); + let mut command = Command::new("/bin/sh"); + command.stdout(std::process::Stdio::piped()); + command.arg("-c").arg(format!( + "fd=${{{TOKEN_FD_ENV}}}; cat <&$fd; test -z \"${{TOKEN:-}}\"" + )); + let prepared = registry.prepare_child(&mut command).expect("prepare token"); + let child = command.spawn().expect("spawn child"); + let registration = prepared.child_spawned(); + let output = child.wait_with_output().expect("wait for child"); + assert!(output.status.success()); + let token = String::from_utf8(output.stdout).expect("ASCII token"); + assert_eq!(token.len(), 43); + assert!(registry.contains(&token)); + drop(registration); + assert!( + registry + .0 + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .is_empty() + ); + } + + #[test] + fn failed_spawn_revokes_registered_token() { + let registry = AdmissionTokenRegistry::default(); + let mut command = Command::new("/definitely/not/an/executable"); + let prepared = registry.prepare_child(&mut command).expect("prepare token"); + assert!(command.spawn().is_err()); + drop(prepared); + assert!( + registry + .0 + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .is_empty() + ); + } + + #[test] + fn prepared_descriptor_remains_close_on_exec_in_parent() { + let registry = AdmissionTokenRegistry::default(); + let mut command = Command::new("/bin/true"); + let prepared = registry.prepare_child(&mut command).expect("prepare token"); + let flags = nix::fcntl::FdFlag::from_bits_truncate( + nix::fcntl::fcntl(prepared.read_fd.as_raw_fd(), nix::fcntl::FcntlArg::F_GETFD) + .expect("read descriptor flags"), + ); + assert!(flags.contains(nix::fcntl::FdFlag::FD_CLOEXEC)); + } +} diff --git a/crates/openshell-supervisor-process/src/lib.rs b/crates/openshell-supervisor-process/src/lib.rs index 743942faa4..00824b1890 100644 --- a/crates/openshell-supervisor-process/src/lib.rs +++ b/crates/openshell-supervisor-process/src/lib.rs @@ -22,8 +22,11 @@ pub mod skills; pub mod ssh; pub mod supervisor_session; +mod admission_token; mod unix_socket; +pub use admission_token::{AdmissionTokenRegistry, REQUIRE_CALLER_TOKEN_ENV}; + #[cfg(target_os = "linux")] pub mod bypass_monitor; #[cfg(target_os = "linux")] diff --git a/crates/openshell-supervisor-process/src/process.rs b/crates/openshell-supervisor-process/src/process.rs index 52557493cd..0bbf59c53c 100644 --- a/crates/openshell-supervisor-process/src/process.rs +++ b/crates/openshell-supervisor-process/src/process.rs @@ -170,6 +170,7 @@ const SUPERVISOR_ONLY_ENV_VARS: &[&str] = &[ openshell_core::sandbox_env::TLS_KEY, openshell_core::sandbox_env::PROVIDER_SPIFFE_WORKLOAD_API_SOCKET, openshell_core::sandbox_env::NETWORK_RUNTIME_CAPABILITIES, + crate::REQUIRE_CALLER_TOKEN_ENV, ]; pub fn is_supervisor_only_env_var(key: &str) -> bool { diff --git a/crates/openshell-supervisor-process/src/run.rs b/crates/openshell-supervisor-process/src/run.rs index f0e792306d..c6f0c95900 100644 --- a/crates/openshell-supervisor-process/src/run.rs +++ b/crates/openshell-supervisor-process/src/run.rs @@ -78,6 +78,7 @@ pub async fn run_process( provider_env: std::collections::HashMap, ca_file_paths: Option<(std::path::PathBuf, std::path::PathBuf)>, agent_proposals: AgentProposals, + admission_tokens: Option, #[cfg(target_os = "linux")] netns: Option<&NetworkNamespace>, #[cfg(target_os = "linux")] bypass_denial_tx: Option< tokio::sync::mpsc::UnboundedSender, @@ -284,6 +285,7 @@ pub async fn run_process( let ca_paths = ca_file_paths.clone(); let provider_credentials_clone = provider_credentials.clone(); let main_session_clone = Arc::clone(&main_session); + let admission_tokens_clone = admission_tokens.clone(); let user_env_clone: std::collections::HashMap = std::env::var(openshell_core::sandbox_env::USER_ENVIRONMENT) .ok() @@ -308,6 +310,7 @@ pub async fn run_process( enforcement_mode, shared_ssh_socket, main_session_clone, + admission_tokens_clone, ) .await { diff --git a/crates/openshell-supervisor-process/src/ssh.rs b/crates/openshell-supervisor-process/src/ssh.rs index 893967b2ac..ff4363eb81 100644 --- a/crates/openshell-supervisor-process/src/ssh.rs +++ b/crates/openshell-supervisor-process/src/ssh.rs @@ -124,6 +124,7 @@ pub async fn run_ssh_server( enforcement_mode: ProcessEnforcementMode, shared_socket: bool, main_session: Arc, + admission_tokens: Option, ) -> Result<()> { let (listener, config, ca_paths) = match ssh_server_init( &listen_path, @@ -161,6 +162,7 @@ pub async fn run_ssh_server( let provider_credentials = provider_credentials.clone(); let user_environment = user_environment.clone(); let main_session = Arc::clone(&main_session); + let admission_tokens = admission_tokens.clone(); tokio::spawn(async move { if let Err(err) = handle_connection( @@ -176,6 +178,7 @@ pub async fn run_ssh_server( resolved_identity, enforcement_mode, main_session, + admission_tokens, ) .await { @@ -342,6 +345,7 @@ async fn handle_connection( resolved_identity: ResolvedProcessIdentity, enforcement_mode: ProcessEnforcementMode, main_session: Arc, + admission_tokens: Option, ) -> Result<()> { // Access is gated by the Unix-socket filesystem permissions (root-only), // not by an application-level preface. The supervisor bridges the @@ -368,6 +372,7 @@ async fn handle_connection( resolved_identity, enforcement_mode, main_session, + admission_tokens, ); russh::server::run_stream(config, stream, handler) .await @@ -454,6 +459,7 @@ struct SshHandler { resolved_identity: ResolvedProcessIdentity, enforcement_mode: ProcessEnforcementMode, main_session: Arc, + admission_tokens: Option, channels: HashMap, } @@ -483,6 +489,7 @@ impl SshHandler { resolved_identity: ResolvedProcessIdentity, enforcement_mode: ProcessEnforcementMode, main_session: Arc, + admission_tokens: Option, ) -> Self { Self { policy, @@ -495,6 +502,7 @@ impl SshHandler { resolved_identity, enforcement_mode, main_session, + admission_tokens, channels: HashMap::new(), } } @@ -815,6 +823,7 @@ impl russh::server::Handler for SshHandler { &self.user_environment, self.resolved_identity, self.enforcement_mode, + None, )?; let state = self.channels.get_mut(&channel).ok_or_else(|| { anyhow::anyhow!("subsystem_request on unknown channel {channel:?}") @@ -1032,6 +1041,7 @@ impl SshHandler { &self.user_environment, self.resolved_identity, self.enforcement_mode, + self.admission_tokens.as_ref(), )?; state.pty_master = Some(pty_master); state.input_sender = Some(InputSender::Process(input_sender)); @@ -1052,6 +1062,7 @@ impl SshHandler { &self.user_environment, self.resolved_identity, self.enforcement_mode, + self.admission_tokens.as_ref(), )?; state.input_sender = Some(InputSender::Process(input_sender)); } @@ -1195,6 +1206,7 @@ fn spawn_pty_shell( user_environment: &HashMap, resolved_identity: ResolvedProcessIdentity, enforcement_mode: ProcessEnforcementMode, + admission_tokens: Option<&crate::admission_token::AdmissionTokenRegistry>, ) -> anyhow::Result<(std::fs::File, mpsc::Sender>)> { let winsize = Winsize { ws_row: to_u16(pty.row_height.max(1)), @@ -1246,6 +1258,9 @@ fn spawn_pty_shell( user_environment, ); cmd.stdin(stdin).stdout(stdout).stderr(stderr); + let prepared_token = admission_tokens + .map(|registry| registry.prepare_child(&mut cmd)) + .transpose()?; if let Some(dir) = workspace.root() { cmd.current_dir(dir); @@ -1282,6 +1297,8 @@ fn spawn_pty_shell( let mut child = crate::process::spawn_std_command_with_supervisor_identity_namespace(cmd)?; #[cfg(not(target_os = "linux"))] let mut child = cmd.spawn()?; + let token_registration = + prepared_token.map(crate::admission_token::PreparedToken::child_spawned); #[cfg(target_os = "linux")] let child_pid = child.id(); #[cfg(target_os = "linux")] @@ -1330,6 +1347,7 @@ fn spawn_pty_shell( let runtime_exit = runtime; std::thread::spawn(move || { let status = child.wait().ok(); + drop(token_registration); #[cfg(target_os = "linux")] managed_children::unregister(child_pid); let code = status.and_then(|s| s.code()).unwrap_or(1).unsigned_abs(); @@ -1370,6 +1388,7 @@ fn spawn_pipe_exec( user_environment: &HashMap, resolved_identity: ResolvedProcessIdentity, enforcement_mode: ProcessEnforcementMode, + admission_tokens: Option<&crate::admission_token::AdmissionTokenRegistry>, ) -> anyhow::Result>> { let mut cmd = command.map_or_else( || { @@ -1405,6 +1424,9 @@ fn spawn_pipe_exec( cmd.stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); + let prepared_token = admission_tokens + .map(|registry| registry.prepare_child(&mut cmd)) + .transpose()?; if let Some(dir) = workspace.root() { cmd.current_dir(dir); @@ -1440,6 +1462,8 @@ fn spawn_pipe_exec( let mut child = crate::process::spawn_std_command_with_supervisor_identity_namespace(cmd)?; #[cfg(not(target_os = "linux"))] let mut child = cmd.spawn()?; + let token_registration = + prepared_token.map(crate::admission_token::PreparedToken::child_spawned); #[cfg(target_os = "linux")] let child_pid = child.id(); #[cfg(target_os = "linux")] @@ -1514,6 +1538,7 @@ fn spawn_pipe_exec( let runtime_exit = runtime; std::thread::spawn(move || { let status = child.wait().ok(); + drop(token_registration); #[cfg(target_os = "linux")] managed_children::unregister(child_pid); let code = status.and_then(|s| s.code()).unwrap_or(1).unsigned_abs(); @@ -2451,6 +2476,7 @@ mod tests { ResolvedProcessIdentity::default(), ProcessEnforcementMode::NetworkOnly, main_session, + None, ); let (server_stream, client_stream) = tokio::io::duplex(64 * 1024); diff --git a/docs/extensibility/supervisor-middleware.mdx b/docs/extensibility/supervisor-middleware.mdx index 9630450158..c8644cf4f9 100644 --- a/docs/extensibility/supervisor-middleware.mdx +++ b/docs/extensibility/supervisor-middleware.mdx @@ -248,6 +248,8 @@ See [Logging](/observability/logging) for log access and [OCSF JSON Export](/obs - Middleware applies only through operation bindings advertised by each implementation. For protocols that have no supported middleware operation at all, such as HTTP/2 prior knowledge or non-HTTP TCP, the existing uninspectable-traffic gate denies a host match containing `fail_closed` and relays an all-`fail_open` match with a detection finding. - The typed operation and phase pairs are `HTTP_REQUEST/PRE_CREDENTIALS`, `WEBSOCKET_MESSAGE/PRE_CREDENTIALS`, and `AGENT_CONVERSATION/AGENT_CONTEXT`. - The initial agent bridge selects one configured agent-conversation middleware and one exact provider host. That middleware may advertise multiple hook and schema bindings. Adding the first agent binding to a running sandbox requires recreating that sandbox so the loopback listener and environment variable are present. +- The bridge requires a per-exec bearer token delivered through the inherited file descriptor named by `OPENSHELL_AGENT_ADMISSION_TOKEN_FD`. The launched harness must read and close that descriptor before starting tools; the token is revoked when the exec process exits. Same-user processes able to read the harness memory remain a known limitation. +- For local debugging only, starting the supervisor with `OPENSHELL_AGENT_ADMISSION_REQUIRE_CALLER_TOKEN=false` disables bridge caller authentication. Caller tokens are required by default, and changing this startup environment requires recreating the sandbox. - A host match does not imply every advertised operation: an HTTP-only attachment can inspect the upgrade GET, then post-upgrade traffic passes with `binding_not_selected` coverage. - The V1 WebSocket binding inspects complete client text messages only. Binary messages pass with `unsupported_message_type` coverage for active stages; control frames and upstream-to-client messages remain outside the middleware operation. - Selection uses destination host include and exclude patterns. From abf1a6253009c78196d8fe4fb560590453216f04 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Wed, 2 Sep 2026 20:24:21 +0000 Subject: [PATCH 17/18] fix(sdk): round-trip credential delivery in Go Signed-off-by: Johnny Greco --- .../v1/internal/converter/coverage_test.go | 1 + .../v1/internal/converter/profile.go | 26 +++++++++++++++++++ .../v1/internal/converter/profile_test.go | 23 ++++++++++++++++ sdk/go/openshell/v1/types/profile.go | 10 +++++++ 4 files changed, 60 insertions(+) diff --git a/sdk/go/openshell/v1/internal/converter/coverage_test.go b/sdk/go/openshell/v1/internal/converter/coverage_test.go index ad20aca260..1c61806b6d 100644 --- a/sdk/go/openshell/v1/internal/converter/coverage_test.go +++ b/sdk/go/openshell/v1/internal/converter/coverage_test.go @@ -301,6 +301,7 @@ func TestConverterCoversAllProtoFields_ProviderProfileCredential(t *testing.T) { "description": true, "env_vars": true, "required": true, + "delivery": true, "auth_style": true, "header_name": true, "query_param": true, diff --git a/sdk/go/openshell/v1/internal/converter/profile.go b/sdk/go/openshell/v1/internal/converter/profile.go index 8bbb346829..be969218d0 100644 --- a/sdk/go/openshell/v1/internal/converter/profile.go +++ b/sdk/go/openshell/v1/internal/converter/profile.go @@ -79,6 +79,30 @@ func CredentialTokenGrantTypeToProto(t types.CredentialTokenGrantType) pb.Provid } } +// CredentialDeliveryFromProto converts a proto credential delivery mode to an SDK credential delivery mode. +func CredentialDeliveryFromProto(d pb.ProviderCredentialDelivery) types.CredentialDelivery { + switch d { + case pb.ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_ENVIRONMENT: + return types.CredentialDeliveryEnvironment + case pb.ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_PROXY: + return types.CredentialDeliveryProxy + default: + return types.CredentialDelivery("") + } +} + +// CredentialDeliveryToProto converts an SDK credential delivery mode to a proto credential delivery mode. +func CredentialDeliveryToProto(d types.CredentialDelivery) pb.ProviderCredentialDelivery { + switch d { + case types.CredentialDeliveryEnvironment: + return pb.ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_ENVIRONMENT + case types.CredentialDeliveryProxy: + return pb.ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_PROXY + default: + return pb.ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED + } +} + // --- NetworkEndpoint --- // NetworkEndpointFromProto converts a proto NetworkEndpoint to an SDK NetworkEndpoint. @@ -141,6 +165,7 @@ func ProfileCredentialFromProto(c *pb.ProviderProfileCredential) *types.ProfileC Description: c.GetDescription(), EnvVars: CopyStringSlice(c.GetEnvVars()), Required: c.GetRequired(), + Delivery: CredentialDeliveryFromProto(c.GetDelivery()), Secret: c.GetRefresh() != nil, Refresh: profileCredentialRefreshFromProto(c.GetRefresh()), AuthStyle: c.GetAuthStyle(), @@ -161,6 +186,7 @@ func ProfileCredentialToProto(c *types.ProfileCredential) *pb.ProviderProfileCre Description: c.Description, EnvVars: CopyStringSlice(c.EnvVars), Required: c.Required, + Delivery: CredentialDeliveryToProto(c.Delivery), AuthStyle: c.AuthStyle, HeaderName: c.HeaderName, QueryParam: c.QueryParam, diff --git a/sdk/go/openshell/v1/internal/converter/profile_test.go b/sdk/go/openshell/v1/internal/converter/profile_test.go index 5d52c0a9ee..5e9f6b9b1d 100644 --- a/sdk/go/openshell/v1/internal/converter/profile_test.go +++ b/sdk/go/openshell/v1/internal/converter/profile_test.go @@ -58,6 +58,25 @@ func TestProfileCategoryToProto(t *testing.T) { } } +func TestCredentialDeliveryRoundTrip(t *testing.T) { + tests := []struct { + proto pb.ProviderCredentialDelivery + sdk v1.CredentialDelivery + }{ + {pb.ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_ENVIRONMENT, v1.CredentialDeliveryEnvironment}, + {pb.ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_PROXY, v1.CredentialDeliveryProxy}, + } + for _, tt := range tests { + t.Run(tt.proto.String(), func(t *testing.T) { + assert.Equal(t, tt.sdk, CredentialDeliveryFromProto(tt.proto)) + assert.Equal(t, tt.proto, CredentialDeliveryToProto(tt.sdk)) + }) + } + + assert.Empty(t, CredentialDeliveryFromProto(pb.ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED)) + assert.Equal(t, pb.ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_UNSPECIFIED, CredentialDeliveryToProto(v1.CredentialDelivery("unknown"))) +} + // --- NetworkEndpoint --- func TestNetworkEndpointFromProto(t *testing.T) { @@ -142,6 +161,7 @@ func TestProfileCredentialFromProto(t *testing.T) { Description: "API key for auth", EnvVars: []string{"ANTHROPIC_API_KEY", "API_KEY"}, Required: true, + Delivery: pb.ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_PROXY, AuthStyle: "header", HeaderName: "X-API-Key", QueryParam: "api_key", @@ -176,6 +196,7 @@ func TestProfileCredentialFromProto(t *testing.T) { assert.Equal(t, "API key for auth", cred.Description) assert.Equal(t, []string{"ANTHROPIC_API_KEY", "API_KEY"}, cred.EnvVars) assert.True(t, cred.Required) + assert.Equal(t, v1.CredentialDeliveryProxy, cred.Delivery) assert.True(t, cred.Secret, "credential with refresh config is secret") assert.Equal(t, "header", cred.AuthStyle) assert.Equal(t, "X-API-Key", cred.HeaderName) @@ -253,6 +274,7 @@ func TestProfileCredentialToProto(t *testing.T) { Description: "API key", EnvVars: []string{"ANTHROPIC_API_KEY"}, Required: true, + Delivery: v1.CredentialDeliveryProxy, Secret: true, Refresh: &v1.ProfileCredentialRefresh{ Strategy: v1.RefreshStrategyOAuth2RefreshToken, @@ -294,6 +316,7 @@ func TestProfileCredentialToProto(t *testing.T) { assert.Equal(t, "API key", proto.Description) assert.Equal(t, []string{"ANTHROPIC_API_KEY"}, proto.EnvVars) assert.True(t, proto.Required) + assert.Equal(t, pb.ProviderCredentialDelivery_PROVIDER_CREDENTIAL_DELIVERY_PROXY, proto.Delivery) assert.Equal(t, "header", proto.AuthStyle) assert.Equal(t, "X-API-Key", proto.HeaderName) assert.Equal(t, "api_key", proto.QueryParam) diff --git a/sdk/go/openshell/v1/types/profile.go b/sdk/go/openshell/v1/types/profile.go index e519df634d..898205a214 100644 --- a/sdk/go/openshell/v1/types/profile.go +++ b/sdk/go/openshell/v1/types/profile.go @@ -35,12 +35,22 @@ type ProviderProfile struct { Scope string } +// CredentialDelivery controls how a static credential reaches the workload. +type CredentialDelivery string + +// CredentialDelivery values. +const ( + CredentialDeliveryEnvironment CredentialDelivery = "Environment" + CredentialDeliveryProxy CredentialDelivery = "Proxy" +) + // ProfileCredential defines a single credential required by a provider profile. type ProfileCredential struct { Name string Description string EnvVars []string Required bool + Delivery CredentialDelivery Secret bool Refresh *ProfileCredentialRefresh AuthStyle string From 4d7194dc8166bfafc7236e0212d2e88aee4f7231 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Wed, 2 Sep 2026 18:37:53 -0400 Subject: [PATCH 18/18] fix(providers): inject credentials in route-selected relays --- .../src/l7/relay.rs | 138 +++++++++++++++++- 1 file changed, 134 insertions(+), 4 deletions(-) diff --git a/crates/openshell-supervisor-network/src/l7/relay.rs b/crates/openshell-supervisor-network/src/l7/relay.rs index 22dd183962..2605565cc0 100644 --- a/crates/openshell-supervisor-network/src/l7/relay.rs +++ b/crates/openshell-supervisor-network/src/l7/relay.rs @@ -791,8 +791,6 @@ where return Ok(()); } }; - let scoped_ctx = scoped_context_for_request(ctx, &req); - let ctx = scoped_ctx.as_ref().unwrap_or(ctx); let mut middleware_session = if let Some(chain) = websocket_chain.as_deref() { let preflight = websocket_middleware_preflight( &req, @@ -826,8 +824,39 @@ where } else { None }; + let req_with_auth = + match crate::l7::token_grant_injection::inject_if_needed(req, ctx).await { + Ok(req) => req, + Err(error) => { + warn!( + host = %ctx.host, + port = ctx.port, + error = %error, + "Token grant failed in route-selected L7 relay" + ); + write_bad_gateway_response(client).await?; + return Ok(()); + } + }; + let scoped_ctx = scoped_context_for_request(ctx, &req_with_auth); + let ctx = scoped_ctx.as_ref().unwrap_or(ctx); + let req_with_auth = + match crate::l7::token_grant_injection::inject_static_if_needed(req_with_auth, ctx) + { + Ok(req) => req, + Err(error) => { + warn!( + host = %ctx.host, + port = ctx.port, + error = %error, + "Static provider credential injection failed in route-selected L7 relay" + ); + write_bad_gateway_response(client).await?; + return Ok(()); + } + }; let outcome_result = relay_http_request_with_credential_rejection( - &req, + &req_with_auth, client, upstream, crate::l7::rest::RelayRequestOptions { @@ -898,7 +927,7 @@ where ctx, websocket_request, &redacted_target, - &req.query_params, + &req_with_auth.query_params, Some(&engine), ); options.websocket.permessage_deflate = websocket_permessage_deflate; @@ -7963,6 +7992,107 @@ network_policies: let _ = tokio::time::timeout(std::time::Duration::from_secs(1), relay).await; } + #[tokio::test] + async fn route_selected_relay_injects_proxy_delivered_credential() { + let data = r#" +network_policies: + route_api: + name: route_api + endpoints: + - host: api.example.test + port: 443 + path: /v1/** + protocol: rest + enforcement: enforce + rules: + - allow: + method: POST + path: "/v1/**" + - host: api.example.test + port: 443 + path: /v2/** + protocol: rest + enforcement: enforce + rules: + - allow: + method: POST + path: "/v2/**" + binaries: + - { path: /usr/bin/node } +"#; + let engine = OpaEngine::from_strings(TEST_POLICY, data).unwrap(); + let tunnel_engine = engine + .clone_engine_for_tunnel(engine.current_generation()) + .unwrap(); + let rest = |path: &str| L7EndpointConfig { + protocol: L7Protocol::Rest, + path: path.into(), + tls: crate::l7::TlsMode::Auto, + enforcement: EnforcementMode::Enforce, + graphql_max_body_bytes: 0, + json_rpc_max_body_bytes: crate::l7::jsonrpc::DEFAULT_MAX_BODY_BYTES, + mcp_strict_tool_names: true, + allow_encoded_slash: false, + websocket_credential_rewrite: false, + request_body_credential_rewrite: false, + allow_uninspected_credentials: false, + provider_credentialed: false, + websocket_graphql_policy: false, + credential_signing: crate::l7::CredentialSigning::None, + signing_service: String::new(), + signing_region: String::new(), + }; + let configs = vec![rest("/v1/**"), rest("/v2/**")]; + let ctx = L7EvalContext { + host: "api.example.test".into(), + port: 443, + request_default_port: Some(443), + policy_name: "route_api".into(), + binary_path: "/usr/bin/node".into(), + provider_credentials: Some(proxy_delivered_state(443)), + ..Default::default() + }; + + let (mut app, mut relay_client) = tokio::io::duplex(8192); + let (mut relay_upstream, mut upstream) = tokio::io::duplex(8192); + let relay = tokio::spawn(async move { + relay_with_route_selection( + &configs, + tunnel_engine, + &mut relay_client, + &mut relay_upstream, + &ctx, + ) + .await + }); + + app.write_all( + b"POST /v1/chat HTTP/1.1\r\nHost: api.example.test\r\nAuthorization: Bearer public-value\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + ) + .await + .unwrap(); + let forwarded = read_http_head(&mut upstream, "route-selected request").await; + assert!( + forwarded.contains("Authorization: Bearer real-secret\r\n"), + "{forwarded}" + ); + assert!(!forwarded.contains("public-value"), "{forwarded}"); + assert_eq!(authorization_header_count(&forwarded), 1, "{forwarded}"); + upstream + .write_all(b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") + .await + .unwrap(); + let response = read_http_head(&mut app, "route-selected response").await; + assert!(response.contains("204 No Content"), "{response}"); + + drop(app); + tokio::time::timeout(std::time::Duration::from_secs(1), relay) + .await + .expect("relay should finish") + .unwrap() + .unwrap(); + } + #[tokio::test] async fn rest_websocket_middleware_inspects_compressed_wss_messages() { let data = r#"