From db5a3bbd3457bb93590c4e4ae23748fbac86f8e8 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Mon, 31 Aug 2026 15:16:17 -0400 Subject: [PATCH 1/4] 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 4dc92be4648cd4db96cbf8e94660e64f4902ad6e Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Wed, 2 Sep 2026 17:20:40 +0000 Subject: [PATCH 2/4] 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 bd5d278fdfc5d8c0c43c1759916868eb61be8f12 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Wed, 2 Sep 2026 18:23:51 +0000 Subject: [PATCH 3/4] 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 9ed1506ad05fc5adc5b387cc5f3e55a348fd63c6 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Wed, 2 Sep 2026 18:37:53 -0400 Subject: [PATCH 4/4] 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 b2e5fbdd88..6eda8148d3 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; @@ -7949,6 +7978,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#"