From 5f8636d70476cbc7b1054543122d407b24594969 Mon Sep 17 00:00:00 2001 From: Mrunal Patel Date: Fri, 21 Aug 2026 14:40:55 -0700 Subject: [PATCH 1/5] fix(providers): classify OAuth refresh failures Signed-off-by: Mrunal Patel --- .agents/skills/openshell-cli/SKILL.md | 10 + architecture/gateway.md | 8 + crates/openshell-cli/src/run.rs | 55 +- .../tests/provider_commands_integration.rs | 4 + .../openshell-server/src/provider_refresh.rs | 545 ++++++- docs/sandboxes/providers-v2.mdx | 22 + proto/openshell.proto | 26 + sdk/go/proto/openshellv1/openshell.pb.go | 1253 +++++++++-------- 8 files changed, 1303 insertions(+), 620 deletions(-) diff --git a/.agents/skills/openshell-cli/SKILL.md b/.agents/skills/openshell-cli/SKILL.md index 69a889a8e2..833fc44d0e 100644 --- a/.agents/skills/openshell-cli/SKILL.md +++ b/.agents/skills/openshell-cli/SKILL.md @@ -207,6 +207,16 @@ output. Use `provider refresh rotate`, reconfigure refresh, or delete refresh before returning those keys to manual management. Unrelated provider fields remain updateable. +When OAuth refresh fails, inspect the `RECOVERY` and `FAILURE_CODE` columns from +`provider refresh status`; do not infer the remedy from HTTP status or parse +`LAST_ERROR`. `retry` means the worker will try again, `reauthorize` means the +user must obtain a new OAuth grant and run `provider refresh configure`, +`fix_configuration` means an operator must repair the OAuth client, scopes, or +administrator policy, and `investigate` means the issuer returned an +unrecognized response. The gateway parks `reauthorize` and +`fix_configuration` records until a manual rotate or reconfiguration. The +existing access credential remains usable only until its recorded expiry. + --- ## Workflow 3: Sandbox Lifecycle diff --git a/architecture/gateway.md b/architecture/gateway.md index 32bca6a1f6..0a4b49e9e5 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -343,6 +343,14 @@ Multi-replica deployments can use that default with a shared database and shared key-encryption key, or opt into an external backend such as Vault or Kubernetes Secrets. +OAuth refresh failures retain a gateway-owned recovery classification alongside +the refresh state. The gateway reads only a bounded error response and maps +recognized OAuth codes to retry, reauthorization, configuration repair, or +investigation without persisting issuer-controlled descriptions. Terminal +reauthorization and configuration failures remain parked until a manual retry +or explicit refresh reconfiguration; short-lived credentials still fail closed +at their recorded expiry. + Credential handles remain bound to the driver that created them. Before the 0.1.0 compatibility boundary, gateways do not migrate inline refresh material or move handles between credential drivers; operators reconfigure affected diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index af8b7d5fd2..fd1096980a 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -46,7 +46,8 @@ use openshell_core::proto::{ GetSandboxRequest, GetServiceRequest, GpuResourceRequirements, ImportProviderProfilesRequest, LintProviderProfilesRequest, ListProviderProfilesRequest, ListProvidersRequest, ListSandboxPoliciesRequest, ListSandboxProvidersRequest, ListSandboxesRequest, - ListServicesRequest, PolicySource, PolicyStatus, Provider, ProviderCredentialRefreshStatus, + ListServicesRequest, PolicySource, PolicyStatus, Provider, + ProviderCredentialRefreshRecoveryAction, ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrantType, ProviderProfile, ProviderProfileDiagnostic, ProviderProfileImportItem, RejectDraftChunkRequest, ResourceRequirements, RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox, @@ -4338,14 +4339,16 @@ pub async fn provider_refresh_status( fn refresh_status_header() -> String { format!( - "{:<24} {:<28} {:<28} {:<18} {:<20} {:<20} {:<20} {}", + "{:<24} {:<28} {:<28} {:<24} {:<18} {:<20} {:<20} {:<20} {:<34} {}", "PROVIDER".bold(), "CREDENTIAL_KEY".bold(), "STRATEGY".bold(), "STATUS".bold(), + "RECOVERY".bold(), "EXPIRES_AT".bold(), "NEXT_REFRESH".bold(), "LAST_REFRESH".bold(), + "FAILURE_CODE".bold(), "LAST_ERROR".bold(), ) } @@ -4497,19 +4500,43 @@ fn print_refresh_status_row(status: &ProviderCredentialRefreshStatus) { fn refresh_status_row(status: &ProviderCredentialRefreshStatus) -> String { let strategy = ProviderCredentialRefreshStrategy::try_from(status.strategy) .unwrap_or(ProviderCredentialRefreshStrategy::Unspecified); + let recovery_action = ProviderCredentialRefreshRecoveryAction::try_from(status.recovery_action) + .unwrap_or(ProviderCredentialRefreshRecoveryAction::Unspecified); format!( - "{:<24} {:<28} {:<28} {:<18} {:<20} {:<20} {:<20} {}", + "{:<24} {:<28} {:<28} {:<24} {:<18} {:<20} {:<20} {:<20} {:<34} {}", status.provider_name, status.credential_key, provider_refresh_strategy_name(strategy), status.status, + provider_refresh_recovery_action_name(recovery_action), format_optional_epoch_ms(status.expires_at_ms), - format_optional_epoch_ms(status.next_refresh_at_ms), + format_refresh_next_at_ms(status.next_refresh_at_ms), format_optional_epoch_ms(status.last_refresh_at_ms), + status.failure_code, truncate_status_field(&status.last_error, 72), ) } +fn format_refresh_next_at_ms(next_refresh_at_ms: i64) -> String { + if next_refresh_at_ms == i64::MAX { + "-".to_string() + } else { + format_optional_epoch_ms(next_refresh_at_ms) + } +} + +fn provider_refresh_recovery_action_name( + action: ProviderCredentialRefreshRecoveryAction, +) -> &'static str { + match action { + ProviderCredentialRefreshRecoveryAction::Retry => "retry", + ProviderCredentialRefreshRecoveryAction::Reauthorize => "reauthorize", + ProviderCredentialRefreshRecoveryAction::FixConfiguration => "fix_configuration", + ProviderCredentialRefreshRecoveryAction::Investigate => "investigate", + ProviderCredentialRefreshRecoveryAction::Unspecified => "-", + } +} + fn provider_refresh_strategy_name(strategy: ProviderCredentialRefreshStrategy) -> &'static str { match strategy { ProviderCredentialRefreshStrategy::Static => "static", @@ -7403,10 +7430,11 @@ mod tests { }; use openshell_core::proto::{ GetSandboxConfigResponse, GpuResourceRequirements, PolicySource, PolicyStatus, Provider, - ProviderCredentialRefresh, ProviderCredentialRefreshStatus, - ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrant, ProviderProfile, - ProviderProfileCredential, ResourceRequirements, Sandbox, SandboxCondition, SandboxPhase, - SandboxPolicyRevision, SandboxStatus, datamodel::v1::ObjectMeta, + ProviderCredentialRefresh, ProviderCredentialRefreshRecoveryAction, + ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy, + ProviderCredentialTokenGrant, ProviderProfile, ProviderProfileCredential, + ResourceRequirements, Sandbox, SandboxCondition, SandboxPhase, SandboxPolicyRevision, + SandboxStatus, datamodel::v1::ObjectMeta, }; #[test] @@ -7645,6 +7673,8 @@ mod tests { let header = refresh_status_header(); assert!(header.contains("NEXT_REFRESH")); assert!(header.contains("LAST_REFRESH")); + assert!(header.contains("RECOVERY")); + assert!(header.contains("FAILURE_CODE")); assert!(header.contains("LAST_ERROR")); let row = refresh_status_row(&ProviderCredentialRefreshStatus { @@ -7654,17 +7684,24 @@ mod tests { strategy: ProviderCredentialRefreshStrategy::Oauth2ClientCredentials as i32, status: "error".to_string(), expires_at_ms: 1_767_225_600_000, - next_refresh_at_ms: 1_767_225_660_000, + next_refresh_at_ms: i64::MAX, last_refresh_at_ms: 1_767_225_000_000, last_error: "token endpoint returned a very long error message that should be truncated for table readability" .to_string(), + recovery_action: ProviderCredentialRefreshRecoveryAction::Reauthorize as i32, + failure_code: "oauth_invalid_grant".to_string(), + provider_error_subtype: "invalid_rapt".to_string(), + last_error_at_ms: 1_767_225_000_000, }); assert!(row.contains("my-graph")); assert!(row.contains("MS_GRAPH_ACCESS_TOKEN")); assert!(row.contains("oauth2_client_credentials")); assert!(row.contains("error")); + assert!(row.contains("reauthorize")); + assert!(row.contains("oauth_invalid_grant")); assert!(row.contains("2026-01-01 00:00:00")); + assert!(!row.contains("292278994")); assert!(row.contains("...")); } diff --git a/crates/openshell-cli/tests/provider_commands_integration.rs b/crates/openshell-cli/tests/provider_commands_integration.rs index c349bbdbb4..2c71e0b39f 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -750,6 +750,10 @@ impl OpenShell for TestOpenShell { next_refresh_at_ms: 0, last_refresh_at_ms: 0, last_error: String::new(), + recovery_action: 0, + failure_code: String::new(), + provider_error_subtype: String::new(), + last_error_at_ms: 0, }; drop(providers); self.state diff --git a/crates/openshell-server/src/provider_refresh.rs b/crates/openshell-server/src/provider_refresh.rs index f562e1d18e..b4b17e5543 100644 --- a/crates/openshell-server/src/provider_refresh.rs +++ b/crates/openshell-server/src/provider_refresh.rs @@ -9,7 +9,8 @@ use crate::credentials::RefreshMaterialScope; use crate::persistence::{ObjectType, PersistenceError, Store, WriteCondition, current_time_ms}; use openshell_core::ObjectWorkspace; use openshell_core::proto::{ - CredentialHandle, Provider, ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy, + CredentialHandle, Provider, ProviderCredentialRefreshRecoveryAction, + ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy, StoredProviderCredentialRefreshState, StoredRefreshMaterialDeletion, }; use openshell_core::{ObjectId, ObjectName, SetResourceVersion}; @@ -24,6 +25,7 @@ const DEFAULT_REFRESH_BEFORE_SECONDS: i64 = 300; const DEFAULT_MAX_LIFETIME_SECONDS: i64 = 3600; const REFRESH_ERROR_RETRY_SECONDS: i64 = 60; const REFRESH_WORKER_PAGE_SIZE: u32 = 1000; +const MAX_OAUTH_ERROR_RESPONSE_BYTES: usize = 8 * 1024; pub fn refresh_material_scope( state: &StoredProviderCredentialRefreshState, @@ -321,6 +323,10 @@ pub fn refresh_status_from_state( next_refresh_at_ms: state.next_refresh_at_ms, last_refresh_at_ms: state.last_refresh_at_ms, last_error: state.last_error.clone(), + recovery_action: state.recovery_action, + failure_code: state.failure_code.clone(), + provider_error_subtype: state.provider_error_subtype.clone(), + last_error_at_ms: state.last_error_at_ms, } } @@ -385,6 +391,10 @@ pub fn new_refresh_state( authorization_epoch: uuid::Uuid::new_v4().to_string(), secret_material_handles: HashMap::new(), pending_secret_deletions: Vec::new(), + recovery_action: ProviderCredentialRefreshRecoveryAction::Unspecified as i32, + failure_code: String::new(), + provider_error_subtype: String::new(), + last_error_at_ms: 0, }) } @@ -403,6 +413,83 @@ struct TokenResponse { refresh_token: Option, } +#[derive(Debug, Deserialize)] +struct OAuthErrorResponse { + error: String, + error_subtype: Option, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum OAuthGrantKind { + UserRefreshToken, + NonInteractive, +} + +#[derive(Debug)] +struct RefreshFailure { + status: Status, + recovery_action: ProviderCredentialRefreshRecoveryAction, + failure_code: &'static str, + provider_error_subtype: Option<&'static str>, + retry_automatically: bool, +} + +impl RefreshFailure { + fn retryable(status: Status, failure_code: &'static str) -> Self { + Self { + status, + recovery_action: ProviderCredentialRefreshRecoveryAction::Retry, + failure_code, + provider_error_subtype: None, + retry_automatically: true, + } + } + + fn investigate(status: Status, failure_code: &'static str) -> Self { + Self { + status, + recovery_action: ProviderCredentialRefreshRecoveryAction::Investigate, + failure_code, + provider_error_subtype: None, + retry_automatically: true, + } + } + + fn reauthorize( + status: Status, + failure_code: &'static str, + provider_error_subtype: Option<&'static str>, + ) -> Self { + Self { + status, + recovery_action: ProviderCredentialRefreshRecoveryAction::Reauthorize, + failure_code, + provider_error_subtype, + retry_automatically: false, + } + } + + fn fix_configuration(status: Status, failure_code: &'static str) -> Self { + Self { + status, + recovery_action: ProviderCredentialRefreshRecoveryAction::FixConfiguration, + failure_code, + provider_error_subtype: None, + retry_automatically: false, + } + } + + fn into_status(self) -> Status { + self.status + } +} + +impl From for RefreshFailure { + fn from(status: Status) -> Self { + Self::retryable(status, "refresh_failed") + } +} + #[derive(Debug, Serialize)] struct GoogleServiceAccountClaims<'a> { iss: &'a str, @@ -559,12 +646,42 @@ async fn persist_refresh_error_state( state: &mut StoredProviderCredentialRefreshState, expected_version: u64, error: &Status, +) -> Result { + let failure = RefreshFailure::retryable( + Status::new(error.code(), error.message().to_string()), + "refresh_failed", + ); + persist_refresh_failure_state(store, state, expected_version, &failure).await +} + +async fn persist_refresh_failure_state( + store: &Store, + state: &mut StoredProviderCredentialRefreshState, + expected_version: u64, + failure: &RefreshFailure, ) -> Result { let now_ms = current_time_ms(); - state.status = "error".to_string(); - state.last_error = error.message().to_string(); - state.next_refresh_at_ms = - now_ms.saturating_add(REFRESH_ERROR_RETRY_SECONDS.saturating_mul(1000)); + state.status = match failure.recovery_action { + ProviderCredentialRefreshRecoveryAction::Retry + | ProviderCredentialRefreshRecoveryAction::Unspecified => "error", + ProviderCredentialRefreshRecoveryAction::Reauthorize => "reauthorization_required", + ProviderCredentialRefreshRecoveryAction::FixConfiguration => "configuration_required", + ProviderCredentialRefreshRecoveryAction::Investigate => "investigation_required", + } + .to_string(); + state.last_error = failure.status.message().to_string(); + state.recovery_action = failure.recovery_action as i32; + state.failure_code = failure.failure_code.to_string(); + state.provider_error_subtype = failure + .provider_error_subtype + .unwrap_or_default() + .to_string(); + state.last_error_at_ms = now_ms; + state.next_refresh_at_ms = if failure.retry_automatically { + now_ms.saturating_add(REFRESH_ERROR_RETRY_SECONDS.saturating_mul(1000)) + } else { + i64::MAX + }; let new_version = persist_refresh_state_if_current(store, state, expected_version) .await? .ok_or_else(|| { @@ -684,7 +801,7 @@ pub async fn refresh_provider_credential( let mint_result = match resolve_refresh_material(Some(credentials), &state).await { Ok(transient_state) => mint_credential(&transient_state).await, - Err(err) => Err(err), + Err(err) => Err(err.into()), }; match mint_result { Ok(minted) => { @@ -716,19 +833,32 @@ pub async fn refresh_provider_credential( { Ok(handles) => handles, Err(store_err) => { - let err = Status::failed_precondition(format!( - "the OAuth provider rotated the refresh token, but the replacement could not be stored; the grant must be re-authorized: {}", - store_err.message() - )); - persist_refresh_error_state(store, &mut state, expected_version, &err) - .await?; - return Err(err); + let failure = RefreshFailure::reauthorize( + Status::failed_precondition(format!( + "the OAuth provider rotated the refresh token, but the replacement could not be stored; the grant must be re-authorized: {}", + store_err.message() + )), + "oauth_rotated_refresh_token_store_failed", + None, + ); + persist_refresh_failure_state( + store, + &mut state, + expected_version, + &failure, + ) + .await?; + return Err(failure.into_status()); } }; let Some(handle) = staged_refresh_token_handles.get("refresh_token").cloned() else { - let err = Status::failed_precondition( - "the OAuth provider rotated the refresh token, but the credential driver returned no replacement handle; the grant must be re-authorized", + let failure = RefreshFailure::reauthorize( + Status::failed_precondition( + "the OAuth provider rotated the refresh token, but the credential driver returned no replacement handle; the grant must be re-authorized", + ), + "oauth_rotated_refresh_token_handle_missing", + None, ); cleanup_staged_refresh_material_handles( credentials, @@ -736,8 +866,9 @@ pub async fn refresh_provider_credential( &staged_refresh_token_handles, ) .await; - persist_refresh_error_state(store, &mut state, expected_version, &err).await?; - return Err(err); + persist_refresh_failure_state(store, &mut state, expected_version, &failure) + .await?; + return Err(failure.into_status()); }; if let Some(previous) = state .secret_material_handles @@ -757,6 +888,10 @@ pub async fn refresh_provider_credential( state.last_refresh_at_ms = now_ms; state.status = "refreshed".to_string(); state.last_error.clear(); + state.recovery_action = ProviderCredentialRefreshRecoveryAction::Unspecified as i32; + state.failure_code.clear(); + state.provider_error_subtype.clear(); + state.last_error_at_ms = 0; // Claim the refresh generation with a version-matched write BEFORE // touching the provider. It succeeds only if the refresh still holds @@ -860,9 +995,9 @@ pub async fn refresh_provider_credential( } Ok(state) } - Err(err) => { + Err(failure) => { let now_ms = current_time_ms(); - persist_refresh_error_state(store, &mut state, expected_version, &err).await?; + persist_refresh_failure_state(store, &mut state, expected_version, &failure).await?; warn!( provider = %state.provider_name, credential_key = %state.credential_key, @@ -870,10 +1005,12 @@ pub async fn refresh_provider_credential( status = %state.status, next_refresh_at_ms = state.next_refresh_at_ms, seconds_until_refresh = seconds_until_ms(now_ms, state.next_refresh_at_ms), - error = %err, + recovery_action = ?failure.recovery_action, + failure_code = failure.failure_code, + error = %failure.status, "provider credential refresh errored" ); - Err(err) + Err(failure.into_status()) } } } @@ -1106,7 +1243,7 @@ async fn ensure_refresh_providers_v2_gate( async fn mint_credential( state: &StoredProviderCredentialRefreshState, -) -> Result { +) -> Result { let strategy = ProviderCredentialRefreshStrategy::try_from(state.strategy) .unwrap_or(ProviderCredentialRefreshStrategy::Unspecified); match strategy { @@ -1126,13 +1263,14 @@ async fn mint_credential( | ProviderCredentialRefreshStrategy::Static | ProviderCredentialRefreshStrategy::Unspecified => Err(Status::failed_precondition( format!("refresh strategy '{strategy:?}' cannot be minted by the gateway"), - )), + ) + .into()), } } async fn mint_oauth2_refresh_token( state: &StoredProviderCredentialRefreshState, -) -> Result { +) -> Result { let token_url = oauth2_token_url(state)?; let client_id = required_material(&state.material, "client_id")?; let refresh_token = required_material(&state.material, "refresh_token")?; @@ -1149,12 +1287,18 @@ async fn mint_oauth2_refresh_token( form.push(("scope".to_string(), scope)); } - request_token(&token_url, &form, state.max_lifetime_seconds).await + request_token( + &token_url, + &form, + state.max_lifetime_seconds, + OAuthGrantKind::UserRefreshToken, + ) + .await } async fn mint_oauth2_client_credentials( state: &StoredProviderCredentialRefreshState, -) -> Result { +) -> Result { let token_url = oauth2_token_url(state)?; let client_id = required_material(&state.material, "client_id")?; let client_secret = required_material(&state.material, "client_secret")?; @@ -1168,14 +1312,19 @@ async fn mint_oauth2_client_credentials( form.push(("scope".to_string(), scope)); } - request_token(&token_url, &form, state.max_lifetime_seconds).await + request_token( + &token_url, + &form, + state.max_lifetime_seconds, + OAuthGrantKind::NonInteractive, + ) + .await } async fn mint_google_service_account_jwt( state: &StoredProviderCredentialRefreshState, -) -> Result { +) -> Result { crate::install_jsonwebtoken_crypto_provider(); - let token_url = google_token_url(state); let client_email = required_material(&state.material, "client_email")?; let private_key = required_material(&state.material, "private_key")?; @@ -1183,7 +1332,8 @@ async fn mint_google_service_account_jwt( if scopes.is_empty() { return Err(Status::invalid_argument( "google_service_account_jwt requires at least one scope", - )); + ) + .into()); } let now_ms = current_time_ms(); let now_secs = now_ms / 1000; @@ -1216,12 +1366,18 @@ async fn mint_google_service_account_jwt( ), ("assertion".to_string(), assertion), ]; - request_token(&token_url, &form, lifetime_secs).await + request_token( + &token_url, + &form, + lifetime_secs, + OAuthGrantKind::NonInteractive, + ) + .await } async fn mint_aws_sts_assume_role( state: &StoredProviderCredentialRefreshState, -) -> Result { +) -> Result { let role_arn = required_material(&state.material, "role_arn")?; let session_name = material_value(&state.material, &["session_name"]) .unwrap_or_else(|| "openshell-sandbox".to_string()); @@ -1256,13 +1412,15 @@ async fn mint_aws_sts_assume_role( (None, None) if session_token.is_some() => { return Err(Status::invalid_argument( "aws_session_token requires aws_access_key_id and aws_secret_access_key", - )); + ) + .into()); } (None, None) => {} _ => { return Err(Status::invalid_argument( "aws_access_key_id and aws_secret_access_key must both be set or both omitted", - )); + ) + .into()); } } @@ -1346,7 +1504,8 @@ async fn request_token( token_url: &str, form: &[(String, String)], max_lifetime_seconds: i64, -) -> Result { + grant_kind: OAuthGrantKind, +) -> Result { let parsed = reqwest::Url::parse(token_url) .map_err(|_| Status::invalid_argument("token_url must be an absolute URL"))?; match parsed.scheme() { @@ -1355,7 +1514,8 @@ async fn request_token( _ => { return Err(Status::invalid_argument( "token_url must use https, except loopback http for local tests", - )); + ) + .into()); } } @@ -1363,25 +1523,27 @@ async fn request_token( .timeout(Duration::from_secs(30)) .build() .map_err(|e| Status::internal(format!("build refresh HTTP client failed: {e}")))?; - let response = client - .post(parsed) - .form(form) - .send() - .await - .map_err(|e| Status::unavailable(format!("token endpoint request failed: {e}")))?; + let response = client.post(parsed).form(form).send().await.map_err(|_| { + RefreshFailure::retryable( + Status::unavailable("token endpoint request failed"), + "oauth_token_endpoint_unavailable", + ) + })?; let status = response.status(); if !status.is_success() { - return Err(Status::failed_precondition(format!( - "token endpoint returned HTTP {status}" - ))); + let body = read_bounded_oauth_error_body(response).await; + return Err(classify_oauth_token_error(status, &body, grant_kind)); } - let token = response - .json::() - .await - .map_err(|_| Status::failed_precondition("token endpoint returned invalid JSON"))?; + let token = response.json::().await.map_err(|_| { + RefreshFailure::investigate( + Status::failed_precondition("token endpoint returned invalid JSON"), + "oauth_invalid_success_response", + ) + })?; if token.access_token.trim().is_empty() { - return Err(Status::failed_precondition( - "token endpoint returned empty access_token", + return Err(RefreshFailure::investigate( + Status::failed_precondition("token endpoint returned empty access_token"), + "oauth_empty_access_token", )); } let now_ms = current_time_ms(); @@ -1405,6 +1567,109 @@ async fn request_token( }) } +async fn read_bounded_oauth_error_body(mut response: reqwest::Response) -> Vec { + let mut body = Vec::new(); + while body.len() < MAX_OAUTH_ERROR_RESPONSE_BYTES { + let Ok(Some(chunk)) = response.chunk().await else { + break; + }; + let remaining = MAX_OAUTH_ERROR_RESPONSE_BYTES.saturating_sub(body.len()); + body.extend_from_slice(&chunk[..chunk.len().min(remaining)]); + if chunk.len() >= remaining { + break; + } + } + body +} + +fn classify_oauth_token_error( + status: reqwest::StatusCode, + body: &[u8], + grant_kind: OAuthGrantKind, +) -> RefreshFailure { + if status == reqwest::StatusCode::TOO_MANY_REQUESTS || status.is_server_error() { + return RefreshFailure::retryable( + Status::unavailable(format!("token endpoint returned HTTP {status}")), + "oauth_token_endpoint_retryable", + ); + } + + let Ok(error_response) = serde_json::from_slice::(body) else { + return RefreshFailure::investigate( + Status::failed_precondition(format!( + "token endpoint returned HTTP {status} without a recognized OAuth error" + )), + "oauth_unrecognized_error_response", + ); + }; + + match error_response.error.as_str() { + "invalid_grant" if grant_kind == OAuthGrantKind::UserRefreshToken => { + let subtype = error_response + .error_subtype + .as_deref() + .filter(|subtype| *subtype == "invalid_rapt") + .map(|_| "invalid_rapt"); + let message = if subtype.is_some() { + "OAuth refresh grant requires interactive reauthorization (invalid_grant/invalid_rapt)" + } else { + "OAuth refresh grant is no longer usable (invalid_grant); user reauthorization is required" + }; + RefreshFailure::reauthorize( + Status::failed_precondition(message), + "oauth_invalid_grant", + subtype, + ) + } + "invalid_client" => RefreshFailure::fix_configuration( + Status::failed_precondition( + "OAuth token endpoint rejected the client configuration (invalid_client)", + ), + "oauth_invalid_client", + ), + "unauthorized_client" => RefreshFailure::fix_configuration( + Status::failed_precondition( + "OAuth token endpoint rejected the client grant (unauthorized_client)", + ), + "oauth_unauthorized_client", + ), + "invalid_scope" => RefreshFailure::fix_configuration( + Status::failed_precondition( + "OAuth token endpoint rejected the configured scopes (invalid_scope)", + ), + "oauth_invalid_scope", + ), + "unsupported_grant_type" => RefreshFailure::fix_configuration( + Status::failed_precondition( + "OAuth token endpoint rejected the configured grant type (unsupported_grant_type)", + ), + "oauth_unsupported_grant_type", + ), + "admin_policy_enforced" => RefreshFailure::fix_configuration( + Status::failed_precondition( + "OAuth access is blocked by an administrator policy (admin_policy_enforced)", + ), + "oauth_admin_policy_enforced", + ), + "invalid_grant" => RefreshFailure::fix_configuration( + Status::failed_precondition( + "OAuth token endpoint rejected the non-interactive grant (invalid_grant)", + ), + "oauth_invalid_grant", + ), + "server_error" | "temporarily_unavailable" => RefreshFailure::retryable( + Status::unavailable("OAuth token endpoint reported a temporary failure"), + "oauth_token_endpoint_retryable", + ), + _ => RefreshFailure::investigate( + Status::failed_precondition(format!( + "token endpoint returned HTTP {status} with an unrecognized OAuth error" + )), + "oauth_unrecognized_error", + ), + } +} + pub fn refresh_scopes(state: &StoredProviderCredentialRefreshState) -> Vec { if !state.scopes.is_empty() { return state.scopes.clone(); @@ -1677,10 +1942,11 @@ async fn run_refresh_worker_tick( #[cfg(test)] mod tests { use super::{ - NewRefreshStateConfig, delete_refresh_state_with_credentials, - effective_authorization_epoch, enqueue_pending_secret_deletion, get_refresh_state, - list_all_refresh_states, list_refresh_states_for_provider, new_refresh_state, - put_refresh_state, refresh_material_scope, refresh_provider_credential, refresh_state_name, + NewRefreshStateConfig, OAuthGrantKind, classify_oauth_token_error, + delete_refresh_state_with_credentials, effective_authorization_epoch, + enqueue_pending_secret_deletion, get_refresh_state, list_all_refresh_states, + list_refresh_states_for_provider, new_refresh_state, put_refresh_state, + refresh_material_scope, refresh_provider_credential, refresh_state_name, refresh_strategy_name, run_refresh_worker_tick, seconds_until_ms, validate_secret_material_references, }; @@ -1689,7 +1955,8 @@ mod tests { use openshell_core::Config; use openshell_core::proto::datamodel::v1::ObjectMeta; use openshell_core::proto::{ - CredentialHandle, Provider, ProviderCredentialRefreshStrategy, Sandbox, SandboxSpec, + CredentialHandle, Provider, ProviderCredentialRefreshRecoveryAction, + ProviderCredentialRefreshStrategy, Sandbox, SandboxSpec, StoredProviderCredentialRefreshState, }; use openshell_core::{ObjectId, ObjectName, ObjectWorkspace}; @@ -1862,6 +2129,159 @@ mod tests { validate_secret_material_references(&state).unwrap(); } + #[test] + fn oauth_invalid_grant_requires_user_reauthorization_without_exposing_description() { + let failure = classify_oauth_token_error( + reqwest::StatusCode::BAD_REQUEST, + br#"{"error":"invalid_grant","error_subtype":"invalid_rapt","error_description":"sensitive provider detail"}"#, + OAuthGrantKind::UserRefreshToken, + ); + + assert_eq!( + failure.recovery_action, + ProviderCredentialRefreshRecoveryAction::Reauthorize + ); + assert_eq!(failure.failure_code, "oauth_invalid_grant"); + assert_eq!(failure.provider_error_subtype, Some("invalid_rapt")); + assert!(!failure.retry_automatically); + assert!( + failure + .status + .message() + .contains("interactive reauthorization") + ); + assert!( + !failure + .status + .message() + .contains("sensitive provider detail") + ); + } + + #[test] + fn oauth_noninteractive_invalid_grant_requires_configuration_fix() { + let failure = classify_oauth_token_error( + reqwest::StatusCode::BAD_REQUEST, + br#"{"error":"invalid_grant"}"#, + OAuthGrantKind::NonInteractive, + ); + + assert_eq!( + failure.recovery_action, + ProviderCredentialRefreshRecoveryAction::FixConfiguration + ); + assert_eq!(failure.failure_code, "oauth_invalid_grant"); + assert!(!failure.retry_automatically); + } + + #[test] + fn oauth_server_failure_remains_retryable() { + let failure = classify_oauth_token_error( + reqwest::StatusCode::SERVICE_UNAVAILABLE, + br#"{"error":"invalid_grant"}"#, + OAuthGrantKind::UserRefreshToken, + ); + + assert_eq!( + failure.recovery_action, + ProviderCredentialRefreshRecoveryAction::Retry + ); + assert_eq!(failure.failure_code, "oauth_token_endpoint_retryable"); + assert!(failure.retry_automatically); + } + + #[test] + fn unrecognized_oauth_error_requests_investigation_without_echoing_body() { + let failure = classify_oauth_token_error( + reqwest::StatusCode::BAD_REQUEST, + br#"{"error":"vendor_secret_error","error_description":"do not expose me"}"#, + OAuthGrantKind::UserRefreshToken, + ); + + assert_eq!( + failure.recovery_action, + ProviderCredentialRefreshRecoveryAction::Investigate + ); + assert_eq!(failure.failure_code, "oauth_unrecognized_error"); + assert!(failure.retry_automatically); + assert!(!failure.status.message().contains("vendor_secret_error")); + assert!(!failure.status.message().contains("do not expose me")); + } + + #[tokio::test] + async fn oauth_invalid_grant_persists_terminal_reauthorization_status() { + let mock_server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/token")) + .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({ + "error": "invalid_grant", + "error_subtype": "invalid_rapt", + "error_description": "provider-controlled detail" + }))) + .mount(&mock_server) + .await; + + let store = test_store().await; + let provider = provider("expired-grant", "outlook"); + store.put_message(&provider).await.unwrap(); + let state = new_refresh_state( + &provider, + "default", + "MS_GRAPH_ACCESS_TOKEN", + NewRefreshStateConfig { + strategy: ProviderCredentialRefreshStrategy::Oauth2RefreshToken, + material: HashMap::from([ + ("client_id".to_string(), "client-id".to_string()), + ( + "refresh_token".to_string(), + "expired-refresh-token".to_string(), + ), + ]), + secret_material_keys: vec!["refresh_token".to_string()], + expires_at_ms: 0, + token_url: format!("{}/token", mock_server.uri()), + scopes: Vec::new(), + refresh_before_seconds: 30, + max_lifetime_seconds: 60, + additional_output_keys: HashMap::new(), + }, + ) + .unwrap(); + put_refresh_state(&store, &state).await.unwrap(); + + let err = refresh_provider_credential( + &store, + "default", + &test_credentials(), + None, + "expired-grant", + "MS_GRAPH_ACCESS_TOKEN", + ) + .await + .unwrap_err(); + + assert_eq!(err.code(), tonic::Code::FailedPrecondition); + let stored = get_refresh_state( + &store, + "default", + provider.object_id(), + "MS_GRAPH_ACCESS_TOKEN", + ) + .await + .unwrap() + .unwrap(); + assert_eq!(stored.status, "reauthorization_required"); + assert_eq!(stored.next_refresh_at_ms, i64::MAX); + assert_eq!( + stored.recovery_action, + ProviderCredentialRefreshRecoveryAction::Reauthorize as i32 + ); + assert_eq!(stored.failure_code, "oauth_invalid_grant"); + assert_eq!(stored.provider_error_subtype, "invalid_rapt"); + assert!(stored.last_error_at_ms > 0); + assert!(!stored.last_error.contains("provider-controlled detail")); + } + #[tokio::test] async fn oauth2_client_credentials_refresh_mints_and_persists_access_token() { let mock_server = MockServer::start().await; @@ -2366,7 +2786,7 @@ mod tests { } #[tokio::test] - async fn rotated_refresh_token_store_failure_persists_error_and_backoff() { + async fn rotated_refresh_token_store_failure_requires_reauthorization() { let mock_server = MockServer::start().await; Mock::given(method("POST")) .and(path("/token")) @@ -2439,9 +2859,18 @@ mod tests { .await .unwrap() .unwrap(); - assert_eq!(stored.status, "error"); + assert_eq!(stored.status, "reauthorization_required"); assert!(stored.last_error.contains("must be re-authorized")); + assert_eq!(stored.next_refresh_at_ms, i64::MAX); assert!(stored.next_refresh_at_ms > before); + assert_eq!( + stored.recovery_action, + ProviderCredentialRefreshRecoveryAction::Reauthorize as i32 + ); + assert_eq!( + stored.failure_code, + "oauth_rotated_refresh_token_store_failed" + ); assert_eq!(credentials.stored_credential_count(), Some(1)); assert_eq!( credentials diff --git a/docs/sandboxes/providers-v2.mdx b/docs/sandboxes/providers-v2.mdx index 96ce68ab32..a6880234f3 100644 --- a/docs/sandboxes/providers-v2.mdx +++ b/docs/sandboxes/providers-v2.mdx @@ -663,6 +663,21 @@ The gateway sends a complete host, port, and path binding for every emitted stat Refresh configuration is stored separately from the current injectable credential value. Non-secret refresh configuration remains in the refresh-state record. Secret material is resolved from the active credential driver only while the gateway mints a new short-lived token. The gateway writes the token back through credential storage and updates credential expiry metadata. If an OAuth issuer rotates its refresh token, the gateway stages the replacement through credential storage before committing the refresh generation and removes the previous handle afterward. +For OAuth token endpoint failures, the gateway parses the bounded standard OAuth +error response and reports a structured recovery action through `provider +refresh status`. It does not retain provider error descriptions or raw response +bodies. The recovery actions are: + +- `retry`: a network, rate-limit, or temporary issuer failure; the worker retries automatically. +- `reauthorize`: a user refresh grant is no longer usable, such as `invalid_grant`; obtain a new grant and run `provider refresh configure`. +- `fix_configuration`: the OAuth client, scopes, grant type, or administrator policy must be corrected. +- `investigate`: the issuer returned an unrecognized response; the worker retries, but the operator should investigate persistent failures. + +The gateway parks `reauthorize` and `fix_configuration` failures instead of +retrying them every worker tick. A manual `provider refresh rotate` still +attempts the exchange. The current access token remains available only until +its recorded expiry, after which credential resolution fails closed. + Before OpenShell 0.1.0, refresh-state and credential-driver migrations are not supported. Upgrading from a build that stored refresh material inline requires reconfiguring the refresh grant. To change credential drivers, delete or @@ -770,6 +785,13 @@ Check refresh status: openshell provider refresh status my-graph ``` +The status table includes `RECOVERY` and `FAILURE_CODE`. Clients should use the +structured recovery action rather than parsing `LAST_ERROR`. For example, +`oauth_invalid_grant` with recovery action `reauthorize` means the user must +complete OAuth authorization again; `oauth_invalid_client` with +`fix_configuration` means changing the user login alone will not repair the +grant. + The status table reports operational state without printing token values or refresh material: ```text diff --git a/proto/openshell.proto b/proto/openshell.proto index 6f1481b521..58ef4e81dc 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -1623,6 +1623,14 @@ message ProviderCredentialRefreshStatus { int64 next_refresh_at_ms = 7; int64 last_refresh_at_ms = 8; string last_error = 9; + ProviderCredentialRefreshRecoveryAction recovery_action = 10; + // Stable gateway-owned failure identifier, for example + // "oauth_invalid_grant". This is not provider-controlled prose. + string failure_code = 11; + // A bounded, recognized provider subtype that refines failure_code. Unknown + // provider-controlled values are not persisted or returned. + string provider_error_subtype = 12; + int64 last_error_at_ms = 13; } // Provider profile local discovery declaration. @@ -1671,6 +1679,12 @@ message StoredProviderCredentialRefreshState { // Cleanup is retried by the refresh worker so a gateway crash or temporary // credential-backend outage does not lose the deletion reference. repeated StoredRefreshMaterialDeletion pending_secret_deletions = 20; + // Structured recovery details for the most recent refresh failure. These + // fields contain only gateway-owned codes and recognized bounded values. + ProviderCredentialRefreshRecoveryAction recovery_action = 21; + string failure_code = 22; + string provider_error_subtype = 23; + int64 last_error_at_ms = 24; } message StoredRefreshMaterialDeletion { @@ -2787,6 +2801,18 @@ enum WorkspaceRole { WORKSPACE_ROLE_ADMIN = 2; } +// Stable recovery action for the most recent provider credential refresh +// failure. Kept after the pre-existing enums so adding it does not renumber +// their generated descriptors. Clients should use this field instead of +// parsing last_error. +enum ProviderCredentialRefreshRecoveryAction { + PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_UNSPECIFIED = 0; + PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY = 1; + PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE = 2; + PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION = 3; + PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE = 4; +} + // 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 778bf4f985..47faaa1c81 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -439,6 +439,65 @@ func (WorkspaceRole) EnumDescriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{6} } +// Stable recovery action for the most recent provider credential refresh +// failure. Kept after the pre-existing enums so adding it does not renumber +// their generated descriptors. Clients should use this field instead of +// parsing last_error. +type ProviderCredentialRefreshRecoveryAction int32 + +const ( + ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_UNSPECIFIED ProviderCredentialRefreshRecoveryAction = 0 + ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY ProviderCredentialRefreshRecoveryAction = 1 + ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE ProviderCredentialRefreshRecoveryAction = 2 + ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION ProviderCredentialRefreshRecoveryAction = 3 + ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE ProviderCredentialRefreshRecoveryAction = 4 +) + +// Enum value maps for ProviderCredentialRefreshRecoveryAction. +var ( + ProviderCredentialRefreshRecoveryAction_name = map[int32]string{ + 0: "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_UNSPECIFIED", + 1: "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY", + 2: "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE", + 3: "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION", + 4: "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE", + } + ProviderCredentialRefreshRecoveryAction_value = map[string]int32{ + "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_UNSPECIFIED": 0, + "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY": 1, + "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE": 2, + "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION": 3, + "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE": 4, + } +) + +func (x ProviderCredentialRefreshRecoveryAction) Enum() *ProviderCredentialRefreshRecoveryAction { + p := new(ProviderCredentialRefreshRecoveryAction) + *p = x + return p +} + +func (x ProviderCredentialRefreshRecoveryAction) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderCredentialRefreshRecoveryAction) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[7].Descriptor() +} + +func (ProviderCredentialRefreshRecoveryAction) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[7] +} + +func (x ProviderCredentialRefreshRecoveryAction) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderCredentialRefreshRecoveryAction.Descriptor instead. +func (ProviderCredentialRefreshRecoveryAction) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{7} +} + // 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). @@ -5861,18 +5920,26 @@ func (x *ProviderCredentialRefresh) GetAdditionalOutputs() []*ProviderCredential } type ProviderCredentialRefreshStatus struct { - state protoimpl.MessageState `protogen:"open.v1"` - ProviderName string `protobuf:"bytes,1,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` - ProviderId string `protobuf:"bytes,2,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` - CredentialKey string `protobuf:"bytes,3,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` - Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,4,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` - Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` - ExpiresAtMs int64 `protobuf:"varint,6,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` - NextRefreshAtMs int64 `protobuf:"varint,7,opt,name=next_refresh_at_ms,json=nextRefreshAtMs,proto3" json:"next_refresh_at_ms,omitempty"` - LastRefreshAtMs int64 `protobuf:"varint,8,opt,name=last_refresh_at_ms,json=lastRefreshAtMs,proto3" json:"last_refresh_at_ms,omitempty"` - LastError string `protobuf:"bytes,9,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + ProviderName string `protobuf:"bytes,1,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` + ProviderId string `protobuf:"bytes,2,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` + CredentialKey string `protobuf:"bytes,3,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` + Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,4,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` + ExpiresAtMs int64 `protobuf:"varint,6,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` + NextRefreshAtMs int64 `protobuf:"varint,7,opt,name=next_refresh_at_ms,json=nextRefreshAtMs,proto3" json:"next_refresh_at_ms,omitempty"` + LastRefreshAtMs int64 `protobuf:"varint,8,opt,name=last_refresh_at_ms,json=lastRefreshAtMs,proto3" json:"last_refresh_at_ms,omitempty"` + LastError string `protobuf:"bytes,9,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` + RecoveryAction ProviderCredentialRefreshRecoveryAction `protobuf:"varint,10,opt,name=recovery_action,json=recoveryAction,proto3,enum=openshell.v1.ProviderCredentialRefreshRecoveryAction" json:"recovery_action,omitempty"` + // Stable gateway-owned failure identifier, for example + // "oauth_invalid_grant". This is not provider-controlled prose. + FailureCode string `protobuf:"bytes,11,opt,name=failure_code,json=failureCode,proto3" json:"failure_code,omitempty"` + // A bounded, recognized provider subtype that refines failure_code. Unknown + // provider-controlled values are not persisted or returned. + ProviderErrorSubtype string `protobuf:"bytes,12,opt,name=provider_error_subtype,json=providerErrorSubtype,proto3" json:"provider_error_subtype,omitempty"` + LastErrorAtMs int64 `protobuf:"varint,13,opt,name=last_error_at_ms,json=lastErrorAtMs,proto3" json:"last_error_at_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ProviderCredentialRefreshStatus) Reset() { @@ -5968,6 +6035,34 @@ func (x *ProviderCredentialRefreshStatus) GetLastError() string { return "" } +func (x *ProviderCredentialRefreshStatus) GetRecoveryAction() ProviderCredentialRefreshRecoveryAction { + if x != nil { + return x.RecoveryAction + } + return ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_UNSPECIFIED +} + +func (x *ProviderCredentialRefreshStatus) GetFailureCode() string { + if x != nil { + return x.FailureCode + } + return "" +} + +func (x *ProviderCredentialRefreshStatus) GetProviderErrorSubtype() string { + if x != nil { + return x.ProviderErrorSubtype + } + return "" +} + +func (x *ProviderCredentialRefreshStatus) GetLastErrorAtMs() int64 { + if x != nil { + return x.LastErrorAtMs + } + return 0 +} + // Provider profile local discovery declaration. type ProviderProfileDiscovery struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -6055,8 +6150,14 @@ type StoredProviderCredentialRefreshState struct { // Cleanup is retried by the refresh worker so a gateway crash or temporary // credential-backend outage does not lose the deletion reference. PendingSecretDeletions []*StoredRefreshMaterialDeletion `protobuf:"bytes,20,rep,name=pending_secret_deletions,json=pendingSecretDeletions,proto3" json:"pending_secret_deletions,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Structured recovery details for the most recent refresh failure. These + // fields contain only gateway-owned codes and recognized bounded values. + RecoveryAction ProviderCredentialRefreshRecoveryAction `protobuf:"varint,21,opt,name=recovery_action,json=recoveryAction,proto3,enum=openshell.v1.ProviderCredentialRefreshRecoveryAction" json:"recovery_action,omitempty"` + FailureCode string `protobuf:"bytes,22,opt,name=failure_code,json=failureCode,proto3" json:"failure_code,omitempty"` + ProviderErrorSubtype string `protobuf:"bytes,23,opt,name=provider_error_subtype,json=providerErrorSubtype,proto3" json:"provider_error_subtype,omitempty"` + LastErrorAtMs int64 `protobuf:"varint,24,opt,name=last_error_at_ms,json=lastErrorAtMs,proto3" json:"last_error_at_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StoredProviderCredentialRefreshState) Reset() { @@ -6229,6 +6330,34 @@ func (x *StoredProviderCredentialRefreshState) GetPendingSecretDeletions() []*St return nil } +func (x *StoredProviderCredentialRefreshState) GetRecoveryAction() ProviderCredentialRefreshRecoveryAction { + if x != nil { + return x.RecoveryAction + } + return ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_UNSPECIFIED +} + +func (x *StoredProviderCredentialRefreshState) GetFailureCode() string { + if x != nil { + return x.FailureCode + } + return "" +} + +func (x *StoredProviderCredentialRefreshState) GetProviderErrorSubtype() string { + if x != nil { + return x.ProviderErrorSubtype + } + return "" +} + +func (x *StoredProviderCredentialRefreshState) GetLastErrorAtMs() int64 { + if x != nil { + return x.LastErrorAtMs + } + return 0 +} + type StoredRefreshMaterialDeletion struct { state protoimpl.MessageState `protogen:"open.v1"` // Original material name used to derive the credential driver's storage key. @@ -14233,7 +14362,7 @@ const file_openshell_proto_rawDesc = "" + "\x16refresh_before_seconds\x18\x04 \x01(\x03R\x14refreshBeforeSeconds\x120\n" + "\x14max_lifetime_seconds\x18\x05 \x01(\x03R\x12maxLifetimeSeconds\x12K\n" + "\bmaterial\x18\x06 \x03(\v2/.openshell.v1.ProviderCredentialRefreshMaterialR\bmaterial\x12\\\n" + - "\x12additional_outputs\x18\a \x03(\v2-.openshell.v1.ProviderCredentialRefreshOutputR\x11additionalOutputs\"\x90\x03\n" + + "\x12additional_outputs\x18\a \x03(\v2-.openshell.v1.ProviderCredentialRefreshOutputR\x11additionalOutputs\"\xf2\x04\n" + "\x1fProviderCredentialRefreshStatus\x12#\n" + "\rprovider_name\x18\x01 \x01(\tR\fproviderName\x12\x1f\n" + "\vprovider_id\x18\x02 \x01(\tR\n" + @@ -14245,9 +14374,14 @@ const file_openshell_proto_rawDesc = "" + "\x12next_refresh_at_ms\x18\a \x01(\x03R\x0fnextRefreshAtMs\x12+\n" + "\x12last_refresh_at_ms\x18\b \x01(\x03R\x0flastRefreshAtMs\x12\x1d\n" + "\n" + - "last_error\x18\t \x01(\tR\tlastError\"<\n" + + "last_error\x18\t \x01(\tR\tlastError\x12^\n" + + "\x0frecovery_action\x18\n" + + " \x01(\x0e25.openshell.v1.ProviderCredentialRefreshRecoveryActionR\x0erecoveryAction\x12!\n" + + "\ffailure_code\x18\v \x01(\tR\vfailureCode\x124\n" + + "\x16provider_error_subtype\x18\f \x01(\tR\x14providerErrorSubtype\x12'\n" + + "\x10last_error_at_ms\x18\r \x01(\x03R\rlastErrorAtMs\"<\n" + "\x18ProviderProfileDiscovery\x12 \n" + - "\vcredentials\x18\x01 \x03(\tR\vcredentials\"\xa7\v\n" + + "\vcredentials\x18\x01 \x03(\tR\vcredentials\"\x89\r\n" + "$StoredProviderCredentialRefreshState\x12>\n" + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12\x1f\n" + "\vprovider_id\x18\x02 \x01(\tR\n" + @@ -14271,7 +14405,11 @@ const file_openshell_proto_rawDesc = "" + "\x16additional_output_keys\x18\x11 \x03(\v2L.openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntryR\x14additionalOutputKeys\x12/\n" + "\x13authorization_epoch\x18\x12 \x01(\tR\x12authorizationEpoch\x12\x85\x01\n" + "\x17secret_material_handles\x18\x13 \x03(\v2M.openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntryR\x15secretMaterialHandles\x12e\n" + - "\x18pending_secret_deletions\x18\x14 \x03(\v2+.openshell.v1.StoredRefreshMaterialDeletionR\x16pendingSecretDeletions\x1a;\n" + + "\x18pending_secret_deletions\x18\x14 \x03(\v2+.openshell.v1.StoredRefreshMaterialDeletionR\x16pendingSecretDeletions\x12^\n" + + "\x0frecovery_action\x18\x15 \x01(\x0e25.openshell.v1.ProviderCredentialRefreshRecoveryActionR\x0erecoveryAction\x12!\n" + + "\ffailure_code\x18\x16 \x01(\tR\vfailureCode\x124\n" + + "\x16provider_error_subtype\x18\x17 \x01(\tR\x14providerErrorSubtype\x12'\n" + + "\x10last_error_at_ms\x18\x18 \x01(\x03R\rlastErrorAtMs\x1a;\n" + "\rMaterialEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aG\n" + @@ -14930,7 +15068,13 @@ const file_openshell_proto_rawDesc = "" + "\rWorkspaceRole\x12\x1e\n" + "\x1aWORKSPACE_ROLE_UNSPECIFIED\x10\x00\x12\x17\n" + "\x13WORKSPACE_ROLE_USER\x10\x01\x12\x18\n" + - "\x14WORKSPACE_ROLE_ADMIN\x10\x022\xacF\n" + + "\x14WORKSPACE_ROLE_ADMIN\x10\x02*\xda\x02\n" + + "'ProviderCredentialRefreshRecoveryAction\x12;\n" + + "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_UNSPECIFIED\x10\x00\x125\n" + + "1PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY\x10\x01\x12;\n" + + "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE\x10\x02\x12A\n" + + "=PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION\x10\x03\x12;\n" + + "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE\x10\x042\xacF\n" + "\tOpenShell\x12Z\n" + "\x06Health\x12\x1b.openshell.v1.HealthRequest\x1a\x1c.openshell.v1.HealthResponse\"\x15\x82\xb5\x18\x11\n" + "\x0funauthenticated\x12i\n" + @@ -15084,7 +15228,7 @@ func file_openshell_proto_rawDescGZIP() []byte { return file_openshell_proto_rawDescData } -var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 7) +var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 8) var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 217) var file_openshell_proto_goTypes = []any{ (SandboxPhase)(0), // 0: openshell.v1.SandboxPhase @@ -15094,546 +15238,549 @@ var file_openshell_proto_goTypes = []any{ (PolicyStatus)(0), // 4: openshell.v1.PolicyStatus (ServiceStatus)(0), // 5: openshell.v1.ServiceStatus (WorkspaceRole)(0), // 6: openshell.v1.WorkspaceRole - (*IssueSandboxTokenRequest)(nil), // 7: openshell.v1.IssueSandboxTokenRequest - (*IssueSandboxTokenResponse)(nil), // 8: openshell.v1.IssueSandboxTokenResponse - (*RefreshSandboxTokenRequest)(nil), // 9: openshell.v1.RefreshSandboxTokenRequest - (*RefreshSandboxTokenResponse)(nil), // 10: openshell.v1.RefreshSandboxTokenResponse - (*HealthRequest)(nil), // 11: openshell.v1.HealthRequest - (*HealthResponse)(nil), // 12: openshell.v1.HealthResponse - (*GetCurrentUserRequest)(nil), // 13: openshell.v1.GetCurrentUserRequest - (*GetCurrentUserResponse)(nil), // 14: openshell.v1.GetCurrentUserResponse - (*GetGatewayInfoRequest)(nil), // 15: openshell.v1.GetGatewayInfoRequest - (*GetGatewayInfoResponse)(nil), // 16: openshell.v1.GetGatewayInfoResponse - (*ComputeDriverInfo)(nil), // 17: openshell.v1.ComputeDriverInfo - (*ComputeDriverCapabilities)(nil), // 18: openshell.v1.ComputeDriverCapabilities - (*Sandbox)(nil), // 19: openshell.v1.Sandbox - (*SandboxSpec)(nil), // 20: openshell.v1.SandboxSpec - (*ResourceRequirements)(nil), // 21: openshell.v1.ResourceRequirements - (*GpuResourceRequirements)(nil), // 22: openshell.v1.GpuResourceRequirements - (*SandboxTemplate)(nil), // 23: openshell.v1.SandboxTemplate - (*SandboxStatus)(nil), // 24: openshell.v1.SandboxStatus - (*SandboxCondition)(nil), // 25: openshell.v1.SandboxCondition - (*PlatformEvent)(nil), // 26: openshell.v1.PlatformEvent - (*CreateSandboxRequest)(nil), // 27: openshell.v1.CreateSandboxRequest - (*GetSandboxRequest)(nil), // 28: openshell.v1.GetSandboxRequest - (*ListSandboxesRequest)(nil), // 29: openshell.v1.ListSandboxesRequest - (*ListSandboxProvidersRequest)(nil), // 30: openshell.v1.ListSandboxProvidersRequest - (*AttachSandboxProviderRequest)(nil), // 31: openshell.v1.AttachSandboxProviderRequest - (*DetachSandboxProviderRequest)(nil), // 32: openshell.v1.DetachSandboxProviderRequest - (*DeleteSandboxRequest)(nil), // 33: openshell.v1.DeleteSandboxRequest - (*StopSandboxRequest)(nil), // 34: openshell.v1.StopSandboxRequest - (*StartSandboxRequest)(nil), // 35: openshell.v1.StartSandboxRequest - (*SandboxResponse)(nil), // 36: openshell.v1.SandboxResponse - (*ListSandboxesResponse)(nil), // 37: openshell.v1.ListSandboxesResponse - (*ListSandboxProvidersResponse)(nil), // 38: openshell.v1.ListSandboxProvidersResponse - (*AttachSandboxProviderResponse)(nil), // 39: openshell.v1.AttachSandboxProviderResponse - (*DetachSandboxProviderResponse)(nil), // 40: openshell.v1.DetachSandboxProviderResponse - (*DeleteSandboxResponse)(nil), // 41: openshell.v1.DeleteSandboxResponse - (*CreateSshSessionRequest)(nil), // 42: openshell.v1.CreateSshSessionRequest - (*CreateSshSessionResponse)(nil), // 43: openshell.v1.CreateSshSessionResponse - (*ExposeServiceRequest)(nil), // 44: openshell.v1.ExposeServiceRequest - (*GetServiceRequest)(nil), // 45: openshell.v1.GetServiceRequest - (*ListServicesRequest)(nil), // 46: openshell.v1.ListServicesRequest - (*ListServicesResponse)(nil), // 47: openshell.v1.ListServicesResponse - (*DeleteServiceRequest)(nil), // 48: openshell.v1.DeleteServiceRequest - (*DeleteServiceResponse)(nil), // 49: openshell.v1.DeleteServiceResponse - (*ServiceEndpoint)(nil), // 50: openshell.v1.ServiceEndpoint - (*ServiceEndpointResponse)(nil), // 51: openshell.v1.ServiceEndpointResponse - (*RevokeSshSessionRequest)(nil), // 52: openshell.v1.RevokeSshSessionRequest - (*RevokeSshSessionResponse)(nil), // 53: openshell.v1.RevokeSshSessionResponse - (*ExecSandboxRequest)(nil), // 54: openshell.v1.ExecSandboxRequest - (*ExecSandboxStdout)(nil), // 55: openshell.v1.ExecSandboxStdout - (*ExecSandboxStderr)(nil), // 56: openshell.v1.ExecSandboxStderr - (*ExecSandboxExit)(nil), // 57: openshell.v1.ExecSandboxExit - (*ExecSandboxEvent)(nil), // 58: openshell.v1.ExecSandboxEvent - (*TcpForwardInit)(nil), // 59: openshell.v1.TcpForwardInit - (*TcpForwardFrame)(nil), // 60: openshell.v1.TcpForwardFrame - (*ExecSandboxInput)(nil), // 61: openshell.v1.ExecSandboxInput - (*ExecSandboxWindowResize)(nil), // 62: openshell.v1.ExecSandboxWindowResize - (*SshSession)(nil), // 63: openshell.v1.SshSession - (*WatchSandboxRequest)(nil), // 64: openshell.v1.WatchSandboxRequest - (*SandboxStreamEvent)(nil), // 65: openshell.v1.SandboxStreamEvent - (*SandboxLogLine)(nil), // 66: openshell.v1.SandboxLogLine - (*SandboxStreamWarning)(nil), // 67: openshell.v1.SandboxStreamWarning - (*CreateProviderRequest)(nil), // 68: openshell.v1.CreateProviderRequest - (*GetProviderRequest)(nil), // 69: openshell.v1.GetProviderRequest - (*ListProvidersRequest)(nil), // 70: openshell.v1.ListProvidersRequest - (*UpdateProviderRequest)(nil), // 71: openshell.v1.UpdateProviderRequest - (*DeleteProviderRequest)(nil), // 72: openshell.v1.DeleteProviderRequest - (*ProviderResponse)(nil), // 73: openshell.v1.ProviderResponse - (*ListProvidersResponse)(nil), // 74: openshell.v1.ListProvidersResponse - (*ListProviderProfilesRequest)(nil), // 75: openshell.v1.ListProviderProfilesRequest - (*GetProviderProfileRequest)(nil), // 76: openshell.v1.GetProviderProfileRequest - (*ProviderProfileImportItem)(nil), // 77: openshell.v1.ProviderProfileImportItem - (*ProviderProfileDiagnostic)(nil), // 78: openshell.v1.ProviderProfileDiagnostic - (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 79: openshell.v1.ProviderCredentialTokenGrantAudienceOverride - (*ProviderCredentialTokenGrantSubjectToken)(nil), // 80: openshell.v1.ProviderCredentialTokenGrantSubjectToken - (*ProviderCredentialTokenGrant)(nil), // 81: openshell.v1.ProviderCredentialTokenGrant - (*ProviderProfileCredential)(nil), // 82: openshell.v1.ProviderProfileCredential - (*ProviderCredentialRefreshMaterial)(nil), // 83: openshell.v1.ProviderCredentialRefreshMaterial - (*ProviderCredentialRefreshOutput)(nil), // 84: openshell.v1.ProviderCredentialRefreshOutput - (*ProviderCredentialRefresh)(nil), // 85: openshell.v1.ProviderCredentialRefresh - (*ProviderCredentialRefreshStatus)(nil), // 86: openshell.v1.ProviderCredentialRefreshStatus - (*ProviderProfileDiscovery)(nil), // 87: openshell.v1.ProviderProfileDiscovery - (*StoredProviderCredentialRefreshState)(nil), // 88: openshell.v1.StoredProviderCredentialRefreshState - (*StoredRefreshMaterialDeletion)(nil), // 89: openshell.v1.StoredRefreshMaterialDeletion - (*GetProviderRefreshStatusRequest)(nil), // 90: openshell.v1.GetProviderRefreshStatusRequest - (*GetProviderRefreshStatusResponse)(nil), // 91: openshell.v1.GetProviderRefreshStatusResponse - (*ConfigureProviderRefreshRequest)(nil), // 92: openshell.v1.ConfigureProviderRefreshRequest - (*ConfigureProviderRefreshResponse)(nil), // 93: openshell.v1.ConfigureProviderRefreshResponse - (*RotateProviderCredentialRequest)(nil), // 94: openshell.v1.RotateProviderCredentialRequest - (*RotateProviderCredentialResponse)(nil), // 95: openshell.v1.RotateProviderCredentialResponse - (*DeleteProviderRefreshRequest)(nil), // 96: openshell.v1.DeleteProviderRefreshRequest - (*DeleteProviderRefreshResponse)(nil), // 97: openshell.v1.DeleteProviderRefreshResponse - (*ProviderProfile)(nil), // 98: openshell.v1.ProviderProfile - (*StoredProviderProfile)(nil), // 99: openshell.v1.StoredProviderProfile - (*ProviderProfileResponse)(nil), // 100: openshell.v1.ProviderProfileResponse - (*ListProviderProfilesResponse)(nil), // 101: openshell.v1.ListProviderProfilesResponse - (*ImportProviderProfilesRequest)(nil), // 102: openshell.v1.ImportProviderProfilesRequest - (*ImportProviderProfilesResponse)(nil), // 103: openshell.v1.ImportProviderProfilesResponse - (*UpdateProviderProfilesRequest)(nil), // 104: openshell.v1.UpdateProviderProfilesRequest - (*UpdateProviderProfilesResponse)(nil), // 105: openshell.v1.UpdateProviderProfilesResponse - (*LintProviderProfilesRequest)(nil), // 106: openshell.v1.LintProviderProfilesRequest - (*LintProviderProfilesResponse)(nil), // 107: openshell.v1.LintProviderProfilesResponse - (*DeleteProviderResponse)(nil), // 108: openshell.v1.DeleteProviderResponse - (*DeleteProviderProfileRequest)(nil), // 109: openshell.v1.DeleteProviderProfileRequest - (*DeleteProviderProfileResponse)(nil), // 110: openshell.v1.DeleteProviderProfileResponse - (*GetSandboxProviderEnvironmentRequest)(nil), // 111: openshell.v1.GetSandboxProviderEnvironmentRequest - (*StaticCredentialEndpointBinding)(nil), // 112: openshell.v1.StaticCredentialEndpointBinding - (*StaticCredentialBinding)(nil), // 113: openshell.v1.StaticCredentialBinding - (*GetSandboxProviderEnvironmentResponse)(nil), // 114: openshell.v1.GetSandboxProviderEnvironmentResponse - (*ExchangeProviderSubjectTokenRequest)(nil), // 115: openshell.v1.ExchangeProviderSubjectTokenRequest - (*ExchangeProviderSubjectTokenResponse)(nil), // 116: openshell.v1.ExchangeProviderSubjectTokenResponse - (*UpdateConfigRequest)(nil), // 117: openshell.v1.UpdateConfigRequest - (*PolicyMergeOperation)(nil), // 118: openshell.v1.PolicyMergeOperation - (*AddNetworkRule)(nil), // 119: openshell.v1.AddNetworkRule - (*RemoveNetworkEndpoint)(nil), // 120: openshell.v1.RemoveNetworkEndpoint - (*RemoveNetworkRule)(nil), // 121: openshell.v1.RemoveNetworkRule - (*AddDenyRules)(nil), // 122: openshell.v1.AddDenyRules - (*AddAllowRules)(nil), // 123: openshell.v1.AddAllowRules - (*RemoveNetworkBinary)(nil), // 124: openshell.v1.RemoveNetworkBinary - (*UpdateConfigResponse)(nil), // 125: openshell.v1.UpdateConfigResponse - (*GetSandboxPolicyStatusRequest)(nil), // 126: openshell.v1.GetSandboxPolicyStatusRequest - (*GetSandboxPolicyStatusResponse)(nil), // 127: openshell.v1.GetSandboxPolicyStatusResponse - (*ListSandboxPoliciesRequest)(nil), // 128: openshell.v1.ListSandboxPoliciesRequest - (*ListSandboxPoliciesResponse)(nil), // 129: openshell.v1.ListSandboxPoliciesResponse - (*ReportPolicyStatusRequest)(nil), // 130: openshell.v1.ReportPolicyStatusRequest - (*ReportPolicyStatusResponse)(nil), // 131: openshell.v1.ReportPolicyStatusResponse - (*SandboxPolicyRevision)(nil), // 132: openshell.v1.SandboxPolicyRevision - (*GetSandboxLogsRequest)(nil), // 133: openshell.v1.GetSandboxLogsRequest - (*PushSandboxLogsRequest)(nil), // 134: openshell.v1.PushSandboxLogsRequest - (*PushSandboxLogsResponse)(nil), // 135: openshell.v1.PushSandboxLogsResponse - (*GetSandboxLogsResponse)(nil), // 136: openshell.v1.GetSandboxLogsResponse - (*SupervisorMessage)(nil), // 137: openshell.v1.SupervisorMessage - (*GatewayMessage)(nil), // 138: openshell.v1.GatewayMessage - (*SupervisorHello)(nil), // 139: openshell.v1.SupervisorHello - (*SessionAccepted)(nil), // 140: openshell.v1.SessionAccepted - (*SessionRejected)(nil), // 141: openshell.v1.SessionRejected - (*SupervisorHeartbeat)(nil), // 142: openshell.v1.SupervisorHeartbeat - (*GatewayHeartbeat)(nil), // 143: openshell.v1.GatewayHeartbeat - (*ReportMainProcessExitRequest)(nil), // 144: openshell.v1.ReportMainProcessExitRequest - (*ReportMainProcessExitResponse)(nil), // 145: openshell.v1.ReportMainProcessExitResponse - (*RelayOpen)(nil), // 146: openshell.v1.RelayOpen - (*SshRelayTarget)(nil), // 147: openshell.v1.SshRelayTarget - (*TcpRelayTarget)(nil), // 148: openshell.v1.TcpRelayTarget - (*RelayInit)(nil), // 149: openshell.v1.RelayInit - (*RelayFrame)(nil), // 150: openshell.v1.RelayFrame - (*RelayOpenResult)(nil), // 151: openshell.v1.RelayOpenResult - (*RelayClose)(nil), // 152: openshell.v1.RelayClose - (*L7RequestSample)(nil), // 153: openshell.v1.L7RequestSample - (*DenialSummary)(nil), // 154: openshell.v1.DenialSummary - (*DenialGroupCount)(nil), // 155: openshell.v1.DenialGroupCount - (*NetworkActivitySummary)(nil), // 156: openshell.v1.NetworkActivitySummary - (*PolicyChunk)(nil), // 157: openshell.v1.PolicyChunk - (*DraftPolicyUpdate)(nil), // 158: openshell.v1.DraftPolicyUpdate - (*SubmitPolicyAnalysisRequest)(nil), // 159: openshell.v1.SubmitPolicyAnalysisRequest - (*SubmitPolicyAnalysisResponse)(nil), // 160: openshell.v1.SubmitPolicyAnalysisResponse - (*GetDraftPolicyRequest)(nil), // 161: openshell.v1.GetDraftPolicyRequest - (*GetDraftPolicyResponse)(nil), // 162: openshell.v1.GetDraftPolicyResponse - (*ApproveDraftChunkRequest)(nil), // 163: openshell.v1.ApproveDraftChunkRequest - (*ApproveDraftChunkResponse)(nil), // 164: openshell.v1.ApproveDraftChunkResponse - (*RejectDraftChunkRequest)(nil), // 165: openshell.v1.RejectDraftChunkRequest - (*RejectDraftChunkResponse)(nil), // 166: openshell.v1.RejectDraftChunkResponse - (*DraftChunkApproval)(nil), // 167: openshell.v1.DraftChunkApproval - (*ApproveAllDraftChunksRequest)(nil), // 168: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 169: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 170: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 171: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 172: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 173: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 174: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 175: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 176: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 177: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 178: openshell.v1.GetDraftHistoryResponse - (*PolicyRevisionPayload)(nil), // 179: openshell.v1.PolicyRevisionPayload - (*DraftChunkPayload)(nil), // 180: openshell.v1.DraftChunkPayload - (*StoredPolicyRevision)(nil), // 181: openshell.v1.StoredPolicyRevision - (*StoredDraftChunk)(nil), // 182: openshell.v1.StoredDraftChunk - (*CreateWorkspaceRequest)(nil), // 183: openshell.v1.CreateWorkspaceRequest - (*CreateWorkspaceResponse)(nil), // 184: openshell.v1.CreateWorkspaceResponse - (*GetWorkspaceRequest)(nil), // 185: openshell.v1.GetWorkspaceRequest - (*GetWorkspaceResponse)(nil), // 186: openshell.v1.GetWorkspaceResponse - (*ListWorkspacesRequest)(nil), // 187: openshell.v1.ListWorkspacesRequest - (*ListWorkspacesResponse)(nil), // 188: openshell.v1.ListWorkspacesResponse - (*DeleteWorkspaceRequest)(nil), // 189: openshell.v1.DeleteWorkspaceRequest - (*DeleteWorkspaceResponse)(nil), // 190: openshell.v1.DeleteWorkspaceResponse - (*WorkspaceMember)(nil), // 191: openshell.v1.WorkspaceMember - (*AddWorkspaceMemberRequest)(nil), // 192: openshell.v1.AddWorkspaceMemberRequest - (*AddWorkspaceMemberResponse)(nil), // 193: openshell.v1.AddWorkspaceMemberResponse - (*RemoveWorkspaceMemberRequest)(nil), // 194: openshell.v1.RemoveWorkspaceMemberRequest - (*RemoveWorkspaceMemberResponse)(nil), // 195: openshell.v1.RemoveWorkspaceMemberResponse - (*ListWorkspaceMembersRequest)(nil), // 196: openshell.v1.ListWorkspaceMembersRequest - (*ListWorkspaceMembersResponse)(nil), // 197: openshell.v1.ListWorkspaceMembersResponse - (*ExtensionServiceCredential)(nil), // 198: openshell.v1.ExtensionServiceCredential - nil, // 199: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 200: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 201: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 202: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 203: openshell.v1.PlatformEvent.MetadataEntry - nil, // 204: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 205: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 206: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 207: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 208: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - nil, // 209: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - nil, // 210: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - nil, // 211: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry - nil, // 212: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 213: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 214: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 215: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - nil, // 216: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 217: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 218: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 219: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 220: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 221: openshell.v1.PolicyRevisionPayload.ProvenanceEntry - nil, // 222: openshell.v1.StoredPolicyRevision.ProvenanceEntry - nil, // 223: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*datamodelv1.ObjectMeta)(nil), // 224: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 225: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 226: google.protobuf.Struct - (*datamodelv1.Provider)(nil), // 227: openshell.datamodel.v1.Provider - (*datamodelv1.CredentialHandle)(nil), // 228: openshell.datamodel.v1.CredentialHandle - (*sandboxv1.NetworkEndpoint)(nil), // 229: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 230: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 231: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 232: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 233: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 234: openshell.sandbox.v1.L7Rule - (*datamodelv1.Workspace)(nil), // 235: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 236: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 237: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 238: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 239: openshell.sandbox.v1.GetGatewayConfigResponse + (ProviderCredentialRefreshRecoveryAction)(0), // 7: openshell.v1.ProviderCredentialRefreshRecoveryAction + (*IssueSandboxTokenRequest)(nil), // 8: openshell.v1.IssueSandboxTokenRequest + (*IssueSandboxTokenResponse)(nil), // 9: openshell.v1.IssueSandboxTokenResponse + (*RefreshSandboxTokenRequest)(nil), // 10: openshell.v1.RefreshSandboxTokenRequest + (*RefreshSandboxTokenResponse)(nil), // 11: openshell.v1.RefreshSandboxTokenResponse + (*HealthRequest)(nil), // 12: openshell.v1.HealthRequest + (*HealthResponse)(nil), // 13: openshell.v1.HealthResponse + (*GetCurrentUserRequest)(nil), // 14: openshell.v1.GetCurrentUserRequest + (*GetCurrentUserResponse)(nil), // 15: openshell.v1.GetCurrentUserResponse + (*GetGatewayInfoRequest)(nil), // 16: openshell.v1.GetGatewayInfoRequest + (*GetGatewayInfoResponse)(nil), // 17: openshell.v1.GetGatewayInfoResponse + (*ComputeDriverInfo)(nil), // 18: openshell.v1.ComputeDriverInfo + (*ComputeDriverCapabilities)(nil), // 19: openshell.v1.ComputeDriverCapabilities + (*Sandbox)(nil), // 20: openshell.v1.Sandbox + (*SandboxSpec)(nil), // 21: openshell.v1.SandboxSpec + (*ResourceRequirements)(nil), // 22: openshell.v1.ResourceRequirements + (*GpuResourceRequirements)(nil), // 23: openshell.v1.GpuResourceRequirements + (*SandboxTemplate)(nil), // 24: openshell.v1.SandboxTemplate + (*SandboxStatus)(nil), // 25: openshell.v1.SandboxStatus + (*SandboxCondition)(nil), // 26: openshell.v1.SandboxCondition + (*PlatformEvent)(nil), // 27: openshell.v1.PlatformEvent + (*CreateSandboxRequest)(nil), // 28: openshell.v1.CreateSandboxRequest + (*GetSandboxRequest)(nil), // 29: openshell.v1.GetSandboxRequest + (*ListSandboxesRequest)(nil), // 30: openshell.v1.ListSandboxesRequest + (*ListSandboxProvidersRequest)(nil), // 31: openshell.v1.ListSandboxProvidersRequest + (*AttachSandboxProviderRequest)(nil), // 32: openshell.v1.AttachSandboxProviderRequest + (*DetachSandboxProviderRequest)(nil), // 33: openshell.v1.DetachSandboxProviderRequest + (*DeleteSandboxRequest)(nil), // 34: openshell.v1.DeleteSandboxRequest + (*StopSandboxRequest)(nil), // 35: openshell.v1.StopSandboxRequest + (*StartSandboxRequest)(nil), // 36: openshell.v1.StartSandboxRequest + (*SandboxResponse)(nil), // 37: openshell.v1.SandboxResponse + (*ListSandboxesResponse)(nil), // 38: openshell.v1.ListSandboxesResponse + (*ListSandboxProvidersResponse)(nil), // 39: openshell.v1.ListSandboxProvidersResponse + (*AttachSandboxProviderResponse)(nil), // 40: openshell.v1.AttachSandboxProviderResponse + (*DetachSandboxProviderResponse)(nil), // 41: openshell.v1.DetachSandboxProviderResponse + (*DeleteSandboxResponse)(nil), // 42: openshell.v1.DeleteSandboxResponse + (*CreateSshSessionRequest)(nil), // 43: openshell.v1.CreateSshSessionRequest + (*CreateSshSessionResponse)(nil), // 44: openshell.v1.CreateSshSessionResponse + (*ExposeServiceRequest)(nil), // 45: openshell.v1.ExposeServiceRequest + (*GetServiceRequest)(nil), // 46: openshell.v1.GetServiceRequest + (*ListServicesRequest)(nil), // 47: openshell.v1.ListServicesRequest + (*ListServicesResponse)(nil), // 48: openshell.v1.ListServicesResponse + (*DeleteServiceRequest)(nil), // 49: openshell.v1.DeleteServiceRequest + (*DeleteServiceResponse)(nil), // 50: openshell.v1.DeleteServiceResponse + (*ServiceEndpoint)(nil), // 51: openshell.v1.ServiceEndpoint + (*ServiceEndpointResponse)(nil), // 52: openshell.v1.ServiceEndpointResponse + (*RevokeSshSessionRequest)(nil), // 53: openshell.v1.RevokeSshSessionRequest + (*RevokeSshSessionResponse)(nil), // 54: openshell.v1.RevokeSshSessionResponse + (*ExecSandboxRequest)(nil), // 55: openshell.v1.ExecSandboxRequest + (*ExecSandboxStdout)(nil), // 56: openshell.v1.ExecSandboxStdout + (*ExecSandboxStderr)(nil), // 57: openshell.v1.ExecSandboxStderr + (*ExecSandboxExit)(nil), // 58: openshell.v1.ExecSandboxExit + (*ExecSandboxEvent)(nil), // 59: openshell.v1.ExecSandboxEvent + (*TcpForwardInit)(nil), // 60: openshell.v1.TcpForwardInit + (*TcpForwardFrame)(nil), // 61: openshell.v1.TcpForwardFrame + (*ExecSandboxInput)(nil), // 62: openshell.v1.ExecSandboxInput + (*ExecSandboxWindowResize)(nil), // 63: openshell.v1.ExecSandboxWindowResize + (*SshSession)(nil), // 64: openshell.v1.SshSession + (*WatchSandboxRequest)(nil), // 65: openshell.v1.WatchSandboxRequest + (*SandboxStreamEvent)(nil), // 66: openshell.v1.SandboxStreamEvent + (*SandboxLogLine)(nil), // 67: openshell.v1.SandboxLogLine + (*SandboxStreamWarning)(nil), // 68: openshell.v1.SandboxStreamWarning + (*CreateProviderRequest)(nil), // 69: openshell.v1.CreateProviderRequest + (*GetProviderRequest)(nil), // 70: openshell.v1.GetProviderRequest + (*ListProvidersRequest)(nil), // 71: openshell.v1.ListProvidersRequest + (*UpdateProviderRequest)(nil), // 72: openshell.v1.UpdateProviderRequest + (*DeleteProviderRequest)(nil), // 73: openshell.v1.DeleteProviderRequest + (*ProviderResponse)(nil), // 74: openshell.v1.ProviderResponse + (*ListProvidersResponse)(nil), // 75: openshell.v1.ListProvidersResponse + (*ListProviderProfilesRequest)(nil), // 76: openshell.v1.ListProviderProfilesRequest + (*GetProviderProfileRequest)(nil), // 77: openshell.v1.GetProviderProfileRequest + (*ProviderProfileImportItem)(nil), // 78: openshell.v1.ProviderProfileImportItem + (*ProviderProfileDiagnostic)(nil), // 79: openshell.v1.ProviderProfileDiagnostic + (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 80: openshell.v1.ProviderCredentialTokenGrantAudienceOverride + (*ProviderCredentialTokenGrantSubjectToken)(nil), // 81: openshell.v1.ProviderCredentialTokenGrantSubjectToken + (*ProviderCredentialTokenGrant)(nil), // 82: openshell.v1.ProviderCredentialTokenGrant + (*ProviderProfileCredential)(nil), // 83: openshell.v1.ProviderProfileCredential + (*ProviderCredentialRefreshMaterial)(nil), // 84: openshell.v1.ProviderCredentialRefreshMaterial + (*ProviderCredentialRefreshOutput)(nil), // 85: openshell.v1.ProviderCredentialRefreshOutput + (*ProviderCredentialRefresh)(nil), // 86: openshell.v1.ProviderCredentialRefresh + (*ProviderCredentialRefreshStatus)(nil), // 87: openshell.v1.ProviderCredentialRefreshStatus + (*ProviderProfileDiscovery)(nil), // 88: openshell.v1.ProviderProfileDiscovery + (*StoredProviderCredentialRefreshState)(nil), // 89: openshell.v1.StoredProviderCredentialRefreshState + (*StoredRefreshMaterialDeletion)(nil), // 90: openshell.v1.StoredRefreshMaterialDeletion + (*GetProviderRefreshStatusRequest)(nil), // 91: openshell.v1.GetProviderRefreshStatusRequest + (*GetProviderRefreshStatusResponse)(nil), // 92: openshell.v1.GetProviderRefreshStatusResponse + (*ConfigureProviderRefreshRequest)(nil), // 93: openshell.v1.ConfigureProviderRefreshRequest + (*ConfigureProviderRefreshResponse)(nil), // 94: openshell.v1.ConfigureProviderRefreshResponse + (*RotateProviderCredentialRequest)(nil), // 95: openshell.v1.RotateProviderCredentialRequest + (*RotateProviderCredentialResponse)(nil), // 96: openshell.v1.RotateProviderCredentialResponse + (*DeleteProviderRefreshRequest)(nil), // 97: openshell.v1.DeleteProviderRefreshRequest + (*DeleteProviderRefreshResponse)(nil), // 98: openshell.v1.DeleteProviderRefreshResponse + (*ProviderProfile)(nil), // 99: openshell.v1.ProviderProfile + (*StoredProviderProfile)(nil), // 100: openshell.v1.StoredProviderProfile + (*ProviderProfileResponse)(nil), // 101: openshell.v1.ProviderProfileResponse + (*ListProviderProfilesResponse)(nil), // 102: openshell.v1.ListProviderProfilesResponse + (*ImportProviderProfilesRequest)(nil), // 103: openshell.v1.ImportProviderProfilesRequest + (*ImportProviderProfilesResponse)(nil), // 104: openshell.v1.ImportProviderProfilesResponse + (*UpdateProviderProfilesRequest)(nil), // 105: openshell.v1.UpdateProviderProfilesRequest + (*UpdateProviderProfilesResponse)(nil), // 106: openshell.v1.UpdateProviderProfilesResponse + (*LintProviderProfilesRequest)(nil), // 107: openshell.v1.LintProviderProfilesRequest + (*LintProviderProfilesResponse)(nil), // 108: openshell.v1.LintProviderProfilesResponse + (*DeleteProviderResponse)(nil), // 109: openshell.v1.DeleteProviderResponse + (*DeleteProviderProfileRequest)(nil), // 110: openshell.v1.DeleteProviderProfileRequest + (*DeleteProviderProfileResponse)(nil), // 111: openshell.v1.DeleteProviderProfileResponse + (*GetSandboxProviderEnvironmentRequest)(nil), // 112: openshell.v1.GetSandboxProviderEnvironmentRequest + (*StaticCredentialEndpointBinding)(nil), // 113: openshell.v1.StaticCredentialEndpointBinding + (*StaticCredentialBinding)(nil), // 114: openshell.v1.StaticCredentialBinding + (*GetSandboxProviderEnvironmentResponse)(nil), // 115: openshell.v1.GetSandboxProviderEnvironmentResponse + (*ExchangeProviderSubjectTokenRequest)(nil), // 116: openshell.v1.ExchangeProviderSubjectTokenRequest + (*ExchangeProviderSubjectTokenResponse)(nil), // 117: openshell.v1.ExchangeProviderSubjectTokenResponse + (*UpdateConfigRequest)(nil), // 118: openshell.v1.UpdateConfigRequest + (*PolicyMergeOperation)(nil), // 119: openshell.v1.PolicyMergeOperation + (*AddNetworkRule)(nil), // 120: openshell.v1.AddNetworkRule + (*RemoveNetworkEndpoint)(nil), // 121: openshell.v1.RemoveNetworkEndpoint + (*RemoveNetworkRule)(nil), // 122: openshell.v1.RemoveNetworkRule + (*AddDenyRules)(nil), // 123: openshell.v1.AddDenyRules + (*AddAllowRules)(nil), // 124: openshell.v1.AddAllowRules + (*RemoveNetworkBinary)(nil), // 125: openshell.v1.RemoveNetworkBinary + (*UpdateConfigResponse)(nil), // 126: openshell.v1.UpdateConfigResponse + (*GetSandboxPolicyStatusRequest)(nil), // 127: openshell.v1.GetSandboxPolicyStatusRequest + (*GetSandboxPolicyStatusResponse)(nil), // 128: openshell.v1.GetSandboxPolicyStatusResponse + (*ListSandboxPoliciesRequest)(nil), // 129: openshell.v1.ListSandboxPoliciesRequest + (*ListSandboxPoliciesResponse)(nil), // 130: openshell.v1.ListSandboxPoliciesResponse + (*ReportPolicyStatusRequest)(nil), // 131: openshell.v1.ReportPolicyStatusRequest + (*ReportPolicyStatusResponse)(nil), // 132: openshell.v1.ReportPolicyStatusResponse + (*SandboxPolicyRevision)(nil), // 133: openshell.v1.SandboxPolicyRevision + (*GetSandboxLogsRequest)(nil), // 134: openshell.v1.GetSandboxLogsRequest + (*PushSandboxLogsRequest)(nil), // 135: openshell.v1.PushSandboxLogsRequest + (*PushSandboxLogsResponse)(nil), // 136: openshell.v1.PushSandboxLogsResponse + (*GetSandboxLogsResponse)(nil), // 137: openshell.v1.GetSandboxLogsResponse + (*SupervisorMessage)(nil), // 138: openshell.v1.SupervisorMessage + (*GatewayMessage)(nil), // 139: openshell.v1.GatewayMessage + (*SupervisorHello)(nil), // 140: openshell.v1.SupervisorHello + (*SessionAccepted)(nil), // 141: openshell.v1.SessionAccepted + (*SessionRejected)(nil), // 142: openshell.v1.SessionRejected + (*SupervisorHeartbeat)(nil), // 143: openshell.v1.SupervisorHeartbeat + (*GatewayHeartbeat)(nil), // 144: openshell.v1.GatewayHeartbeat + (*ReportMainProcessExitRequest)(nil), // 145: openshell.v1.ReportMainProcessExitRequest + (*ReportMainProcessExitResponse)(nil), // 146: openshell.v1.ReportMainProcessExitResponse + (*RelayOpen)(nil), // 147: openshell.v1.RelayOpen + (*SshRelayTarget)(nil), // 148: openshell.v1.SshRelayTarget + (*TcpRelayTarget)(nil), // 149: openshell.v1.TcpRelayTarget + (*RelayInit)(nil), // 150: openshell.v1.RelayInit + (*RelayFrame)(nil), // 151: openshell.v1.RelayFrame + (*RelayOpenResult)(nil), // 152: openshell.v1.RelayOpenResult + (*RelayClose)(nil), // 153: openshell.v1.RelayClose + (*L7RequestSample)(nil), // 154: openshell.v1.L7RequestSample + (*DenialSummary)(nil), // 155: openshell.v1.DenialSummary + (*DenialGroupCount)(nil), // 156: openshell.v1.DenialGroupCount + (*NetworkActivitySummary)(nil), // 157: openshell.v1.NetworkActivitySummary + (*PolicyChunk)(nil), // 158: openshell.v1.PolicyChunk + (*DraftPolicyUpdate)(nil), // 159: openshell.v1.DraftPolicyUpdate + (*SubmitPolicyAnalysisRequest)(nil), // 160: openshell.v1.SubmitPolicyAnalysisRequest + (*SubmitPolicyAnalysisResponse)(nil), // 161: openshell.v1.SubmitPolicyAnalysisResponse + (*GetDraftPolicyRequest)(nil), // 162: openshell.v1.GetDraftPolicyRequest + (*GetDraftPolicyResponse)(nil), // 163: openshell.v1.GetDraftPolicyResponse + (*ApproveDraftChunkRequest)(nil), // 164: openshell.v1.ApproveDraftChunkRequest + (*ApproveDraftChunkResponse)(nil), // 165: openshell.v1.ApproveDraftChunkResponse + (*RejectDraftChunkRequest)(nil), // 166: openshell.v1.RejectDraftChunkRequest + (*RejectDraftChunkResponse)(nil), // 167: openshell.v1.RejectDraftChunkResponse + (*DraftChunkApproval)(nil), // 168: openshell.v1.DraftChunkApproval + (*ApproveAllDraftChunksRequest)(nil), // 169: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 170: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 171: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 172: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 173: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 174: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 175: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 176: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 177: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 178: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 179: openshell.v1.GetDraftHistoryResponse + (*PolicyRevisionPayload)(nil), // 180: openshell.v1.PolicyRevisionPayload + (*DraftChunkPayload)(nil), // 181: openshell.v1.DraftChunkPayload + (*StoredPolicyRevision)(nil), // 182: openshell.v1.StoredPolicyRevision + (*StoredDraftChunk)(nil), // 183: openshell.v1.StoredDraftChunk + (*CreateWorkspaceRequest)(nil), // 184: openshell.v1.CreateWorkspaceRequest + (*CreateWorkspaceResponse)(nil), // 185: openshell.v1.CreateWorkspaceResponse + (*GetWorkspaceRequest)(nil), // 186: openshell.v1.GetWorkspaceRequest + (*GetWorkspaceResponse)(nil), // 187: openshell.v1.GetWorkspaceResponse + (*ListWorkspacesRequest)(nil), // 188: openshell.v1.ListWorkspacesRequest + (*ListWorkspacesResponse)(nil), // 189: openshell.v1.ListWorkspacesResponse + (*DeleteWorkspaceRequest)(nil), // 190: openshell.v1.DeleteWorkspaceRequest + (*DeleteWorkspaceResponse)(nil), // 191: openshell.v1.DeleteWorkspaceResponse + (*WorkspaceMember)(nil), // 192: openshell.v1.WorkspaceMember + (*AddWorkspaceMemberRequest)(nil), // 193: openshell.v1.AddWorkspaceMemberRequest + (*AddWorkspaceMemberResponse)(nil), // 194: openshell.v1.AddWorkspaceMemberResponse + (*RemoveWorkspaceMemberRequest)(nil), // 195: openshell.v1.RemoveWorkspaceMemberRequest + (*RemoveWorkspaceMemberResponse)(nil), // 196: openshell.v1.RemoveWorkspaceMemberResponse + (*ListWorkspaceMembersRequest)(nil), // 197: openshell.v1.ListWorkspaceMembersRequest + (*ListWorkspaceMembersResponse)(nil), // 198: openshell.v1.ListWorkspaceMembersResponse + (*ExtensionServiceCredential)(nil), // 199: openshell.v1.ExtensionServiceCredential + nil, // 200: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 201: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 202: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 203: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 204: openshell.v1.PlatformEvent.MetadataEntry + nil, // 205: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 206: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 207: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 208: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 209: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + nil, // 210: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + nil, // 211: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + nil, // 212: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry + nil, // 213: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 214: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 215: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 216: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + nil, // 217: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 218: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 219: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 220: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 221: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 222: openshell.v1.PolicyRevisionPayload.ProvenanceEntry + nil, // 223: openshell.v1.StoredPolicyRevision.ProvenanceEntry + nil, // 224: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*datamodelv1.ObjectMeta)(nil), // 225: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 226: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 227: google.protobuf.Struct + (*datamodelv1.Provider)(nil), // 228: openshell.datamodel.v1.Provider + (*datamodelv1.CredentialHandle)(nil), // 229: openshell.datamodel.v1.CredentialHandle + (*sandboxv1.NetworkEndpoint)(nil), // 230: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 231: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 232: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 233: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 234: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 235: openshell.sandbox.v1.L7Rule + (*datamodelv1.Workspace)(nil), // 236: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 237: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 238: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 239: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 240: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 198, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential + 199, // 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 - 17, // 3: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo - 18, // 4: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities - 224, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 20, // 6: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec - 24, // 7: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus - 199, // 8: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry - 23, // 9: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 225, // 10: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 21, // 11: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements - 22, // 12: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 200, // 13: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 201, // 14: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 202, // 15: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 226, // 16: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 226, // 17: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 25, // 18: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition + 18, // 3: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo + 19, // 4: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities + 225, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 21, // 6: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec + 25, // 7: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus + 200, // 8: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 24, // 9: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate + 226, // 10: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 22, // 11: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements + 23, // 12: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements + 201, // 13: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 202, // 14: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 203, // 15: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 227, // 16: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 227, // 17: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 26, // 18: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition 0, // 19: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 203, // 20: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 20, // 21: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 204, // 22: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 205, // 23: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 19, // 24: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 19, // 25: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 227, // 26: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 19, // 27: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 19, // 28: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 51, // 29: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 224, // 30: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 50, // 31: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 206, // 32: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 55, // 33: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 56, // 34: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 57, // 35: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 147, // 36: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 148, // 37: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 59, // 38: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 54, // 39: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 62, // 40: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 224, // 41: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 19, // 42: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 66, // 43: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 26, // 44: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 67, // 45: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 158, // 46: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 207, // 47: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 227, // 48: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 227, // 49: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 208, // 50: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - 227, // 51: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 227, // 52: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 98, // 53: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 79, // 54: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 204, // 20: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 21, // 21: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec + 205, // 22: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 206, // 23: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 20, // 24: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 20, // 25: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 228, // 26: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 20, // 27: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 20, // 28: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 52, // 29: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 225, // 30: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 51, // 31: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 207, // 32: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 56, // 33: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 57, // 34: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 58, // 35: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 148, // 36: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 149, // 37: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 60, // 38: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 55, // 39: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 63, // 40: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 225, // 41: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 20, // 42: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 67, // 43: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 27, // 44: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 68, // 45: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 159, // 46: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 208, // 47: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 228, // 48: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 228, // 49: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 209, // 50: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + 228, // 51: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 228, // 52: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 99, // 53: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 80, // 54: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride 1, // 55: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType - 80, // 56: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken - 85, // 57: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 81, // 58: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 81, // 56: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 86, // 57: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 82, // 58: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant 2, // 59: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 83, // 60: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 84, // 61: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 84, // 60: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 85, // 61: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput 2, // 62: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 224, // 63: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 2, // 64: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 209, // 65: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - 210, // 66: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - 211, // 67: openshell.v1.StoredProviderCredentialRefreshState.secret_material_handles:type_name -> openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry - 89, // 68: openshell.v1.StoredProviderCredentialRefreshState.pending_secret_deletions:type_name -> openshell.v1.StoredRefreshMaterialDeletion - 228, // 69: openshell.v1.StoredRefreshMaterialDeletion.handle:type_name -> openshell.datamodel.v1.CredentialHandle - 86, // 70: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 2, // 71: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 212, // 72: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 86, // 73: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 86, // 74: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 3, // 75: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 82, // 76: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 229, // 77: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 230, // 78: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 87, // 79: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 213, // 80: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 224, // 81: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 98, // 82: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile - 98, // 83: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 98, // 84: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 77, // 85: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 78, // 86: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 98, // 87: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 77, // 88: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 78, // 89: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 98, // 90: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 77, // 91: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 78, // 92: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 112, // 93: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 214, // 94: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 215, // 95: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - 216, // 96: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 217, // 97: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 225, // 98: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 231, // 99: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 118, // 100: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 218, // 101: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 119, // 102: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 120, // 103: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 121, // 104: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 122, // 105: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 123, // 106: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 124, // 107: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 232, // 108: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 233, // 109: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 234, // 110: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 219, // 111: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 132, // 112: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 132, // 113: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 4, // 114: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 4, // 115: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 225, // 116: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 220, // 117: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 66, // 118: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 66, // 119: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 139, // 120: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 142, // 121: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 151, // 122: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 152, // 123: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 140, // 124: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 141, // 125: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 143, // 126: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 146, // 127: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 152, // 128: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 147, // 129: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 148, // 130: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 149, // 131: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 153, // 132: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 155, // 133: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 232, // 134: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 225, // 135: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 225, // 136: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 154, // 137: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 157, // 138: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 156, // 139: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 157, // 140: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 167, // 141: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 232, // 142: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 177, // 143: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 225, // 144: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 221, // 145: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry - 232, // 146: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 225, // 147: openshell.v1.DraftChunkPayload.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 225, // 148: openshell.v1.DraftChunkPayload.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 222, // 149: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry - 225, // 150: openshell.v1.StoredDraftChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 225, // 151: openshell.v1.StoredDraftChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 223, // 152: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 235, // 153: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 235, // 154: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 235, // 155: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 224, // 156: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 6, // 157: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 6, // 158: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 191, // 159: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 191, // 160: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 228, // 161: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle - 82, // 162: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 113, // 163: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 11, // 164: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 13, // 165: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 15, // 166: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 27, // 167: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 28, // 168: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 29, // 169: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 30, // 170: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 31, // 171: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 32, // 172: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 33, // 173: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 34, // 174: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 35, // 175: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 42, // 176: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 44, // 177: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 45, // 178: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 46, // 179: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 48, // 180: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 52, // 181: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 54, // 182: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 60, // 183: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 61, // 184: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 68, // 185: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 69, // 186: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 70, // 187: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 75, // 188: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 76, // 189: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 102, // 190: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 104, // 191: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 106, // 192: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 71, // 193: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 90, // 194: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 92, // 195: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 94, // 196: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 96, // 197: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 72, // 198: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 109, // 199: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 236, // 200: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 237, // 201: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 117, // 202: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 126, // 203: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 128, // 204: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 130, // 205: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 111, // 206: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 115, // 207: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 133, // 208: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 134, // 209: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 137, // 210: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 144, // 211: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 150, // 212: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 64, // 213: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 159, // 214: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 161, // 215: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 163, // 216: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 165, // 217: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 168, // 218: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 170, // 219: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 172, // 220: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 174, // 221: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 176, // 222: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 7, // 223: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 9, // 224: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 183, // 225: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 185, // 226: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 187, // 227: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 189, // 228: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 192, // 229: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 194, // 230: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 196, // 231: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 12, // 232: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 14, // 233: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 16, // 234: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 36, // 235: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 36, // 236: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 37, // 237: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 38, // 238: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 39, // 239: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 40, // 240: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 41, // 241: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 36, // 242: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 36, // 243: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 43, // 244: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 51, // 245: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 51, // 246: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 47, // 247: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 49, // 248: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 53, // 249: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 58, // 250: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 60, // 251: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 58, // 252: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 73, // 253: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 73, // 254: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 74, // 255: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 101, // 256: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 100, // 257: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 103, // 258: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 105, // 259: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 107, // 260: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 73, // 261: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 91, // 262: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 93, // 263: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 95, // 264: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 97, // 265: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 108, // 266: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 110, // 267: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 238, // 268: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 239, // 269: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 125, // 270: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 127, // 271: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 129, // 272: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 131, // 273: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 114, // 274: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 116, // 275: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 136, // 276: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 135, // 277: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 138, // 278: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 145, // 279: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 150, // 280: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 65, // 281: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 160, // 282: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 162, // 283: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 164, // 284: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 166, // 285: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 169, // 286: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 171, // 287: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 173, // 288: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 175, // 289: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 178, // 290: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 8, // 291: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 10, // 292: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 184, // 293: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 186, // 294: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 188, // 295: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 190, // 296: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 193, // 297: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 195, // 298: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 197, // 299: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 232, // [232:300] is the sub-list for method output_type - 164, // [164:232] is the sub-list for method input_type - 164, // [164:164] is the sub-list for extension type_name - 164, // [164:164] is the sub-list for extension extendee - 0, // [0:164] is the sub-list for field type_name + 7, // 63: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 225, // 64: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 2, // 65: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 210, // 66: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + 211, // 67: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + 212, // 68: openshell.v1.StoredProviderCredentialRefreshState.secret_material_handles:type_name -> openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry + 90, // 69: openshell.v1.StoredProviderCredentialRefreshState.pending_secret_deletions:type_name -> openshell.v1.StoredRefreshMaterialDeletion + 7, // 70: openshell.v1.StoredProviderCredentialRefreshState.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 229, // 71: openshell.v1.StoredRefreshMaterialDeletion.handle:type_name -> openshell.datamodel.v1.CredentialHandle + 87, // 72: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 2, // 73: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 213, // 74: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 87, // 75: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 87, // 76: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 3, // 77: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 83, // 78: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 230, // 79: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 231, // 80: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 88, // 81: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 214, // 82: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 225, // 83: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 99, // 84: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile + 99, // 85: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 99, // 86: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 78, // 87: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 79, // 88: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 99, // 89: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 78, // 90: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 79, // 91: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 99, // 92: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 78, // 93: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 79, // 94: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 113, // 95: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 215, // 96: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 216, // 97: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + 217, // 98: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 218, // 99: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 226, // 100: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 232, // 101: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 119, // 102: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 219, // 103: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 120, // 104: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 121, // 105: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 122, // 106: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 123, // 107: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 124, // 108: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 125, // 109: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 233, // 110: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 234, // 111: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 235, // 112: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 220, // 113: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 133, // 114: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 133, // 115: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 4, // 116: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 4, // 117: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 226, // 118: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 221, // 119: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 67, // 120: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 67, // 121: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 140, // 122: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 143, // 123: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 152, // 124: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 153, // 125: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 141, // 126: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 142, // 127: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 144, // 128: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 147, // 129: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 153, // 130: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 148, // 131: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 149, // 132: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 150, // 133: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 154, // 134: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 156, // 135: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 233, // 136: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 226, // 137: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 226, // 138: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 155, // 139: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 158, // 140: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 157, // 141: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 158, // 142: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 168, // 143: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 233, // 144: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 178, // 145: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 226, // 146: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 222, // 147: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry + 233, // 148: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 226, // 149: openshell.v1.DraftChunkPayload.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 226, // 150: openshell.v1.DraftChunkPayload.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 223, // 151: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry + 226, // 152: openshell.v1.StoredDraftChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 226, // 153: openshell.v1.StoredDraftChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 224, // 154: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 236, // 155: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 236, // 156: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 236, // 157: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 225, // 158: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 6, // 159: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 6, // 160: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 192, // 161: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 192, // 162: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 229, // 163: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle + 83, // 164: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 114, // 165: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 12, // 166: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 14, // 167: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 16, // 168: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 28, // 169: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 29, // 170: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 30, // 171: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 31, // 172: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 32, // 173: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 33, // 174: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 34, // 175: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 35, // 176: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 36, // 177: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 43, // 178: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 45, // 179: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 46, // 180: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 47, // 181: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 49, // 182: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 53, // 183: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 55, // 184: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 61, // 185: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 62, // 186: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 69, // 187: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 70, // 188: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 71, // 189: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 76, // 190: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 77, // 191: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 103, // 192: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 105, // 193: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 107, // 194: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 72, // 195: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 91, // 196: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 93, // 197: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 95, // 198: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 97, // 199: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 73, // 200: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 110, // 201: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 237, // 202: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 238, // 203: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 118, // 204: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 127, // 205: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 129, // 206: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 131, // 207: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 112, // 208: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 116, // 209: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 134, // 210: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 135, // 211: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 138, // 212: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 145, // 213: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 151, // 214: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 65, // 215: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 160, // 216: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 162, // 217: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 164, // 218: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 166, // 219: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 169, // 220: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 171, // 221: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 173, // 222: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 175, // 223: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 177, // 224: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 8, // 225: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 10, // 226: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 184, // 227: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 186, // 228: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 188, // 229: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 190, // 230: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 193, // 231: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 195, // 232: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 197, // 233: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 13, // 234: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 15, // 235: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 17, // 236: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 37, // 237: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 37, // 238: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 38, // 239: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 39, // 240: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 40, // 241: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 41, // 242: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 42, // 243: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 37, // 244: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 37, // 245: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 44, // 246: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 52, // 247: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 52, // 248: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 48, // 249: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 50, // 250: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 54, // 251: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 59, // 252: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 61, // 253: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 59, // 254: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 74, // 255: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 74, // 256: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 75, // 257: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 102, // 258: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 101, // 259: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 104, // 260: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 106, // 261: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 108, // 262: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 74, // 263: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 92, // 264: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 94, // 265: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 96, // 266: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 98, // 267: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 109, // 268: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 111, // 269: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 239, // 270: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 240, // 271: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 126, // 272: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 128, // 273: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 130, // 274: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 132, // 275: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 115, // 276: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 117, // 277: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 137, // 278: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 136, // 279: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 139, // 280: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 146, // 281: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 151, // 282: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 66, // 283: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 161, // 284: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 163, // 285: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 165, // 286: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 167, // 287: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 170, // 288: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 172, // 289: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 174, // 290: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 176, // 291: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 179, // 292: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 9, // 293: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 11, // 294: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 185, // 295: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 187, // 296: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 189, // 297: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 191, // 298: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 194, // 299: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 196, // 300: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 198, // 301: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 234, // [234:302] is the sub-list for method output_type + 166, // [166:234] is the sub-list for method input_type + 166, // [166:166] is the sub-list for extension type_name + 166, // [166:166] is the sub-list for extension extendee + 0, // [0:166] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -15706,7 +15853,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: 7, + NumEnums: 8, NumMessages: 217, NumExtensions: 0, NumServices: 1, From c3468f9bb20db9ae134f78999d57861c9c4a27e9 Mon Sep 17 00:00:00 2001 From: Mrunal Patel Date: Fri, 21 Aug 2026 15:35:06 -0700 Subject: [PATCH 2/5] test(providers): add Keycloak refresh e2e lane Signed-off-by: Mrunal Patel --- .github/workflows/e2e-test.yml | 6 + e2e/rust/Cargo.toml | 6 + e2e/rust/tests/provider_refresh_keycloak.rs | 343 ++++++++++++++++++++ tasks/test.toml | 6 + 4 files changed, 361 insertions(+) create mode 100644 e2e/rust/tests/provider_refresh_keycloak.rs diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 006f5cf440..7091529644 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -157,6 +157,12 @@ jobs: podman_package_version: "5.7.0+ds2-3build1" conmon_package_version: "2.1.13+ds1-2" cmd: "env -u OPENSHELL_GATEWAY_BIN mise run --no-deps --skip-deps e2e:podman:external-driver" + - suite: provider-refresh-keycloak + runner: ubuntu-26.04 + podman_major: "5" + podman_package_version: "5.7.0+ds2-3build1" + conmon_package_version: "2.1.13+ds1-2" + cmd: "mise run --no-deps --skip-deps e2e:provider-refresh-keycloak" env: IMAGE_TAG: ${{ inputs.image-tag }} MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/e2e/rust/Cargo.toml b/e2e/rust/Cargo.toml index 42f989ce42..44881e1682 100644 --- a/e2e/rust/Cargo.toml +++ b/e2e/rust/Cargo.toml @@ -34,6 +34,7 @@ e2e-kubernetes-workspace-operator = ["e2e-kubernetes"] e2e-podman = ["e2e", "e2e-host-gateway", "e2e-local-container-driver"] e2e-podman-gpu = ["e2e-podman", "e2e-gpu"] e2e-oidc-pkce = [] +e2e-provider-refresh-keycloak = [] e2e-vm = ["e2e", "e2e-host-gateway"] [[test]] @@ -41,6 +42,11 @@ name = "oidc_pkce" path = "tests/oidc_pkce.rs" required-features = ["e2e-oidc-pkce"] +[[test]] +name = "provider_refresh_keycloak" +path = "tests/provider_refresh_keycloak.rs" +required-features = ["e2e-provider-refresh-keycloak"] + [[test]] name = "custom_image" path = "tests/custom_image.rs" diff --git a/e2e/rust/tests/provider_refresh_keycloak.rs b/e2e/rust/tests/provider_refresh_keycloak.rs new file mode 100644 index 0000000000..34dfbe7871 --- /dev/null +++ b/e2e/rust/tests/provider_refresh_keycloak.rs @@ -0,0 +1,343 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(target_os = "linux")] + +//! End-to-end coverage for provider OAuth refresh recovery against Keycloak. +//! +//! OpenShell itself uses the local gateway's mTLS authentication. Keycloak is +//! only the provider token issuer: the test refreshes a valid grant, revokes +//! its Keycloak session, and verifies that the gateway reports the next +//! refresh as requiring user reauthorization. + +use std::io::Write as _; +use std::process::{Output, Stdio}; + +use openshell_e2e::harness::binary::openshell_cmd; +use serde_json::Value; +use tempfile::{Builder as TempFileBuilder, NamedTempFile}; +use tokio::io::AsyncWriteExt as _; +use tokio::process::Command; + +const PROVIDER_NAME: &str = "e2e-keycloak-refresh"; +const PROFILE_ID: &str = "e2e-keycloak-refresh"; +const CREDENTIAL_KEY: &str = "KEYCLOAK_ACCESS_TOKEN"; + +fn combined_output(output: &Output) -> String { + format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) +} + +async fn run_cli(args: &[&str], env: &[(&str, &str)]) -> Result { + openshell_cmd() + .args(args) + .env("NO_COLOR", "1") + .envs(env.iter().copied()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .map_err(|error| format!("run openshell command: {error}")) +} + +async fn run_cli_success(args: &[&str], env: &[(&str, &str)]) -> Result { + let output = run_cli(args, env).await?; + let combined = combined_output(&output); + if !output.status.success() { + return Err(format!( + "openshell command failed (exit {:?}):\n{combined}", + output.status.code() + )); + } + Ok(combined) +} + +async fn acquire_keycloak_grant(issuer: &str) -> Result<(String, String), String> { + let token_endpoint = format!("{issuer}/protocol/openid-connect/token"); + let output = Command::new("curl") + .args([ + "--fail", + "--silent", + "--show-error", + "--request", + "POST", + &token_endpoint, + "--data-urlencode", + "grant_type=password", + "--data-urlencode", + "client_id=openshell-cli", + "--data-urlencode", + "username=admin@test", + "--data-urlencode", + "password=admin", + "--data-urlencode", + "scope=openid", + ]) + .output() + .await + .map_err(|error| format!("request Keycloak grant: {error}"))?; + if !output.status.success() { + return Err(format!( + "Keycloak grant request failed (exit {:?}): {}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + )); + } + + let response: Value = serde_json::from_slice(&output.stdout) + .map_err(|error| format!("decode Keycloak grant response: {error}"))?; + let access_token = response + .get("access_token") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .ok_or_else(|| "Keycloak grant response omitted access_token".to_string())?; + let refresh_token = response + .get("refresh_token") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .ok_or_else(|| "Keycloak grant response omitted refresh_token".to_string())?; + Ok((access_token.to_string(), refresh_token.to_string())) +} + +async fn revoke_keycloak_grant(issuer: &str, refresh_token: &str) -> Result<(), String> { + let logout_endpoint = format!("{issuer}/protocol/openid-connect/logout"); + let mut child = Command::new("curl") + .args([ + "--fail", + "--silent", + "--show-error", + "--output", + "/dev/null", + "--request", + "POST", + &logout_endpoint, + "--data-urlencode", + "client_id=openshell-cli", + "--data-urlencode", + "refresh_token@-", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|error| format!("start Keycloak logout request: {error}"))?; + child + .stdin + .take() + .ok_or_else(|| "Keycloak logout stdin was not piped".to_string())? + .write_all(refresh_token.as_bytes()) + .await + .map_err(|error| format!("write Keycloak logout request: {error}"))?; + let output = child + .wait_with_output() + .await + .map_err(|error| format!("wait for Keycloak logout request: {error}"))?; + if !output.status.success() { + return Err(format!( + "Keycloak logout failed (exit {:?}): {}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + )); + } + Ok(()) +} + +fn write_profile(issuer: &str) -> Result { + let mut file = TempFileBuilder::new() + .suffix(".yaml") + .tempfile() + .map_err(|error| format!("create provider profile: {error}"))?; + let profile = format!( + r#"id: {PROFILE_ID} +display_name: Keycloak provider refresh E2E +category: other +credentials: + - name: access_token + env_vars: [{CREDENTIAL_KEY}] + required: true + auth_style: bearer + header_name: authorization + refresh: + strategy: oauth2_refresh_token + token_url: {issuer}/protocol/openid-connect/token + scopes: [openid] + refresh_before_seconds: 60 + max_lifetime_seconds: 3600 + material: + - name: client_id + required: true + - name: refresh_token + required: true + secret: true +endpoints: + - host: keycloak.test.invalid + port: 443 + protocol: rest + access: read-only + enforcement: enforce +binaries: + - /usr/bin/curl +"# + ); + file.write_all(profile.as_bytes()) + .map_err(|error| format!("write provider profile: {error}"))?; + file.flush() + .map_err(|error| format!("flush provider profile: {error}"))?; + Ok(file) +} + +async fn delete_provider_resources() { + let _ = run_cli(&["provider", "delete", PROVIDER_NAME], &[]).await; + let _ = run_cli(&["provider", "profile", "delete", PROFILE_ID], &[]).await; +} + +#[tokio::test] +async fn revoked_refresh_grant_requires_user_reauthorization() -> Result<(), String> { + let issuer = std::env::var("OPENSHELL_E2E_OIDC_ISSUER") + .map_err(|_| "OPENSHELL_E2E_OIDC_ISSUER is required".to_string())?; + let (access_token, refresh_token) = acquire_keycloak_grant(&issuer).await?; + let profile = write_profile(&issuer)?; + let profile_path = profile.path().to_string_lossy().into_owned(); + + delete_provider_resources().await; + let result = async { + run_cli_success( + &[ + "settings", + "set", + "--global", + "--key", + "providers_v2_enabled", + "--value", + "true", + "--yes", + ], + &[], + ) + .await?; + run_cli_success( + &["provider", "profile", "import", "--file", &profile_path], + &[], + ) + .await?; + run_cli_success( + &[ + "provider", + "create", + "--name", + PROVIDER_NAME, + "--type", + PROFILE_ID, + "--credential", + CREDENTIAL_KEY, + ], + &[(CREDENTIAL_KEY, &access_token)], + ) + .await?; + run_cli_success( + &[ + "provider", + "refresh", + "configure", + PROVIDER_NAME, + "--credential-key", + CREDENTIAL_KEY, + "--strategy", + "oauth2-refresh-token", + "--material", + "client_id=openshell-cli", + "--secret-material-env", + "refresh_token=KEYCLOAK_REFRESH_TOKEN", + ], + &[("KEYCLOAK_REFRESH_TOKEN", &refresh_token)], + ) + .await?; + + run_cli_success( + &[ + "provider", + "refresh", + "rotate", + PROVIDER_NAME, + "--credential-key", + CREDENTIAL_KEY, + ], + &[], + ) + .await?; + let valid_status = run_cli_success( + &[ + "provider", + "refresh", + "status", + PROVIDER_NAME, + "--credential-key", + CREDENTIAL_KEY, + ], + &[], + ) + .await?; + if !valid_status.contains("refreshed") { + return Err(format!( + "valid Keycloak refresh did not reach refreshed state:\n{valid_status}" + )); + } + + revoke_keycloak_grant(&issuer, &refresh_token).await?; + let failed_rotation = run_cli( + &[ + "provider", + "refresh", + "rotate", + PROVIDER_NAME, + "--credential-key", + CREDENTIAL_KEY, + ], + &[], + ) + .await?; + let failed_rotation_output = combined_output(&failed_rotation); + if failed_rotation.status.success() || !failed_rotation_output.contains("invalid_grant") { + return Err(format!( + "revoked Keycloak refresh did not fail with invalid_grant:\n{failed_rotation_output}" + )); + } + + let revoked_status = run_cli_success( + &[ + "provider", + "refresh", + "status", + PROVIDER_NAME, + "--credential-key", + CREDENTIAL_KEY, + ], + &[], + ) + .await?; + for expected in [ + "reauthorization_required", + "reauthorize", + "oauth_invalid_grant", + ] { + if !revoked_status.contains(expected) { + return Err(format!( + "revoked refresh status omitted {expected}:\n{revoked_status}" + )); + } + } + if revoked_status.contains("292278994") { + return Err(format!( + "parked refresh rendered the i64::MAX scheduling sentinel as a date:\n{revoked_status}" + )); + } + Ok(()) + } + .await; + + delete_provider_resources().await; + result +} diff --git a/tasks/test.toml b/tasks/test.toml index a796ea67b4..d9373bd643 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -116,6 +116,12 @@ run = [ "CONTAINER_RUNTIME=podman e2e/with-keycloak.sh env OPENSHELL_E2E_OIDC_GATEWAY=1 e2e/with-podman-gateway.sh cargo test --manifest-path e2e/rust/Cargo.toml --features e2e-oidc-pkce --test oidc_pkce", ] +["e2e:provider-refresh-keycloak"] +description = "Run provider refresh recovery e2e against Keycloak and a Podman gateway" +run = [ + "CONTAINER_RUNTIME=podman e2e/with-keycloak.sh e2e/with-podman-gateway.sh cargo test --manifest-path e2e/rust/Cargo.toml --features e2e-provider-refresh-keycloak --test provider_refresh_keycloak -- --nocapture", +] + ["e2e:oidc-pkce:docker"] description = "Run Linux browser PKCE and RBAC e2e tests against Keycloak and a Docker gateway" run = [ From 18d7389d6e26a1b0b69707a457be497e9315b3c6 Mon Sep 17 00:00:00 2001 From: Mrunal Patel Date: Fri, 21 Aug 2026 16:21:24 -0700 Subject: [PATCH 3/5] fix(providers): harden OAuth refresh recovery Signed-off-by: Mrunal Patel --- .agents/skills/openshell-cli/SKILL.md | 8 +- architecture/gateway.md | 7 +- crates/openshell-cli/src/run.rs | 8 +- .../openshell-server/src/provider_refresh.rs | 344 ++++++++++++++++-- docs/sandboxes/providers-v2.mdx | 10 +- e2e/rust/tests/provider_refresh_keycloak.rs | 84 ++++- proto/openshell.proto | 13 +- .../v1/internal/converter/refresh.go | 50 ++- .../v1/internal/converter/refresh_test.go | 62 +++- sdk/go/openshell/v1/types/refresh.go | 46 ++- sdk/go/proto/openshellv1/openshell.pb.go | 31 +- 11 files changed, 582 insertions(+), 81 deletions(-) diff --git a/.agents/skills/openshell-cli/SKILL.md b/.agents/skills/openshell-cli/SKILL.md index 833fc44d0e..313e239883 100644 --- a/.agents/skills/openshell-cli/SKILL.md +++ b/.agents/skills/openshell-cli/SKILL.md @@ -213,9 +213,11 @@ When OAuth refresh fails, inspect the `RECOVERY` and `FAILURE_CODE` columns from user must obtain a new OAuth grant and run `provider refresh configure`, `fix_configuration` means an operator must repair the OAuth client, scopes, or administrator policy, and `investigate` means the issuer returned an -unrecognized response. The gateway parks `reauthorize` and -`fix_configuration` records until a manual rotate or reconfiguration. The -existing access credential remains usable only until its recorded expiry. +unrecognized response. The gateway parks `reauthorize` records until a manual +rotate or reconfiguration. It retries +`fix_configuration` records hourly so externally repaired configuration can +recover without rapid token-endpoint traffic. The existing access credential +remains usable only until its recorded expiry. --- diff --git a/architecture/gateway.md b/architecture/gateway.md index 0a4b49e9e5..8b3f754bde 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -347,9 +347,10 @@ OAuth refresh failures retain a gateway-owned recovery classification alongside the refresh state. The gateway reads only a bounded error response and maps recognized OAuth codes to retry, reauthorization, configuration repair, or investigation without persisting issuer-controlled descriptions. Terminal -reauthorization and configuration failures remain parked until a manual retry -or explicit refresh reconfiguration; short-lived credentials still fail closed -at their recorded expiry. +reauthorization failures remain parked until a manual retry or explicit refresh +reconfiguration. Configuration failures retry hourly so an externally repaired +clock, policy, or stored credential can recover without rapid endpoint traffic; +short-lived credentials still fail closed at their recorded expiry. Credential handles remain bound to the driver that created them. Before the 0.1.0 compatibility boundary, gateways do not migrate inline refresh material diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index fd1096980a..0a0b21a7f4 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -4339,7 +4339,7 @@ pub async fn provider_refresh_status( fn refresh_status_header() -> String { format!( - "{:<24} {:<28} {:<28} {:<24} {:<18} {:<20} {:<20} {:<20} {:<34} {}", + "{:<24} {:<28} {:<28} {:<24} {:<18} {:<20} {:<20} {:<20} {:<44} {}", "PROVIDER".bold(), "CREDENTIAL_KEY".bold(), "STRATEGY".bold(), @@ -4503,7 +4503,7 @@ fn refresh_status_row(status: &ProviderCredentialRefreshStatus) -> String { let recovery_action = ProviderCredentialRefreshRecoveryAction::try_from(status.recovery_action) .unwrap_or(ProviderCredentialRefreshRecoveryAction::Unspecified); format!( - "{:<24} {:<28} {:<28} {:<24} {:<18} {:<20} {:<20} {:<20} {:<34} {}", + "{:<24} {:<28} {:<28} {:<24} {:<18} {:<20} {:<20} {:<20} {:<44} {}", status.provider_name, status.credential_key, provider_refresh_strategy_name(strategy), @@ -7689,7 +7689,7 @@ mod tests { last_error: "token endpoint returned a very long error message that should be truncated for table readability" .to_string(), recovery_action: ProviderCredentialRefreshRecoveryAction::Reauthorize as i32, - failure_code: "oauth_invalid_grant".to_string(), + failure_code: "oauth_rotated_refresh_token_handle_missing".to_string(), provider_error_subtype: "invalid_rapt".to_string(), last_error_at_ms: 1_767_225_000_000, }); @@ -7699,7 +7699,7 @@ mod tests { assert!(row.contains("oauth2_client_credentials")); assert!(row.contains("error")); assert!(row.contains("reauthorize")); - assert!(row.contains("oauth_invalid_grant")); + assert!(row.contains("oauth_rotated_refresh_token_handle_missing")); assert!(row.contains("2026-01-01 00:00:00")); assert!(!row.contains("292278994")); assert!(row.contains("...")); diff --git a/crates/openshell-server/src/provider_refresh.rs b/crates/openshell-server/src/provider_refresh.rs index b4b17e5543..40fad0b2a9 100644 --- a/crates/openshell-server/src/provider_refresh.rs +++ b/crates/openshell-server/src/provider_refresh.rs @@ -18,12 +18,13 @@ use prost::Message; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; -use tonic::Status; +use tonic::{Code, Status}; use tracing::{info, warn}; const DEFAULT_REFRESH_BEFORE_SECONDS: i64 = 300; const DEFAULT_MAX_LIFETIME_SECONDS: i64 = 3600; const REFRESH_ERROR_RETRY_SECONDS: i64 = 60; +const REFRESH_CONFIGURATION_RETRY_SECONDS: i64 = 60 * 60; const REFRESH_WORKER_PAGE_SIZE: u32 = 1000; const MAX_OAUTH_ERROR_RESPONSE_BYTES: usize = 8 * 1024; @@ -431,7 +432,14 @@ struct RefreshFailure { recovery_action: ProviderCredentialRefreshRecoveryAction, failure_code: &'static str, provider_error_subtype: Option<&'static str>, - retry_automatically: bool, + retry_schedule: RefreshRetrySchedule, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum RefreshRetrySchedule { + Short, + Configuration, + Parked, } impl RefreshFailure { @@ -441,7 +449,7 @@ impl RefreshFailure { recovery_action: ProviderCredentialRefreshRecoveryAction::Retry, failure_code, provider_error_subtype: None, - retry_automatically: true, + retry_schedule: RefreshRetrySchedule::Short, } } @@ -451,7 +459,7 @@ impl RefreshFailure { recovery_action: ProviderCredentialRefreshRecoveryAction::Investigate, failure_code, provider_error_subtype: None, - retry_automatically: true, + retry_schedule: RefreshRetrySchedule::Short, } } @@ -465,7 +473,7 @@ impl RefreshFailure { recovery_action: ProviderCredentialRefreshRecoveryAction::Reauthorize, failure_code, provider_error_subtype, - retry_automatically: false, + retry_schedule: RefreshRetrySchedule::Parked, } } @@ -475,7 +483,21 @@ impl RefreshFailure { recovery_action: ProviderCredentialRefreshRecoveryAction::FixConfiguration, failure_code, provider_error_subtype: None, - retry_automatically: false, + retry_schedule: RefreshRetrySchedule::Configuration, + } + } + + fn fix_configuration_with_subtype( + status: Status, + failure_code: &'static str, + provider_error_subtype: &'static str, + ) -> Self { + Self { + status, + recovery_action: ProviderCredentialRefreshRecoveryAction::FixConfiguration, + failure_code, + provider_error_subtype: Some(provider_error_subtype), + retry_schedule: RefreshRetrySchedule::Configuration, } } @@ -486,7 +508,20 @@ impl RefreshFailure { impl From for RefreshFailure { fn from(status: Status) -> Self { - Self::retryable(status, "refresh_failed") + match status.code() { + Code::InvalidArgument + | Code::FailedPrecondition + | Code::PermissionDenied + | Code::Unauthenticated => { + Self::fix_configuration(status, "refresh_configuration_invalid") + } + Code::Unavailable + | Code::DeadlineExceeded + | Code::ResourceExhausted + | Code::Aborted + | Code::Internal => Self::retryable(status, "refresh_failed"), + _ => Self::investigate(status, "refresh_failed"), + } } } @@ -677,10 +712,14 @@ async fn persist_refresh_failure_state( .unwrap_or_default() .to_string(); state.last_error_at_ms = now_ms; - state.next_refresh_at_ms = if failure.retry_automatically { - now_ms.saturating_add(REFRESH_ERROR_RETRY_SECONDS.saturating_mul(1000)) - } else { - i64::MAX + state.next_refresh_at_ms = match failure.retry_schedule { + RefreshRetrySchedule::Short => { + now_ms.saturating_add(REFRESH_ERROR_RETRY_SECONDS.saturating_mul(1000)) + } + RefreshRetrySchedule::Configuration => { + now_ms.saturating_add(REFRESH_CONFIGURATION_RETRY_SECONDS.saturating_mul(1000)) + } + RefreshRetrySchedule::Parked => i64::MAX, }; let new_version = persist_refresh_state_if_current(store, state, expected_version) .await? @@ -1523,12 +1562,31 @@ async fn request_token( .timeout(Duration::from_secs(30)) .build() .map_err(|e| Status::internal(format!("build refresh HTTP client failed: {e}")))?; - let response = client.post(parsed).form(form).send().await.map_err(|_| { - RefreshFailure::retryable( - Status::unavailable("token endpoint request failed"), - "oauth_token_endpoint_unavailable", - ) - })?; + let response = client + .post(parsed) + .form(form) + .send() + .await + .map_err(|error| { + let error_kind = if error.is_timeout() { + "timeout" + } else if error.is_connect() { + "connect" + } else if error.is_request() { + "request" + } else { + "other" + }; + warn!( + error = %error.without_url(), + error_kind, + "OAuth token endpoint request failed" + ); + RefreshFailure::retryable( + Status::unavailable("token endpoint request failed"), + "oauth_token_endpoint_unavailable", + ) + })?; let status = response.status(); if !status.is_success() { let body = read_bounded_oauth_error_body(response).await; @@ -1651,6 +1709,17 @@ fn classify_oauth_token_error( ), "oauth_admin_policy_enforced", ), + "access_denied" + if error_response.error_subtype.as_deref() == Some("admin_policy_enforced") => + { + RefreshFailure::fix_configuration_with_subtype( + Status::failed_precondition( + "OAuth access is blocked by an administrator policy (access_denied/admin_policy_enforced)", + ), + "oauth_admin_policy_enforced", + "admin_policy_enforced", + ) + } "invalid_grant" => RefreshFailure::fix_configuration( Status::failed_precondition( "OAuth token endpoint rejected the non-interactive grant (invalid_grant)", @@ -1942,12 +2011,13 @@ async fn run_refresh_worker_tick( #[cfg(test)] mod tests { use super::{ - NewRefreshStateConfig, OAuthGrantKind, classify_oauth_token_error, + MAX_OAUTH_ERROR_RESPONSE_BYTES, NewRefreshStateConfig, OAuthGrantKind, RefreshFailure, + RefreshRetrySchedule, Status, classify_oauth_token_error, delete_refresh_state_with_credentials, effective_authorization_epoch, enqueue_pending_secret_deletion, get_refresh_state, list_all_refresh_states, list_refresh_states_for_provider, new_refresh_state, put_refresh_state, - refresh_material_scope, refresh_provider_credential, refresh_state_name, - refresh_strategy_name, run_refresh_worker_tick, seconds_until_ms, + read_bounded_oauth_error_body, refresh_material_scope, refresh_provider_credential, + refresh_state_name, refresh_strategy_name, run_refresh_worker_tick, seconds_until_ms, validate_secret_material_references, }; use crate::credentials::CredentialRuntime; @@ -2073,6 +2143,37 @@ mod tests { assert_eq!(refresh_strategy_name(i32::MAX), "unspecified"); } + #[test] + fn local_refresh_statuses_default_to_safe_recovery_actions() { + for status in [ + Status::invalid_argument("missing material"), + Status::failed_precondition("strategy cannot be minted"), + Status::permission_denied("client is not allowed"), + ] { + let failure = RefreshFailure::from(status); + assert_eq!( + failure.recovery_action, + ProviderCredentialRefreshRecoveryAction::FixConfiguration + ); + assert_eq!(failure.failure_code, "refresh_configuration_invalid"); + assert_eq!(failure.retry_schedule, RefreshRetrySchedule::Configuration); + } + + let retry = RefreshFailure::from(Status::unavailable("temporary backend outage")); + assert_eq!( + retry.recovery_action, + ProviderCredentialRefreshRecoveryAction::Retry + ); + assert_eq!(retry.retry_schedule, RefreshRetrySchedule::Short); + + let investigate = RefreshFailure::from(Status::unknown("unclassified failure")); + assert_eq!( + investigate.recovery_action, + ProviderCredentialRefreshRecoveryAction::Investigate + ); + assert_eq!(investigate.retry_schedule, RefreshRetrySchedule::Short); + } + #[test] fn pending_secret_deletions_preserve_multiple_generations_for_one_key() { let mut state = StoredProviderCredentialRefreshState::default(); @@ -2143,7 +2244,7 @@ mod tests { ); assert_eq!(failure.failure_code, "oauth_invalid_grant"); assert_eq!(failure.provider_error_subtype, Some("invalid_rapt")); - assert!(!failure.retry_automatically); + assert_eq!(failure.retry_schedule, RefreshRetrySchedule::Parked); assert!( failure .status @@ -2171,7 +2272,27 @@ mod tests { ProviderCredentialRefreshRecoveryAction::FixConfiguration ); assert_eq!(failure.failure_code, "oauth_invalid_grant"); - assert!(!failure.retry_automatically); + assert_eq!(failure.retry_schedule, RefreshRetrySchedule::Configuration); + } + + #[test] + fn oauth_admin_policy_subtype_requires_configuration_fix() { + let failure = classify_oauth_token_error( + reqwest::StatusCode::BAD_REQUEST, + br#"{"error":"access_denied","error_subtype":"admin_policy_enforced"}"#, + OAuthGrantKind::UserRefreshToken, + ); + + assert_eq!( + failure.recovery_action, + ProviderCredentialRefreshRecoveryAction::FixConfiguration + ); + assert_eq!(failure.failure_code, "oauth_admin_policy_enforced"); + assert_eq!( + failure.provider_error_subtype, + Some("admin_policy_enforced") + ); + assert_eq!(failure.retry_schedule, RefreshRetrySchedule::Configuration); } #[test] @@ -2187,7 +2308,7 @@ mod tests { ProviderCredentialRefreshRecoveryAction::Retry ); assert_eq!(failure.failure_code, "oauth_token_endpoint_retryable"); - assert!(failure.retry_automatically); + assert_eq!(failure.retry_schedule, RefreshRetrySchedule::Short); } #[test] @@ -2203,11 +2324,117 @@ mod tests { ProviderCredentialRefreshRecoveryAction::Investigate ); assert_eq!(failure.failure_code, "oauth_unrecognized_error"); - assert!(failure.retry_automatically); + assert_eq!(failure.retry_schedule, RefreshRetrySchedule::Short); assert!(!failure.status.message().contains("vendor_secret_error")); assert!(!failure.status.message().contains("do not expose me")); } + #[test] + fn html_and_malformed_oauth_errors_are_investigated_without_echoing_body() { + for body in [b"issuer failure".as_slice(), b"{".as_slice()] { + let failure = classify_oauth_token_error( + reqwest::StatusCode::BAD_REQUEST, + body, + OAuthGrantKind::UserRefreshToken, + ); + + assert_eq!( + failure.recovery_action, + ProviderCredentialRefreshRecoveryAction::Investigate + ); + assert_eq!(failure.failure_code, "oauth_unrecognized_error_response"); + assert!(!failure.status.message().contains("issuer failure")); + } + } + + #[tokio::test] + async fn oversized_oauth_error_body_is_bounded_and_investigated() { + let mock_server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/oversized")) + .respond_with(ResponseTemplate::new(400).set_body_bytes(vec![ + b'x'; + MAX_OAUTH_ERROR_RESPONSE_BYTES + + 512 + ])) + .mount(&mock_server) + .await; + + let response = reqwest::get(format!("{}/oversized", mock_server.uri())) + .await + .unwrap(); + let status = response.status(); + let body = read_bounded_oauth_error_body(response).await; + assert_eq!(body.len(), MAX_OAUTH_ERROR_RESPONSE_BYTES); + + let failure = classify_oauth_token_error(status, &body, OAuthGrantKind::UserRefreshToken); + assert_eq!( + failure.recovery_action, + ProviderCredentialRefreshRecoveryAction::Investigate + ); + assert_eq!(failure.failure_code, "oauth_unrecognized_error_response"); + } + + #[tokio::test] + async fn invalid_local_oauth_configuration_retries_hourly() { + let store = test_store().await; + let provider = provider("invalid-local-config", "outlook"); + store.put_message(&provider).await.unwrap(); + let state = new_refresh_state( + &provider, + "default", + "MS_GRAPH_ACCESS_TOKEN", + NewRefreshStateConfig { + strategy: ProviderCredentialRefreshStrategy::Oauth2RefreshToken, + material: HashMap::from([ + ("client_id".to_string(), "client-id".to_string()), + ("refresh_token".to_string(), "refresh-token".to_string()), + ]), + secret_material_keys: vec!["refresh_token".to_string()], + expires_at_ms: 0, + token_url: "not-an-absolute-url".to_string(), + scopes: Vec::new(), + refresh_before_seconds: 30, + max_lifetime_seconds: 60, + additional_output_keys: HashMap::new(), + }, + ) + .unwrap(); + put_refresh_state(&store, &state).await.unwrap(); + + let err = refresh_provider_credential( + &store, + "default", + &test_credentials(), + None, + "invalid-local-config", + "MS_GRAPH_ACCESS_TOKEN", + ) + .await + .unwrap_err(); + + assert_eq!(err.code(), tonic::Code::InvalidArgument); + let stored = get_refresh_state( + &store, + "default", + provider.object_id(), + "MS_GRAPH_ACCESS_TOKEN", + ) + .await + .unwrap() + .unwrap(); + assert_eq!(stored.status, "configuration_required"); + assert_eq!( + stored.recovery_action, + ProviderCredentialRefreshRecoveryAction::FixConfiguration as i32 + ); + assert_eq!(stored.failure_code, "refresh_configuration_invalid"); + assert_eq!( + stored.next_refresh_at_ms - stored.last_error_at_ms, + 60 * 60 * 1000 + ); + } + #[tokio::test] async fn oauth_invalid_grant_persists_terminal_reauthorization_status() { let mock_server = MockServer::start().await; @@ -2304,7 +2531,7 @@ mod tests { let provider = provider("my-graph", "outlook"); store.put_message(&provider).await.unwrap(); let before_refresh_ms = current_time_ms(); - let state = new_refresh_state( + let mut state = new_refresh_state( &provider, "default", "MS_GRAPH_ACCESS_TOKEN", @@ -2324,6 +2551,12 @@ mod tests { }, ) .unwrap(); + state.status = "investigation_required".to_string(); + state.last_error = "safe prior error".to_string(); + state.recovery_action = ProviderCredentialRefreshRecoveryAction::Investigate as i32; + state.failure_code = "oauth_unrecognized_error".to_string(); + state.provider_error_subtype = "prior_subtype".to_string(); + state.last_error_at_ms = current_time_ms(); put_refresh_state(&store, &state).await.unwrap(); let authorization_epoch = state.authorization_epoch.clone(); let credentials = test_credentials(); @@ -2344,6 +2577,13 @@ mod tests { assert!(refreshed.next_refresh_at_ms > 0); assert!(refreshed.expires_at_ms <= before_refresh_ms + 120_000); assert!(refreshed.last_error.is_empty()); + assert_eq!( + refreshed.recovery_action, + ProviderCredentialRefreshRecoveryAction::Unspecified as i32 + ); + assert!(refreshed.failure_code.is_empty()); + assert!(refreshed.provider_error_subtype.is_empty()); + assert_eq!(refreshed.last_error_at_ms, 0); let stored = store .get_message_by_name::("default", "my-graph") @@ -2835,7 +3075,6 @@ mod tests { state.material.remove("refresh_token"); put_refresh_state(&store, &state).await.unwrap(); credentials.fail_next_store(); - let before = current_time_ms(); let err = refresh_provider_credential( &store, @@ -2862,7 +3101,6 @@ mod tests { assert_eq!(stored.status, "reauthorization_required"); assert!(stored.last_error.contains("must be re-authorized")); assert_eq!(stored.next_refresh_at_ms, i64::MAX); - assert!(stored.next_refresh_at_ms > before); assert_eq!( stored.recovery_action, ProviderCredentialRefreshRecoveryAction::Reauthorize as i32 @@ -3015,6 +3253,58 @@ mod tests { ); } + #[tokio::test] + async fn refresh_worker_skips_parked_reauthorization_state() { + let store = test_store().await; + let provider = provider("parked-refresh", "outlook"); + store.put_message(&provider).await.unwrap(); + let mut state = new_refresh_state( + &provider, + "default", + "MS_GRAPH_ACCESS_TOKEN", + NewRefreshStateConfig { + strategy: ProviderCredentialRefreshStrategy::Oauth2RefreshToken, + material: HashMap::new(), + secret_material_keys: Vec::new(), + expires_at_ms: 0, + token_url: "https://issuer.example/token".to_string(), + scopes: Vec::new(), + refresh_before_seconds: 30, + max_lifetime_seconds: 60, + additional_output_keys: HashMap::new(), + }, + ) + .unwrap(); + state.status = "reauthorization_required".to_string(); + state.last_error = "OAuth refresh grant is no longer usable".to_string(); + state.recovery_action = ProviderCredentialRefreshRecoveryAction::Reauthorize as i32; + state.failure_code = "oauth_invalid_grant".to_string(); + state.last_error_at_ms = current_time_ms(); + state.next_refresh_at_ms = i64::MAX; + put_refresh_state(&store, &state).await.unwrap(); + + run_refresh_worker_tick(&store, Some(&test_credentials()), None) + .await + .unwrap(); + + let stored = get_refresh_state( + &store, + "default", + provider.object_id(), + "MS_GRAPH_ACCESS_TOKEN", + ) + .await + .unwrap() + .unwrap(); + assert_eq!(stored.status, "reauthorization_required"); + assert_eq!(stored.next_refresh_at_ms, i64::MAX); + assert_eq!(stored.failure_code, "oauth_invalid_grant"); + assert_eq!( + stored.recovery_action, + ProviderCredentialRefreshRecoveryAction::Reauthorize as i32 + ); + } + #[tokio::test] async fn refresh_worker_finalizes_tombstoned_refresh_material() { let store = test_store().await; diff --git a/docs/sandboxes/providers-v2.mdx b/docs/sandboxes/providers-v2.mdx index a6880234f3..7a0f96b5ac 100644 --- a/docs/sandboxes/providers-v2.mdx +++ b/docs/sandboxes/providers-v2.mdx @@ -790,13 +790,17 @@ structured recovery action rather than parsing `LAST_ERROR`. For example, `oauth_invalid_grant` with recovery action `reauthorize` means the user must complete OAuth authorization again; `oauth_invalid_client` with `fix_configuration` means changing the user login alone will not repair the -grant. +grant. OpenShell parks `reauthorize` failures until an explicit rotation or +reconfiguration. It retries `fix_configuration` failures hourly so externally +repaired clocks, policies, or credential-store values can recover without +creating rapid token-endpoint traffic. Transient and unrecognized failures use +the existing bounded 60-second retry interval. The status table reports operational state without printing token values or refresh material: ```text -PROVIDER CREDENTIAL_KEY STRATEGY STATUS EXPIRES_AT NEXT_REFRESH LAST_REFRESH LAST_ERROR -my-graph MS_GRAPH_ACCESS_TOKEN oauth2_refresh_token refreshed 2026-06-01 00:00:00 2026-05-31 23:50:00 2026-05-31 23:00:00 - +PROVIDER CREDENTIAL_KEY STRATEGY STATUS RECOVERY EXPIRES_AT NEXT_REFRESH LAST_REFRESH FAILURE_CODE LAST_ERROR +my-graph MS_GRAPH_ACCESS_TOKEN oauth2_refresh_token reauthorization_required reauthorize 2026-06-01 00:00:00 - 2026-05-31 23:00:00 oauth_invalid_grant OAuth refresh grant is no longer usable ``` When no refresh configuration exists, the CLI distinguishes whole-provider checks from credential-specific checks: diff --git a/e2e/rust/tests/provider_refresh_keycloak.rs b/e2e/rust/tests/provider_refresh_keycloak.rs index 34dfbe7871..57bcb45fb7 100644 --- a/e2e/rust/tests/provider_refresh_keycloak.rs +++ b/e2e/rust/tests/provider_refresh_keycloak.rs @@ -55,8 +55,14 @@ async fn run_cli_success(args: &[&str], env: &[(&str, &str)]) -> Result Result<(String, String), String> { +async fn acquire_keycloak_grant( + issuer: &str, + username: &str, + password: &str, +) -> Result<(String, String), String> { let token_endpoint = format!("{issuer}/protocol/openid-connect/token"); + let username_form = format!("username={username}"); + let password_form = format!("password={password}"); let output = Command::new("curl") .args([ "--fail", @@ -70,9 +76,9 @@ async fn acquire_keycloak_grant(issuer: &str) -> Result<(String, String), String "--data-urlencode", "client_id=openshell-cli", "--data-urlencode", - "username=admin@test", + &username_form, "--data-urlencode", - "password=admin", + &password_form, "--data-urlencode", "scope=openid", ]) @@ -194,11 +200,70 @@ async fn delete_provider_resources() { let _ = run_cli(&["provider", "profile", "delete", PROFILE_ID], &[]).await; } +async fn read_providers_v2_setting() -> Result, String> { + let output = run_cli(&["settings", "get", "--global", "--json"], &[]).await?; + if !output.status.success() { + return Err(format!( + "read global settings failed (exit {:?}):\n{}", + output.status.code(), + combined_output(&output) + )); + } + let response: Value = serde_json::from_slice(&output.stdout) + .map_err(|error| format!("decode global settings: {error}"))?; + let value = response + .get("settings") + .and_then(|settings| settings.get("providers_v2_enabled")) + .and_then(Value::as_str) + .filter(|value| *value != "") + .map(ToString::to_string); + Ok(value) +} + +async fn restore_providers_v2_setting(previous: Option<&str>) -> Result<(), String> { + if let Some(value) = previous { + run_cli_success( + &[ + "settings", + "set", + "--global", + "--key", + "providers_v2_enabled", + "--value", + value, + "--yes", + ], + &[], + ) + .await?; + } else { + run_cli_success( + &[ + "settings", + "delete", + "--global", + "--key", + "providers_v2_enabled", + "--yes", + ], + &[], + ) + .await?; + } + Ok(()) +} + #[tokio::test] async fn revoked_refresh_grant_requires_user_reauthorization() -> Result<(), String> { let issuer = std::env::var("OPENSHELL_E2E_OIDC_ISSUER") .map_err(|_| "OPENSHELL_E2E_OIDC_ISSUER is required".to_string())?; - let (access_token, refresh_token) = acquire_keycloak_grant(&issuer).await?; + let username = std::env::var("OPENSHELL_E2E_OIDC_USERNAME") + .map_err(|_| "OPENSHELL_E2E_OIDC_USERNAME is required".to_string())?; + let password = std::env::var("OPENSHELL_E2E_OIDC_PASSWORD") + .map_err(|_| "OPENSHELL_E2E_OIDC_PASSWORD is required".to_string())?; + let previous_providers_v2_setting = read_providers_v2_setting().await?; + let (access_token, refresh_token) = + acquire_keycloak_grant(&issuer, &username, &password).await?; let profile = write_profile(&issuer)?; let profile_path = profile.path().to_string_lossy().into_owned(); @@ -339,5 +404,14 @@ async fn revoked_refresh_grant_requires_user_reauthorization() -> Result<(), Str .await; delete_provider_resources().await; - result + let cleanup_result = + restore_providers_v2_setting(previous_providers_v2_setting.as_deref()).await; + match (result, cleanup_result) { + (Ok(()), Ok(())) => Ok(()), + (Err(test_error), Ok(())) => Err(test_error), + (Ok(()), Err(cleanup_error)) => Err(cleanup_error), + (Err(test_error), Err(cleanup_error)) => Err(format!( + "{test_error}\ncleanup also failed: {cleanup_error}" + )), + } } diff --git a/proto/openshell.proto b/proto/openshell.proto index 58ef4e81dc..246fe0626f 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -1620,15 +1620,21 @@ message ProviderCredentialRefreshStatus { ProviderCredentialRefreshStrategy strategy = 4; string status = 5; int64 expires_at_ms = 6; + // Next automatic refresh time in Unix epoch milliseconds. A value of + // 9223372036854775807 (int64 max) means no automatic retry is scheduled; + // consumers should render it as unset and use recovery_action to determine + // the required recovery workflow. int64 next_refresh_at_ms = 7; int64 last_refresh_at_ms = 8; string last_error = 9; ProviderCredentialRefreshRecoveryAction recovery_action = 10; // Stable gateway-owned failure identifier, for example - // "oauth_invalid_grant". This is not provider-controlled prose. + // "oauth_invalid_grant". This is not provider-controlled prose and + // incorporates any recognized top-level OAuth error classification. string failure_code = 11; - // A bounded, recognized provider subtype that refines failure_code. Unknown - // provider-controlled values are not persisted or returned. + // A bounded, recognized provider subtype that refines failure_code; clients + // do not need a separate provider_error field. Unknown provider-controlled + // values are not persisted or returned. string provider_error_subtype = 12; int64 last_error_at_ms = 13; } @@ -1651,6 +1657,7 @@ message StoredProviderCredentialRefreshState { // are not automatically migrated before OpenShell 0.1.0. repeated string secret_material_keys = 7; int64 expires_at_ms = 8; + // int64 max parks the refresh until an explicit rotation or reconfiguration. int64 next_refresh_at_ms = 9; int64 last_refresh_at_ms = 10; string status = 11; diff --git a/sdk/go/openshell/v1/internal/converter/refresh.go b/sdk/go/openshell/v1/internal/converter/refresh.go index b417718a5b..be5ee8d03a 100644 --- a/sdk/go/openshell/v1/internal/converter/refresh.go +++ b/sdk/go/openshell/v1/internal/converter/refresh.go @@ -4,6 +4,9 @@ package converter import ( + "math" + "time" + "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types" pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1" ) @@ -30,6 +33,31 @@ func RefreshStrategyFromProto(s pb.ProviderCredentialRefreshStrategy) types.Refr } } +// --- RefreshRecoveryAction enum mapping --- + +// RefreshRecoveryActionFromProto converts a proto recovery action to the curated SDK type. +func RefreshRecoveryActionFromProto(a pb.ProviderCredentialRefreshRecoveryAction) types.RefreshRecoveryAction { + switch a { + case pb.ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY: + return types.RefreshRecoveryActionRetry + case pb.ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE: + return types.RefreshRecoveryActionReauthorize + case pb.ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION: + return types.RefreshRecoveryActionFixConfiguration + case pb.ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE: + return types.RefreshRecoveryActionInvestigate + default: + return types.RefreshRecoveryActionUnspecified + } +} + +func refreshNextTimeFromMillis(ms int64) time.Time { + if ms == math.MaxInt64 { + return time.Time{} + } + return TimeFromMillis(ms) +} + // RefreshStrategyToProto converts an SDK RefreshStrategy to a proto ProviderCredentialRefreshStrategy. func RefreshStrategyToProto(s types.RefreshStrategy) pb.ProviderCredentialRefreshStrategy { switch s { @@ -58,15 +86,19 @@ func RefreshStatusFromProto(s *pb.ProviderCredentialRefreshStatus) *types.Refres return nil } return &types.RefreshStatus{ - ProviderName: s.GetProviderName(), - ProviderID: s.GetProviderId(), - CredentialKey: s.GetCredentialKey(), - Strategy: RefreshStrategyFromProto(s.GetStrategy()), - Status: s.GetStatus(), - ExpiresAt: TimeFromMillis(s.GetExpiresAtMs()), - NextRefreshAt: TimeFromMillis(s.GetNextRefreshAtMs()), - LastRefreshAt: TimeFromMillis(s.GetLastRefreshAtMs()), - LastError: s.GetLastError(), + ProviderName: s.GetProviderName(), + ProviderID: s.GetProviderId(), + CredentialKey: s.GetCredentialKey(), + Strategy: RefreshStrategyFromProto(s.GetStrategy()), + Status: s.GetStatus(), + ExpiresAt: TimeFromMillis(s.GetExpiresAtMs()), + NextRefreshAt: refreshNextTimeFromMillis(s.GetNextRefreshAtMs()), + LastRefreshAt: TimeFromMillis(s.GetLastRefreshAtMs()), + LastError: s.GetLastError(), + RecoveryAction: RefreshRecoveryActionFromProto(s.GetRecoveryAction()), + FailureCode: s.GetFailureCode(), + ProviderErrorSubtype: s.GetProviderErrorSubtype(), + LastErrorAt: TimeFromMillis(s.GetLastErrorAtMs()), } } diff --git a/sdk/go/openshell/v1/internal/converter/refresh_test.go b/sdk/go/openshell/v1/internal/converter/refresh_test.go index b426857c96..9ffa6e590a 100644 --- a/sdk/go/openshell/v1/internal/converter/refresh_test.go +++ b/sdk/go/openshell/v1/internal/converter/refresh_test.go @@ -4,6 +4,7 @@ package converter import ( + "math" "testing" "time" @@ -35,6 +36,23 @@ func TestRefreshStrategyFromProto(t *testing.T) { } } +func TestRefreshRecoveryActionFromProto(t *testing.T) { + tests := []struct { + proto pb.ProviderCredentialRefreshRecoveryAction + want v1.RefreshRecoveryAction + }{ + {pb.ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_UNSPECIFIED, v1.RefreshRecoveryActionUnspecified}, + {pb.ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY, v1.RefreshRecoveryActionRetry}, + {pb.ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE, v1.RefreshRecoveryActionReauthorize}, + {pb.ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION, v1.RefreshRecoveryActionFixConfiguration}, + {pb.ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE, v1.RefreshRecoveryActionInvestigate}, + {pb.ProviderCredentialRefreshRecoveryAction(999), v1.RefreshRecoveryActionUnspecified}, + } + for _, tt := range tests { + assert.Equal(t, tt.want, RefreshRecoveryActionFromProto(tt.proto)) + } +} + func TestRefreshStrategyToProto(t *testing.T) { tests := []struct { sdk v1.RefreshStrategy @@ -60,15 +78,19 @@ func TestRefreshStrategyToProto(t *testing.T) { func TestRefreshStatusFromProto(t *testing.T) { proto := &pb.ProviderCredentialRefreshStatus{ - ProviderName: "anthropic", - ProviderId: "prov-1", - CredentialKey: "API_KEY", - Strategy: pb.ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_OAUTH2_REFRESH_TOKEN, - Status: "active", - ExpiresAtMs: 1700000000000, - NextRefreshAtMs: 1699999000000, - LastRefreshAtMs: 1699998000000, - LastError: "none", + ProviderName: "anthropic", + ProviderId: "prov-1", + CredentialKey: "API_KEY", + Strategy: pb.ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_OAUTH2_REFRESH_TOKEN, + Status: "active", + ExpiresAtMs: 1700000000000, + NextRefreshAtMs: 1699999000000, + LastRefreshAtMs: 1699998000000, + LastError: "none", + RecoveryAction: pb.ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE, + FailureCode: "oauth_invalid_grant", + ProviderErrorSubtype: "invalid_rapt", + LastErrorAtMs: 1699997000000, } status := RefreshStatusFromProto(proto) @@ -83,6 +105,10 @@ func TestRefreshStatusFromProto(t *testing.T) { assert.Equal(t, TimeFromMillis(1699999000000), status.NextRefreshAt) assert.Equal(t, TimeFromMillis(1699998000000), status.LastRefreshAt) assert.Equal(t, "none", status.LastError) + assert.Equal(t, v1.RefreshRecoveryActionReauthorize, status.RecoveryAction) + assert.Equal(t, "oauth_invalid_grant", status.FailureCode) + assert.Equal(t, "invalid_rapt", status.ProviderErrorSubtype) + assert.Equal(t, TimeFromMillis(1699997000000), status.LastErrorAt) } func TestRefreshStatusFromProto_Nil(t *testing.T) { @@ -104,6 +130,24 @@ func TestRefreshStatusFromProto_ZeroTimestamps(t *testing.T) { assert.True(t, status.ExpiresAt.IsZero()) assert.True(t, status.NextRefreshAt.IsZero()) assert.True(t, status.LastRefreshAt.IsZero()) + assert.True(t, status.LastErrorAt.IsZero()) + assert.Equal(t, v1.RefreshRecoveryActionUnspecified, status.RecoveryAction) +} + +func TestRefreshStatusFromProto_ParkedRefreshHasNoNextTime(t *testing.T) { + proto := &pb.ProviderCredentialRefreshStatus{ + ProviderName: "test", + CredentialKey: "KEY", + NextRefreshAtMs: math.MaxInt64, + RecoveryAction: pb.ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE, + } + + status := RefreshStatusFromProto(proto) + + require.NotNil(t, status) + assert.True(t, status.NextRefreshAt.IsZero()) + assert.Equal(t, v1.RefreshRecoveryActionReauthorize, status.RecoveryAction) + assert.False(t, TimeFromMillis(math.MaxInt64).IsZero()) } // --- RefreshConfig --- diff --git a/sdk/go/openshell/v1/types/refresh.go b/sdk/go/openshell/v1/types/refresh.go index b67ba35849..6ca49310e4 100644 --- a/sdk/go/openshell/v1/types/refresh.go +++ b/sdk/go/openshell/v1/types/refresh.go @@ -18,6 +18,40 @@ const ( RefreshStrategyAWSStsAssumeRole RefreshStrategy = "AWSStsAssumeRole" ) +// RefreshRecoveryAction describes the action required after a refresh failure. +type RefreshRecoveryAction int + +const ( + // RefreshRecoveryActionUnspecified means no recovery action is required. + RefreshRecoveryActionUnspecified RefreshRecoveryAction = iota + // RefreshRecoveryActionRetry means OpenShell will retry automatically. + RefreshRecoveryActionRetry + // RefreshRecoveryActionReauthorize means the user must replace the OAuth grant. + RefreshRecoveryActionReauthorize + // RefreshRecoveryActionFixConfiguration means an operator must repair configuration. + RefreshRecoveryActionFixConfiguration + // RefreshRecoveryActionInvestigate means the failure is not recognized. + RefreshRecoveryActionInvestigate +) + +// String returns the provider-neutral recovery action name. +func (a RefreshRecoveryAction) String() string { + switch a { + case RefreshRecoveryActionUnspecified: + return "unspecified" + case RefreshRecoveryActionRetry: + return "retry" + case RefreshRecoveryActionReauthorize: + return "reauthorize" + case RefreshRecoveryActionFixConfiguration: + return "fix_configuration" + case RefreshRecoveryActionInvestigate: + return "investigate" + default: + return "unknown" + } +} + // RefreshStatus reports the current state of credential refresh for a specific // provider credential. type RefreshStatus struct { @@ -27,9 +61,15 @@ type RefreshStatus struct { Strategy RefreshStrategy Status string ExpiresAt time.Time - NextRefreshAt time.Time - LastRefreshAt time.Time - LastError string + // NextRefreshAt is zero when no automatic refresh is scheduled. Use + // RecoveryAction to distinguish a parked refresh from an unset timestamp. + NextRefreshAt time.Time + LastRefreshAt time.Time + LastError string + RecoveryAction RefreshRecoveryAction + FailureCode string + ProviderErrorSubtype string + LastErrorAt time.Time } // RefreshConfig holds configuration parameters for gateway-owned credential diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 47faaa1c81..6b3b740a0b 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -5920,22 +5920,28 @@ func (x *ProviderCredentialRefresh) GetAdditionalOutputs() []*ProviderCredential } type ProviderCredentialRefreshStatus struct { - state protoimpl.MessageState `protogen:"open.v1"` - ProviderName string `protobuf:"bytes,1,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` - ProviderId string `protobuf:"bytes,2,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` - CredentialKey string `protobuf:"bytes,3,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` - Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,4,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` - Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` - ExpiresAtMs int64 `protobuf:"varint,6,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + ProviderName string `protobuf:"bytes,1,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` + ProviderId string `protobuf:"bytes,2,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` + CredentialKey string `protobuf:"bytes,3,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` + Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,4,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` + ExpiresAtMs int64 `protobuf:"varint,6,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` + // Next automatic refresh time in Unix epoch milliseconds. A value of + // 9223372036854775807 (int64 max) means no automatic retry is scheduled; + // consumers should render it as unset and use recovery_action to determine + // the required recovery workflow. NextRefreshAtMs int64 `protobuf:"varint,7,opt,name=next_refresh_at_ms,json=nextRefreshAtMs,proto3" json:"next_refresh_at_ms,omitempty"` LastRefreshAtMs int64 `protobuf:"varint,8,opt,name=last_refresh_at_ms,json=lastRefreshAtMs,proto3" json:"last_refresh_at_ms,omitempty"` LastError string `protobuf:"bytes,9,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` RecoveryAction ProviderCredentialRefreshRecoveryAction `protobuf:"varint,10,opt,name=recovery_action,json=recoveryAction,proto3,enum=openshell.v1.ProviderCredentialRefreshRecoveryAction" json:"recovery_action,omitempty"` // Stable gateway-owned failure identifier, for example - // "oauth_invalid_grant". This is not provider-controlled prose. + // "oauth_invalid_grant". This is not provider-controlled prose and + // incorporates any recognized top-level OAuth error classification. FailureCode string `protobuf:"bytes,11,opt,name=failure_code,json=failureCode,proto3" json:"failure_code,omitempty"` - // A bounded, recognized provider subtype that refines failure_code. Unknown - // provider-controlled values are not persisted or returned. + // A bounded, recognized provider subtype that refines failure_code; clients + // do not need a separate provider_error field. Unknown provider-controlled + // values are not persisted or returned. ProviderErrorSubtype string `protobuf:"bytes,12,opt,name=provider_error_subtype,json=providerErrorSubtype,proto3" json:"provider_error_subtype,omitempty"` LastErrorAtMs int64 `protobuf:"varint,13,opt,name=last_error_at_ms,json=lastErrorAtMs,proto3" json:"last_error_at_ms,omitempty"` unknownFields protoimpl.UnknownFields @@ -6120,8 +6126,9 @@ type StoredProviderCredentialRefreshState struct { // Material names classified as secret. Newly configured values live in the // active credential driver and are absent from material. Legacy inline values // are not automatically migrated before OpenShell 0.1.0. - SecretMaterialKeys []string `protobuf:"bytes,7,rep,name=secret_material_keys,json=secretMaterialKeys,proto3" json:"secret_material_keys,omitempty"` - ExpiresAtMs int64 `protobuf:"varint,8,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` + SecretMaterialKeys []string `protobuf:"bytes,7,rep,name=secret_material_keys,json=secretMaterialKeys,proto3" json:"secret_material_keys,omitempty"` + ExpiresAtMs int64 `protobuf:"varint,8,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` + // int64 max parks the refresh until an explicit rotation or reconfiguration. NextRefreshAtMs int64 `protobuf:"varint,9,opt,name=next_refresh_at_ms,json=nextRefreshAtMs,proto3" json:"next_refresh_at_ms,omitempty"` LastRefreshAtMs int64 `protobuf:"varint,10,opt,name=last_refresh_at_ms,json=lastRefreshAtMs,proto3" json:"last_refresh_at_ms,omitempty"` Status string `protobuf:"bytes,11,opt,name=status,proto3" json:"status,omitempty"` From e1f905b9a0c0169d5228a02151af0157268ec020 Mon Sep 17 00:00:00 2001 From: Mrunal Patel Date: Fri, 21 Aug 2026 16:35:37 -0700 Subject: [PATCH 4/5] fix(providers): classify post-mint refresh failures Signed-off-by: Mrunal Patel --- crates/openshell-server/src/grpc/provider.rs | 20 ++- .../openshell-server/src/provider_refresh.rs | 132 +++++++++++++++++- 2 files changed, 142 insertions(+), 10 deletions(-) diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index e20edc055f..c8c8149a82 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -4755,10 +4755,11 @@ mod tests { ImportProviderProfilesRequest, L7Allow, L7Rule, LintProviderProfilesRequest, ListProviderProfilesRequest, ListProvidersRequest, NetworkBinary, NetworkEndpoint, NetworkPolicyRule, ProviderCredentialRefresh, ProviderCredentialRefreshMaterial, - ProviderCredentialTokenGrant, ProviderCredentialTokenGrantAudienceOverride, - ProviderProfile, ProviderProfileCategory, ProviderProfileCredential, - ProviderProfileImportItem, RotateProviderCredentialRequest, Sandbox, SandboxPolicy, - SandboxSpec, StoredProviderProfile, UpdateProviderProfilesRequest, UpdateProviderRequest, + ProviderCredentialRefreshRecoveryAction, ProviderCredentialTokenGrant, + ProviderCredentialTokenGrantAudienceOverride, ProviderProfile, ProviderProfileCategory, + ProviderProfileCredential, ProviderProfileImportItem, RotateProviderCredentialRequest, + Sandbox, SandboxPolicy, SandboxSpec, StoredProviderProfile, UpdateProviderProfilesRequest, + UpdateProviderRequest, }; use openshell_core::{ObjectId, ObjectName}; use tonic::{Code, Request}; @@ -11988,7 +11989,16 @@ mod tests { .await .unwrap() .expect("refresh state should exist"); - assert_eq!(refresh_state.status, "error"); + assert_eq!(refresh_state.status, "configuration_required"); + assert_eq!( + refresh_state.recovery_action, + ProviderCredentialRefreshRecoveryAction::FixConfiguration as i32 + ); + assert_eq!(refresh_state.failure_code, "refresh_configuration_invalid"); + assert_eq!( + refresh_state.next_refresh_at_ms - refresh_state.last_error_at_ms, + 60 * 60 * 1000 + ); } #[tokio::test] diff --git a/crates/openshell-server/src/provider_refresh.rs b/crates/openshell-server/src/provider_refresh.rs index 40fad0b2a9..58297b47eb 100644 --- a/crates/openshell-server/src/provider_refresh.rs +++ b/crates/openshell-server/src/provider_refresh.rs @@ -504,6 +504,10 @@ impl RefreshFailure { fn into_status(self) -> Status { self.status } + + fn from_status(status: &Status) -> Self { + Self::from(Status::new(status.code(), status.message().to_string())) + } } impl From for RefreshFailure { @@ -676,7 +680,7 @@ async fn cleanup_pending_secret_deletions( Ok(new_version) } -async fn persist_refresh_error_state( +async fn persist_retryable_refresh_error_state( store: &Store, state: &mut StoredProviderCredentialRefreshState, expected_version: u64, @@ -826,7 +830,8 @@ pub async fn refresh_provider_credential( // Otherwise disabling providers_v2_enabled leaves already-configured refresh // states that the worker and manual rotation keep minting from. if let Err(err) = ensure_refresh_providers_v2_gate(store, &state).await { - persist_refresh_error_state(store, &mut state, expected_version, &err).await?; + let failure = RefreshFailure::from_status(&err); + persist_refresh_failure_state(store, &mut state, expected_version, &failure).await?; warn!( provider = %state.provider_name, credential_key = %state.credential_key, @@ -974,7 +979,13 @@ pub async fn refresh_provider_credential( // upstream-valid grant. If that also fails, leave the // staged object intact for operator recovery rather than // deleting an irreplaceable rotated token. - persist_refresh_error_state(store, &mut state, expected_version, &err).await?; + persist_retryable_refresh_error_state( + store, + &mut state, + expected_version, + &err, + ) + .await?; return Err(err); } }; @@ -993,7 +1004,8 @@ pub async fn refresh_provider_credential( { // Reflect the failure on the state we just wrote; skip silently // if it was deleted concurrently (it is not recreated). - persist_refresh_error_state(store, &mut state, new_version, &err).await?; + let failure = RefreshFailure::from_status(&err); + persist_refresh_failure_state(store, &mut state, new_version, &failure).await?; warn!( provider = %state.provider_name, credential_key = %state.credential_key, @@ -1720,6 +1732,21 @@ fn classify_oauth_token_error( "admin_policy_enforced", ) } + "access_denied" if grant_kind == OAuthGrantKind::UserRefreshToken => { + RefreshFailure::reauthorize( + Status::failed_precondition( + "OAuth access was denied; user reauthorization is required (access_denied)", + ), + "oauth_access_denied", + None, + ) + } + "access_denied" => RefreshFailure::fix_configuration( + Status::failed_precondition( + "OAuth access was denied for the non-interactive grant (access_denied)", + ), + "oauth_access_denied", + ), "invalid_grant" => RefreshFailure::fix_configuration( Status::failed_precondition( "OAuth token endpoint rejected the non-interactive grant (invalid_grant)", @@ -2295,6 +2322,36 @@ mod tests { assert_eq!(failure.retry_schedule, RefreshRetrySchedule::Configuration); } + #[test] + fn oauth_access_denied_recovery_depends_on_grant_kind() { + let user_failure = classify_oauth_token_error( + reqwest::StatusCode::FORBIDDEN, + br#"{"error":"access_denied"}"#, + OAuthGrantKind::UserRefreshToken, + ); + assert_eq!( + user_failure.recovery_action, + ProviderCredentialRefreshRecoveryAction::Reauthorize + ); + assert_eq!(user_failure.failure_code, "oauth_access_denied"); + assert_eq!(user_failure.retry_schedule, RefreshRetrySchedule::Parked); + + let service_failure = classify_oauth_token_error( + reqwest::StatusCode::FORBIDDEN, + br#"{"error":"access_denied"}"#, + OAuthGrantKind::NonInteractive, + ); + assert_eq!( + service_failure.recovery_action, + ProviderCredentialRefreshRecoveryAction::FixConfiguration + ); + assert_eq!(service_failure.failure_code, "oauth_access_denied"); + assert_eq!( + service_failure.retry_schedule, + RefreshRetrySchedule::Configuration + ); + } + #[test] fn oauth_server_failure_remains_retryable() { let failure = classify_oauth_token_error( @@ -2435,6 +2492,62 @@ mod tests { ); } + #[tokio::test] + async fn disabled_provider_gate_requires_configuration_and_retries_hourly() { + let store = test_store().await; + let provider = provider("disabled-provider-gate", "aws-s3"); + store.put_message(&provider).await.unwrap(); + let state = new_refresh_state( + &provider, + "default", + "AWS_ACCESS_KEY_ID", + NewRefreshStateConfig { + strategy: ProviderCredentialRefreshStrategy::AwsStsAssumeRole, + material: HashMap::from([( + "role_arn".to_string(), + "arn:aws:iam::123456789012:role/test".to_string(), + )]), + secret_material_keys: Vec::new(), + expires_at_ms: 0, + token_url: String::new(), + scopes: Vec::new(), + refresh_before_seconds: 30, + max_lifetime_seconds: 60, + additional_output_keys: HashMap::new(), + }, + ) + .unwrap(); + put_refresh_state(&store, &state).await.unwrap(); + + let err = refresh_provider_credential( + &store, + "default", + &test_credentials(), + None, + "disabled-provider-gate", + "AWS_ACCESS_KEY_ID", + ) + .await + .unwrap_err(); + + assert_eq!(err.code(), tonic::Code::FailedPrecondition); + let stored = + get_refresh_state(&store, "default", provider.object_id(), "AWS_ACCESS_KEY_ID") + .await + .unwrap() + .unwrap(); + assert_eq!(stored.status, "configuration_required"); + assert_eq!( + stored.recovery_action, + ProviderCredentialRefreshRecoveryAction::FixConfiguration as i32 + ); + assert_eq!(stored.failure_code, "refresh_configuration_invalid"); + assert_eq!( + stored.next_refresh_at_ms - stored.last_error_at_ms, + 60 * 60 * 1000 + ); + } + #[tokio::test] async fn oauth_invalid_grant_persists_terminal_reauthorization_status() { let mock_server = MockServer::start().await; @@ -2795,7 +2908,16 @@ mod tests { .await .unwrap() .unwrap(); - assert_eq!(stored_state.status, "error"); + assert_eq!(stored_state.status, "configuration_required"); + assert_eq!( + stored_state.recovery_action, + ProviderCredentialRefreshRecoveryAction::FixConfiguration as i32 + ); + assert_eq!(stored_state.failure_code, "refresh_configuration_invalid"); + assert_eq!( + stored_state.next_refresh_at_ms - stored_state.last_error_at_ms, + 60 * 60 * 1000 + ); assert!(stored_state.last_error.contains("MS_GRAPH_ACCESS_TOKEN")); let stored_provider = store .get_message_by_name::("default", "refreshing-graph") From ad9591ef2f78c121db2e950dd1f6fbc65077568e Mon Sep 17 00:00:00 2001 From: Mrunal Patel Date: Mon, 24 Aug 2026 07:11:17 -0700 Subject: [PATCH 5/5] fix(providers): tolerate malformed OAuth subtypes Signed-off-by: Mrunal Patel --- .../openshell-server/src/provider_refresh.rs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/openshell-server/src/provider_refresh.rs b/crates/openshell-server/src/provider_refresh.rs index 58297b47eb..176f86ae22 100644 --- a/crates/openshell-server/src/provider_refresh.rs +++ b/crates/openshell-server/src/provider_refresh.rs @@ -417,9 +417,18 @@ struct TokenResponse { #[derive(Debug, Deserialize)] struct OAuthErrorResponse { error: String, + #[serde(default, deserialize_with = "deserialize_optional_oauth_string")] error_subtype: Option, } +fn deserialize_optional_oauth_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = serde_json::Value::deserialize(deserializer)?; + Ok(value.as_str().map(str::to_owned)) +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum OAuthGrantKind { UserRefreshToken, @@ -2286,6 +2295,23 @@ mod tests { ); } + #[test] + fn oauth_invalid_grant_ignores_non_string_optional_subtype() { + let failure = classify_oauth_token_error( + reqwest::StatusCode::BAD_REQUEST, + br#"{"error":"invalid_grant","error_subtype":{"vendor":"value"}}"#, + OAuthGrantKind::UserRefreshToken, + ); + + assert_eq!( + failure.recovery_action, + ProviderCredentialRefreshRecoveryAction::Reauthorize + ); + assert_eq!(failure.failure_code, "oauth_invalid_grant"); + assert_eq!(failure.provider_error_subtype, None); + assert_eq!(failure.retry_schedule, RefreshRetrySchedule::Parked); + } + #[test] fn oauth_noninteractive_invalid_grant_requires_configuration_fix() { let failure = classify_oauth_token_error(