diff --git a/crates/openshell-server/src/config_delivery.rs b/crates/openshell-server/src/config_delivery.rs deleted file mode 100644 index a28878213..000000000 --- a/crates/openshell-server/src/config_delivery.rs +++ /dev/null @@ -1,1417 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Build and route complete supervisor configuration snapshots. - -use std::collections::HashMap; -use std::collections::hash_map::Entry; -use std::fmt; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -use futures::StreamExt; -use metrics::counter; -use openshell_core::config::ConfigDeliveryMode; -use openshell_core::proto::{ - ConfigBootstrap, PeerConfigSandboxTarget, PeerConfigUpdateHintRequest, - PeerConfigUpdateHintResponse, ProviderEnvironmentSnapshot, Sandbox, SandboxConfigSnapshot, - peer_config_update_hint_request, -}; -use tokio::sync::{OwnedSemaphorePermit, Semaphore}; -use tonic::{Code, Request, Response, Status}; -use tracing::warn; - -use crate::ServerState; -use crate::auth::principal::Principal; -use crate::grpc::policy::{build_provider_environment_snapshot, build_sandbox_config_snapshot}; -use crate::persistence::ObjectWorkspace; -use crate::supervisor_owner::{OWNER_TTL, SupervisorOwnerIndex}; -use crate::supervisor_session::SupervisorSessionRegistry; - -/// Leaves headroom below tonic's default 4 MiB decode limit for framing and -/// future envelope fields. -pub const MAX_SUPERVISOR_CONFIG_MESSAGE_BYTES: usize = 3 * 1024 * 1024; -const CONFIG_SNAPSHOT_BUILD_TIMEOUT: Duration = Duration::from_secs(45); -// Stage 1 bootstrap is optional. Keep credential backend stalls well below -// the 15-second relay session-wait budget while polling remains authoritative. -const CONFIG_BOOTSTRAP_BUILD_TIMEOUT: Duration = Duration::from_secs(1); -const MAX_ACTIVE_FANOUT_WORKERS: usize = 64; -/// Concurrent snapshot builds allowed per pooled database connection. Builds -/// are short bursts of small queries, so a little oversubscription keeps the -/// pool busy without stacking every waiter on the acquire timeout. -const SNAPSHOT_BUILDS_PER_DB_CONNECTION: usize = 2; -const MIN_CONCURRENT_SNAPSHOT_BUILDS: usize = 4; - -/// One complete configuration component awaiting delivery to a supervisor. -#[derive(Clone)] -pub enum SupervisorConfigMessage { - SandboxConfig(Box), - ProviderEnvironment(ProviderEnvironmentSnapshot), -} - -impl SupervisorConfigMessage { - pub(crate) fn component_name(&self) -> &'static str { - match self { - Self::SandboxConfig(_) => "sandbox_config", - Self::ProviderEnvironment(_) => "provider_environment", - } - } -} - -impl fmt::Debug for SupervisorConfigMessage { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(match self { - Self::SandboxConfig(_) => "SandboxConfig()", - Self::ProviderEnvironment(_) => "ProviderEnvironment()", - }) - } -} - -/// Result of routing one configuration snapshot toward a supervisor session. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DeliveryDisposition { - Enqueued, - NoActiveSession, - UnsupportedSession, - QueueFull, - SessionClosed, - PayloadTooLarge, -} - -/// Transport boundary for configuration delivery. -#[tonic::async_trait] -pub trait SupervisorConfigRouter: fmt::Debug + Send + Sync { - async fn deliver( - &self, - sandbox_id: &str, - message: SupervisorConfigMessage, - ) -> DeliveryDisposition; - - async fn routable_sandbox_ids(&self) -> Vec; -} - -#[derive(Debug)] -pub struct LocalSupervisorConfigRouter { - sessions: Arc, -} - -impl LocalSupervisorConfigRouter { - #[must_use] - pub fn new(sessions: Arc) -> Self { - Self { sessions } - } -} - -#[tonic::async_trait] -impl SupervisorConfigRouter for LocalSupervisorConfigRouter { - async fn deliver( - &self, - sandbox_id: &str, - message: SupervisorConfigMessage, - ) -> DeliveryDisposition { - self.sessions.deliver_config(sandbox_id, message) - } - - async fn routable_sandbox_ids(&self) -> Vec { - self.sessions.connected_sandbox_ids() - } -} - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct ConfigComponents { - pub sandbox_config: bool, - pub provider_environment: bool, -} - -impl ConfigComponents { - pub const ALL: Self = Self { - sandbox_config: true, - provider_environment: true, - }; - - pub const SANDBOX_AND_PROVIDER: Self = Self { - sandbox_config: true, - provider_environment: true, - }; - - pub const SANDBOX_CONFIG: Self = Self { - sandbox_config: true, - provider_environment: false, - }; - - fn selected(self) -> impl Iterator { - [ - (self.sandbox_config, ConfigComponentKind::SandboxConfig), - ( - self.provider_environment, - ConfigComponentKind::ProviderEnvironment, - ), - ] - .into_iter() - .filter_map(|(selected, component)| selected.then_some(component)) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -enum ConfigComponentKind { - SandboxConfig, - ProviderEnvironment, -} - -impl ConfigComponentKind { - fn name(self) -> &'static str { - match self { - Self::SandboxConfig => "sandbox_config", - Self::ProviderEnvironment => "provider_environment", - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -struct DeliveryKey { - sandbox_id: String, - component: ConfigComponentKind, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -enum FanoutScope { - Workspace(String), - AllConnected, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -struct FanoutKey { - scope: FanoutScope, - component: ConfigComponentKind, - route_remote: bool, -} - -/// Coalesces publications and bounds workers per sandbox and component. -/// -/// The map entry is also the worker lease. Its boolean is set when another -/// mutation arrives during a build or route operation. The worker then rebuilds -/// the current full snapshot once, regardless of how many mutations arrived. -/// -/// Each pending map entry owns a delivery permit for its worker. Direct -/// publications fail fast when all permits are held. Fanout workers wait for a -/// permit before admitting the next recipient, which bounds both spawned tasks -/// and pending keys while still walking the full recipient list. -#[derive(Debug)] -pub struct ConfigDeliveryQueue { - pending: Mutex>, - fanout_pending: Mutex>, - delivery_permits: Arc, - build_permits: Semaphore, -} - -impl Default for ConfigDeliveryQueue { - fn default() -> Self { - Self::new(MIN_CONCURRENT_SNAPSHOT_BUILDS) - } -} - -impl ConfigDeliveryQueue { - #[must_use] - pub fn new(max_concurrent_builds: usize) -> Self { - let max_concurrent_builds = max_concurrent_builds.max(1); - Self { - pending: Mutex::default(), - fanout_pending: Mutex::default(), - delivery_permits: Arc::new(Semaphore::new(max_concurrent_builds)), - build_permits: Semaphore::new(max_concurrent_builds), - } - } - - /// Size the build bound from the persistence pool that every build reads. - #[must_use] - pub fn for_db_connections(max_connections: u32) -> Self { - let max_connections = usize::try_from(max_connections).unwrap_or(usize::MAX); - Self::new( - max_connections - .saturating_mul(SNAPSHOT_BUILDS_PER_DB_CONNECTION) - .max(MIN_CONCURRENT_SNAPSHOT_BUILDS), - ) - } - - #[cfg(test)] - fn max_concurrent_builds(&self) -> usize { - self.build_permits.available_permits() - } - - /// Run one snapshot build under the concurrency bound. The deadline starts - /// only once a permit is held so queued builds do not spend their budget - /// waiting. - async fn run_bounded_build( - &self, - build: impl Future, - ) -> Result { - let _permit = self - .build_permits - .acquire() - .await - .expect("snapshot build semaphore is never closed"); - tokio::time::timeout(CONFIG_SNAPSHOT_BUILD_TIMEOUT, build).await - } - - fn enqueue(&self, key: DeliveryKey) -> DeliveryEnqueue { - let mut pending = self.pending.lock().unwrap(); - match pending.entry(key) { - Entry::Occupied(mut entry) => { - *entry.get_mut() = true; - DeliveryEnqueue::Coalesced - } - Entry::Vacant(entry) => { - let Ok(permit) = Arc::clone(&self.delivery_permits).try_acquire_owned() else { - return DeliveryEnqueue::Full; - }; - entry.insert(true); - DeliveryEnqueue::StartWorker(permit) - } - } - } - - async fn enqueue_from_fanout(&self, key: DeliveryKey) -> DeliveryEnqueue { - { - let mut pending = self.pending.lock().unwrap(); - if let Entry::Occupied(mut entry) = pending.entry(key.clone()) { - *entry.get_mut() = true; - return DeliveryEnqueue::Coalesced; - } - } - - let permit = Arc::clone(&self.delivery_permits) - .acquire_owned() - .await - .expect("delivery worker semaphore is never closed"); - let mut pending = self.pending.lock().unwrap(); - match pending.entry(key) { - Entry::Occupied(mut entry) => { - *entry.get_mut() = true; - DeliveryEnqueue::Coalesced - } - Entry::Vacant(entry) => { - entry.insert(true); - DeliveryEnqueue::StartWorker(permit) - } - } - } - - fn take(&self, key: &DeliveryKey) { - let mut pending = self.pending.lock().unwrap(); - if let Some(changed) = pending.get_mut(key) { - *changed = false; - } - } - - fn finish_pass(&self, key: &DeliveryKey) -> bool { - let mut pending = self.pending.lock().unwrap(); - if pending.get(key).is_some_and(|changed| !changed) { - pending.remove(key); - false - } else { - pending.contains_key(key) - } - } - - fn enqueue_fanout(&self, key: FanoutKey) -> FanoutEnqueue { - let mut pending = self.fanout_pending.lock().unwrap(); - if let Some(changed) = pending.get_mut(&key) { - *changed = true; - return FanoutEnqueue::Coalesced; - } - if pending.len() >= MAX_ACTIVE_FANOUT_WORKERS { - FanoutEnqueue::Full - } else { - pending.insert(key, true); - FanoutEnqueue::StartWorker - } - } - - fn take_fanout(&self, key: &FanoutKey) { - let mut pending = self.fanout_pending.lock().unwrap(); - if let Some(changed) = pending.get_mut(key) { - *changed = false; - } - } - - fn finish_fanout_pass(&self, key: &FanoutKey) -> bool { - let mut pending = self.fanout_pending.lock().unwrap(); - if pending.get(key).is_some_and(|changed| !changed) { - pending.remove(key); - false - } else { - pending.contains_key(key) - } - } -} - -#[derive(Debug)] -enum DeliveryEnqueue { - StartWorker(OwnedSemaphorePermit), - Coalesced, - Full, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum FanoutEnqueue { - StartWorker, - Coalesced, - Full, -} - -pub async fn build_config_bootstrap( - state: &Arc, - sandbox: &Sandbox, -) -> Result { - tokio::time::timeout( - CONFIG_BOOTSTRAP_BUILD_TIMEOUT, - build_consistent_config_bootstrap(state, sandbox), - ) - .await - .map_err(|_| Status::deadline_exceeded("supervisor configuration bootstrap timed out"))? -} - -async fn build_consistent_config_bootstrap( - state: &Arc, - sandbox: &Sandbox, -) -> Result { - const MAX_BUILD_ATTEMPTS: usize = 3; - for _ in 0..MAX_BUILD_ATTEMPTS { - // Components are independent projections. The provider revision is a - // fence for the only overlapping input between sandbox configuration - // and provider environment state. - let (sandbox_config, provider_environment) = tokio::join!( - build_sandbox_config_snapshot(state, sandbox), - build_provider_environment_snapshot(state, sandbox, true), - ); - let bootstrap = ConfigBootstrap { - sandbox_config: Some(sandbox_config?), - provider_environment: Some(provider_environment?), - }; - if bootstrap_revisions_match(&bootstrap) { - return Ok(bootstrap); - } - counter!("openshell_supervisor_config_bootstrap_revision_mismatches_total").increment(1); - } - Err(Status::aborted( - "configuration changed while building supervisor bootstrap", - )) -} - -fn bootstrap_revisions_match(bootstrap: &ConfigBootstrap) -> bool { - bootstrap - .sandbox_config - .as_ref() - .zip(bootstrap.provider_environment.as_ref()) - .is_some_and(|(sandbox, provider)| { - sandbox.provider_env_revision == provider.provider_env_revision - }) -} - -pub fn publish_sandbox_components( - state: &Arc, - sandbox_id: &str, - components: ConfigComponents, -) { - if state.config.config_delivery_mode != ConfigDeliveryMode::Push { - return; - } - enqueue_sandbox(state, sandbox_id, components); -} - -fn enqueue_sandbox( - state: &Arc, - sandbox_id: &str, - components: ConfigComponents, -) -> bool { - let mut accepted = true; - for component in components.selected() { - let key = DeliveryKey { - sandbox_id: sandbox_id.to_string(), - component, - }; - match state.config_delivery_queue.enqueue(key.clone()) { - DeliveryEnqueue::StartWorker(permit) => { - spawn_delivery_worker(state, key, permit); - } - DeliveryEnqueue::Coalesced => {} - DeliveryEnqueue::Full => { - accepted = false; - record_delivery_worker_full(sandbox_id, component.name()); - } - } - } - accepted -} - -fn spawn_delivery_worker(state: &Arc, key: DeliveryKey, permit: OwnedSemaphorePermit) { - let state = Arc::clone(state); - tokio::spawn(async move { - let _permit = permit; - loop { - state.config_delivery_queue.take(&key); - publish_sandbox_component_now(&state, &key).await; - if !state.config_delivery_queue.finish_pass(&key) { - break; - } - } - }); -} - -async fn enqueue_sandbox_from_fanout( - state: &Arc, - sandbox_id: &str, - component: ConfigComponentKind, -) { - let key = DeliveryKey { - sandbox_id: sandbox_id.to_string(), - component, - }; - match state - .config_delivery_queue - .enqueue_from_fanout(key.clone()) - .await - { - DeliveryEnqueue::StartWorker(permit) => spawn_delivery_worker(state, key, permit), - DeliveryEnqueue::Coalesced => {} - DeliveryEnqueue::Full => unreachable!("fanout waits for delivery worker capacity"), - } -} - -async fn publish_sandbox_component_now(state: &Arc, key: &DeliveryKey) { - if route_remote_sandbox_component(state, key).await { - return; - } - if !is_current_local_owner(state, &key.sandbox_id).await { - return; - } - if !state - .supervisor_sessions - .is_config_push_capable(&key.sandbox_id) - { - return; - } - let component = key.component.name(); - let build = async { - let sandbox = state - .store - .get_message::(&key.sandbox_id) - .await - .map_err(|error| Status::internal(format!("fetch sandbox failed: {error}")))?; - let Some(sandbox) = sandbox else { - return Ok(None); - }; - match key.component { - ConfigComponentKind::SandboxConfig => build_sandbox_config_snapshot(state, &sandbox) - .await - .map(|snapshot| SupervisorConfigMessage::SandboxConfig(Box::new(snapshot))), - ConfigComponentKind::ProviderEnvironment => { - build_provider_environment_snapshot(state, &sandbox, true) - .await - .map(SupervisorConfigMessage::ProviderEnvironment) - } - } - .map(Some) - }; - match state.config_delivery_queue.run_bounded_build(build).await { - Ok(Ok(None)) => {} - Ok(Ok(Some(message))) => { - if !is_current_local_owner(state, &key.sandbox_id).await { - return; - } - let disposition = state - .supervisor_config_router() - .deliver(&key.sandbox_id, message) - .await; - record_delivery(component, disposition); - } - Ok(Err(error)) => { - record_build_failure(&key.sandbox_id, component, error.code()); - } - Err(_) => { - record_build_failure(&key.sandbox_id, component, Code::DeadlineExceeded); - } - } -} - -async fn is_current_local_owner(state: &Arc, sandbox_id: &str) -> bool { - let owners = SupervisorOwnerIndex::new(Arc::clone(&state.store), OWNER_TTL); - match owners.read(sandbox_id).await { - Ok(Some(owner)) => { - owner.is_fresh(OWNER_TTL) - && owner.owner_replica_id == state.replica_id - && state - .supervisor_sessions - .is_current_session(sandbox_id, &owner.session_id) - } - Ok(None) => false, - Err(error) => { - warn!(sandbox_id, error = %error, "configuration owner lookup failed"); - false - } - } -} - -/// Return true when the current owner is remote or the session is gone. -/// The owner builds its own snapshot after receiving this secret-free hint. -async fn route_remote_sandbox_component(state: &Arc, key: &DeliveryKey) -> bool { - let owners = SupervisorOwnerIndex::new(Arc::clone(&state.store), OWNER_TTL); - let mut final_outcome = "stale_owner"; - for attempt in 0..2 { - let owner = match owners.read(&key.sandbox_id).await { - Ok(Some(owner)) if owner.is_fresh(OWNER_TTL) => owner, - Ok(_) => return true, - Err(error) => { - warn!(sandbox_id = %key.sandbox_id, error = %error, "configuration owner lookup failed"); - return true; - } - }; - if owner.owner_replica_id == state.replica_id { - return !state - .supervisor_sessions - .is_current_session(&key.sandbox_id, &owner.session_id); - } - if !owner.owner_peer_endpoint.starts_with("http://") - && !owner.owner_peer_endpoint.starts_with("https://") - { - warn!(sandbox_id = %key.sandbox_id, owner = %owner.owner_replica_id, "configuration owner has no reachable peer endpoint"); - return true; - } - let request = PeerConfigUpdateHintRequest { - scope: Some(peer_config_update_hint_request::Scope::Sandbox( - PeerConfigSandboxTarget { - sandbox_id: key.sandbox_id.clone(), - session_id: owner.session_id, - }, - )), - sandbox_config: key.component == ConfigComponentKind::SandboxConfig, - provider_environment: key.component == ConfigComponentKind::ProviderEnvironment, - }; - let result = tokio::time::timeout( - Duration::from_secs(5), - crate::supervisor_session::forward_config_hint_to_peer( - state, - &owner.owner_peer_endpoint, - request, - ), - ) - .await; - match result { - Ok(Ok(response)) if !response.stale_owner && !response.queue_full => { - counter!("openshell_supervisor_config_peer_hints_total", "outcome" => "accepted") - .increment(1); - return true; - } - Ok(Ok(response)) if response.queue_full => { - counter!("openshell_supervisor_config_peer_hints_total", "outcome" => "queue_full") - .increment(1); - return true; - } - Ok(Err(error)) if error.code() == Code::Unimplemented => { - counter!("openshell_supervisor_config_peer_hints_total", "outcome" => "unsupported_peer") - .increment(1); - return true; - } - Ok(Err(error)) => { - final_outcome = "peer_error"; - warn!(sandbox_id = %key.sandbox_id, owner = %owner.owner_replica_id, attempt, code = ?error.code(), "configuration peer hint failed"); - } - Err(_) => { - final_outcome = "timeout"; - warn!(sandbox_id = %key.sandbox_id, owner = %owner.owner_replica_id, attempt, "configuration peer hint timed out"); - } - Ok(Ok(_)) => final_outcome = "stale_owner", - } - } - counter!("openshell_supervisor_config_peer_hints_total", "outcome" => final_outcome) - .increment(1); - true -} - -pub fn publish_workspace_components( - state: &Arc, - workspace: &str, - components: ConfigComponents, -) { - if state.config.config_delivery_mode != ConfigDeliveryMode::Push { - return; - } - enqueue_fanout( - state, - FanoutScope::Workspace(workspace.to_string()), - components, - true, - ); -} - -pub fn publish_all_connected(state: &Arc, components: ConfigComponents) { - if state.config.config_delivery_mode != ConfigDeliveryMode::Push { - return; - } - enqueue_fanout(state, FanoutScope::AllConnected, components, true); -} - -fn enqueue_fanout( - state: &Arc, - scope: FanoutScope, - components: ConfigComponents, - route_remote: bool, -) -> bool { - let mut accepted = true; - for component in components.selected() { - let key = FanoutKey { - scope: scope.clone(), - component, - route_remote, - }; - match state.config_delivery_queue.enqueue_fanout(key.clone()) { - FanoutEnqueue::StartWorker => { - let state = Arc::clone(state); - tokio::spawn(async move { - loop { - state.config_delivery_queue.take_fanout(&key); - publish_fanout_now(&state, &key).await; - if !state.config_delivery_queue.finish_fanout_pass(&key) { - break; - } - } - }); - } - FanoutEnqueue::Coalesced => {} - FanoutEnqueue::Full => { - accepted = false; - counter!("openshell_supervisor_config_fanout_total", "outcome" => "queue_full") - .increment(1); - warn!( - component = component.name(), - "supervisor configuration fanout queue is full" - ); - } - } - } - accepted -} - -async fn publish_fanout_now(state: &Arc, key: &FanoutKey) { - if key.route_remote { - notify_remote_fanout(state, key).await; - } - let sandbox_ids = state - .supervisor_config_router() - .routable_sandbox_ids() - .await; - for sandbox_id in sandbox_ids { - if let FanoutScope::Workspace(workspace) = &key.scope { - let sandbox = match state.store.get_message::(&sandbox_id).await { - Ok(Some(sandbox)) => sandbox, - Ok(None) => continue, - Err(_) => { - record_build_failure(&sandbox_id, "sandbox", Code::Internal); - continue; - } - }; - if sandbox.object_workspace() != workspace { - continue; - } - } - enqueue_sandbox_from_fanout(state, &sandbox_id, key.component).await; - } -} - -async fn notify_remote_fanout(state: &Arc, key: &FanoutKey) { - let owners = SupervisorOwnerIndex::new(Arc::clone(&state.store), OWNER_TTL); - let endpoints = match owners.list_fresh_peer_endpoints(&state.replica_id).await { - Ok(endpoints) => endpoints, - Err(error) => { - warn!(error = %error, "configuration peer fanout owner listing failed"); - return; - } - }; - futures::stream::iter(endpoints) - .for_each_concurrent(8, |endpoint| async move { - let scope = match &key.scope { - FanoutScope::Workspace(workspace) => { - peer_config_update_hint_request::Scope::Workspace(workspace.clone()) - } - FanoutScope::AllConnected => { - peer_config_update_hint_request::Scope::AllConnected(true) - } - }; - let request = PeerConfigUpdateHintRequest { - scope: Some(scope), - sandbox_config: key.component == ConfigComponentKind::SandboxConfig, - provider_environment: key.component == ConfigComponentKind::ProviderEnvironment, - }; - let result = tokio::time::timeout( - Duration::from_secs(5), - crate::supervisor_session::forward_config_hint_to_peer(state, &endpoint, request), - ) - .await; - match result { - Ok(Ok(response)) if !response.queue_full => { - counter!("openshell_supervisor_config_peer_hints_total", "outcome" => "accepted") - .increment(1); - } - Ok(Ok(_)) => { - counter!("openshell_supervisor_config_peer_hints_total", "outcome" => "queue_full") - .increment(1); - } - Ok(Err(error)) => { - warn!(endpoint = %endpoint, code = ?error.code(), "configuration peer fanout hint failed"); - } - Err(_) => { - warn!(endpoint = %endpoint, "configuration peer fanout hint timed out"); - } - } - }) - .await; -} - -pub async fn handle_peer_config_update_hint( - state: &Arc, - request: Request, -) -> Result, Status> { - if !matches!( - request.extensions().get::(), - Some(Principal::Peer(_)) - ) { - return Err(Status::permission_denied("gateway peer principal required")); - } - if state.config.config_delivery_mode != ConfigDeliveryMode::Push { - return Err(Status::failed_precondition( - "configuration push is disabled", - )); - } - let hint = request.into_inner(); - let components = ConfigComponents { - sandbox_config: hint.sandbox_config, - provider_environment: hint.provider_environment, - }; - if !components.sandbox_config && !components.provider_environment { - return Err(Status::invalid_argument( - "at least one configuration component is required", - )); - } - let mut response = PeerConfigUpdateHintResponse::default(); - match hint.scope { - Some(peer_config_update_hint_request::Scope::Sandbox(target)) => { - if target.sandbox_id.is_empty() || target.session_id.is_empty() { - return Err(Status::invalid_argument( - "sandbox and session IDs are required", - )); - } - let owners = SupervisorOwnerIndex::new(Arc::clone(&state.store), OWNER_TTL); - let owner = owners - .read(&target.sandbox_id) - .await - .map_err(|_| Status::unavailable("configuration owner lookup failed"))?; - response.stale_owner = owner.is_none_or(|owner| { - !owner.is_fresh(OWNER_TTL) - || owner.owner_replica_id != state.replica_id - || owner.session_id != target.session_id - || !state - .supervisor_sessions - .is_current_session(&target.sandbox_id, &target.session_id) - }); - if !response.stale_owner { - response.queue_full = !enqueue_sandbox(state, &target.sandbox_id, components); - } - } - Some(peer_config_update_hint_request::Scope::Workspace(workspace)) => { - if workspace.is_empty() { - return Err(Status::invalid_argument("workspace is required")); - } - response.queue_full = - !enqueue_fanout(state, FanoutScope::Workspace(workspace), components, false); - } - Some(peer_config_update_hint_request::Scope::AllConnected(true)) => { - response.queue_full = - !enqueue_fanout(state, FanoutScope::AllConnected, components, false); - } - _ => { - return Err(Status::invalid_argument( - "configuration hint scope is required", - )); - } - } - Ok(Response::new(response)) -} - -fn record_delivery_worker_full(sandbox_id: &str, component: &'static str) { - counter!( - "openshell_supervisor_config_delivery_workers_total", - "outcome" => "queue_full", - ) - .increment(1); - warn!( - sandbox_id, - component, "supervisor configuration delivery worker queue is full" - ); -} - -fn record_delivery(component: &'static str, disposition: DeliveryDisposition) { - let outcome = match disposition { - DeliveryDisposition::Enqueued => "enqueued", - DeliveryDisposition::NoActiveSession => "no_active_session", - DeliveryDisposition::UnsupportedSession => "unsupported_session", - DeliveryDisposition::QueueFull => "queue_full", - DeliveryDisposition::SessionClosed => "session_closed", - DeliveryDisposition::PayloadTooLarge => "payload_too_large", - }; - counter!( - "openshell_supervisor_config_deliveries_total", - "component" => component, - "outcome" => outcome, - ) - .increment(1); -} - -fn record_build_failure(sandbox_id: &str, component: &'static str, error_code: Code) { - counter!( - "openshell_supervisor_config_snapshot_failures_total", - "component" => component, - ) - .increment(1); - warn!( - sandbox_id = %sandbox_id, - component, - ?error_code, - "failed to build supervisor configuration snapshot" - ); -} - -#[cfg(test)] -mod tests { - use std::sync::atomic::{AtomicUsize, Ordering}; - - use super::*; - use crate::grpc::test_support::{connect_supervisor_stream, test_server_state}; - use openshell_core::proto::{GatewayMessage, ObjectMeta, SandboxSpec, gateway_message}; - - fn peer_hint(sandbox_id: &str, session_id: &str) -> Request { - let mut request = Request::new(PeerConfigUpdateHintRequest { - scope: Some(peer_config_update_hint_request::Scope::Sandbox( - PeerConfigSandboxTarget { - sandbox_id: sandbox_id.into(), - session_id: session_id.into(), - }, - )), - sandbox_config: true, - provider_environment: false, - }); - request - .extensions_mut() - .insert(Principal::Peer(crate::auth::principal::PeerPrincipal { - replica_id: "other-replica".into(), - pod_uid: "peer-pod".into(), - })); - request - } - - #[tokio::test] - async fn peer_hint_requires_push_and_current_session() { - let mut state = test_server_state().await; - let error = handle_peer_config_update_hint(&state, peer_hint("sandbox", "old-session")) - .await - .unwrap_err(); - assert_eq!(error.code(), Code::FailedPrecondition); - - Arc::get_mut(&mut state) - .unwrap() - .config - .config_delivery_mode = ConfigDeliveryMode::Push; - let response = handle_peer_config_update_hint(&state, peer_hint("sandbox", "old-session")) - .await - .unwrap() - .into_inner(); - assert!(response.stale_owner); - assert!(!response.queue_full); - - let error = handle_peer_config_update_hint( - &state, - Request::new(PeerConfigUpdateHintRequest::default()), - ) - .await - .unwrap_err(); - assert_eq!(error.code(), Code::PermissionDenied); - } - - #[tokio::test] - async fn peer_hint_rebuilds_snapshot_on_the_session_owner() { - let mut state = test_server_state().await; - Arc::get_mut(&mut state) - .unwrap() - .config - .config_delivery_mode = ConfigDeliveryMode::Push; - state - .store - .put_message(&Sandbox { - metadata: Some(ObjectMeta { - id: "owned-sandbox".into(), - name: "owned-sandbox".into(), - workspace: "default".into(), - ..Default::default() - }), - spec: Some(SandboxSpec::default()), - ..Default::default() - }) - .await - .unwrap(); - - let mut harness = connect_supervisor_stream( - &state, - "owned-sandbox", - openshell_core::proto::SUPERVISOR_PROTOCOL_REVISION, - ) - .await - .unwrap(); - let accepted = harness.inbound.message().await.unwrap().unwrap(); - let Some(gateway_message::Payload::SessionAccepted(accepted)) = accepted.payload else { - panic!("expected SessionAccepted"); - }; - let response = handle_peer_config_update_hint( - &state, - peer_hint("owned-sandbox", &accepted.session_id), - ) - .await - .unwrap() - .into_inner(); - assert!(!response.stale_owner && !response.queue_full); - let update = tokio::time::timeout(Duration::from_secs(5), harness.inbound.message()) - .await - .unwrap() - .unwrap() - .unwrap(); - assert!(matches!( - update.payload, - Some(gateway_message::Payload::ConfigUpdate(_)) - )); - } - - fn key(sandbox_id: &str, component: ConfigComponentKind) -> DeliveryKey { - DeliveryKey { - sandbox_id: sandbox_id.to_string(), - component, - } - } - - #[test] - fn queue_coalesces_repeated_component_changes_while_worker_is_active() { - let queue = ConfigDeliveryQueue::default(); - let key = key("sb-1", ConfigComponentKind::SandboxConfig); - let DeliveryEnqueue::StartWorker(permit) = queue.enqueue(key.clone()) else { - panic!("first publication must start a worker"); - }; - queue.take(&key); - assert!(matches!( - queue.enqueue(key.clone()), - DeliveryEnqueue::Coalesced - )); - assert!(matches!( - queue.enqueue(key.clone()), - DeliveryEnqueue::Coalesced - )); - assert!(queue.finish_pass(&key)); - queue.take(&key); - assert!(!queue.finish_pass(&key)); - drop(permit); - } - - #[tokio::test] - async fn direct_and_fanout_updates_share_one_component_worker() { - let queue = ConfigDeliveryQueue::default(); - let key = key("sb-1", ConfigComponentKind::SandboxConfig); - let DeliveryEnqueue::StartWorker(permit) = queue.enqueue(key.clone()) else { - panic!("direct update must start a worker"); - }; - queue.take(&key); - assert!(matches!( - queue.enqueue_from_fanout(key.clone()).await, - DeliveryEnqueue::Coalesced - )); - assert!(queue.finish_pass(&key)); - queue.take(&key); - assert!(!queue.finish_pass(&key)); - drop(permit); - } - - #[test] - fn build_bound_is_sized_from_the_database_pool() { - assert_eq!( - ConfigDeliveryQueue::for_db_connections(10).max_concurrent_builds(), - 20 - ); - assert_eq!( - ConfigDeliveryQueue::for_db_connections(1).max_concurrent_builds(), - MIN_CONCURRENT_SNAPSHOT_BUILDS - ); - assert_eq!(ConfigDeliveryQueue::new(0).max_concurrent_builds(), 1); - } - - #[tokio::test(start_paused = true)] - async fn bounded_builds_never_exceed_the_permit_count() { - const PERMITS: usize = 4; - const BUILDS: usize = 40; - let queue = Arc::new(ConfigDeliveryQueue::new(PERMITS)); - let active = Arc::new(AtomicUsize::new(0)); - let peak = Arc::new(AtomicUsize::new(0)); - - let workers = (0..BUILDS) - .map(|_| { - let queue = Arc::clone(&queue); - let active = Arc::clone(&active); - let peak = Arc::clone(&peak); - tokio::spawn(async move { - queue - .run_bounded_build(async { - let now = active.fetch_add(1, Ordering::SeqCst) + 1; - peak.fetch_max(now, Ordering::SeqCst); - tokio::time::sleep(Duration::from_millis(50)).await; - active.fetch_sub(1, Ordering::SeqCst); - }) - .await - .expect("build must not time out"); - }) - }) - .collect::>(); - for worker in workers { - worker.await.unwrap(); - } - - assert_eq!(peak.load(Ordering::SeqCst), PERMITS); - assert_eq!(active.load(Ordering::SeqCst), 0); - assert_eq!(queue.max_concurrent_builds(), PERMITS); - } - - #[tokio::test(start_paused = true)] - async fn build_deadline_starts_after_a_permit_is_held() { - let queue = Arc::new(ConfigDeliveryQueue::new(1)); - let almost_deadline = CONFIG_SNAPSHOT_BUILD_TIMEOUT - .checked_sub(Duration::from_secs(1)) - .unwrap(); - let first = { - let queue = Arc::clone(&queue); - tokio::spawn(async move { - queue - .run_bounded_build(tokio::time::sleep(almost_deadline)) - .await - }) - }; - tokio::task::yield_now().await; - let second = queue.run_bounded_build(tokio::time::sleep(almost_deadline)); - - let (first, second) = tokio::join!(first, second); - assert!(first.unwrap().is_ok()); - assert!( - second.is_ok(), - "waiting for a permit must not consume the build deadline" - ); - - assert!( - queue - .run_bounded_build(tokio::time::sleep( - CONFIG_SNAPSHOT_BUILD_TIMEOUT + Duration::from_secs(1), - )) - .await - .is_err() - ); - } - - #[test] - fn queue_runs_components_and_sandboxes_independently() { - let queue = ConfigDeliveryQueue::new(3); - assert!(matches!( - queue.enqueue(key("sb-1", ConfigComponentKind::SandboxConfig)), - DeliveryEnqueue::StartWorker(_) - )); - assert!(matches!( - queue.enqueue(key("sb-1", ConfigComponentKind::ProviderEnvironment)), - DeliveryEnqueue::StartWorker(_) - )); - assert!(matches!( - queue.enqueue(key("sb-2", ConfigComponentKind::SandboxConfig)), - DeliveryEnqueue::StartWorker(_) - )); - } - - #[tokio::test] - async fn fleet_fanout_waits_without_creating_unbounded_delivery_workers() { - const ROUTED_SANDBOXES: usize = 10_000; - let queue = Arc::new(ConfigDeliveryQueue::new(1)); - let first = key("sandbox-0", ConfigComponentKind::SandboxConfig); - let DeliveryEnqueue::StartWorker(_blocked_worker) = queue.enqueue(first) else { - panic!("first publication must start a worker"); - }; - - let sandbox_ids = (1..ROUTED_SANDBOXES) - .map(|index| format!("sandbox-{index}")) - .collect::>(); - let fanout = async { - for sandbox_id in sandbox_ids { - for component in ConfigComponents::ALL.selected() { - let _ = queue.enqueue_from_fanout(key(&sandbox_id, component)).await; - } - } - }; - tokio::pin!(fanout); - tokio::select! { - () = &mut fanout => panic!("fanout must wait for worker capacity"), - () = tokio::task::yield_now() => {} - } - - assert_eq!(queue.pending.lock().unwrap().len(), 1); - } - - #[test] - fn fanout_queue_coalesces_and_bounds_distinct_scopes() { - let queue = ConfigDeliveryQueue::default(); - let first = FanoutKey { - scope: FanoutScope::Workspace("workspace-0".into()), - component: ConfigComponentKind::SandboxConfig, - route_remote: true, - }; - assert_eq!( - queue.enqueue_fanout(first.clone()), - FanoutEnqueue::StartWorker - ); - queue.take_fanout(&first); - assert_eq!( - queue.enqueue_fanout(first.clone()), - FanoutEnqueue::Coalesced - ); - assert!(queue.finish_fanout_pass(&first)); - - for index in 1..MAX_ACTIVE_FANOUT_WORKERS { - assert_eq!( - queue.enqueue_fanout(FanoutKey { - scope: FanoutScope::Workspace(format!("workspace-{index}")), - component: ConfigComponentKind::SandboxConfig, - route_remote: true, - }), - FanoutEnqueue::StartWorker - ); - } - assert_eq!( - queue.enqueue_fanout(FanoutKey { - scope: FanoutScope::Workspace("overflow".into()), - component: ConfigComponentKind::SandboxConfig, - route_remote: true, - }), - FanoutEnqueue::Full - ); - } - - #[test] - fn configuration_message_debug_output_redacts_payloads() { - let message = SupervisorConfigMessage::ProviderEnvironment(ProviderEnvironmentSnapshot { - values: vec![openshell_core::proto::ProviderEnvironmentValue { - name: "TOKEN".into(), - value: "secret-marker".into(), - ..Default::default() - }], - ..Default::default() - }); - assert!(!format!("{message:?}").contains("secret-marker")); - } - - #[tokio::test] - async fn session_acceptance_precedes_live_configuration_updates() { - let mut state = test_server_state().await; - Arc::get_mut(&mut state) - .unwrap() - .config - .config_delivery_mode = ConfigDeliveryMode::Push; - state - .store - .put_message(&Sandbox { - metadata: Some(ObjectMeta { - id: "sandbox".into(), - name: "sandbox".into(), - workspace: "default".into(), - ..Default::default() - }), - spec: Some(SandboxSpec::default()), - ..Default::default() - }) - .await - .unwrap(); - - let mut harness = connect_supervisor_stream( - &state, - "sandbox", - openshell_core::proto::SUPERVISOR_PROTOCOL_REVISION, - ) - .await - .unwrap(); - - let first = tokio::time::timeout(Duration::from_secs(5), harness.inbound.message()) - .await - .unwrap() - .unwrap() - .unwrap(); - assert!(matches!( - first.payload, - Some(gateway_message::Payload::SessionAccepted(_)) - )); - - publish_sandbox_components(&state, "sandbox", ConfigComponents::SANDBOX_CONFIG); - let update = tokio::time::timeout(Duration::from_secs(5), harness.inbound.message()) - .await - .unwrap() - .unwrap() - .unwrap(); - assert!(matches!( - update, - GatewayMessage { - payload: Some(gateway_message::Payload::ConfigUpdate(_)) - } - )); - } - - #[tokio::test] - async fn legacy_supervisor_keeps_polling_when_gateway_shadow_push_is_enabled() { - let mut state = test_server_state().await; - Arc::get_mut(&mut state) - .unwrap() - .config - .config_delivery_mode = ConfigDeliveryMode::Push; - state - .store - .put_message(&Sandbox { - metadata: Some(ObjectMeta { - id: "legacy-sandbox".into(), - name: "legacy-sandbox".into(), - workspace: "default".into(), - ..Default::default() - }), - spec: Some(SandboxSpec::default()), - ..Default::default() - }) - .await - .unwrap(); - let mut harness = connect_supervisor_stream( - &state, - "legacy-sandbox", - openshell_core::proto::LEGACY_SUPERVISOR_PROTOCOL_REVISION, - ) - .await - .unwrap(); - let first = harness.inbound.message().await.unwrap().unwrap(); - let Some(gateway_message::Payload::SessionAccepted(accepted)) = first.payload else { - panic!("expected SessionAccepted"); - }; - assert!(accepted.bootstrap.is_none()); - publish_sandbox_components(&state, "legacy-sandbox", ConfigComponents::SANDBOX_CONFIG); - assert!( - tokio::time::timeout(Duration::from_millis(100), harness.inbound.message()) - .await - .is_err() - ); - } - - #[test] - fn bootstrap_requires_matching_provider_revision_fence() { - let mut bootstrap = ConfigBootstrap { - sandbox_config: Some(SandboxConfigSnapshot { - provider_env_revision: 7, - ..Default::default() - }), - provider_environment: Some(ProviderEnvironmentSnapshot { - provider_env_revision: 8, - ..Default::default() - }), - }; - assert!(!bootstrap_revisions_match(&bootstrap)); - bootstrap - .provider_environment - .as_mut() - .unwrap() - .provider_env_revision = 7; - assert!(bootstrap_revisions_match(&bootstrap)); - } - - #[tokio::test] - async fn stalled_credentials_do_not_block_session_acceptance() { - use openshell_core::proto::{CredentialHandle, Provider}; - - let mut state = test_server_state().await; - Arc::get_mut(&mut state) - .unwrap() - .config - .config_delivery_mode = ConfigDeliveryMode::Push; - state - .store - .put_message(&Provider { - metadata: Some(ObjectMeta { - id: "provider".into(), - name: "provider".into(), - workspace: "default".into(), - ..Default::default() - }), - r#type: "github".into(), - credential_handles: HashMap::from([( - "GITHUB_TOKEN".into(), - CredentialHandle { - driver: "test-static".into(), - handle: "blocked".into(), - ..Default::default() - }, - )]), - ..Default::default() - }) - .await - .unwrap(); - state - .store - .put_message(&Sandbox { - metadata: Some(ObjectMeta { - id: "sandbox".into(), - name: "sandbox".into(), - workspace: "default".into(), - ..Default::default() - }), - spec: Some(SandboxSpec { - providers: vec!["provider".into()], - ..Default::default() - }), - ..Default::default() - }) - .await - .unwrap(); - let (resolve_hit, _release_resolve) = state.credentials.gate_next_resolve(); - tokio::time::timeout(Duration::from_secs(10), async { - let connect = connect_supervisor_stream( - &state, - "sandbox", - openshell_core::proto::SUPERVISOR_PROTOCOL_REVISION, - ); - let (response, hit) = tokio::join!(connect, resolve_hit); - hit.expect("bootstrap must reach the stalled credential driver"); - let mut harness = response.unwrap(); - let first = harness.inbound.message().await.unwrap().unwrap(); - let Some(gateway_message::Payload::SessionAccepted(accepted)) = first.payload else { - panic!("expected session acceptance"); - }; - assert!(accepted.bootstrap.is_none()); - assert!( - state - .supervisor_sessions - .is_current_session("sandbox", &accepted.session_id) - ); - // Relay control remains usable while credential resolution is stalled. - let (_, relay) = state - .supervisor_sessions - .open_relay("sandbox", Duration::from_secs(1)) - .await - .unwrap(); - let message = harness.inbound.message().await.unwrap().unwrap(); - assert!(matches!( - message.payload, - Some(gateway_message::Payload::RelayOpen(_)) - )); - drop(relay); - }) - .await - .expect("optional bootstrap must not consume the relay reconnect budget"); - } -} diff --git a/crates/openshell-server/src/config_delivery/mod.rs b/crates/openshell-server/src/config_delivery/mod.rs new file mode 100644 index 000000000..b147926c0 --- /dev/null +++ b/crates/openshell-server/src/config_delivery/mod.rs @@ -0,0 +1,1431 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Build and route complete supervisor configuration snapshots. + +mod scheduler; +mod session_slots; + +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; +use std::time::{Duration, Instant}; + +use futures::StreamExt; +use metrics::counter; +use openshell_core::config::ConfigDeliveryMode; +use openshell_core::proto::{ + ConfigBootstrap, PeerConfigProviderTarget, PeerConfigSandboxTarget, + PeerConfigUpdateHintRequest, PeerConfigUpdateHintResponse, ProviderEnvironmentSnapshot, + Sandbox, SandboxConfigSnapshot, peer_config_update_hint_request, +}; +use tokio::sync::Notify; +use tokio::task::JoinSet; +use tonic::{Code, Request, Response, Status}; +use tracing::warn; + +use crate::ServerState; +use crate::auth::principal::Principal; +use crate::grpc::policy::{build_provider_environment_snapshot, build_sandbox_config_snapshot}; +use crate::supervisor_owner::{OWNER_TTL, SupervisorOwnerIndex}; +use crate::supervisor_session::SupervisorSessionRegistry; + +pub use scheduler::Registration; +use scheduler::{ + BuildOutcome, BuildTicket, DeliveryKey, FanoutScope, Lane, Limits, RouteTarget, RouteTicket, + Scheduler, Work, +}; +pub use session_slots::{ConfigSlots, SessionOutbound}; + +/// Leaves headroom below tonic's default 4 MiB decode limit for framing and +/// future envelope fields. +pub const MAX_SUPERVISOR_CONFIG_MESSAGE_BYTES: usize = 3 * 1024 * 1024; +const CONFIG_SNAPSHOT_BUILD_TIMEOUT: Duration = Duration::from_secs(45); +// Stage 1 bootstrap is optional. Keep credential backend stalls well below +// the 15-second relay session-wait budget while polling remains authoritative. +const CONFIG_BOOTSTRAP_BUILD_TIMEOUT: Duration = Duration::from_secs(1); +/// Concurrent snapshot builds allowed per pooled database connection. Builds +/// are short bursts of small queries, so a little oversubscription keeps the +/// pool busy without stacking every waiter on the acquire timeout. +const SNAPSHOT_BUILDS_PER_DB_CONNECTION: usize = 2; +const MIN_CONCURRENT_SNAPSHOT_BUILDS: usize = 4; +const MAX_CONCURRENT_ROUTES: usize = 8; +const PEER_HINT_TIMEOUT: Duration = Duration::from_secs(5); +const PEER_FANOUT_CONCURRENCY: usize = 8; + +/// One complete configuration component awaiting delivery to a supervisor. +#[derive(Clone)] +pub enum SupervisorConfigMessage { + SandboxConfig(Box), + ProviderEnvironment(ProviderEnvironmentSnapshot), +} + +impl SupervisorConfigMessage { + pub(crate) fn component(&self) -> ConfigComponentKind { + match self { + Self::SandboxConfig(_) => ConfigComponentKind::SandboxConfig, + Self::ProviderEnvironment(_) => ConfigComponentKind::ProviderEnvironment, + } + } +} + +impl fmt::Debug for SupervisorConfigMessage { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::SandboxConfig(_) => "SandboxConfig()", + Self::ProviderEnvironment(_) => "ProviderEnvironment()", + }) + } +} + +/// Result of handing one configuration snapshot to a supervisor session. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeliveryDisposition { + Queued, + /// Replaced an older snapshot the session had not sent yet. + Replaced, + NoActiveSession, + UnsupportedSession, + PayloadTooLarge, +} + +impl DeliveryDisposition { + fn metric_label(self) -> &'static str { + match self { + Self::Queued => "queued", + Self::Replaced => "replaced", + Self::NoActiveSession => "no_active_session", + Self::UnsupportedSession => "unsupported_session", + Self::PayloadTooLarge => "payload_too_large", + } + } +} + +/// Transport boundary for configuration delivery. +#[tonic::async_trait] +pub trait SupervisorConfigRouter: fmt::Debug + Send + Sync { + async fn deliver( + &self, + sandbox_id: &str, + session_id: &str, + message: SupervisorConfigMessage, + ) -> DeliveryDisposition; +} + +#[derive(Debug)] +pub struct LocalSupervisorConfigRouter { + sessions: Arc, +} + +impl LocalSupervisorConfigRouter { + #[must_use] + pub fn new(sessions: Arc) -> Self { + Self { sessions } + } +} + +#[tonic::async_trait] +impl SupervisorConfigRouter for LocalSupervisorConfigRouter { + async fn deliver( + &self, + sandbox_id: &str, + session_id: &str, + message: SupervisorConfigMessage, + ) -> DeliveryDisposition { + self.sessions + .deliver_config(sandbox_id, session_id, message) + } +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct ConfigComponents { + pub sandbox_config: bool, + pub provider_environment: bool, +} + +impl ConfigComponents { + pub const ALL: Self = Self { + sandbox_config: true, + provider_environment: true, + }; + + pub const SANDBOX_AND_PROVIDER: Self = Self { + sandbox_config: true, + provider_environment: true, + }; + + pub const SANDBOX_CONFIG: Self = Self { + sandbox_config: true, + provider_environment: false, + }; + + fn only(component: ConfigComponentKind) -> Self { + Self { + sandbox_config: component == ConfigComponentKind::SandboxConfig, + provider_environment: component == ConfigComponentKind::ProviderEnvironment, + } + } + + fn union(self, other: Self) -> Self { + Self { + sandbox_config: self.sandbox_config || other.sandbox_config, + provider_environment: self.provider_environment || other.provider_environment, + } + } + + fn is_empty(self) -> bool { + !self.sandbox_config && !self.provider_environment + } + + fn selected(self) -> impl Iterator { + [ + (self.sandbox_config, ConfigComponentKind::SandboxConfig), + ( + self.provider_environment, + ConfigComponentKind::ProviderEnvironment, + ), + ] + .into_iter() + .filter_map(|(selected, component)| selected.then_some(component)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ConfigComponentKind { + SandboxConfig, + ProviderEnvironment, +} + +impl ConfigComponentKind { + fn name(self) -> &'static str { + match self { + Self::SandboxConfig => "sandbox_config", + Self::ProviderEnvironment => "provider_environment", + } + } +} + +/// Coalescing delivery scheduler shared by publications, supervisor sessions, +/// and one on-demand dispatcher task. +#[derive(Debug)] +pub struct ConfigDelivery { + state: Mutex, + wake: Notify, +} + +#[derive(Debug)] +struct DeliveryState { + scheduler: Scheduler, + dispatcher_running: bool, +} + +impl ConfigDelivery { + /// Size the build bound from the persistence pool that every build reads. + #[must_use] + pub fn for_db_connections(max_connections: u32) -> Self { + let max_connections = usize::try_from(max_connections).unwrap_or(usize::MAX); + let builds = max_connections + .saturating_mul(SNAPSHOT_BUILDS_PER_DB_CONNECTION) + .max(MIN_CONCURRENT_SNAPSHOT_BUILDS); + Self { + state: Mutex::new(DeliveryState { + scheduler: Scheduler::new(Limits::new(builds, MAX_CONCURRENT_ROUTES)), + dispatcher_running: false, + }), + wake: Notify::new(), + } + } + + pub(crate) fn current_seq(&self) -> u64 { + self.lock().scheduler.current_seq() + } + + fn with_scheduler(&self, update: impl FnOnce(&mut Scheduler) -> T) -> T { + update(&mut self.lock().scheduler) + } + + /// Delivery is best effort beside the session lifecycle, so a poisoned + /// lock must not turn session teardown into a panic. + fn lock(&self) -> MutexGuard<'_, DeliveryState> { + self.state.lock().unwrap_or_else(PoisonError::into_inner) + } +} + +pub async fn build_config_bootstrap( + state: &Arc, + sandbox: &Sandbox, +) -> Result { + tokio::time::timeout( + CONFIG_BOOTSTRAP_BUILD_TIMEOUT, + build_consistent_config_bootstrap(state, sandbox), + ) + .await + .map_err(|_| Status::deadline_exceeded("supervisor configuration bootstrap timed out"))? +} + +async fn build_consistent_config_bootstrap( + state: &Arc, + sandbox: &Sandbox, +) -> Result { + const MAX_BUILD_ATTEMPTS: usize = 3; + for _ in 0..MAX_BUILD_ATTEMPTS { + // Components are independent projections. The provider revision is a + // fence for the only overlapping input between sandbox configuration + // and provider environment state. + let (sandbox_config, provider_environment) = tokio::join!( + build_sandbox_config_snapshot(state, sandbox), + build_provider_environment_snapshot(state, sandbox, true), + ); + let bootstrap = ConfigBootstrap { + sandbox_config: Some(sandbox_config?), + provider_environment: Some(provider_environment?), + }; + if bootstrap_revisions_match(&bootstrap) { + return Ok(bootstrap); + } + counter!("openshell_supervisor_config_bootstrap_revision_mismatches_total").increment(1); + } + Err(Status::aborted( + "configuration changed while building supervisor bootstrap", + )) +} + +fn bootstrap_revisions_match(bootstrap: &ConfigBootstrap) -> bool { + bootstrap + .sandbox_config + .as_ref() + .zip(bootstrap.provider_environment.as_ref()) + .is_some_and(|(sandbox, provider)| { + sandbox.provider_env_revision == provider.provider_env_revision + }) +} + +fn push_enabled(state: &ServerState) -> bool { + state.config.config_delivery_mode == ConfigDeliveryMode::Push +} + +pub fn publish_sandbox_components( + state: &Arc, + sandbox_id: &str, + components: ConfigComponents, +) { + if !push_enabled(state) { + return; + } + let now = Instant::now(); + // A single-replica gateway owns every session, and registration covers a + // session that is still connecting. + let may_be_remote = !state.store.is_single_replica(); + state.config_delivery.with_scheduler(|scheduler| { + if !scheduler.publish_sandbox(sandbox_id, components, now) && may_be_remote { + scheduler.route(RouteTarget::Sandbox(sandbox_id.to_string()), components); + } + }); + kick(state); +} + +pub fn publish_workspace_components( + state: &Arc, + workspace: &str, + components: ConfigComponents, +) { + publish_fanout( + state, + &FanoutScope::Workspace(workspace.to_string()), + components, + ); +} + +/// Publish a provider change to the sandboxes that attach that provider. +pub fn publish_provider_components( + state: &Arc, + workspace: &str, + provider_name: &str, + components: ConfigComponents, +) { + publish_fanout( + state, + &FanoutScope::Provider { + workspace: workspace.to_string(), + name: provider_name.to_string(), + }, + components, + ); +} + +pub fn publish_all_connected(state: &Arc, components: ConfigComponents) { + publish_fanout(state, &FanoutScope::AllConnected, components); +} + +fn publish_fanout(state: &Arc, scope: &FanoutScope, components: ConfigComponents) { + if !push_enabled(state) { + return; + } + let notify_peers = !state.store.is_single_replica(); + state.config_delivery.with_scheduler(|scheduler| { + scheduler.publish_fanout(scope, components, notify_peers, Instant::now()); + }); + kick(state); +} + +/// Make a push-capable session visible to fanouts and sandbox updates. +pub fn register_session(state: &Arc, registration: Registration) { + state.config_delivery.with_scheduler(|scheduler| { + // A concurrent reconnect may already own the registry entry. Checking + // under the scheduler lock keeps the newest session registered. + if state + .supervisor_sessions + .is_current_session(®istration.sandbox_id, ®istration.session_id) + { + scheduler.register(registration, Instant::now()); + } + }); + kick(state); +} + +pub fn unregister_session(state: &ServerState, sandbox_id: &str, session_id: &str) { + state + .config_delivery + .with_scheduler(|scheduler| scheduler.unregister(sandbox_id, session_id)); +} + +fn kick(state: &Arc) { + let start = { + let mut delivery = state.config_delivery.lock(); + !std::mem::replace(&mut delivery.dispatcher_running, true) + }; + if start { + tokio::spawn(run_dispatcher(Arc::clone(state))); + } else { + state.config_delivery.wake.notify_one(); + } +} + +/// Releases outstanding work if the dispatcher unwinds, so a later +/// publication starts a replacement with consistent slot accounting. +struct DispatcherGuard { + state: Arc, + tickets: HashMap, + finished: bool, +} + +impl Drop for DispatcherGuard { + fn drop(&mut self) { + if self.finished { + return; + } + let mut delivery = self.state.config_delivery.lock(); + let now = Instant::now(); + for work in self.tickets.values() { + match work { + Work::Build(ticket) => { + delivery + .scheduler + .complete_build(ticket, BuildOutcome::Failed, now); + } + Work::Route(ticket) => delivery.scheduler.complete_route(ticket), + } + } + delivery.dispatcher_running = false; + } +} + +async fn run_dispatcher(state: Arc) { + let delivery = &state.config_delivery; + let mut tasks = JoinSet::new(); + let mut guard = DispatcherGuard { + state: Arc::clone(&state), + tickets: HashMap::new(), + finished: false, + }; + loop { + let started = { + let mut delivery_state = delivery.lock(); + let now = Instant::now(); + let mut started = Vec::new(); + while let Some(work) = delivery_state.scheduler.next_work(now) { + started.push(work); + } + delivery_state.scheduler.record_gauges(); + // Clearing the flag under the scheduler lock means a concurrent + // publication either sees this dispatcher running or starts one. + if started.is_empty() && !delivery_state.scheduler.has_running_work() { + delivery_state.dispatcher_running = false; + guard.finished = true; + return; + } + started + }; + for work in started { + let handle = tasks.spawn(run_work(Arc::clone(&state), work.clone())); + guard.tickets.insert(handle.id(), work); + } + tokio::select! { + joined = tasks.join_next_with_id(), if !tasks.is_empty() => { + let (id, result) = match joined.expect("join set is not empty") { + Ok((id, result)) => (id, Some(result)), + Err(error) => { + // Panic payloads may contain credential backend data. + warn!( + cancelled = error.is_cancelled(), + panicked = error.is_panic(), + "supervisor configuration delivery task failed" + ); + (error.id(), None) + } + }; + let work = guard.tickets.remove(&id).expect("delivery task has a ticket"); + complete_work(&state, &work, result); + } + () = delivery.wake.notified() => {} + } + } +} + +enum WorkResult { + Build(BuildResult), + Route(RouteResult), +} + +struct BuildResult { + outcome: BuildOutcome, + /// The session moved to another gateway after the build started. + reroute: bool, +} + +enum RouteResult { + Done, + /// This gateway owns the current session for the routed sandbox. + Local, +} + +async fn run_work(state: Arc, work: Work) -> WorkResult { + match work { + Work::Build(ticket) => WorkResult::Build(run_build(&state, &ticket).await), + Work::Route(ticket) => WorkResult::Route(run_route(&state, &ticket).await), + } +} + +fn complete_work(state: &ServerState, work: &Work, result: Option) { + let now = Instant::now(); + state + .config_delivery + .with_scheduler(|scheduler| match (work, result) { + (Work::Build(ticket), Some(WorkResult::Build(result))) => { + scheduler.complete_build(ticket, result.outcome, now); + if result.reroute { + scheduler.route( + RouteTarget::Sandbox(ticket.key.sandbox_id.clone()), + ConfigComponents::only(ticket.key.component), + ); + } + } + (Work::Build(ticket), _) => { + scheduler.complete_build(ticket, BuildOutcome::Failed, now); + } + (Work::Route(ticket), result) => { + scheduler.complete_route(ticket); + if let ( + RouteTarget::Sandbox(sandbox_id), + Some(WorkResult::Route(RouteResult::Local)), + ) = (&ticket.target, result) + { + scheduler.publish_sandbox(sandbox_id, ticket.components, now); + } + } + }); +} + +async fn run_build(state: &Arc, ticket: &BuildTicket) -> BuildResult { + let BuildTicket { + key, + lane, + session_id, + .. + } = ticket; + let failed = BuildResult { + outcome: BuildOutcome::Failed, + reroute: false, + }; + let built = + tokio::time::timeout(CONFIG_SNAPSHOT_BUILD_TIMEOUT, build_component(state, key)).await; + let (message, providers) = match built { + Ok(Ok(Some(built))) => built, + Ok(Ok(None)) => { + record_build(key.component, *lane, "ok"); + return BuildResult { + outcome: BuildOutcome::Built { providers: None }, + reroute: false, + }; + } + Ok(Err(error)) => { + record_build_failure(key, *lane, "failed", error.code()); + return failed; + } + Err(_) => { + record_build_failure(key, *lane, "timeout", Code::DeadlineExceeded); + return failed; + } + }; + record_build(key.component, *lane, "ok"); + let reroute = match owner_check(state, &key.sandbox_id, session_id).await { + OwnerCheck::Current => { + let disposition = state + .supervisor_config_router() + .deliver(&key.sandbox_id, session_id, message) + .await; + record_delivery(key.component, disposition.metric_label()); + false + } + OwnerCheck::Remote => { + record_delivery(key.component, "owner_moved"); + true + } + OwnerCheck::Gone => { + record_delivery(key.component, "no_active_session"); + false + } + OwnerCheck::Unknown => { + record_delivery(key.component, "owner_lookup_failed"); + return failed; + } + }; + BuildResult { + outcome: BuildOutcome::Built { + providers: Some(providers), + }, + reroute, + } +} + +async fn build_component( + state: &Arc, + key: &DeliveryKey, +) -> Result)>, Status> { + let Some(sandbox) = state + .store + .get_message::(&key.sandbox_id) + .await + .map_err(|error| Status::internal(format!("fetch sandbox failed: {error}")))? + else { + return Ok(None); + }; + let providers = sandbox + .spec + .as_ref() + .map(|spec| spec.providers.iter().cloned().collect()) + .unwrap_or_default(); + let message = match key.component { + ConfigComponentKind::SandboxConfig => SupervisorConfigMessage::SandboxConfig(Box::new( + build_sandbox_config_snapshot(state, &sandbox).await?, + )), + ConfigComponentKind::ProviderEnvironment => SupervisorConfigMessage::ProviderEnvironment( + build_provider_environment_snapshot(state, &sandbox, true).await?, + ), + }; + Ok(Some((message, providers))) +} + +enum OwnerCheck { + /// This gateway owns the session the build was dispatched for. + Current, + Remote, + Gone, + Unknown, +} + +async fn owner_check(state: &Arc, sandbox_id: &str, session_id: &str) -> OwnerCheck { + let owners = SupervisorOwnerIndex::new(Arc::clone(&state.store), OWNER_TTL); + match owners.read(sandbox_id).await { + Ok(Some(owner)) if owner.is_fresh(OWNER_TTL) => { + if owner.owner_replica_id != state.replica_id { + OwnerCheck::Remote + } else if owner.session_id == session_id { + OwnerCheck::Current + } else { + OwnerCheck::Gone + } + } + Ok(_) => OwnerCheck::Gone, + Err(error) => { + warn!(sandbox_id, error = %error, "configuration owner lookup failed"); + OwnerCheck::Unknown + } + } +} + +async fn run_route(state: &Arc, ticket: &RouteTicket) -> RouteResult { + match &ticket.target { + RouteTarget::Sandbox(sandbox_id) => { + route_sandbox_hint(state, sandbox_id, ticket.components).await + } + RouteTarget::Peers(scope) => { + notify_peers(state, scope, ticket.components).await; + RouteResult::Done + } + } +} + +/// Send a secret-free hint to the gateway that owns the sandbox session. The +/// owner builds its own snapshot from shared state. +async fn route_sandbox_hint( + state: &Arc, + sandbox_id: &str, + components: ConfigComponents, +) -> RouteResult { + let owners = SupervisorOwnerIndex::new(Arc::clone(&state.store), OWNER_TTL); + let mut final_outcome = "stale_owner"; + for attempt in 0..2 { + let owner = match owners.read(sandbox_id).await { + Ok(Some(owner)) if owner.is_fresh(OWNER_TTL) => owner, + Ok(_) => return RouteResult::Done, + Err(error) => { + warn!(sandbox_id, error = %error, "configuration owner lookup failed"); + return RouteResult::Done; + } + }; + if owner.owner_replica_id == state.replica_id { + return if state + .supervisor_sessions + .is_current_session(sandbox_id, &owner.session_id) + { + RouteResult::Local + } else { + RouteResult::Done + }; + } + if !is_peer_endpoint(&owner.owner_peer_endpoint) { + warn!(sandbox_id, owner = %owner.owner_replica_id, "configuration owner has no reachable peer endpoint"); + return RouteResult::Done; + } + let request = PeerConfigUpdateHintRequest { + scope: Some(peer_config_update_hint_request::Scope::Sandbox( + PeerConfigSandboxTarget { + sandbox_id: sandbox_id.to_string(), + session_id: owner.session_id, + }, + )), + sandbox_config: components.sandbox_config, + provider_environment: components.provider_environment, + }; + match send_peer_hint(state, &owner.owner_peer_endpoint, request).await { + HintOutcome::StaleOwner => final_outcome = "stale_owner", + HintOutcome::Failed(outcome) => { + final_outcome = outcome; + warn!(sandbox_id, owner = %owner.owner_replica_id, attempt, outcome, "configuration peer hint failed"); + } + outcome => { + record_peer_hint(outcome.label()); + return RouteResult::Done; + } + } + } + record_peer_hint(final_outcome); + RouteResult::Done +} + +async fn notify_peers(state: &Arc, scope: &FanoutScope, components: ConfigComponents) { + let owners = SupervisorOwnerIndex::new(Arc::clone(&state.store), OWNER_TTL); + let endpoints = match owners.list_fresh_peer_endpoints(&state.replica_id).await { + Ok(endpoints) => endpoints, + Err(error) => { + warn!(error = %error, "configuration peer fanout owner listing failed"); + return; + } + }; + let scope = peer_scope(scope); + futures::stream::iter(endpoints) + .for_each_concurrent(PEER_FANOUT_CONCURRENCY, |endpoint| { + let request = PeerConfigUpdateHintRequest { + scope: Some(scope.clone()), + sandbox_config: components.sandbox_config, + provider_environment: components.provider_environment, + }; + async move { + let outcome = send_peer_hint(state, &endpoint, request).await; + if let HintOutcome::Failed(outcome) = outcome { + warn!(endpoint = %endpoint, outcome, "configuration peer fanout hint failed"); + } + record_peer_hint(outcome.label()); + } + }) + .await; +} + +fn peer_scope(scope: &FanoutScope) -> peer_config_update_hint_request::Scope { + match scope { + FanoutScope::AllConnected => peer_config_update_hint_request::Scope::AllConnected(true), + FanoutScope::Workspace(workspace) => { + peer_config_update_hint_request::Scope::Workspace(workspace.clone()) + } + FanoutScope::Provider { workspace, name } => { + peer_config_update_hint_request::Scope::Provider(PeerConfigProviderTarget { + workspace: workspace.clone(), + name: name.clone(), + }) + } + } +} + +fn is_peer_endpoint(endpoint: &str) -> bool { + endpoint.starts_with("http://") || endpoint.starts_with("https://") +} + +enum HintOutcome { + Accepted, + StaleOwner, + UnsupportedPeer, + Failed(&'static str), +} + +impl HintOutcome { + fn label(&self) -> &'static str { + match self { + Self::Accepted => "accepted", + Self::StaleOwner => "stale_owner", + Self::UnsupportedPeer => "unsupported_peer", + Self::Failed(outcome) => outcome, + } + } +} + +async fn send_peer_hint( + state: &Arc, + endpoint: &str, + request: PeerConfigUpdateHintRequest, +) -> HintOutcome { + match tokio::time::timeout( + PEER_HINT_TIMEOUT, + crate::supervisor_session::forward_config_hint_to_peer(state, endpoint, request), + ) + .await + { + Ok(Ok(response)) if response.stale_owner => HintOutcome::StaleOwner, + Ok(Ok(_)) => HintOutcome::Accepted, + Ok(Err(error)) if error.code() == Code::Unimplemented => HintOutcome::UnsupportedPeer, + Ok(Err(_)) => HintOutcome::Failed("peer_error"), + Err(_) => HintOutcome::Failed("timeout"), + } +} + +pub fn handle_peer_config_update_hint( + state: &Arc, + request: Request, +) -> Result, Status> { + if !matches!( + request.extensions().get::(), + Some(Principal::Peer(_)) + ) { + return Err(Status::permission_denied("gateway peer principal required")); + } + if !push_enabled(state) { + return Err(Status::failed_precondition( + "configuration push is disabled", + )); + } + let hint = request.into_inner(); + let components = ConfigComponents { + sandbox_config: hint.sandbox_config, + provider_environment: hint.provider_environment, + }; + if components.is_empty() { + return Err(Status::invalid_argument( + "at least one configuration component is required", + )); + } + let mut response = PeerConfigUpdateHintResponse::default(); + // Hints from peers never fan out to peers again. + let scope = match hint.scope { + Some(peer_config_update_hint_request::Scope::Sandbox(target)) => { + if target.sandbox_id.is_empty() || target.session_id.is_empty() { + return Err(Status::invalid_argument( + "sandbox and session IDs are required", + )); + } + // The sender resolved this session from the owner index. The + // owner check after the build guards against a later move. + if state + .supervisor_sessions + .is_current_session(&target.sandbox_id, &target.session_id) + { + state.config_delivery.with_scheduler(|scheduler| { + scheduler.publish_sandbox(&target.sandbox_id, components, Instant::now()) + }); + kick(state); + } else { + response.stale_owner = true; + } + return Ok(Response::new(response)); + } + Some(peer_config_update_hint_request::Scope::Workspace(workspace)) => { + if workspace.is_empty() { + return Err(Status::invalid_argument("workspace is required")); + } + FanoutScope::Workspace(workspace) + } + Some(peer_config_update_hint_request::Scope::Provider(target)) => { + if target.workspace.is_empty() || target.name.is_empty() { + return Err(Status::invalid_argument( + "provider workspace and name are required", + )); + } + FanoutScope::Provider { + workspace: target.workspace, + name: target.name, + } + } + Some(peer_config_update_hint_request::Scope::AllConnected(true)) => { + FanoutScope::AllConnected + } + _ => { + return Err(Status::invalid_argument( + "configuration hint scope is required", + )); + } + }; + state.config_delivery.with_scheduler(|scheduler| { + scheduler.publish_fanout(&scope, components, false, Instant::now()); + }); + kick(state); + Ok(Response::new(response)) +} + +/// Supervisor session registered for configuration push without a gRPC stream. +#[cfg(test)] +pub struct TestPushSession { + _control: tokio::sync::mpsc::Sender, + pub outbound: SessionOutbound, +} + +#[cfg(test)] +pub fn register_test_push_session( + state: &Arc, + sandbox: &Sandbox, + session_id: &str, +) -> TestPushSession { + use crate::persistence::{ObjectId, ObjectWorkspace}; + + let (control, rx) = tokio::sync::mpsc::channel(4); + let slots = Arc::new(ConfigSlots::default()); + let sandbox_id = sandbox.object_id().to_string(); + state.supervisor_sessions.register_with_config_slots( + sandbox_id.clone(), + session_id.to_string(), + control.clone(), + tokio::sync::oneshot::channel().0, + Some(Arc::clone(&slots)), + ); + register_session( + state, + Registration { + sandbox_id, + session_id: session_id.to_string(), + workspace: sandbox.object_workspace().to_string(), + providers: sandbox + .spec + .as_ref() + .map(|spec| spec.providers.iter().cloned().collect()) + .unwrap_or_default(), + captured_seq: state.config_delivery.current_seq(), + bootstrap_built: true, + }, + ); + TestPushSession { + _control: control, + outbound: SessionOutbound::new(rx, slots), + } +} + +fn record_build(component: ConfigComponentKind, lane: Lane, outcome: &'static str) { + counter!( + "openshell_supervisor_config_builds_total", + "component" => component.name(), + "lane" => lane.name(), + "outcome" => outcome, + ) + .increment(1); +} + +fn record_build_failure(key: &DeliveryKey, lane: Lane, outcome: &'static str, error_code: Code) { + record_build(key.component, lane, outcome); + warn!( + sandbox_id = %key.sandbox_id, + component = key.component.name(), + ?error_code, + "failed to build supervisor configuration snapshot" + ); +} + +fn record_delivery(component: ConfigComponentKind, outcome: &'static str) { + counter!( + "openshell_supervisor_config_deliveries_total", + "component" => component.name(), + "outcome" => outcome, + ) + .increment(1); +} + +fn record_peer_hint(outcome: &'static str) { + counter!("openshell_supervisor_config_peer_hints_total", "outcome" => outcome).increment(1); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::grpc::test_support::{ + SupervisorStreamHarness, connect_supervisor_stream, test_server_state, + }; + use openshell_core::proto::{ + GatewayMessage, ObjectMeta, Provider, SandboxSpec, gateway_message, + }; + + fn peer_request( + scope: peer_config_update_hint_request::Scope, + ) -> Request { + let mut request = Request::new(PeerConfigUpdateHintRequest { + scope: Some(scope), + sandbox_config: true, + provider_environment: false, + }); + request + .extensions_mut() + .insert(Principal::Peer(crate::auth::principal::PeerPrincipal { + replica_id: "other-replica".into(), + pod_uid: "peer-pod".into(), + })); + request + } + + fn peer_hint(sandbox_id: &str, session_id: &str) -> Request { + peer_request(peer_config_update_hint_request::Scope::Sandbox( + PeerConfigSandboxTarget { + sandbox_id: sandbox_id.into(), + session_id: session_id.into(), + }, + )) + } + + async fn push_state() -> Arc { + let mut state = test_server_state().await; + Arc::get_mut(&mut state) + .unwrap() + .config + .config_delivery_mode = ConfigDeliveryMode::Push; + state + } + + async fn put_sandbox(state: &ServerState, sandbox_id: &str, providers: &[&str]) { + state + .store + .put_message(&Sandbox { + metadata: Some(ObjectMeta { + id: sandbox_id.into(), + name: sandbox_id.into(), + workspace: "default".into(), + ..Default::default() + }), + spec: Some(SandboxSpec { + providers: providers.iter().map(ToString::to_string).collect(), + ..Default::default() + }), + ..Default::default() + }) + .await + .unwrap(); + } + + async fn put_provider(state: &ServerState, name: &str) { + state + .store + .put_message(&Provider { + metadata: Some(ObjectMeta { + id: format!("{name}-id"), + name: name.into(), + workspace: "default".into(), + ..Default::default() + }), + r#type: "github".into(), + ..Default::default() + }) + .await + .unwrap(); + } + + async fn accepted_session( + state: &Arc, + sandbox_id: &str, + ) -> (SupervisorStreamHarness, String) { + let mut harness = connect_supervisor_stream( + state, + sandbox_id, + openshell_core::proto::SUPERVISOR_PROTOCOL_REVISION, + ) + .await + .unwrap(); + let first = tokio::time::timeout(Duration::from_secs(5), harness.inbound.message()) + .await + .unwrap() + .unwrap() + .unwrap(); + let Some(gateway_message::Payload::SessionAccepted(accepted)) = first.payload else { + panic!("expected SessionAccepted"); + }; + (harness, accepted.session_id) + } + + async fn next_update( + harness: &mut SupervisorStreamHarness, + wait: Duration, + ) -> Option { + tokio::time::timeout(wait, harness.inbound.message()) + .await + .ok() + .map(|message| message.unwrap().unwrap()) + } + + fn is_config_update(message: &GatewayMessage) -> bool { + matches!( + message.payload, + Some(gateway_message::Payload::ConfigUpdate(_)) + ) + } + + #[tokio::test] + async fn peer_hint_requires_push_and_current_session() { + let state = test_server_state().await; + let error = handle_peer_config_update_hint(&state, peer_hint("sandbox", "old-session")) + .unwrap_err(); + assert_eq!(error.code(), Code::FailedPrecondition); + + let state = push_state().await; + let response = handle_peer_config_update_hint(&state, peer_hint("sandbox", "old-session")) + .unwrap() + .into_inner(); + assert!(response.stale_owner); + + let error = handle_peer_config_update_hint( + &state, + Request::new(PeerConfigUpdateHintRequest::default()), + ) + .unwrap_err(); + assert_eq!(error.code(), Code::PermissionDenied); + + let error = handle_peer_config_update_hint( + &state, + peer_request(peer_config_update_hint_request::Scope::Provider( + PeerConfigProviderTarget { + workspace: "default".into(), + name: String::new(), + }, + )), + ) + .unwrap_err(); + assert_eq!(error.code(), Code::InvalidArgument); + } + + #[tokio::test] + async fn peer_hint_rebuilds_snapshot_on_the_session_owner() { + let state = push_state().await; + put_sandbox(&state, "owned-sandbox", &[]).await; + let (mut harness, session_id) = accepted_session(&state, "owned-sandbox").await; + + let response = + handle_peer_config_update_hint(&state, peer_hint("owned-sandbox", &session_id)) + .unwrap() + .into_inner(); + assert!(!response.stale_owner); + let update = next_update(&mut harness, Duration::from_secs(5)) + .await + .expect("configuration update"); + assert!(is_config_update(&update)); + } + + #[tokio::test] + async fn provider_change_reaches_only_attached_sessions() { + let state = push_state().await; + put_provider(&state, "github").await; + put_sandbox(&state, "attached", &["github"]).await; + put_sandbox(&state, "unattached", &[]).await; + let (mut attached, _) = accepted_session(&state, "attached").await; + let (mut unattached, _) = accepted_session(&state, "unattached").await; + + publish_provider_components(&state, "default", "github", ConfigComponents::ALL); + + for _ in 0..2 { + let update = next_update(&mut attached, Duration::from_secs(5)) + .await + .expect("attached sandbox receives both components"); + assert!(is_config_update(&update)); + } + assert!( + next_update(&mut unattached, Duration::from_millis(200)) + .await + .is_none() + ); + + publish_workspace_components(&state, "default", ConfigComponents::SANDBOX_CONFIG); + assert!( + next_update(&mut unattached, Duration::from_secs(5)) + .await + .is_some_and(|update| is_config_update(&update)) + ); + } + + fn has_recipient(state: &ServerState, sandbox_id: &str) -> bool { + state + .config_delivery + .with_scheduler(|scheduler| scheduler.has_recipient(sandbox_id)) + } + + #[tokio::test] + async fn poll_mode_leaves_delivery_dormant() { + let state = test_server_state().await; + put_provider(&state, "github").await; + put_sandbox(&state, "sandbox", &["github"]).await; + let (mut harness, _) = accepted_session(&state, "sandbox").await; + assert!(!has_recipient(&state, "sandbox")); + + publish_sandbox_components(&state, "sandbox", ConfigComponents::ALL); + publish_workspace_components(&state, "default", ConfigComponents::ALL); + publish_provider_components(&state, "default", "github", ConfigComponents::ALL); + publish_all_connected(&state, ConfigComponents::ALL); + + assert_eq!(state.config_delivery.current_seq(), 0); + assert!(!state.config_delivery.lock().dispatcher_running); + assert!( + next_update(&mut harness, Duration::from_millis(100)) + .await + .is_none() + ); + } + + #[tokio::test] + async fn stale_local_session_gets_nothing_after_its_owner_moves() { + let state = push_state().await; + put_sandbox(&state, "moved", &[]).await; + let (mut harness, _) = accepted_session(&state, "moved").await; + assert!(has_recipient(&state, "moved")); + SupervisorOwnerIndex::new(Arc::clone(&state.store), OWNER_TTL) + .publish( + "moved", + "remote-session", + "instance", + 1, + "other-replica", + "http://127.0.0.1:9", + ) + .await + .unwrap(); + + publish_sandbox_components(&state, "moved", ConfigComponents::SANDBOX_CONFIG); + assert!( + next_update(&mut harness, Duration::from_millis(500)) + .await + .is_none() + ); + } + + #[tokio::test] + async fn disconnected_sessions_leave_the_scheduler() { + let state = push_state().await; + put_sandbox(&state, "sandbox", &[]).await; + let (_harness, _) = accepted_session(&state, "sandbox").await; + assert!(has_recipient(&state, "sandbox")); + + assert!(state.supervisor_sessions.disconnect("sandbox")); + tokio::time::timeout(Duration::from_secs(5), async { + while has_recipient(&state, "sandbox") { + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("session cleanup unregisters the recipient"); + } + + #[tokio::test] + async fn build_for_a_moved_session_routes_to_the_new_owner() { + let state = push_state().await; + let work = state.config_delivery.with_scheduler(|scheduler| { + scheduler.register( + Registration { + sandbox_id: "moved".into(), + session_id: "session".into(), + workspace: "default".into(), + providers: HashSet::new(), + captured_seq: 0, + bootstrap_built: true, + }, + Instant::now(), + ); + scheduler.publish_sandbox("moved", ConfigComponents::SANDBOX_CONFIG, Instant::now()); + scheduler.next_work(Instant::now()).unwrap() + }); + + complete_work( + &state, + &work, + Some(WorkResult::Build(BuildResult { + outcome: BuildOutcome::Built { providers: None }, + reroute: true, + })), + ); + let next = state + .config_delivery + .with_scheduler(|scheduler| scheduler.next_work(Instant::now())); + assert_eq!( + next, + Some(Work::Route(RouteTicket { + target: RouteTarget::Sandbox("moved".into()), + components: ConfigComponents::SANDBOX_CONFIG, + })) + ); + } + + #[test] + fn build_slots_are_sized_from_the_database_pool() { + let builds = |connections| { + ConfigDelivery::for_db_connections(connections) + .with_scheduler(|scheduler| scheduler.limits().builds()) + }; + assert_eq!(builds(10), 20); + assert_eq!(builds(1), MIN_CONCURRENT_SNAPSHOT_BUILDS); + } + + #[test] + fn configuration_message_debug_output_redacts_payloads() { + let message = SupervisorConfigMessage::ProviderEnvironment(ProviderEnvironmentSnapshot { + values: vec![openshell_core::proto::ProviderEnvironmentValue { + name: "TOKEN".into(), + value: "secret-marker".into(), + ..Default::default() + }], + ..Default::default() + }); + assert!(!format!("{message:?}").contains("secret-marker")); + } + + #[tokio::test] + async fn session_acceptance_precedes_live_configuration_updates() { + let state = push_state().await; + put_sandbox(&state, "sandbox", &[]).await; + let (mut harness, _) = accepted_session(&state, "sandbox").await; + assert!(has_recipient(&state, "sandbox")); + + publish_sandbox_components(&state, "sandbox", ConfigComponents::SANDBOX_CONFIG); + let update = next_update(&mut harness, Duration::from_secs(5)) + .await + .expect("configuration update"); + assert!(is_config_update(&update)); + } + + #[tokio::test] + async fn legacy_supervisor_keeps_polling_when_gateway_shadow_push_is_enabled() { + let state = push_state().await; + put_sandbox(&state, "legacy-sandbox", &[]).await; + let mut harness = connect_supervisor_stream( + &state, + "legacy-sandbox", + openshell_core::proto::LEGACY_SUPERVISOR_PROTOCOL_REVISION, + ) + .await + .unwrap(); + let first = harness.inbound.message().await.unwrap().unwrap(); + let Some(gateway_message::Payload::SessionAccepted(accepted)) = first.payload else { + panic!("expected SessionAccepted"); + }; + assert!(accepted.bootstrap.is_none()); + publish_sandbox_components(&state, "legacy-sandbox", ConfigComponents::SANDBOX_CONFIG); + publish_all_connected(&state, ConfigComponents::ALL); + assert!( + next_update(&mut harness, Duration::from_millis(100)) + .await + .is_none() + ); + } + + #[test] + fn bootstrap_requires_matching_provider_revision_fence() { + let mut bootstrap = ConfigBootstrap { + sandbox_config: Some(SandboxConfigSnapshot { + provider_env_revision: 7, + ..Default::default() + }), + provider_environment: Some(ProviderEnvironmentSnapshot { + provider_env_revision: 8, + ..Default::default() + }), + }; + assert!(!bootstrap_revisions_match(&bootstrap)); + bootstrap + .provider_environment + .as_mut() + .unwrap() + .provider_env_revision = 7; + assert!(bootstrap_revisions_match(&bootstrap)); + } + + #[tokio::test] + async fn stalled_credentials_do_not_block_session_acceptance() { + use openshell_core::proto::CredentialHandle; + + let state = push_state().await; + state + .store + .put_message(&Provider { + metadata: Some(ObjectMeta { + id: "provider".into(), + name: "provider".into(), + workspace: "default".into(), + ..Default::default() + }), + r#type: "github".into(), + credential_handles: HashMap::from([( + "GITHUB_TOKEN".into(), + CredentialHandle { + driver: "test-static".into(), + handle: "blocked".into(), + ..Default::default() + }, + )]), + ..Default::default() + }) + .await + .unwrap(); + put_sandbox(&state, "sandbox", &["provider"]).await; + let (resolve_hit, _release_resolve) = state.credentials.gate_next_resolve(); + tokio::time::timeout(Duration::from_secs(10), async { + let connect = connect_supervisor_stream( + &state, + "sandbox", + openshell_core::proto::SUPERVISOR_PROTOCOL_REVISION, + ); + let (response, hit) = tokio::join!(connect, resolve_hit); + hit.expect("bootstrap must reach the stalled credential driver"); + let mut harness = response.unwrap(); + let first = harness.inbound.message().await.unwrap().unwrap(); + let Some(gateway_message::Payload::SessionAccepted(accepted)) = first.payload else { + panic!("expected session acceptance"); + }; + assert!(accepted.bootstrap.is_none()); + assert!( + state + .supervisor_sessions + .is_current_session("sandbox", &accepted.session_id) + ); + // Relay control remains usable while credential resolution is stalled. + let (_, relay) = state + .supervisor_sessions + .open_relay("sandbox", Duration::from_secs(1)) + .await + .unwrap(); + let message = harness.inbound.message().await.unwrap().unwrap(); + assert!(matches!( + message.payload, + Some(gateway_message::Payload::RelayOpen(_)) + )); + drop(relay); + }) + .await + .expect("optional bootstrap must not consume the relay reconnect budget"); + } +} diff --git a/crates/openshell-server/src/config_delivery/scheduler.rs b/crates/openshell-server/src/config_delivery/scheduler.rs new file mode 100644 index 000000000..cf3c63c75 --- /dev/null +++ b/crates/openshell-server/src/config_delivery/scheduler.rs @@ -0,0 +1,1342 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Synchronous scheduling policy for supervisor configuration delivery. +//! +//! Every publication takes a sequence number. When a build is dispatched, its +//! key records the highest sequence issued so far. Publications are issued +//! after their commits, so that build observes state at least as new as every +//! publication it covers, and a key needs work for publication `P` only while +//! its `last_build_seq < P`. +//! +//! Admission never rejects: every key set is bounded by sandboxes, workspaces, +//! and providers, so repeated publications only coalesce. Fanouts are cursors +//! over the sorted local recipients rather than queued keys, and build slots +//! are granted only here, with a reserve for sandbox-scoped work. + +use std::collections::hash_map::Entry; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; +use std::ops::Bound; +use std::time::Instant; + +use metrics::{counter, gauge, histogram}; + +use super::{ConfigComponentKind, ConfigComponents}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DeliveryKey { + pub sandbox_id: String, + pub component: ConfigComponentKind, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum FanoutScope { + AllConnected, + Workspace(String), + /// Sandboxes in `workspace` that attach the named provider. + Provider { + workspace: String, + name: String, + }, +} + +impl FanoutScope { + fn includes(&self, other: &Self) -> bool { + match (self, other) { + (Self::AllConnected, _) => true, + ( + Self::Workspace(workspace), + Self::Workspace(other) + | Self::Provider { + workspace: other, .. + }, + ) => workspace == other, + (Self::Provider { .. }, Self::Provider { .. }) => self == other, + _ => false, + } + } + + fn workspace(&self) -> Option<&str> { + match self { + Self::AllConnected => None, + Self::Workspace(workspace) | Self::Provider { workspace, .. } => Some(workspace), + } + } + + fn metric_label(&self) -> &'static str { + match self { + Self::AllConnected => "all_connected", + Self::Workspace(_) => "workspace", + Self::Provider { .. } => "provider", + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct FanoutKey { + scope: FanoutScope, + component: ConfigComponentKind, +} + +/// Lanes are ordered by priority. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Lane { + Fanout, + Direct, +} + +impl Lane { + pub(crate) fn name(self) -> &'static str { + match self { + Self::Fanout => "fanout", + Self::Direct => "direct", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Limits { + builds: usize, + direct_reserve: usize, + routes: usize, +} + +impl Limits { + /// Fanout work may use every build slot except `direct_reserve`, and + /// sandbox-scoped work leaves one slot for waiting fanout work. + pub(crate) fn new(builds: usize, routes: usize) -> Self { + let builds = builds.max(2); + Self { + builds, + direct_reserve: (builds / 4).clamp(1, builds - 1), + routes: routes.max(1), + } + } + + #[cfg(test)] + pub(crate) fn builds(self) -> usize { + self.builds + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Work { + Build(BuildTicket), + Route(RouteTicket), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BuildTicket { + pub key: DeliveryKey, + pub lane: Lane, + /// Session registered when the build was dispatched. Delivery must not + /// cross into a replacement session, which received its own bootstrap. + pub session_id: String, + epoch: u64, + seq: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum RouteTarget { + /// Sandbox without a local push recipient; its owner may be a peer. + Sandbox(String), + /// Scope hint for every peer gateway. + Peers(FanoutScope), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RouteTicket { + pub target: RouteTarget, + pub components: ConfigComponents, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BuildOutcome { + /// `providers` is the attachment list read with the snapshot, when the + /// sandbox still exists. + Built { + providers: Option>, + }, + Failed, +} + +#[derive(Debug, Clone)] +pub struct Registration { + pub sandbox_id: String, + pub session_id: String, + pub workspace: String, + pub providers: HashSet, + /// `current_seq()` observed before the bootstrap build started. + pub captured_seq: u64, + pub bootstrap_built: bool, +} + +#[derive(Debug, Default)] +struct KeyState { + last_build_seq: u64, + rollback_seq: u64, + in_flight: bool, + queued: Option, + requeue: Option, + /// A fanout skipped this in-flight key as current; rebuild if it fails. + requeue_on_failure: Option, + waiting_since: Option, +} + +#[derive(Debug)] +struct Recipient { + session_id: String, + epoch: u64, + workspace: String, + providers: HashSet, + /// Dispatch sequence of the build that last refreshed `providers`. Builds + /// can finish out of order, and an older attachment list must not win. + providers_seq: u64, + sandbox_config: KeyState, + provider_environment: KeyState, +} + +impl Recipient { + fn key(&self, component: ConfigComponentKind) -> &KeyState { + match component { + ConfigComponentKind::SandboxConfig => &self.sandbox_config, + ConfigComponentKind::ProviderEnvironment => &self.provider_environment, + } + } + + fn key_mut(&mut self, component: ConfigComponentKind) -> &mut KeyState { + match component { + ConfigComponentKind::SandboxConfig => &mut self.sandbox_config, + ConfigComponentKind::ProviderEnvironment => &mut self.provider_environment, + } + } +} + +#[derive(Debug)] +struct Cursor { + target_seq: u64, + /// Last sandbox handed out or skipped in sorted order. + position: Option, + /// Position at the latest publication. After wrapping, the pass ends here. + lap_end: Option, + wrapped: bool, + started: Instant, +} + +#[derive(Debug, Default)] +struct RouteState { + pending: ConfigComponents, + queued: bool, + in_flight: bool, +} + +#[derive(Debug, Default)] +struct Running { + direct: usize, + fanout: usize, + routes: usize, +} + +impl Running { + fn builds(&self) -> usize { + self.direct + self.fanout + } + + fn lane_mut(&mut self, lane: Lane) -> &mut usize { + match lane { + Lane::Direct => &mut self.direct, + Lane::Fanout => &mut self.fanout, + } + } +} + +#[derive(Debug, Default)] +struct QueuedCounts { + direct: usize, + fanout: usize, +} + +impl QueuedCounts { + fn transition(&mut self, from: Option, to: Option) { + if let Some(lane) = from { + *self.lane_mut(lane) -= 1; + } + if let Some(lane) = to { + *self.lane_mut(lane) += 1; + } + } + + fn lane_mut(&mut self, lane: Lane) -> &mut usize { + match lane { + Lane::Direct => &mut self.direct, + Lane::Fanout => &mut self.fanout, + } + } +} + +#[derive(Debug)] +pub struct Scheduler { + limits: Limits, + seq: u64, + next_epoch: u64, + recipients: BTreeMap, + by_workspace: HashMap>, + direct: VecDeque, + fanout_retry: VecDeque, + queued: QueuedCounts, + cursors: HashMap, + cursor_order: VecDeque, + routes: HashMap, + route_ready: VecDeque, + running: Running, +} + +impl Scheduler { + pub(crate) fn new(limits: Limits) -> Self { + Self { + limits, + seq: 0, + next_epoch: 0, + recipients: BTreeMap::new(), + by_workspace: HashMap::new(), + direct: VecDeque::new(), + fanout_retry: VecDeque::new(), + queued: QueuedCounts::default(), + cursors: HashMap::new(), + cursor_order: VecDeque::new(), + routes: HashMap::new(), + route_ready: VecDeque::new(), + running: Running::default(), + } + } + + #[cfg(test)] + pub(crate) fn limits(&self) -> Limits { + self.limits + } + + pub(crate) fn current_seq(&self) -> u64 { + self.seq + } + + #[cfg(test)] + pub(crate) fn has_recipient(&self, sandbox_id: &str) -> bool { + self.recipients.contains_key(sandbox_id) + } + + /// Add or replace the local push recipient for a sandbox. + /// + /// A publication between bootstrap and registration may have passed this + /// sandbox's cursor position before it became visible, so it is rebuilt + /// directly. + pub(crate) fn register(&mut self, registration: Registration, now: Instant) { + let Registration { + sandbox_id, + session_id, + workspace, + providers, + captured_seq, + bootstrap_built, + } = registration; + self.remove_recipient(&sandbox_id); + self.next_epoch += 1; + let built_seq = if bootstrap_built { captured_seq } else { 0 }; + let built = || KeyState { + last_build_seq: built_seq, + rollback_seq: built_seq, + ..KeyState::default() + }; + self.by_workspace + .entry(workspace.clone()) + .or_default() + .insert(sandbox_id.clone()); + self.recipients.insert( + sandbox_id.clone(), + Recipient { + session_id, + epoch: self.next_epoch, + workspace, + providers, + providers_seq: captured_seq, + sandbox_config: built(), + provider_environment: built(), + }, + ); + if self.seq > captured_seq { + for component in ConfigComponents::ALL.selected() { + self.enqueue_key(&sandbox_id, component, Lane::Direct, now); + } + } + } + + pub(crate) fn unregister(&mut self, sandbox_id: &str, session_id: &str) { + if self + .recipients + .get(sandbox_id) + .is_some_and(|recipient| recipient.session_id == session_id) + { + self.remove_recipient(sandbox_id); + } + } + + /// Returns false when this gateway has no push recipient for the sandbox. + pub(crate) fn publish_sandbox( + &mut self, + sandbox_id: &str, + components: ConfigComponents, + now: Instant, + ) -> bool { + self.seq += 1; + if !self.recipients.contains_key(sandbox_id) { + return false; + } + for component in components.selected() { + self.enqueue_key(sandbox_id, component, Lane::Direct, now); + } + true + } + + pub(crate) fn publish_fanout( + &mut self, + scope: &FanoutScope, + components: ConfigComponents, + notify_peers: bool, + now: Instant, + ) { + self.seq += 1; + let seq = self.seq; + for component in components.selected() { + self.cursors.retain(|key, _| { + key.component != component || key.scope == *scope || !scope.includes(&key.scope) + }); + let key = FanoutKey { + scope: scope.clone(), + component, + }; + match self.cursors.entry(key.clone()) { + Entry::Occupied(mut entry) => { + let cursor = entry.get_mut(); + cursor.target_seq = seq; + cursor.lap_end.clone_from(&cursor.position); + cursor.wrapped = false; + } + Entry::Vacant(entry) => { + entry.insert(Cursor { + target_seq: seq, + position: None, + lap_end: None, + wrapped: false, + started: now, + }); + self.cursor_order.push_back(key); + } + } + } + let cursors = &self.cursors; + self.cursor_order.retain(|key| cursors.contains_key(key)); + if notify_peers { + self.route(RouteTarget::Peers(scope.clone()), components); + } + } + + pub(crate) fn route(&mut self, target: RouteTarget, components: ConfigComponents) { + let state = self.routes.entry(target.clone()).or_default(); + state.pending = state.pending.union(components); + if !state.queued && !state.in_flight { + state.queued = true; + self.route_ready.push_back(target); + } + } + + pub(crate) fn next_work(&mut self, now: Instant) -> Option { + if self.running.routes < self.limits.routes + && let Some(target) = self.route_ready.pop_front() + { + let state = self.routes.get_mut(&target).expect("ready route has state"); + state.queued = false; + state.in_flight = true; + self.running.routes += 1; + return Some(Work::Route(RouteTicket { + target, + components: std::mem::take(&mut state.pending), + })); + } + self.next_build(now) + } + + pub(crate) fn complete_build( + &mut self, + ticket: &BuildTicket, + outcome: BuildOutcome, + now: Instant, + ) { + *self.running.lane_mut(ticket.lane) -= 1; + let Some(recipient) = self.recipients.get_mut(&ticket.key.sandbox_id) else { + return; + }; + if recipient.epoch != ticket.epoch { + return; + } + let failed = outcome == BuildOutcome::Failed; + if let BuildOutcome::Built { + providers: Some(providers), + } = outcome + && ticket.seq >= recipient.providers_seq + { + recipient.providers = providers; + recipient.providers_seq = ticket.seq; + } + let state = recipient.key_mut(ticket.key.component); + state.in_flight = false; + let requeue = if failed { + state.last_build_seq = state.rollback_seq; + state.requeue.take().max(state.requeue_on_failure.take()) + } else { + state.requeue_on_failure = None; + state.requeue.take() + }; + if let Some(lane) = requeue { + self.enqueue_key(&ticket.key.sandbox_id, ticket.key.component, lane, now); + } + } + + pub(crate) fn complete_route(&mut self, ticket: &RouteTicket) { + self.running.routes -= 1; + let Some(state) = self.routes.get_mut(&ticket.target) else { + return; + }; + state.in_flight = false; + if state.pending.is_empty() { + self.routes.remove(&ticket.target); + } else { + state.queued = true; + self.route_ready.push_back(ticket.target.clone()); + } + } + + pub(crate) fn has_running_work(&self) -> bool { + self.running.builds() > 0 || self.running.routes > 0 + } + + pub(crate) fn record_gauges(&self) { + gauge!("openshell_supervisor_config_pending", "lane" => "direct") + .set(u32::try_from(self.queued.direct).unwrap_or(u32::MAX)); + gauge!("openshell_supervisor_config_pending", "lane" => "fanout") + .set(u32::try_from(self.queued.fanout).unwrap_or(u32::MAX)); + gauge!("openshell_supervisor_config_pending", "lane" => "route") + .set(u32::try_from(self.route_ready.len()).unwrap_or(u32::MAX)); + gauge!("openshell_supervisor_config_active_fanouts") + .set(u32::try_from(self.cursors.len()).unwrap_or(u32::MAX)); + } + + fn next_build(&mut self, now: Instant) -> Option { + if self.running.builds() >= self.limits.builds { + return None; + } + // Direct work leaves the last slot to waiting fanout work that has no + // build running yet. + let fanout_waiting = !self.fanout_retry.is_empty() || !self.cursors.is_empty(); + let keeps_fanout_floor = fanout_waiting + && self.running.fanout == 0 + && self.running.builds() + 1 == self.limits.builds; + if !keeps_fanout_floor && let Some(key) = self.pop_queued(Lane::Direct) { + return Some(self.dispatch(key, Lane::Direct, now)); + } + if self.running.fanout < self.limits.builds - self.limits.direct_reserve + && let Some(work) = self.next_fanout(now) + { + return Some(work); + } + // Fanout had nothing to start, so the floor is not needed. + if keeps_fanout_floor && let Some(key) = self.pop_queued(Lane::Direct) { + return Some(self.dispatch(key, Lane::Direct, now)); + } + None + } + + fn next_fanout(&mut self, now: Instant) -> Option { + if let Some(key) = self.pop_queued(Lane::Fanout) { + return Some(self.dispatch(key, Lane::Fanout, now)); + } + for _ in 0..self.cursor_order.len() { + let key = self.cursor_order.pop_front()?; + let Some(mut cursor) = self.cursors.remove(&key) else { + continue; + }; + if let Some(sandbox_id) = self.advance(&key, &mut cursor) { + let component = key.component; + self.cursors.insert(key.clone(), cursor); + self.cursor_order.push_back(key); + return Some(self.dispatch( + DeliveryKey { + sandbox_id, + component, + }, + Lane::Fanout, + now, + )); + } + histogram!( + "openshell_supervisor_config_fanout_pass_seconds", + "scope" => key.scope.metric_label(), + ) + .record(now.saturating_duration_since(cursor.started).as_secs_f64()); + } + None + } + + fn advance(&mut self, key: &FanoutKey, cursor: &mut Cursor) -> Option { + let component = key.component; + loop { + let Some(sandbox_id) = self.next_in_scope(&key.scope, cursor.position.as_deref()) + else { + if cursor.wrapped || cursor.lap_end.is_none() { + return None; + } + cursor.wrapped = true; + cursor.position = None; + continue; + }; + if cursor.wrapped + && cursor + .lap_end + .as_deref() + .is_some_and(|end| sandbox_id.as_str() > end) + { + return None; + } + cursor.position = Some(sandbox_id.clone()); + let recipient = self + .recipients + .get_mut(&sandbox_id) + .expect("scope index lists only recipients"); + // An in-flight build may be the one that installs a new attachment + // in the provider cache, so it cannot be filtered out yet. + if let FanoutScope::Provider { name, .. } = &key.scope + && !recipient.providers.contains(name) + && !recipient.key(component).in_flight + { + record_skip(component, "not_attached"); + continue; + } + let state = recipient.key_mut(component); + if state.in_flight { + if state.last_build_seq < cursor.target_seq { + state.requeue = state.requeue.max(Some(Lane::Fanout)); + } else { + state.requeue_on_failure = state.requeue_on_failure.max(Some(Lane::Fanout)); + } + record_skip(component, "in_flight"); + continue; + } + if state.last_build_seq >= cursor.target_seq { + record_skip(component, "current"); + continue; + } + if state.queued.is_some() { + record_skip(component, "queued"); + continue; + } + return Some(sandbox_id); + } + } + + fn next_in_scope(&self, scope: &FanoutScope, after: Option<&str>) -> Option { + let range = ( + after.map_or(Bound::Unbounded, Bound::Excluded), + Bound::Unbounded, + ); + match scope.workspace() { + None => self + .recipients + .range::(range) + .next() + .map(|(sandbox_id, _)| sandbox_id.clone()), + Some(workspace) => self + .by_workspace + .get(workspace)? + .range::(range) + .next() + .cloned(), + } + } + + fn pop_queued(&mut self, lane: Lane) -> Option { + loop { + let key = match lane { + Lane::Direct => self.direct.pop_front(), + Lane::Fanout => self.fanout_retry.pop_front(), + }?; + if self + .recipients + .get(&key.sandbox_id) + .is_some_and(|recipient| recipient.key(key.component).queued == Some(lane)) + { + return Some(key); + } + } + } + + fn dispatch(&mut self, key: DeliveryKey, lane: Lane, now: Instant) -> Work { + let seq = self.seq; + let recipient = self + .recipients + .get_mut(&key.sandbox_id) + .expect("dispatched key has a recipient"); + let state = recipient.key_mut(key.component); + self.queued.transition(state.queued, None); + state.queued = None; + state.in_flight = true; + state.rollback_seq = state.last_build_seq; + state.last_build_seq = seq; + if let Some(since) = state.waiting_since.take() { + histogram!( + "openshell_supervisor_config_dispatch_wait_seconds", + "lane" => lane.name(), + ) + .record(now.saturating_duration_since(since).as_secs_f64()); + } + *self.running.lane_mut(lane) += 1; + Work::Build(BuildTicket { + key, + lane, + session_id: recipient.session_id.clone(), + epoch: recipient.epoch, + seq, + }) + } + + fn enqueue_key( + &mut self, + sandbox_id: &str, + component: ConfigComponentKind, + lane: Lane, + now: Instant, + ) { + let Some(recipient) = self.recipients.get_mut(sandbox_id) else { + return; + }; + let state = recipient.key_mut(component); + if lane == Lane::Direct && state.waiting_since.is_none() { + state.waiting_since = Some(now); + } + if state.in_flight { + state.requeue = state.requeue.max(Some(lane)); + return; + } + if state.queued.is_some_and(|queued| queued >= lane) { + return; + } + self.queued.transition(state.queued, Some(lane)); + state.queued = Some(lane); + let key = DeliveryKey { + sandbox_id: sandbox_id.to_string(), + component, + }; + match lane { + Lane::Direct => self.direct.push_back(key), + Lane::Fanout => self.fanout_retry.push_back(key), + } + } + + fn remove_recipient(&mut self, sandbox_id: &str) { + let Some(recipient) = self.recipients.remove(sandbox_id) else { + return; + }; + self.queued + .transition(recipient.sandbox_config.queued, None); + self.queued + .transition(recipient.provider_environment.queued, None); + if let Some(sandboxes) = self.by_workspace.get_mut(&recipient.workspace) { + sandboxes.remove(sandbox_id); + if sandboxes.is_empty() { + self.by_workspace.remove(&recipient.workspace); + } + } + } +} + +fn record_skip(component: ConfigComponentKind, reason: &'static str) { + counter!( + "openshell_supervisor_config_build_skips_total", + "component" => component.name(), + "reason" => reason, + ) + .increment(1); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn registration(sandbox_id: &str, workspace: &str, providers: &[&str]) -> Registration { + Registration { + sandbox_id: sandbox_id.to_string(), + session_id: format!("{sandbox_id}-session"), + workspace: workspace.to_string(), + providers: providers.iter().map(ToString::to_string).collect(), + captured_seq: 0, + bootstrap_built: true, + } + } + + fn scheduler(builds: usize) -> Scheduler { + Scheduler::new(Limits::new(builds, 2)) + } + + fn with_recipients(builds: usize, count: usize) -> Scheduler { + let mut scheduler = scheduler(builds); + for index in 0..count { + scheduler.register( + registration(&format!("sb-{index:04}"), "ws", &[]), + Instant::now(), + ); + } + scheduler + } + + fn build(work: Work) -> BuildTicket { + match work { + Work::Build(ticket) => ticket, + Work::Route(ticket) => panic!("expected a build, got {ticket:?}"), + } + } + + fn built() -> BuildOutcome { + BuildOutcome::Built { providers: None } + } + + /// Run all available work to completion, one ticket at a time. + fn drain(scheduler: &mut Scheduler) -> Vec { + let mut completed = Vec::new(); + while let Some(work) = scheduler.next_work(Instant::now()) { + match work { + Work::Build(ticket) => { + scheduler.complete_build(&ticket, built(), Instant::now()); + completed.push(ticket); + } + Work::Route(ticket) => scheduler.complete_route(&ticket), + } + } + completed + } + + fn sandbox_ids(tickets: &[BuildTicket]) -> Vec<&str> { + tickets + .iter() + .map(|ticket| ticket.key.sandbox_id.as_str()) + .collect() + } + + #[test] + fn sandbox_update_starts_while_a_fleet_fanout_holds_its_slots() { + let mut scheduler = with_recipients(4, 1000); + scheduler.publish_fanout( + &FanoutScope::AllConnected, + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + let mut fanout = Vec::new(); + while let Some(work) = scheduler.next_work(Instant::now()) { + fanout.push(build(work)); + } + assert_eq!(fanout.len(), 3, "fanout leaves the direct reserve free"); + assert!(fanout.iter().all(|ticket| ticket.lane == Lane::Fanout)); + + assert!(scheduler.publish_sandbox( + "sb-0999", + ConfigComponents::SANDBOX_CONFIG, + Instant::now() + )); + let direct = build(scheduler.next_work(Instant::now()).unwrap()); + assert_eq!(direct.key.sandbox_id, "sb-0999"); + assert_eq!(direct.lane, Lane::Direct); + } + + #[test] + fn sandbox_updates_never_freeze_a_fanout() { + let mut scheduler = scheduler(4); + for index in 0..100 { + scheduler.register( + registration(&format!("quiet-{index:03}"), "quiet", &[]), + Instant::now(), + ); + scheduler.register( + registration(&format!("busy-{index:03}"), "busy", &[]), + Instant::now(), + ); + } + scheduler.publish_fanout( + &FanoutScope::Workspace("quiet".into()), + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + for index in 0..100 { + scheduler.publish_sandbox( + &format!("busy-{index:03}"), + ConfigComponents::SANDBOX_CONFIG, + Instant::now(), + ); + } + let lanes = (0..4) + .map(|_| build(scheduler.next_work(Instant::now()).unwrap()).lane) + .collect::>(); + assert_eq!( + lanes, + [Lane::Direct, Lane::Direct, Lane::Direct, Lane::Fanout] + ); + } + + #[test] + fn repeated_updates_coalesce_without_rejection() { + let mut scheduler = with_recipients(2, 50); + for _ in 0..3 { + for index in 0..50 { + scheduler.publish_sandbox( + &format!("sb-{index:04}"), + ConfigComponents::ALL, + Instant::now(), + ); + } + } + let completed = drain(&mut scheduler); + assert_eq!(completed.len(), 100, "one build per sandbox component"); + let distinct = completed + .iter() + .map(|ticket| ticket.key.clone()) + .collect::>(); + assert_eq!(distinct.len(), 100); + } + + #[test] + fn publication_during_a_pass_costs_one_circle() { + const RECIPIENTS: usize = 100; + const BEFORE: usize = 30; + let mut scheduler = with_recipients(2, RECIPIENTS); + let scope = FanoutScope::AllConnected; + scheduler.publish_fanout( + &scope, + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + for _ in 0..BEFORE { + let ticket = build(scheduler.next_work(Instant::now()).unwrap()); + scheduler.complete_build(&ticket, built(), Instant::now()); + } + scheduler.publish_fanout( + &scope, + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + let second = scheduler.current_seq(); + let completed = drain(&mut scheduler); + + assert_eq!(completed.len(), RECIPIENTS); + assert_eq!( + completed.first().unwrap().key.sandbox_id, + format!("sb-{BEFORE:04}"), + "the pass continues from its position" + ); + assert!( + scheduler + .recipients + .values() + .all(|recipient| recipient.sandbox_config.last_build_seq >= second) + ); + assert!(scheduler.cursors.is_empty()); + } + + #[test] + fn wider_publication_replaces_narrower_passes() { + let mut scheduler = scheduler(4); + for (sandbox_id, workspace) in [("a", "ws-1"), ("b", "ws-1"), ("c", "ws-2")] { + scheduler.register(registration(sandbox_id, workspace, &["p"]), Instant::now()); + } + scheduler.publish_fanout( + &FanoutScope::Provider { + workspace: "ws-1".into(), + name: "p".into(), + }, + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + scheduler.publish_fanout( + &FanoutScope::Workspace("ws-1".into()), + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + scheduler.publish_fanout( + &FanoutScope::AllConnected, + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + assert_eq!(scheduler.cursors.len(), 1); + assert_eq!(sandbox_ids(&drain(&mut scheduler)), ["a", "b", "c"]); + } + + #[test] + fn overlapping_passes_build_each_sandbox_once() { + let mut scheduler = with_recipients(4, 20); + scheduler.publish_fanout( + &FanoutScope::AllConnected, + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + scheduler.publish_fanout( + &FanoutScope::Workspace("ws".into()), + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + assert_eq!(scheduler.cursors.len(), 2); + assert_eq!(drain(&mut scheduler).len(), 20); + } + + #[test] + fn fanout_skips_a_sandbox_rebuilt_after_its_publication() { + let mut scheduler = with_recipients(4, 3); + scheduler.publish_fanout( + &FanoutScope::AllConnected, + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + scheduler.publish_sandbox("sb-0002", ConfigComponents::SANDBOX_CONFIG, Instant::now()); + let completed = drain(&mut scheduler); + assert_eq!(sandbox_ids(&completed), ["sb-0002", "sb-0000", "sb-0001"]); + } + + #[test] + fn publication_during_a_build_rebuilds_once_in_its_lane() { + let mut scheduler = with_recipients(4, 1); + scheduler.publish_sandbox("sb-0000", ConfigComponents::SANDBOX_CONFIG, Instant::now()); + let first = build(scheduler.next_work(Instant::now()).unwrap()); + scheduler.publish_fanout( + &FanoutScope::AllConnected, + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + scheduler.publish_sandbox("sb-0000", ConfigComponents::SANDBOX_CONFIG, Instant::now()); + assert!( + scheduler.next_work(Instant::now()).is_none(), + "an in-flight key is not built twice concurrently" + ); + scheduler.complete_build(&first, built(), Instant::now()); + let second = build(scheduler.next_work(Instant::now()).unwrap()); + assert_eq!(second.lane, Lane::Direct); + scheduler.complete_build(&second, built(), Instant::now()); + assert!(drain(&mut scheduler).is_empty()); + } + + #[test] + fn failed_build_restores_its_sequence() { + let mut scheduler = with_recipients(4, 1); + scheduler.publish_sandbox("sb-0000", ConfigComponents::SANDBOX_CONFIG, Instant::now()); + let ticket = build(scheduler.next_work(Instant::now()).unwrap()); + scheduler.complete_build(&ticket, BuildOutcome::Failed, Instant::now()); + assert!(scheduler.next_work(Instant::now()).is_none()); + + scheduler.publish_fanout( + &FanoutScope::AllConnected, + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + assert_eq!(drain(&mut scheduler).len(), 1); + } + + #[test] + fn failed_build_retries_when_a_fanout_skipped_it_as_current() { + let mut scheduler = with_recipients(4, 1); + scheduler.publish_fanout( + &FanoutScope::AllConnected, + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + scheduler.publish_sandbox("sb-0000", ConfigComponents::SANDBOX_CONFIG, Instant::now()); + let ticket = build(scheduler.next_work(Instant::now()).unwrap()); + assert_eq!(ticket.lane, Lane::Direct); + assert!(scheduler.next_work(Instant::now()).is_none()); + scheduler.complete_build(&ticket, BuildOutcome::Failed, Instant::now()); + let retry = build(scheduler.next_work(Instant::now()).unwrap()); + assert_eq!(retry.lane, Lane::Fanout); + } + + #[test] + fn bootstrap_covers_registration_until_a_publication_races_it() { + let mut scheduler = scheduler(4); + let captured = scheduler.current_seq(); + scheduler.register( + Registration { + captured_seq: captured, + ..registration("fresh", "ws", &[]) + }, + Instant::now(), + ); + scheduler.publish_fanout( + &FanoutScope::Workspace("other".into()), + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + assert!(drain(&mut scheduler).is_empty()); + + let captured = scheduler.current_seq(); + scheduler.publish_sandbox( + "unrelated", + ConfigComponents::SANDBOX_CONFIG, + Instant::now(), + ); + scheduler.register( + Registration { + captured_seq: captured, + ..registration("raced", "ws", &[]) + }, + Instant::now(), + ); + let completed = drain(&mut scheduler); + assert_eq!(completed.len(), 2); + assert!(completed.iter().all(|ticket| ticket.lane == Lane::Direct)); + } + + #[test] + fn provider_pass_builds_only_attached_sandboxes() { + let mut scheduler = scheduler(4); + scheduler.register(registration("attached", "ws", &["github"]), Instant::now()); + scheduler.register(registration("other", "ws", &["gitlab"]), Instant::now()); + scheduler.register( + registration("elsewhere", "ws-2", &["github"]), + Instant::now(), + ); + scheduler.publish_fanout( + &FanoutScope::Provider { + workspace: "ws".into(), + name: "github".into(), + }, + ConfigComponents::ALL, + false, + Instant::now(), + ); + let completed = drain(&mut scheduler); + assert_eq!(completed.len(), 2); + assert!( + completed + .iter() + .all(|ticket| ticket.key.sandbox_id == "attached") + ); + } + + #[test] + fn provider_pass_includes_an_attachment_still_being_built() { + let mut scheduler = scheduler(4); + scheduler.register(registration("sb", "ws", &[]), Instant::now()); + scheduler.publish_sandbox("sb", ConfigComponents::SANDBOX_CONFIG, Instant::now()); + let attach = build(scheduler.next_work(Instant::now()).unwrap()); + scheduler.publish_fanout( + &FanoutScope::Provider { + workspace: "ws".into(), + name: "github".into(), + }, + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + assert!(scheduler.next_work(Instant::now()).is_none()); + scheduler.complete_build( + &attach, + BuildOutcome::Built { + providers: Some(HashSet::from(["github".to_string()])), + }, + Instant::now(), + ); + assert_eq!(sandbox_ids(&drain(&mut scheduler)), ["sb"]); + + scheduler.publish_fanout( + &FanoutScope::Provider { + workspace: "ws".into(), + name: "github".into(), + }, + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + assert_eq!( + sandbox_ids(&drain(&mut scheduler)), + ["sb"], + "the refreshed attachment cache keeps the sandbox in scope" + ); + } + + #[test] + fn late_build_does_not_roll_back_the_attachment_cache() { + let mut scheduler = scheduler(4); + scheduler.register(registration("sb", "ws", &[]), Instant::now()); + scheduler.publish_sandbox("sb", ConfigComponents::SANDBOX_CONFIG, Instant::now()); + let before_attach = build(scheduler.next_work(Instant::now()).unwrap()); + scheduler.publish_sandbox("sb", ConfigComponents::ALL, Instant::now()); + let after_attach = build(scheduler.next_work(Instant::now()).unwrap()); + assert_eq!( + after_attach.key.component, + ConfigComponentKind::ProviderEnvironment + ); + scheduler.complete_build( + &after_attach, + BuildOutcome::Built { + providers: Some(HashSet::from(["github".to_string()])), + }, + Instant::now(), + ); + scheduler.complete_build( + &before_attach, + BuildOutcome::Built { + providers: Some(HashSet::new()), + }, + Instant::now(), + ); + scheduler.publish_fanout( + &FanoutScope::Provider { + workspace: "ws".into(), + name: "github".into(), + }, + ConfigComponents::ALL, + false, + Instant::now(), + ); + let components = drain(&mut scheduler) + .into_iter() + .map(|ticket| ticket.key.component) + .collect::>(); + assert!( + components.contains(&ConfigComponentKind::ProviderEnvironment), + "the provider pass must reach the attached sandbox" + ); + } + + #[test] + fn fanout_floor_releases_when_fanout_has_nothing_to_start() { + let mut scheduler = scheduler(4); + for index in 0..4 { + scheduler.register( + registration(&format!("busy-{index}"), "busy", &[]), + Instant::now(), + ); + } + scheduler.publish_fanout( + &FanoutScope::Workspace("empty".into()), + ConfigComponents::SANDBOX_CONFIG, + false, + Instant::now(), + ); + for index in 0..4 { + scheduler.publish_sandbox( + &format!("busy-{index}"), + ConfigComponents::SANDBOX_CONFIG, + Instant::now(), + ); + } + let lanes = (0..4) + .map(|_| build(scheduler.next_work(Instant::now()).unwrap()).lane) + .collect::>(); + assert_eq!(lanes, [Lane::Direct; 4]); + } + + #[test] + fn remote_sandbox_updates_coalesce_into_one_route() { + let mut scheduler = scheduler(4); + assert!(!scheduler.publish_sandbox( + "remote", + ConfigComponents::SANDBOX_CONFIG, + Instant::now() + )); + scheduler.route( + RouteTarget::Sandbox("remote".into()), + ConfigComponents::SANDBOX_CONFIG, + ); + scheduler.route( + RouteTarget::Sandbox("remote".into()), + ConfigComponents { + sandbox_config: false, + provider_environment: true, + }, + ); + let Some(Work::Route(ticket)) = scheduler.next_work(Instant::now()) else { + panic!("expected a route"); + }; + assert_eq!(ticket.components, ConfigComponents::ALL); + assert!(scheduler.next_work(Instant::now()).is_none()); + + scheduler.route( + RouteTarget::Sandbox("remote".into()), + ConfigComponents::SANDBOX_CONFIG, + ); + assert!(scheduler.next_work(Instant::now()).is_none()); + scheduler.complete_route(&ticket); + let Some(Work::Route(again)) = scheduler.next_work(Instant::now()) else { + panic!("an update during routing routes again"); + }; + assert_eq!(again.components, ConfigComponents::SANDBOX_CONFIG); + scheduler.complete_route(&again); + assert!(scheduler.routes.is_empty()); + } + + #[test] + fn local_fanout_notifies_peers_once_per_publication() { + let mut scheduler = scheduler(4); + let scope = FanoutScope::Workspace("ws".into()); + scheduler.publish_fanout( + &scope, + ConfigComponents::SANDBOX_CONFIG, + true, + Instant::now(), + ); + scheduler.publish_fanout(&scope, ConfigComponents::ALL, true, Instant::now()); + let Some(Work::Route(ticket)) = scheduler.next_work(Instant::now()) else { + panic!("expected a peer hint"); + }; + assert_eq!(ticket.target, RouteTarget::Peers(scope)); + assert_eq!(ticket.components, ConfigComponents::ALL); + scheduler.complete_route(&ticket); + assert!(scheduler.next_work(Instant::now()).is_none()); + } + + #[test] + fn replaced_or_removed_sessions_drop_their_work() { + let mut scheduler = with_recipients(4, 1); + scheduler.publish_sandbox("sb-0000", ConfigComponents::SANDBOX_CONFIG, Instant::now()); + let stale = build(scheduler.next_work(Instant::now()).unwrap()); + scheduler.register(registration("sb-0000", "ws", &[]), Instant::now()); + scheduler.publish_sandbox("sb-0000", ConfigComponents::SANDBOX_CONFIG, Instant::now()); + scheduler.complete_build(&stale, built(), Instant::now()); + let current = build(scheduler.next_work(Instant::now()).unwrap()); + assert_ne!(current.epoch, stale.epoch); + + scheduler.publish_sandbox("sb-0000", ConfigComponents::SANDBOX_CONFIG, Instant::now()); + scheduler.unregister("sb-0000", "other-session"); + assert!(scheduler.recipients.contains_key("sb-0000")); + scheduler.unregister("sb-0000", "sb-0000-session"); + scheduler.complete_build(¤t, built(), Instant::now()); + assert!(scheduler.next_work(Instant::now()).is_none()); + assert!(!scheduler.has_running_work()); + assert!(scheduler.by_workspace.is_empty()); + assert_eq!((scheduler.queued.direct, scheduler.queued.fanout), (0, 0)); + } + + #[test] + fn limits_keep_both_lanes_schedulable() { + assert_eq!(Limits::new(0, 0), Limits::new(2, 1)); + let small = Limits::new(2, 1); + assert_eq!((small.builds, small.direct_reserve), (2, 1)); + let large = Limits::new(20, 8); + assert_eq!((large.builds, large.direct_reserve), (20, 5)); + } +} diff --git a/crates/openshell-server/src/config_delivery/session_slots.rs b/crates/openshell-server/src/config_delivery/session_slots.rs new file mode 100644 index 000000000..16ac2ab80 --- /dev/null +++ b/crates/openshell-server/src/config_delivery/session_slots.rs @@ -0,0 +1,178 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Latest-wins configuration slots for one supervisor session stream. + +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll}; + +use futures::task::AtomicWaker; +use openshell_core::proto::GatewayMessage; +use tokio::sync::mpsc; +use tonic::Status; + +use super::ConfigComponentKind; + +/// One unsent snapshot per component. A newer snapshot replaces an unsent one, +/// so a slow stream holds at most one message per component. +#[derive(Debug, Default)] +pub struct ConfigSlots { + pending: Mutex, + waker: AtomicWaker, +} + +#[derive(Debug, Default)] +struct PendingSlots { + next_order: u64, + sandbox_config: Option<(u64, GatewayMessage)>, + provider_environment: Option<(u64, GatewayMessage)>, +} + +impl ConfigSlots { + /// Returns true when an unsent snapshot was replaced. + pub(crate) fn replace(&self, component: ConfigComponentKind, message: GatewayMessage) -> bool { + let replaced = { + let mut pending = self.pending.lock().unwrap(); + let order = pending.next_order; + pending.next_order += 1; + let slot = match component { + ConfigComponentKind::SandboxConfig => &mut pending.sandbox_config, + ConfigComponentKind::ProviderEnvironment => &mut pending.provider_environment, + }; + slot.replace((order, message)).is_some() + }; + self.waker.wake(); + replaced + } + + fn take_oldest(&self) -> Option { + let mut pending = self.pending.lock().unwrap(); + let sandbox_first = match (&pending.sandbox_config, &pending.provider_environment) { + (Some((sandbox, _)), Some((provider, _))) => sandbox < provider, + (Some(_), None) => true, + (None, Some(_)) => false, + (None, None) => return None, + }; + let slot = if sandbox_first { + pending.sandbox_config.take() + } else { + pending.provider_environment.take() + }; + slot.map(|(_, message)| message) + } +} + +/// Session stream that sends queued control frames before configuration. +/// +/// `SessionAccepted` is queued on the control channel before the session can +/// receive configuration, so it always stays first. +pub struct SessionOutbound { + control: mpsc::Receiver, + config: Arc, +} + +impl SessionOutbound { + pub(crate) fn new(control: mpsc::Receiver, config: Arc) -> Self { + Self { control, config } + } +} + +impl tokio_stream::Stream for SessionOutbound { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + match this.control.poll_recv(cx) { + Poll::Ready(Some(message)) => return Poll::Ready(Some(Ok(message))), + Poll::Ready(None) => return Poll::Ready(None), + Poll::Pending => {} + } + this.config.waker.register(cx.waker()); + this.config + .take_oldest() + .map_or(Poll::Pending, |message| Poll::Ready(Some(Ok(message)))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use openshell_core::proto::{ConfigUpdate, RelayOpen, gateway_message}; + use tokio_stream::StreamExt; + + fn config(update_id: &str) -> GatewayMessage { + GatewayMessage { + payload: Some(gateway_message::Payload::ConfigUpdate(ConfigUpdate { + update_id: update_id.into(), + ..Default::default() + })), + } + } + + fn relay_open() -> GatewayMessage { + GatewayMessage { + payload: Some(gateway_message::Payload::RelayOpen(RelayOpen::default())), + } + } + + fn update_id(message: GatewayMessage) -> String { + match message.payload { + Some(gateway_message::Payload::ConfigUpdate(update)) => update.update_id, + other => panic!("expected a configuration update, got {other:?}"), + } + } + + #[tokio::test] + async fn control_frames_go_before_queued_configuration() { + let (tx, rx) = mpsc::channel(4); + let slots = Arc::new(ConfigSlots::default()); + let mut outbound = SessionOutbound::new(rx, Arc::clone(&slots)); + assert!(!slots.replace(ConfigComponentKind::SandboxConfig, config("config"))); + tx.send(relay_open()).await.unwrap(); + + let first = outbound.next().await.unwrap().unwrap(); + assert!(matches!( + first.payload, + Some(gateway_message::Payload::RelayOpen(_)) + )); + assert_eq!(update_id(outbound.next().await.unwrap().unwrap()), "config"); + } + + #[tokio::test] + async fn newer_snapshot_replaces_an_unsent_one() { + let (_tx, rx) = mpsc::channel(4); + let slots = Arc::new(ConfigSlots::default()); + let mut outbound = SessionOutbound::new(rx, Arc::clone(&slots)); + assert!(!slots.replace(ConfigComponentKind::SandboxConfig, config("old"))); + assert!(!slots.replace(ConfigComponentKind::ProviderEnvironment, config("provider"))); + assert!(slots.replace(ConfigComponentKind::SandboxConfig, config("new"))); + + assert_eq!( + update_id(outbound.next().await.unwrap().unwrap()), + "provider" + ); + assert_eq!(update_id(outbound.next().await.unwrap().unwrap()), "new"); + } + + #[tokio::test] + async fn waiting_stream_wakes_for_configuration() { + let (_tx, rx) = mpsc::channel(4); + let slots = Arc::new(ConfigSlots::default()); + let mut outbound = SessionOutbound::new(rx, Arc::clone(&slots)); + let next = tokio::spawn(async move { outbound.next().await.unwrap().unwrap() }); + tokio::task::yield_now().await; + slots.replace(ConfigComponentKind::ProviderEnvironment, config("late")); + assert_eq!(update_id(next.await.unwrap()), "late"); + } + + #[tokio::test] + async fn stream_ends_with_the_control_channel() { + let (tx, rx) = mpsc::channel(4); + let slots = Arc::new(ConfigSlots::default()); + let mut outbound = SessionOutbound::new(rx, Arc::clone(&slots)); + slots.replace(ConfigComponentKind::SandboxConfig, config("discarded")); + drop(tx); + assert!(outbound.next().await.is_none()); + } +} diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index 3794ac51d..a9c7bd860 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -895,7 +895,7 @@ impl OpenShell for OpenShellService { &self, request: Request, ) -> Result, Status> { - crate::config_delivery::handle_peer_config_update_hint(&self.state, request).await + crate::config_delivery::handle_peer_config_update_hint(&self.state, request) } } diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 2356770d9..48c9582f5 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -12746,14 +12746,8 @@ mod tests { sandbox.spec.as_mut().unwrap().policy = None; state.store.put_message(&sandbox).await.unwrap(); - let (tx, mut rx) = tokio::sync::mpsc::channel(4); - let (shutdown_tx, _shutdown_rx) = tokio::sync::oneshot::channel(); - state.supervisor_sessions.register( - "sb-published-policy".to_string(), - "session-1".to_string(), - tx, - shutdown_tx, - ); + let mut session = + crate::config_delivery::register_test_push_session(&state, &sandbox, "session-1"); let owner_index = crate::supervisor_owner::SupervisorOwnerIndex::new( Arc::clone(&state.store), crate::supervisor_owner::OWNER_TTL, @@ -12790,7 +12784,10 @@ mod tests { .expect("committed policy"); let snapshot = tokio::time::timeout(std::time::Duration::from_secs(2), async { loop { - let message = rx.recv().await.expect("configuration channel closed"); + let message = tokio_stream::StreamExt::next(&mut session.outbound) + .await + .expect("configuration stream closed") + .unwrap(); let Some(openshell_core::proto::gateway_message::Payload::ConfigUpdate(update)) = message.payload else { diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index 41437384d..7322c5731 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -2558,7 +2558,18 @@ async fn authorize_and_resolve_profile_workspace( } } -fn publish_provider_change(state: &Arc, workspace: &str) { +/// Publish a provider record change to the sandboxes that attach it. +fn publish_provider_change(state: &Arc, workspace: &str, provider_name: &str) { + crate::config_delivery::publish_provider_components( + state, + workspace, + provider_name, + crate::config_delivery::ConfigComponents::ALL, + ); +} + +/// A profile change affects every provider of its type in scope. +fn publish_provider_profile_change(state: &Arc, workspace: &str) { if workspace.is_empty() { crate::config_delivery::publish_all_connected( state, @@ -2642,7 +2653,7 @@ pub(super) async fn handle_create_provider( LifecycleOperation::Create, TelemetryOutcome::Success, ); - publish_provider_change(state, &workspace); + publish_provider_change(state, &workspace, provider.object_name()); Ok(Response::new(ProviderResponse { provider: Some(provider), ..Default::default() @@ -2912,7 +2923,7 @@ pub(super) async fn handle_import_provider_profiles( stored.profile.unwrap_or_default(), resource_version, )); - publish_provider_change(state, &workspace); + publish_provider_profile_change(state, &workspace); } Ok(Response::new(ImportProviderProfilesResponse { @@ -3048,7 +3059,7 @@ pub(super) async fn handle_update_provider_profiles( replay_facts.resource(&stored)?; let resource_version = stored_profile_resource_version(&stored); let profile = profile_response_payload(stored.profile.unwrap_or_default(), resource_version); - publish_provider_change(state, &workspace); + publish_provider_profile_change(state, &workspace); Ok(Response::new(UpdateProviderProfilesResponse { diagnostics: Vec::new(), @@ -3143,7 +3154,7 @@ pub(super) async fn handle_delete_provider_profile( .delete(StoredProviderProfile::object_type(), existing.object_id()) .await .map_err(|e| Status::internal(format!("delete provider profile failed: {e}")))?; - publish_provider_change(state, &workspace); + publish_provider_profile_change(state, &workspace); Ok(Response::new(DeleteProviderProfileResponse { outcome: openshell_core::proto::DeletionOutcome::Completed.into(), @@ -3996,7 +4007,7 @@ pub(super) async fn handle_update_provider( LifecycleOperation::Update, TelemetryOutcome::Success, ); - publish_provider_change(state, &workspace); + publish_provider_change(state, &workspace, provider.object_name()); Ok(Response::new(ProviderResponse { provider: Some(provider), target_receipts, @@ -4964,10 +4975,10 @@ pub(super) async fn handle_configure_provider_refresh( updated, ) .await; - publish_provider_change(state, &workspace); + publish_provider_change(state, &workspace, provider_name); result?; } else { - publish_provider_change(state, &workspace); + publish_provider_change(state, &workspace, provider_name); } replay_facts.refresh(&state_record)?; @@ -5013,7 +5024,7 @@ pub(super) async fn handle_rotate_provider_credential( credential_key, ) .await?; - publish_provider_change(state, &workspace); + publish_provider_change(state, &workspace, provider_name); replay_facts.refresh(&refresh_state)?; Ok(Response::new(RotateProviderCredentialResponse { @@ -5126,7 +5137,7 @@ pub(super) async fn handle_delete_provider_refresh( }) .map(|_| ())?; } - publish_provider_change(state, &workspace); + publish_provider_change(state, &workspace, provider_name); Ok(Response::new(DeleteProviderRefreshResponse { outcome: openshell_core::proto::DeletionOutcome::Completed.into(), @@ -5168,7 +5179,7 @@ pub(super) async fn handle_delete_provider( outcome, ); if deleted { - publish_provider_change(state, &workspace); + publish_provider_change(state, &workspace, &name); } Ok(Response::new(DeleteProviderResponse { outcome: super::deletion_outcome(deleted, req.allow_missing, "provider")?, diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 371664feb..778b414ee 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -304,8 +304,8 @@ pub struct ServerState { /// Set once graceful gateway shutdown begins so stream handlers can /// distinguish expected transport closes from runtime failures. pub(crate) gateway_shutting_down: AtomicBool, - /// Per-sandbox scheduler for coalesced supervisor configuration delivery. - pub(crate) config_delivery_queue: config_delivery::ConfigDeliveryQueue, + /// Coalescing scheduler for supervisor configuration delivery. + pub(crate) config_delivery: config_delivery::ConfigDelivery, /// Routing boundary for local or remote supervisor configuration delivery. pub(crate) supervisor_config_router: Arc, @@ -435,8 +435,8 @@ impl ServerState { let supervisor_config_router: Arc = Arc::new( config_delivery::LocalSupervisorConfigRouter::new(Arc::clone(&supervisor_sessions)), ); - let config_delivery_queue = - config_delivery::ConfigDeliveryQueue::for_db_connections(store.max_connections()); + let config_delivery = + config_delivery::ConfigDelivery::for_db_connections(store.max_connections()); Self { config, store, @@ -451,7 +451,7 @@ impl ServerState { settings_mutex: tokio::sync::Mutex::new(()), supervisor_sessions, gateway_shutting_down: AtomicBool::new(false), - config_delivery_queue, + config_delivery, supervisor_config_router, replica_id, peer_endpoint, diff --git a/crates/openshell-server/src/provider_refresh.rs b/crates/openshell-server/src/provider_refresh.rs index 296795640..10e59a44a 100644 --- a/crates/openshell-server/src/provider_refresh.rs +++ b/crates/openshell-server/src/provider_refresh.rs @@ -1925,11 +1925,12 @@ pub fn spawn_refresh_worker(state: std::sync::Arc, interval: )) .await { - Ok(workspaces) => { - for workspace in workspaces { - crate::config_delivery::publish_workspace_components( + Ok(providers) => { + for (workspace, provider_name) in providers { + crate::config_delivery::publish_provider_components( &state, &workspace, + &provider_name, crate::config_delivery::ConfigComponents::ALL, ); } @@ -1942,12 +1943,13 @@ pub fn spawn_refresh_worker(state: std::sync::Arc, interval: }); } +/// Returns the `(workspace, provider)` pairs whose provider records changed. async fn run_refresh_worker_tick( store: &Store, credentials: Option<&crate::credentials::CredentialRuntime>, compute: Option<&crate::compute::ComputeRuntime>, -) -> Result, Status> { - let changed_workspaces = std::collections::HashSet::new(); +) -> Result, Status> { + let changed_providers = std::collections::HashSet::new(); let now_ms = current_time_ms(); let states = list_all_refresh_states(store).await?; let watched_count = states.len(); @@ -1967,7 +1969,7 @@ async fn run_refresh_worker_tick( .iter() .any(|state| refresh_state_has_work(state, now_ms)) { - return Ok(changed_workspaces); + return Ok(changed_providers); } let span = tracing::info_span!( "refresh", @@ -1998,8 +2000,8 @@ async fn refresh_states( compute: Option<&crate::compute::ComputeRuntime>, states: Vec, now_ms: i64, -) -> std::collections::HashSet { - let mut changed_workspaces = std::collections::HashSet::new(); +) -> std::collections::HashSet<(String, String)> { + let mut changed_providers = std::collections::HashSet::new(); for state in states { if state .metadata @@ -2030,7 +2032,7 @@ async fn refresh_states( "failed to finalize tombstoned provider refresh; retrying on the next sweep" ); } else { - changed_workspaces.insert(state.object_workspace().to_string()); + changed_providers.insert(changed_provider(&state)); } continue; } @@ -2115,10 +2117,17 @@ async fn refresh_states( "provider credential refresh failed" ); } else { - changed_workspaces.insert(state.object_workspace().to_string()); + changed_providers.insert(changed_provider(&state)); } } - changed_workspaces + changed_providers +} + +fn changed_provider(state: &StoredProviderCredentialRefreshState) -> (String, String) { + ( + state.object_workspace().to_string(), + state.provider_name.clone(), + ) } #[cfg(test)] @@ -3581,10 +3590,18 @@ mod tests { put_refresh_state(&store, &state).await.unwrap(); assert_eq!(credentials.stored_credential_count(), Some(1)); - Box::pin(run_refresh_worker_tick(&store, Some(&credentials), None)) + let changed = Box::pin(run_refresh_worker_tick(&store, Some(&credentials), None)) .await .unwrap(); + assert_eq!( + changed, + std::collections::HashSet::from([( + "default".to_string(), + "tombstoned-refresh".to_string() + )]), + "only the refreshed provider's attached sandboxes need an update" + ); assert!( get_refresh_state( &store, diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index eef3bc0fa..a0695cac6 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -130,7 +130,7 @@ mod tests { // Service authorization also extends both schemas additively. Legacy // payloads retain the safe Strip default. const PUBLIC_RPC_SCHEMA_SHA256: &str = - "73d5454909634839151b6d47fe4b62793d5426a783517541e91972766cf81aef"; + "95d4b5caeecb2fd482e2657ec264a5093f1180adee0f5a85b716bef3fdb48ad9"; const DURABLE_SCHEMA_SHA256: &str = "38165d9d76f49fcfe98a12f241e032838a2376c1d1a87ea2796fd33b9b1a3541"; const PUBLIC_DURABLE_OVERLAP_SHA256: &str = @@ -614,7 +614,7 @@ mod tests { overlap_hash.as_str(), ), ( - (321, 28), + (322, 28), (93, 21), (81, 21), PUBLIC_RPC_SCHEMA_SHA256, diff --git a/crates/openshell-server/src/supervisor_session.rs b/crates/openshell-server/src/supervisor_session.rs index 61d2acd5e..efcdb3d44 100644 --- a/crates/openshell-server/src/supervisor_session.rs +++ b/crates/openshell-server/src/supervisor_session.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::pin::Pin; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; @@ -33,12 +33,13 @@ use openshell_core::transport_errors::is_expected_transport_close_status; use crate::ServerState; use crate::auth::principal::Principal; use crate::config_delivery::{ - DeliveryDisposition, MAX_SUPERVISOR_CONFIG_MESSAGE_BYTES, SupervisorConfigMessage, + ConfigComponentKind, ConfigSlots, DeliveryDisposition, MAX_SUPERVISOR_CONFIG_MESSAGE_BYTES, + SessionOutbound, SupervisorConfigMessage, }; #[cfg(test)] use crate::config_delivery::{LocalSupervisorConfigRouter, SupervisorConfigRouter}; use crate::grpc::provider_readiness::ProviderReadinessEvidence; -use crate::persistence::ObjectId; +use crate::persistence::{ObjectId, ObjectWorkspace}; use crate::supervisor_owner::{OWNER_TTL, OwnerError, OwnerGuard, SupervisorOwnerIndex}; const HEARTBEAT_INTERVAL_SECS: u32 = 15; @@ -283,7 +284,8 @@ struct LiveSession { /// Uniquely identifies this session instance. Used by cleanup to avoid /// removing a session that has since been superseded by a reconnect. session_id: String, - config_push_capable: bool, + /// Present only for push-capable sessions on a gateway in push mode. + config_slots: Option>, tx: mpsc::Sender, config_sequences: ConfigSequences, /// Fires when this session is superseded by a reconnect so the old session @@ -422,16 +424,16 @@ impl SupervisorSessionRegistry { tx: mpsc::Sender, shutdown: oneshot::Sender<()>, ) -> bool { - self.register_with_config_push(sandbox_id, session_id, tx, shutdown, true) + self.register_with_config_slots(sandbox_id, session_id, tx, shutdown, None) } - pub fn register_with_config_push( + pub(crate) fn register_with_config_slots( &self, sandbox_id: String, session_id: String, tx: mpsc::Sender, shutdown: oneshot::Sender<()>, - config_push_capable: bool, + config_slots: Option>, ) -> bool { let mut sessions = self.sessions.lock().unwrap(); let previous = sessions.remove(&sandbox_id); @@ -440,7 +442,7 @@ impl SupervisorSessionRegistry { LiveSession { sandbox_id, session_id, - config_push_capable, + config_slots, tx, config_sequences: ConfigSequences::default(), shutdown, @@ -558,49 +560,36 @@ impl SupervisorSessionRegistry { true } - pub(crate) fn connected_sandbox_ids(&self) -> Vec { - self.sessions - .lock() - .unwrap() - .iter() - .filter(|(_, session)| session.config_push_capable) - .map(|(sandbox_id, _)| sandbox_id.clone()) - .collect() - } - - pub(crate) fn is_config_push_capable(&self, sandbox_id: &str) -> bool { - self.sessions - .lock() - .unwrap() - .get(sandbox_id) - .is_some_and(|session| session.config_push_capable) - } - + /// Hand a snapshot to the session it was built for. A replacement session + /// received its own bootstrap and must not receive an older build. pub(crate) fn deliver_config( &self, sandbox_id: &str, + session_id: &str, message: SupervisorConfigMessage, ) -> DeliveryDisposition { - let component_name = message.component_name(); + let component = message.component(); let mut sessions = self.sessions.lock().unwrap(); - let Some(session) = sessions.get_mut(sandbox_id) else { + let Some(session) = sessions + .get_mut(sandbox_id) + .filter(|session| session.session_id == session_id) + else { return DeliveryDisposition::NoActiveSession; }; - if !session.config_push_capable { + let Some(slots) = session.config_slots.clone() else { return DeliveryDisposition::UnsupportedSession; - } - let sequence = match &message { - SupervisorConfigMessage::SandboxConfig(_) => { - &mut session.config_sequences.sandbox_config - } - SupervisorConfigMessage::ProviderEnvironment(_) => { + }; + let sequence = match component { + ConfigComponentKind::SandboxConfig => &mut session.config_sequences.sandbox_config, + ConfigComponentKind::ProviderEnvironment => { &mut session.config_sequences.provider_environment } }; + // Replacing an unsent snapshot leaves a gap in the sequence. *sequence = sequence.saturating_add(1); let component_sequence = *sequence; - let component = match message { + let snapshot = match message { SupervisorConfigMessage::SandboxConfig(snapshot) => { config_update::Component::SandboxConfig(*snapshot) } @@ -612,25 +601,19 @@ impl SupervisorSessionRegistry { payload: Some(gateway_message::Payload::ConfigUpdate(ConfigUpdate { update_id: Uuid::new_v4().to_string(), component_sequence, - component: Some(component), + component: Some(snapshot), })), }; if gateway_message.encoded_len() > MAX_SUPERVISOR_CONFIG_MESSAGE_BYTES { return DeliveryDisposition::PayloadTooLarge; } + drop(sessions); - match session.tx.try_send(gateway_message) { - Ok(()) => DeliveryDisposition::Enqueued, - Err(mpsc::error::TrySendError::Full(_)) => { - warn!( - sandbox_id = %sandbox_id, - component = component_name, - "supervisor configuration queue is full" - ); - DeliveryDisposition::QueueFull - } - Err(mpsc::error::TrySendError::Closed(_)) => DeliveryDisposition::SessionClosed, + if slots.replace(component, gateway_message) { + DeliveryDisposition::Replaced + } else { + DeliveryDisposition::Queued } } @@ -1989,12 +1972,24 @@ pub async fn handle_connect_supervisor( if let Some(principal) = principal.as_ref() { crate::auth::guard::ensure_sandbox_principal_scope(principal, &sandbox_id)?; } + // Captured before reading any bootstrap input, including the sandbox + // record, so registration detects publications the bootstrap may miss. + let captured_seq = (state.config.config_delivery_mode + == openshell_core::config::ConfigDeliveryMode::Push + && hello.protocol_revision == SUPERVISOR_PROTOCOL_REVISION) + .then(|| state.config_delivery.current_seq()); let sandbox = require_persisted_sandbox(&state.store, &sandbox_id).await?; - let bootstrap = if state.config.config_delivery_mode - == openshell_core::config::ConfigDeliveryMode::Poll - || hello.protocol_revision == LEGACY_SUPERVISOR_PROTOCOL_REVISION - { + let config_push = captured_seq.map(|captured_seq| ConfigPushSetup { + captured_seq, + workspace: sandbox.object_workspace().to_string(), + providers: sandbox + .spec + .as_ref() + .map(|spec| spec.providers.iter().cloned().collect()) + .unwrap_or_default(), + }); + let bootstrap = if config_push.is_none() { None } else { match crate::config_delivery::build_config_bootstrap(state, &sandbox).await { @@ -2034,6 +2029,7 @@ pub async fn handle_connect_supervisor( inbound, hello, bootstrap, + config_push, provider_readiness, session_lifetime, )) @@ -2041,11 +2037,18 @@ pub async fn handle_connect_supervisor( .map_err(|error| Status::internal(format!("supervisor session setup failed: {error}")))? } +struct ConfigPushSetup { + captured_seq: u64, + workspace: String, + providers: HashSet, +} + async fn establish_supervisor_session( state: Arc, mut inbound: tonic::Streaming, hello: SupervisorHello, bootstrap: Option, + config_push: Option, provider_readiness: ProviderReadinessEvidence, session_lifetime: OwnedRwLockReadGuard<()>, ) -> Result< @@ -2109,6 +2112,16 @@ async fn establish_supervisor_session( }; accepted_payload.bootstrap = None; } + let bootstrap_built = matches!( + &accepted.payload, + Some(gateway_message::Payload::SessionAccepted(SessionAccepted { + bootstrap: Some(_), + .. + })) + ); + let config_slots = config_push + .as_ref() + .map(|_| Arc::new(ConfigSlots::default())); if tx.send(accepted).await.is_err() { if let Err(err) = owner_index.release_if_current(&owner_guard).await { warn!(sandbox_id = %sandbox_id, session_id = %session_id, error = %err, "supervisor session: failed to release owner after accept queue failure"); @@ -2116,12 +2129,12 @@ async fn establish_supervisor_session( return Err(Status::internal("failed to send session accepted")); } - let superseded = state.supervisor_sessions.register_with_config_push( + let superseded = state.supervisor_sessions.register_with_config_slots( sandbox_id.clone(), session_id.clone(), tx.clone(), shutdown_tx, - hello.protocol_revision == SUPERVISOR_PROTOCOL_REVISION, + config_slots.clone(), ); if superseded { info!( @@ -2210,6 +2223,20 @@ async fn establish_supervisor_session( .replay_pending_relays(&sandbox_id, &session_id, &tx) .await; + if let Some(setup) = config_push { + crate::config_delivery::register_session( + &state, + crate::config_delivery::Registration { + sandbox_id: sandbox_id.clone(), + session_id: session_id.clone(), + workspace: setup.workspace, + providers: setup.providers, + captured_seq: setup.captured_seq, + bootstrap_built, + }, + ); + } + // Step 4: Spawn the session loop that reads inbound messages. let state_clone = Arc::clone(&state); let sandbox_id_clone = sandbox_id.clone(); @@ -2229,6 +2256,8 @@ async fn establish_supervisor_session( let terminal_finalized = state_clone .supervisor_sessions .remove_if_current(&sandbox_id_clone, &session_id); + // Every removal path ends this task, including supersede and disconnect. + crate::config_delivery::unregister_session(&state_clone, &sandbox_id_clone, &session_id); // Release only this exact ownership record. A newer supervisor session // may already have published replacement ownership on another replica. let owner_index = SupervisorOwnerIndex::new(state_clone.store.clone(), OWNER_TTL); @@ -2264,10 +2293,12 @@ async fn establish_supervisor_session( }); // Return the outbound stream. - let stream = ReceiverStream::new(rx); let stream: Pin< Box> + Send + 'static>, - > = Box::pin(tokio_stream::StreamExt::map(stream, Ok)); + > = match config_slots { + Some(slots) => Box::pin(SessionOutbound::new(rx, slots)), + None => Box::pin(tokio_stream::StreamExt::map(ReceiverStream::new(rx), Ok)), + }; Ok(Response::new(stream)) } @@ -2699,7 +2730,7 @@ mod tests { assert_eq!(status.code(), tonic::Code::FailedPrecondition); assert!(status.message().contains("revision mismatch")); - assert!(state.supervisor_sessions.connected_sandbox_ids().is_empty()); + assert!(!state.supervisor_sessions.has_session("sb-future")); } #[test] @@ -2710,6 +2741,36 @@ mod tests { assert!(error.message().contains("revision mismatch")); } + fn register_push_session( + registry: &SupervisorSessionRegistry, + sandbox_id: &str, + session_id: &str, + ) -> (mpsc::Sender, SessionOutbound) { + let (tx, rx) = mpsc::channel(4); + let slots = Arc::new(ConfigSlots::default()); + registry.register_with_config_slots( + sandbox_id.into(), + session_id.into(), + tx.clone(), + make_shutdown(), + Some(Arc::clone(&slots)), + ); + (tx, SessionOutbound::new(rx, slots)) + } + + fn component_sequence(message: GatewayMessage) -> (bool, u64) { + match message.payload { + Some(gateway_message::Payload::ConfigUpdate(update)) => ( + matches!( + update.component, + Some(config_update::Component::SandboxConfig(_)) + ), + update.component_sequence, + ), + other => panic!("expected config update, got {other:?}"), + } + } + #[tokio::test] async fn config_router_reports_missing_session() { let router = LocalSupervisorConfigRouter::new(Arc::new(SupervisorSessionRegistry::new())); @@ -2717,6 +2778,7 @@ mod tests { router .deliver( "missing", + "session", SupervisorConfigMessage::SandboxConfig(Box::default()), ) .await, @@ -2725,136 +2787,102 @@ mod tests { } #[tokio::test] - async fn config_router_assigns_sequences_per_component() { + async fn config_router_keeps_the_latest_unsent_snapshot_per_component() { + use tokio_stream::StreamExt as _; + let registry = Arc::new(SupervisorSessionRegistry::new()); let router = LocalSupervisorConfigRouter::new(Arc::clone(®istry)); - let (tx, mut rx) = mpsc::channel(4); - let (shutdown_tx, _shutdown_rx) = oneshot::channel(); - registry.register("sb-1".into(), "session-1".into(), tx, shutdown_tx); + let (_tx, mut outbound) = register_push_session(®istry, "sb-1", "session-1"); + for expected in [DeliveryDisposition::Queued, DeliveryDisposition::Replaced] { + assert_eq!( + router + .deliver( + "sb-1", + "session-1", + SupervisorConfigMessage::SandboxConfig(Box::default()), + ) + .await, + expected + ); + } assert_eq!( router .deliver( "sb-1", - SupervisorConfigMessage::SandboxConfig(Box::default()), - ) - .await, - DeliveryDisposition::Enqueued - ); - assert_eq!( - router - .deliver( - "sb-1", - SupervisorConfigMessage::SandboxConfig(Box::default()), - ) - .await, - DeliveryDisposition::Enqueued - ); - assert_eq!( - router - .deliver( - "sb-1", + "session-1", SupervisorConfigMessage::ProviderEnvironment( ProviderEnvironmentSnapshot::default(), ), ) .await, - DeliveryDisposition::Enqueued + DeliveryDisposition::Queued ); - let first = rx.recv().await.expect("first config update"); - let second = rx.recv().await.expect("second config update"); - let third = rx.recv().await.expect("provider config update"); - let sequence = |message: GatewayMessage| match message.payload { - Some(gateway_message::Payload::ConfigUpdate(update)) => update.component_sequence, - other => panic!("expected config update, got {other:?}"), - }; - assert_eq!(sequence(first), 1); - assert_eq!(sequence(second), 2); - assert_eq!(sequence(third), 1); + let first = outbound.next().await.unwrap().unwrap(); + let second = outbound.next().await.unwrap().unwrap(); + assert_eq!(component_sequence(first), (true, 2)); + assert_eq!(component_sequence(second), (false, 1)); } #[tokio::test] - async fn config_router_uses_replacement_session() { + async fn config_router_never_crosses_into_a_replacement_session() { + use tokio_stream::StreamExt as _; + let registry = Arc::new(SupervisorSessionRegistry::new()); let router = LocalSupervisorConfigRouter::new(Arc::clone(®istry)); - let (old_tx, mut old_rx) = mpsc::channel(2); - let (old_shutdown_tx, _old_shutdown_rx) = oneshot::channel(); - registry.register("sb-1".into(), "old-session".into(), old_tx, old_shutdown_tx); + let (_old_tx, _old_outbound) = register_push_session(®istry, "sb-1", "old-session"); + let (_new_tx, mut new_outbound) = register_push_session(®istry, "sb-1", "new-session"); assert_eq!( router .deliver( "sb-1", + "old-session", SupervisorConfigMessage::SandboxConfig(Box::default()), ) .await, - DeliveryDisposition::Enqueued + DeliveryDisposition::NoActiveSession ); - let old_update = old_rx.recv().await.expect("old-session config update"); - let Some(gateway_message::Payload::ConfigUpdate(old_update)) = old_update.payload else { - panic!("expected config update"); - }; - assert_eq!(old_update.component_sequence, 1); - - let (new_tx, mut new_rx) = mpsc::channel(1); - let (new_shutdown_tx, _new_shutdown_rx) = oneshot::channel(); - assert!(registry.register("sb-1".into(), "new-session".into(), new_tx, new_shutdown_tx,)); - assert_eq!( router .deliver( "sb-1", + "new-session", SupervisorConfigMessage::SandboxConfig(Box::default()), ) .await, - DeliveryDisposition::Enqueued + DeliveryDisposition::Queued ); - assert!(old_rx.try_recv().is_err()); - let new_update = new_rx.try_recv().expect("new-session config update"); - let Some(gateway_message::Payload::ConfigUpdate(new_update)) = new_update.payload else { - panic!("expected config update"); - }; - assert_eq!(new_update.component_sequence, 1); + let update = new_outbound.next().await.unwrap().unwrap(); + assert_eq!(component_sequence(update), (true, 1)); } #[tokio::test] - async fn config_router_reports_full_and_closed_queues() { + async fn config_router_skips_sessions_without_push() { let registry = Arc::new(SupervisorSessionRegistry::new()); let router = LocalSupervisorConfigRouter::new(Arc::clone(®istry)); - let (tx, rx) = mpsc::channel(1); - tx.try_send(GatewayMessage::default()).unwrap(); - let (shutdown_tx, _shutdown_rx) = oneshot::channel(); - registry.register("sb-1".into(), "session-1".into(), tx, shutdown_tx); + let (tx, _rx) = mpsc::channel(1); + registry.register("sb-1".into(), "session-1".into(), tx, make_shutdown()); assert_eq!( router .deliver( "sb-1", + "session-1", SupervisorConfigMessage::SandboxConfig(Box::default()), ) .await, - DeliveryDisposition::QueueFull - ); - - drop(rx); - assert_eq!( - router - .deliver( - "sb-1", - SupervisorConfigMessage::SandboxConfig(Box::default()), - ) - .await, - DeliveryDisposition::SessionClosed + DeliveryDisposition::UnsupportedSession ); } #[tokio::test] async fn config_router_rejects_oversized_messages() { + use tokio_stream::StreamExt as _; + let registry = Arc::new(SupervisorSessionRegistry::new()); let router = LocalSupervisorConfigRouter::new(Arc::clone(®istry)); - let (tx, mut rx) = mpsc::channel(1); - let (shutdown_tx, _shutdown_rx) = oneshot::channel(); - registry.register("sb-1".into(), "session-1".into(), tx, shutdown_tx); + let (tx, mut outbound) = register_push_session(®istry, "sb-1", "session-1"); let snapshot = ProviderEnvironmentSnapshot { values: vec![ProviderEnvironmentValue { @@ -2868,12 +2896,15 @@ mod tests { router .deliver( "sb-1", + "session-1", SupervisorConfigMessage::ProviderEnvironment(snapshot), ) .await, DeliveryDisposition::PayloadTooLarge ); - assert!(rx.try_recv().is_err()); + drop(tx); + registry.remove_if_current("sb-1", "session-1"); + assert!(outbound.next().await.is_none()); } use tokio::io::{AsyncReadExt, AsyncWriteExt}; diff --git a/proto/openshell.proto b/proto/openshell.proto index 4e76b4afd..c16ca7704 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -3225,11 +3225,18 @@ message PeerConfigSandboxTarget { string session_id = 2; } +// Sandboxes in a workspace that attach the named provider. +message PeerConfigProviderTarget { + string workspace = 1; + string name = 2; +} + message PeerConfigUpdateHintRequest { oneof scope { PeerConfigSandboxTarget sandbox = 1; string workspace = 2; bool all_connected = 3; + PeerConfigProviderTarget provider = 6; } bool sandbox_config = 4; bool provider_environment = 5; @@ -3238,8 +3245,6 @@ message PeerConfigUpdateHintRequest { message PeerConfigUpdateHintResponse { // The targeted session moved before the receiving replica accepted the hint. bool stale_owner = 1; - // The bounded local delivery or fanout queue could not accept the hint. - bool queue_full = 2; } // Supervisor reports the result of a relay open request. diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 1167707d5..ed316688c 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -15052,6 +15052,59 @@ func (x *PeerConfigSandboxTarget) GetSessionId() string { return "" } +// Sandboxes in a workspace that attach the named provider. +type PeerConfigProviderTarget struct { + state protoimpl.MessageState `protogen:"open.v1"` + Workspace string `protobuf:"bytes,1,opt,name=workspace,proto3" json:"workspace,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerConfigProviderTarget) Reset() { + *x = PeerConfigProviderTarget{} + mi := &file_openshell_proto_msgTypes[191] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerConfigProviderTarget) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerConfigProviderTarget) ProtoMessage() {} + +func (x *PeerConfigProviderTarget) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[191] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerConfigProviderTarget.ProtoReflect.Descriptor instead. +func (*PeerConfigProviderTarget) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{191} +} + +func (x *PeerConfigProviderTarget) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +func (x *PeerConfigProviderTarget) GetName() string { + if x != nil { + return x.Name + } + return "" +} + type PeerConfigUpdateHintRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Scope: @@ -15059,6 +15112,7 @@ type PeerConfigUpdateHintRequest struct { // *PeerConfigUpdateHintRequest_Sandbox // *PeerConfigUpdateHintRequest_Workspace // *PeerConfigUpdateHintRequest_AllConnected + // *PeerConfigUpdateHintRequest_Provider Scope isPeerConfigUpdateHintRequest_Scope `protobuf_oneof:"scope"` SandboxConfig bool `protobuf:"varint,4,opt,name=sandbox_config,json=sandboxConfig,proto3" json:"sandbox_config,omitempty"` ProviderEnvironment bool `protobuf:"varint,5,opt,name=provider_environment,json=providerEnvironment,proto3" json:"provider_environment,omitempty"` @@ -15068,7 +15122,7 @@ type PeerConfigUpdateHintRequest struct { func (x *PeerConfigUpdateHintRequest) Reset() { *x = PeerConfigUpdateHintRequest{} - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[192] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15080,7 +15134,7 @@ func (x *PeerConfigUpdateHintRequest) String() string { func (*PeerConfigUpdateHintRequest) ProtoMessage() {} func (x *PeerConfigUpdateHintRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[192] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15093,7 +15147,7 @@ func (x *PeerConfigUpdateHintRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerConfigUpdateHintRequest.ProtoReflect.Descriptor instead. func (*PeerConfigUpdateHintRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{191} + return file_openshell_proto_rawDescGZIP(), []int{192} } func (x *PeerConfigUpdateHintRequest) GetScope() isPeerConfigUpdateHintRequest_Scope { @@ -15130,6 +15184,15 @@ func (x *PeerConfigUpdateHintRequest) GetAllConnected() bool { return false } +func (x *PeerConfigUpdateHintRequest) GetProvider() *PeerConfigProviderTarget { + if x != nil { + if x, ok := x.Scope.(*PeerConfigUpdateHintRequest_Provider); ok { + return x.Provider + } + } + return nil +} + func (x *PeerConfigUpdateHintRequest) GetSandboxConfig() bool { if x != nil { return x.SandboxConfig @@ -15160,25 +15223,29 @@ type PeerConfigUpdateHintRequest_AllConnected struct { AllConnected bool `protobuf:"varint,3,opt,name=all_connected,json=allConnected,proto3,oneof"` } +type PeerConfigUpdateHintRequest_Provider struct { + Provider *PeerConfigProviderTarget `protobuf:"bytes,6,opt,name=provider,proto3,oneof"` +} + func (*PeerConfigUpdateHintRequest_Sandbox) isPeerConfigUpdateHintRequest_Scope() {} func (*PeerConfigUpdateHintRequest_Workspace) isPeerConfigUpdateHintRequest_Scope() {} func (*PeerConfigUpdateHintRequest_AllConnected) isPeerConfigUpdateHintRequest_Scope() {} +func (*PeerConfigUpdateHintRequest_Provider) isPeerConfigUpdateHintRequest_Scope() {} + type PeerConfigUpdateHintResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The targeted session moved before the receiving replica accepted the hint. - StaleOwner bool `protobuf:"varint,1,opt,name=stale_owner,json=staleOwner,proto3" json:"stale_owner,omitempty"` - // The bounded local delivery or fanout queue could not accept the hint. - QueueFull bool `protobuf:"varint,2,opt,name=queue_full,json=queueFull,proto3" json:"queue_full,omitempty"` + StaleOwner bool `protobuf:"varint,1,opt,name=stale_owner,json=staleOwner,proto3" json:"stale_owner,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PeerConfigUpdateHintResponse) Reset() { *x = PeerConfigUpdateHintResponse{} - mi := &file_openshell_proto_msgTypes[192] + mi := &file_openshell_proto_msgTypes[193] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15190,7 +15257,7 @@ func (x *PeerConfigUpdateHintResponse) String() string { func (*PeerConfigUpdateHintResponse) ProtoMessage() {} func (x *PeerConfigUpdateHintResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[192] + mi := &file_openshell_proto_msgTypes[193] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15203,7 +15270,7 @@ func (x *PeerConfigUpdateHintResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerConfigUpdateHintResponse.ProtoReflect.Descriptor instead. func (*PeerConfigUpdateHintResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{192} + return file_openshell_proto_rawDescGZIP(), []int{193} } func (x *PeerConfigUpdateHintResponse) GetStaleOwner() bool { @@ -15213,13 +15280,6 @@ func (x *PeerConfigUpdateHintResponse) GetStaleOwner() bool { return false } -func (x *PeerConfigUpdateHintResponse) GetQueueFull() bool { - if x != nil { - return x.QueueFull - } - return false -} - // Supervisor reports the result of a relay open request. type RelayOpenResult struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -15235,7 +15295,7 @@ type RelayOpenResult struct { func (x *RelayOpenResult) Reset() { *x = RelayOpenResult{} - mi := &file_openshell_proto_msgTypes[193] + mi := &file_openshell_proto_msgTypes[194] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15247,7 +15307,7 @@ func (x *RelayOpenResult) String() string { func (*RelayOpenResult) ProtoMessage() {} func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[193] + mi := &file_openshell_proto_msgTypes[194] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15260,7 +15320,7 @@ func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpenResult.ProtoReflect.Descriptor instead. func (*RelayOpenResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{193} + return file_openshell_proto_rawDescGZIP(), []int{194} } func (x *RelayOpenResult) GetChannelId() string { @@ -15297,7 +15357,7 @@ type RelayClose struct { func (x *RelayClose) Reset() { *x = RelayClose{} - mi := &file_openshell_proto_msgTypes[194] + mi := &file_openshell_proto_msgTypes[195] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15309,7 +15369,7 @@ func (x *RelayClose) String() string { func (*RelayClose) ProtoMessage() {} func (x *RelayClose) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[194] + mi := &file_openshell_proto_msgTypes[195] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15322,7 +15382,7 @@ func (x *RelayClose) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayClose.ProtoReflect.Descriptor instead. func (*RelayClose) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{194} + return file_openshell_proto_rawDescGZIP(), []int{195} } func (x *RelayClose) GetChannelId() string { @@ -15356,7 +15416,7 @@ type L7RequestSample struct { func (x *L7RequestSample) Reset() { *x = L7RequestSample{} - mi := &file_openshell_proto_msgTypes[195] + mi := &file_openshell_proto_msgTypes[196] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15368,7 +15428,7 @@ func (x *L7RequestSample) String() string { func (*L7RequestSample) ProtoMessage() {} func (x *L7RequestSample) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[195] + mi := &file_openshell_proto_msgTypes[196] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15381,7 +15441,7 @@ func (x *L7RequestSample) ProtoReflect() protoreflect.Message { // Deprecated: Use L7RequestSample.ProtoReflect.Descriptor instead. func (*L7RequestSample) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{195} + return file_openshell_proto_rawDescGZIP(), []int{196} } func (x *L7RequestSample) GetMethod() string { @@ -15455,7 +15515,7 @@ type DenialSummary struct { func (x *DenialSummary) Reset() { *x = DenialSummary{} - mi := &file_openshell_proto_msgTypes[196] + mi := &file_openshell_proto_msgTypes[197] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15467,7 +15527,7 @@ func (x *DenialSummary) String() string { func (*DenialSummary) ProtoMessage() {} func (x *DenialSummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[196] + mi := &file_openshell_proto_msgTypes[197] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15480,7 +15540,7 @@ func (x *DenialSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialSummary.ProtoReflect.Descriptor instead. func (*DenialSummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{196} + return file_openshell_proto_rawDescGZIP(), []int{197} } func (x *DenialSummary) GetSandboxId() string { @@ -15615,7 +15675,7 @@ type DenialGroupCount struct { func (x *DenialGroupCount) Reset() { *x = DenialGroupCount{} - mi := &file_openshell_proto_msgTypes[197] + mi := &file_openshell_proto_msgTypes[198] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15627,7 +15687,7 @@ func (x *DenialGroupCount) String() string { func (*DenialGroupCount) ProtoMessage() {} func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[197] + mi := &file_openshell_proto_msgTypes[198] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15640,7 +15700,7 @@ func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialGroupCount.ProtoReflect.Descriptor instead. func (*DenialGroupCount) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{197} + return file_openshell_proto_rawDescGZIP(), []int{198} } func (x *DenialGroupCount) GetDenyGroup() string { @@ -15673,7 +15733,7 @@ type NetworkActivitySummary struct { func (x *NetworkActivitySummary) Reset() { *x = NetworkActivitySummary{} - mi := &file_openshell_proto_msgTypes[198] + mi := &file_openshell_proto_msgTypes[199] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15685,7 +15745,7 @@ func (x *NetworkActivitySummary) String() string { func (*NetworkActivitySummary) ProtoMessage() {} func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[198] + mi := &file_openshell_proto_msgTypes[199] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15698,7 +15758,7 @@ func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkActivitySummary.ProtoReflect.Descriptor instead. func (*NetworkActivitySummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{198} + return file_openshell_proto_rawDescGZIP(), []int{199} } func (x *NetworkActivitySummary) GetNetworkActivityCount() uint32 { @@ -15786,7 +15846,7 @@ type PolicyChunk struct { func (x *PolicyChunk) Reset() { *x = PolicyChunk{} - mi := &file_openshell_proto_msgTypes[199] + mi := &file_openshell_proto_msgTypes[200] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15798,7 +15858,7 @@ func (x *PolicyChunk) String() string { func (*PolicyChunk) ProtoMessage() {} func (x *PolicyChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[199] + mi := &file_openshell_proto_msgTypes[200] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15811,7 +15871,7 @@ func (x *PolicyChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyChunk.ProtoReflect.Descriptor instead. func (*PolicyChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{199} + return file_openshell_proto_rawDescGZIP(), []int{200} } func (x *PolicyChunk) GetId() string { @@ -15999,7 +16059,7 @@ type DraftPolicyUpdate struct { func (x *DraftPolicyUpdate) Reset() { *x = DraftPolicyUpdate{} - mi := &file_openshell_proto_msgTypes[200] + mi := &file_openshell_proto_msgTypes[201] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16011,7 +16071,7 @@ func (x *DraftPolicyUpdate) String() string { func (*DraftPolicyUpdate) ProtoMessage() {} func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[200] + mi := &file_openshell_proto_msgTypes[201] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16024,7 +16084,7 @@ func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftPolicyUpdate.ProtoReflect.Descriptor instead. func (*DraftPolicyUpdate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{200} + return file_openshell_proto_rawDescGZIP(), []int{201} } func (x *DraftPolicyUpdate) GetDraftVersion() uint64 { @@ -16083,7 +16143,7 @@ type SubmitPolicyAnalysisRequest struct { func (x *SubmitPolicyAnalysisRequest) Reset() { *x = SubmitPolicyAnalysisRequest{} - mi := &file_openshell_proto_msgTypes[201] + mi := &file_openshell_proto_msgTypes[202] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16095,7 +16155,7 @@ func (x *SubmitPolicyAnalysisRequest) String() string { func (*SubmitPolicyAnalysisRequest) ProtoMessage() {} func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[201] + mi := &file_openshell_proto_msgTypes[202] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16108,7 +16168,7 @@ func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisRequest.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{201} + return file_openshell_proto_rawDescGZIP(), []int{202} } func (x *SubmitPolicyAnalysisRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16171,7 +16231,7 @@ type SubmitPolicyAnalysisResponse struct { func (x *SubmitPolicyAnalysisResponse) Reset() { *x = SubmitPolicyAnalysisResponse{} - mi := &file_openshell_proto_msgTypes[202] + mi := &file_openshell_proto_msgTypes[203] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16183,7 +16243,7 @@ func (x *SubmitPolicyAnalysisResponse) String() string { func (*SubmitPolicyAnalysisResponse) ProtoMessage() {} func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[202] + mi := &file_openshell_proto_msgTypes[203] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16196,7 +16256,7 @@ func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisResponse.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{202} + return file_openshell_proto_rawDescGZIP(), []int{203} } func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunks() uint32 { @@ -16241,7 +16301,7 @@ type GetDraftPolicyRequest struct { func (x *GetDraftPolicyRequest) Reset() { *x = GetDraftPolicyRequest{} - mi := &file_openshell_proto_msgTypes[203] + mi := &file_openshell_proto_msgTypes[204] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16253,7 +16313,7 @@ func (x *GetDraftPolicyRequest) String() string { func (*GetDraftPolicyRequest) ProtoMessage() {} func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[203] + mi := &file_openshell_proto_msgTypes[204] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16266,7 +16326,7 @@ func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyRequest.ProtoReflect.Descriptor instead. func (*GetDraftPolicyRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{203} + return file_openshell_proto_rawDescGZIP(), []int{204} } func (x *GetDraftPolicyRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16306,7 +16366,7 @@ type GetDraftPolicyResponse struct { func (x *GetDraftPolicyResponse) Reset() { *x = GetDraftPolicyResponse{} - mi := &file_openshell_proto_msgTypes[204] + mi := &file_openshell_proto_msgTypes[205] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16318,7 +16378,7 @@ func (x *GetDraftPolicyResponse) String() string { func (*GetDraftPolicyResponse) ProtoMessage() {} func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[204] + mi := &file_openshell_proto_msgTypes[205] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16331,7 +16391,7 @@ func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyResponse.ProtoReflect.Descriptor instead. func (*GetDraftPolicyResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{204} + return file_openshell_proto_rawDescGZIP(), []int{205} } func (x *GetDraftPolicyResponse) GetChunks() []*PolicyChunk { @@ -16382,7 +16442,7 @@ type ApproveDraftChunkRequest struct { func (x *ApproveDraftChunkRequest) Reset() { *x = ApproveDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[205] + mi := &file_openshell_proto_msgTypes[206] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16394,7 +16454,7 @@ func (x *ApproveDraftChunkRequest) String() string { func (*ApproveDraftChunkRequest) ProtoMessage() {} func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[205] + mi := &file_openshell_proto_msgTypes[206] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16407,7 +16467,7 @@ func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkRequest.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{205} + return file_openshell_proto_rawDescGZIP(), []int{206} } func (x *ApproveDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16457,7 +16517,7 @@ type ApproveDraftChunkResponse struct { func (x *ApproveDraftChunkResponse) Reset() { *x = ApproveDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[206] + mi := &file_openshell_proto_msgTypes[207] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16469,7 +16529,7 @@ func (x *ApproveDraftChunkResponse) String() string { func (*ApproveDraftChunkResponse) ProtoMessage() {} func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[206] + mi := &file_openshell_proto_msgTypes[207] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16482,7 +16542,7 @@ func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkResponse.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{206} + return file_openshell_proto_rawDescGZIP(), []int{207} } func (x *ApproveDraftChunkResponse) GetPolicyVersion() uint32 { @@ -16518,7 +16578,7 @@ type RejectDraftChunkRequest struct { func (x *RejectDraftChunkRequest) Reset() { *x = RejectDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[207] + mi := &file_openshell_proto_msgTypes[208] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16530,7 +16590,7 @@ func (x *RejectDraftChunkRequest) String() string { func (*RejectDraftChunkRequest) ProtoMessage() {} func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[207] + mi := &file_openshell_proto_msgTypes[208] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16543,7 +16603,7 @@ func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkRequest.ProtoReflect.Descriptor instead. func (*RejectDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{207} + return file_openshell_proto_rawDescGZIP(), []int{208} } func (x *RejectDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16589,7 +16649,7 @@ type RejectDraftChunkResponse struct { func (x *RejectDraftChunkResponse) Reset() { *x = RejectDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[208] + mi := &file_openshell_proto_msgTypes[209] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16601,7 +16661,7 @@ func (x *RejectDraftChunkResponse) String() string { func (*RejectDraftChunkResponse) ProtoMessage() {} func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[208] + mi := &file_openshell_proto_msgTypes[209] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16614,7 +16674,7 @@ func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkResponse.ProtoReflect.Descriptor instead. func (*RejectDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{208} + return file_openshell_proto_rawDescGZIP(), []int{209} } // Approve all pending chunks. @@ -16628,7 +16688,7 @@ type DraftChunkApproval struct { func (x *DraftChunkApproval) Reset() { *x = DraftChunkApproval{} - mi := &file_openshell_proto_msgTypes[209] + mi := &file_openshell_proto_msgTypes[210] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16640,7 +16700,7 @@ func (x *DraftChunkApproval) String() string { func (*DraftChunkApproval) ProtoMessage() {} func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[209] + mi := &file_openshell_proto_msgTypes[210] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16653,7 +16713,7 @@ func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftChunkApproval.ProtoReflect.Descriptor instead. func (*DraftChunkApproval) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{209} + return file_openshell_proto_rawDescGZIP(), []int{210} } func (x *DraftChunkApproval) GetChunkId() string { @@ -16689,7 +16749,7 @@ type ApproveAllDraftChunksRequest struct { func (x *ApproveAllDraftChunksRequest) Reset() { *x = ApproveAllDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[210] + mi := &file_openshell_proto_msgTypes[211] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16701,7 +16761,7 @@ func (x *ApproveAllDraftChunksRequest) String() string { func (*ApproveAllDraftChunksRequest) ProtoMessage() {} func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[210] + mi := &file_openshell_proto_msgTypes[211] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16714,7 +16774,7 @@ func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{210} + return file_openshell_proto_rawDescGZIP(), []int{211} } func (x *ApproveAllDraftChunksRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16769,7 +16829,7 @@ type ApproveAllDraftChunksResponse struct { func (x *ApproveAllDraftChunksResponse) Reset() { *x = ApproveAllDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[211] + mi := &file_openshell_proto_msgTypes[212] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16781,7 +16841,7 @@ func (x *ApproveAllDraftChunksResponse) String() string { func (*ApproveAllDraftChunksResponse) ProtoMessage() {} func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[211] + mi := &file_openshell_proto_msgTypes[212] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16794,7 +16854,7 @@ func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{211} + return file_openshell_proto_rawDescGZIP(), []int{212} } func (x *ApproveAllDraftChunksResponse) GetPolicyVersion() uint32 { @@ -16844,7 +16904,7 @@ type EditDraftChunkRequest struct { func (x *EditDraftChunkRequest) Reset() { *x = EditDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[212] + mi := &file_openshell_proto_msgTypes[213] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16856,7 +16916,7 @@ func (x *EditDraftChunkRequest) String() string { func (*EditDraftChunkRequest) ProtoMessage() {} func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[212] + mi := &file_openshell_proto_msgTypes[213] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16869,7 +16929,7 @@ func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkRequest.ProtoReflect.Descriptor instead. func (*EditDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{212} + return file_openshell_proto_rawDescGZIP(), []int{213} } func (x *EditDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16915,7 +16975,7 @@ type EditDraftChunkResponse struct { func (x *EditDraftChunkResponse) Reset() { *x = EditDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[213] + mi := &file_openshell_proto_msgTypes[214] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16927,7 +16987,7 @@ func (x *EditDraftChunkResponse) String() string { func (*EditDraftChunkResponse) ProtoMessage() {} func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[213] + mi := &file_openshell_proto_msgTypes[214] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16940,7 +17000,7 @@ func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkResponse.ProtoReflect.Descriptor instead. func (*EditDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{213} + return file_openshell_proto_rawDescGZIP(), []int{214} } // Reverse an approval (remove merged rule from active policy). @@ -16960,7 +17020,7 @@ type UndoDraftChunkRequest struct { func (x *UndoDraftChunkRequest) Reset() { *x = UndoDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[214] + mi := &file_openshell_proto_msgTypes[215] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16972,7 +17032,7 @@ func (x *UndoDraftChunkRequest) String() string { func (*UndoDraftChunkRequest) ProtoMessage() {} func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[214] + mi := &file_openshell_proto_msgTypes[215] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16985,7 +17045,7 @@ func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkRequest.ProtoReflect.Descriptor instead. func (*UndoDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{214} + return file_openshell_proto_rawDescGZIP(), []int{215} } func (x *UndoDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17028,7 +17088,7 @@ type UndoDraftChunkResponse struct { func (x *UndoDraftChunkResponse) Reset() { *x = UndoDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[215] + mi := &file_openshell_proto_msgTypes[216] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17040,7 +17100,7 @@ func (x *UndoDraftChunkResponse) String() string { func (*UndoDraftChunkResponse) ProtoMessage() {} func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[215] + mi := &file_openshell_proto_msgTypes[216] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17053,7 +17113,7 @@ func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkResponse.ProtoReflect.Descriptor instead. func (*UndoDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{215} + return file_openshell_proto_rawDescGZIP(), []int{216} } func (x *UndoDraftChunkResponse) GetPolicyVersion() uint32 { @@ -17085,7 +17145,7 @@ type ClearDraftChunksRequest struct { func (x *ClearDraftChunksRequest) Reset() { *x = ClearDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[216] + mi := &file_openshell_proto_msgTypes[217] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17097,7 +17157,7 @@ func (x *ClearDraftChunksRequest) String() string { func (*ClearDraftChunksRequest) ProtoMessage() {} func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[216] + mi := &file_openshell_proto_msgTypes[217] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17110,7 +17170,7 @@ func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ClearDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{216} + return file_openshell_proto_rawDescGZIP(), []int{217} } func (x *ClearDraftChunksRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17144,7 +17204,7 @@ type ClearDraftChunksResponse struct { func (x *ClearDraftChunksResponse) Reset() { *x = ClearDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[217] + mi := &file_openshell_proto_msgTypes[218] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17156,7 +17216,7 @@ func (x *ClearDraftChunksResponse) String() string { func (*ClearDraftChunksResponse) ProtoMessage() {} func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[217] + mi := &file_openshell_proto_msgTypes[218] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17169,7 +17229,7 @@ func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ClearDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{217} + return file_openshell_proto_rawDescGZIP(), []int{218} } func (x *ClearDraftChunksResponse) GetChunksCleared() uint32 { @@ -17191,7 +17251,7 @@ type GetDraftHistoryRequest struct { func (x *GetDraftHistoryRequest) Reset() { *x = GetDraftHistoryRequest{} - mi := &file_openshell_proto_msgTypes[218] + mi := &file_openshell_proto_msgTypes[219] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17203,7 +17263,7 @@ func (x *GetDraftHistoryRequest) String() string { func (*GetDraftHistoryRequest) ProtoMessage() {} func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[218] + mi := &file_openshell_proto_msgTypes[219] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17216,7 +17276,7 @@ func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryRequest.ProtoReflect.Descriptor instead. func (*GetDraftHistoryRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{218} + return file_openshell_proto_rawDescGZIP(), []int{219} } func (x *GetDraftHistoryRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17250,7 +17310,7 @@ type DraftHistoryEntry struct { func (x *DraftHistoryEntry) Reset() { *x = DraftHistoryEntry{} - mi := &file_openshell_proto_msgTypes[219] + mi := &file_openshell_proto_msgTypes[220] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17262,7 +17322,7 @@ func (x *DraftHistoryEntry) String() string { func (*DraftHistoryEntry) ProtoMessage() {} func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[219] + mi := &file_openshell_proto_msgTypes[220] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17275,7 +17335,7 @@ func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftHistoryEntry.ProtoReflect.Descriptor instead. func (*DraftHistoryEntry) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{219} + return file_openshell_proto_rawDescGZIP(), []int{220} } func (x *DraftHistoryEntry) GetEventTime() *timestamppb.Timestamp { @@ -17316,7 +17376,7 @@ type GetDraftHistoryResponse struct { func (x *GetDraftHistoryResponse) Reset() { *x = GetDraftHistoryResponse{} - mi := &file_openshell_proto_msgTypes[220] + mi := &file_openshell_proto_msgTypes[221] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17328,7 +17388,7 @@ func (x *GetDraftHistoryResponse) String() string { func (*GetDraftHistoryResponse) ProtoMessage() {} func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[220] + mi := &file_openshell_proto_msgTypes[221] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17341,7 +17401,7 @@ func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryResponse.ProtoReflect.Descriptor instead. func (*GetDraftHistoryResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{220} + return file_openshell_proto_rawDescGZIP(), []int{221} } func (x *GetDraftHistoryResponse) GetEntries() []*DraftHistoryEntry { @@ -17366,7 +17426,7 @@ type CreateWorkspaceRequest struct { func (x *CreateWorkspaceRequest) Reset() { *x = CreateWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[221] + mi := &file_openshell_proto_msgTypes[222] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17378,7 +17438,7 @@ func (x *CreateWorkspaceRequest) String() string { func (*CreateWorkspaceRequest) ProtoMessage() {} func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[221] + mi := &file_openshell_proto_msgTypes[222] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17391,7 +17451,7 @@ func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceRequest.ProtoReflect.Descriptor instead. func (*CreateWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{221} + return file_openshell_proto_rawDescGZIP(), []int{222} } func (x *CreateWorkspaceRequest) GetName() string { @@ -17425,7 +17485,7 @@ type CreateWorkspaceResponse struct { func (x *CreateWorkspaceResponse) Reset() { *x = CreateWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[222] + mi := &file_openshell_proto_msgTypes[223] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17437,7 +17497,7 @@ func (x *CreateWorkspaceResponse) String() string { func (*CreateWorkspaceResponse) ProtoMessage() {} func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[222] + mi := &file_openshell_proto_msgTypes[223] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17450,7 +17510,7 @@ func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceResponse.ProtoReflect.Descriptor instead. func (*CreateWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{222} + return file_openshell_proto_rawDescGZIP(), []int{223} } func (x *CreateWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -17471,7 +17531,7 @@ type GetWorkspaceRequest struct { func (x *GetWorkspaceRequest) Reset() { *x = GetWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[223] + mi := &file_openshell_proto_msgTypes[224] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17483,7 +17543,7 @@ func (x *GetWorkspaceRequest) String() string { func (*GetWorkspaceRequest) ProtoMessage() {} func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[223] + mi := &file_openshell_proto_msgTypes[224] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17496,7 +17556,7 @@ func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceRequest.ProtoReflect.Descriptor instead. func (*GetWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{223} + return file_openshell_proto_rawDescGZIP(), []int{224} } func (x *GetWorkspaceRequest) GetName() string { @@ -17516,7 +17576,7 @@ type GetWorkspaceResponse struct { func (x *GetWorkspaceResponse) Reset() { *x = GetWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[224] + mi := &file_openshell_proto_msgTypes[225] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17528,7 +17588,7 @@ func (x *GetWorkspaceResponse) String() string { func (*GetWorkspaceResponse) ProtoMessage() {} func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[224] + mi := &file_openshell_proto_msgTypes[225] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17541,7 +17601,7 @@ func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceResponse.ProtoReflect.Descriptor instead. func (*GetWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{224} + return file_openshell_proto_rawDescGZIP(), []int{225} } func (x *GetWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -17568,7 +17628,7 @@ type ListWorkspacesRequest struct { func (x *ListWorkspacesRequest) Reset() { *x = ListWorkspacesRequest{} - mi := &file_openshell_proto_msgTypes[225] + mi := &file_openshell_proto_msgTypes[226] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17580,7 +17640,7 @@ func (x *ListWorkspacesRequest) String() string { func (*ListWorkspacesRequest) ProtoMessage() {} func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[225] + mi := &file_openshell_proto_msgTypes[226] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17593,7 +17653,7 @@ func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesRequest.ProtoReflect.Descriptor instead. func (*ListWorkspacesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{225} + return file_openshell_proto_rawDescGZIP(), []int{226} } func (x *ListWorkspacesRequest) GetPageSize() int32 { @@ -17629,7 +17689,7 @@ type ListWorkspacesResponse struct { func (x *ListWorkspacesResponse) Reset() { *x = ListWorkspacesResponse{} - mi := &file_openshell_proto_msgTypes[226] + mi := &file_openshell_proto_msgTypes[227] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17641,7 +17701,7 @@ func (x *ListWorkspacesResponse) String() string { func (*ListWorkspacesResponse) ProtoMessage() {} func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[226] + mi := &file_openshell_proto_msgTypes[227] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17654,7 +17714,7 @@ func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesResponse.ProtoReflect.Descriptor instead. func (*ListWorkspacesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{226} + return file_openshell_proto_rawDescGZIP(), []int{227} } func (x *ListWorkspacesResponse) GetWorkspaces() []*datamodelv1.Workspace { @@ -17685,7 +17745,7 @@ type DeleteWorkspaceRequest struct { func (x *DeleteWorkspaceRequest) Reset() { *x = DeleteWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[227] + mi := &file_openshell_proto_msgTypes[228] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17697,7 +17757,7 @@ func (x *DeleteWorkspaceRequest) String() string { func (*DeleteWorkspaceRequest) ProtoMessage() {} func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[227] + mi := &file_openshell_proto_msgTypes[228] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17710,7 +17770,7 @@ func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceRequest.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{227} + return file_openshell_proto_rawDescGZIP(), []int{228} } func (x *DeleteWorkspaceRequest) GetName() string { @@ -17744,7 +17804,7 @@ type DeleteWorkspaceResponse struct { func (x *DeleteWorkspaceResponse) Reset() { *x = DeleteWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[228] + mi := &file_openshell_proto_msgTypes[229] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17756,7 +17816,7 @@ func (x *DeleteWorkspaceResponse) String() string { func (*DeleteWorkspaceResponse) ProtoMessage() {} func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[228] + mi := &file_openshell_proto_msgTypes[229] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17769,7 +17829,7 @@ func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceResponse.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{228} + return file_openshell_proto_rawDescGZIP(), []int{229} } func (x *DeleteWorkspaceResponse) GetOutcome() DeletionOutcome { @@ -17793,7 +17853,7 @@ type WorkspaceMember struct { func (x *WorkspaceMember) Reset() { *x = WorkspaceMember{} - mi := &file_openshell_proto_msgTypes[229] + mi := &file_openshell_proto_msgTypes[230] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17805,7 +17865,7 @@ func (x *WorkspaceMember) String() string { func (*WorkspaceMember) ProtoMessage() {} func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[229] + mi := &file_openshell_proto_msgTypes[230] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17818,7 +17878,7 @@ func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceMember.ProtoReflect.Descriptor instead. func (*WorkspaceMember) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{229} + return file_openshell_proto_rawDescGZIP(), []int{230} } func (x *WorkspaceMember) GetMetadata() *datamodelv1.ObjectMeta { @@ -17859,7 +17919,7 @@ type AddWorkspaceMemberRequest struct { func (x *AddWorkspaceMemberRequest) Reset() { *x = AddWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[230] + mi := &file_openshell_proto_msgTypes[231] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17871,7 +17931,7 @@ func (x *AddWorkspaceMemberRequest) String() string { func (*AddWorkspaceMemberRequest) ProtoMessage() {} func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[230] + mi := &file_openshell_proto_msgTypes[231] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17884,7 +17944,7 @@ func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{230} + return file_openshell_proto_rawDescGZIP(), []int{231} } func (x *AddWorkspaceMemberRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17925,7 +17985,7 @@ type AddWorkspaceMemberResponse struct { func (x *AddWorkspaceMemberResponse) Reset() { *x = AddWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[231] + mi := &file_openshell_proto_msgTypes[232] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17937,7 +17997,7 @@ func (x *AddWorkspaceMemberResponse) String() string { func (*AddWorkspaceMemberResponse) ProtoMessage() {} func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[231] + mi := &file_openshell_proto_msgTypes[232] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17950,7 +18010,7 @@ func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{231} + return file_openshell_proto_rawDescGZIP(), []int{232} } func (x *AddWorkspaceMemberResponse) GetMember() *WorkspaceMember { @@ -17976,7 +18036,7 @@ type RemoveWorkspaceMemberRequest struct { func (x *RemoveWorkspaceMemberRequest) Reset() { *x = RemoveWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[232] + mi := &file_openshell_proto_msgTypes[233] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17988,7 +18048,7 @@ func (x *RemoveWorkspaceMemberRequest) String() string { func (*RemoveWorkspaceMemberRequest) ProtoMessage() {} func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[232] + mi := &file_openshell_proto_msgTypes[233] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18001,7 +18061,7 @@ func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{232} + return file_openshell_proto_rawDescGZIP(), []int{233} } func (x *RemoveWorkspaceMemberRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -18042,7 +18102,7 @@ type RemoveWorkspaceMemberResponse struct { func (x *RemoveWorkspaceMemberResponse) Reset() { *x = RemoveWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[233] + mi := &file_openshell_proto_msgTypes[234] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18054,7 +18114,7 @@ func (x *RemoveWorkspaceMemberResponse) String() string { func (*RemoveWorkspaceMemberResponse) ProtoMessage() {} func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[233] + mi := &file_openshell_proto_msgTypes[234] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18067,7 +18127,7 @@ func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{233} + return file_openshell_proto_rawDescGZIP(), []int{234} } func (x *RemoveWorkspaceMemberResponse) GetOutcome() DeletionOutcome { @@ -18094,7 +18154,7 @@ type ListWorkspaceMembersRequest struct { func (x *ListWorkspaceMembersRequest) Reset() { *x = ListWorkspaceMembersRequest{} - mi := &file_openshell_proto_msgTypes[234] + mi := &file_openshell_proto_msgTypes[235] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18106,7 +18166,7 @@ func (x *ListWorkspaceMembersRequest) String() string { func (*ListWorkspaceMembersRequest) ProtoMessage() {} func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[234] + mi := &file_openshell_proto_msgTypes[235] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18119,7 +18179,7 @@ func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersRequest.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{234} + return file_openshell_proto_rawDescGZIP(), []int{235} } func (x *ListWorkspaceMembersRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -18155,7 +18215,7 @@ type ListWorkspaceMembersResponse struct { func (x *ListWorkspaceMembersResponse) Reset() { *x = ListWorkspaceMembersResponse{} - mi := &file_openshell_proto_msgTypes[235] + mi := &file_openshell_proto_msgTypes[236] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18167,7 +18227,7 @@ func (x *ListWorkspaceMembersResponse) String() string { func (*ListWorkspaceMembersResponse) ProtoMessage() {} func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[235] + mi := &file_openshell_proto_msgTypes[236] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18180,7 +18240,7 @@ func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersResponse.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{235} + return file_openshell_proto_rawDescGZIP(), []int{236} } func (x *ListWorkspaceMembersResponse) GetMembers() []*WorkspaceMember { @@ -18215,7 +18275,7 @@ type ExtensionServiceCredential struct { func (x *ExtensionServiceCredential) Reset() { *x = ExtensionServiceCredential{} - mi := &file_openshell_proto_msgTypes[236] + mi := &file_openshell_proto_msgTypes[237] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18227,7 +18287,7 @@ func (x *ExtensionServiceCredential) String() string { func (*ExtensionServiceCredential) ProtoMessage() {} func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[236] + mi := &file_openshell_proto_msgTypes[237] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18240,7 +18300,7 @@ func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ExtensionServiceCredential.ProtoReflect.Descriptor instead. func (*ExtensionServiceCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{236} + return file_openshell_proto_rawDescGZIP(), []int{237} } func (x *ExtensionServiceCredential) GetServiceName() string { @@ -18277,7 +18337,7 @@ type EndpointObservation struct { func (x *EndpointObservation) Reset() { *x = EndpointObservation{} - mi := &file_openshell_proto_msgTypes[237] + mi := &file_openshell_proto_msgTypes[238] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18289,7 +18349,7 @@ func (x *EndpointObservation) String() string { func (*EndpointObservation) ProtoMessage() {} func (x *EndpointObservation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[237] + mi := &file_openshell_proto_msgTypes[238] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18302,7 +18362,7 @@ func (x *EndpointObservation) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointObservation.ProtoReflect.Descriptor instead. func (*EndpointObservation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{237} + return file_openshell_proto_rawDescGZIP(), []int{238} } func (x *EndpointObservation) GetEndpointId() string { @@ -18345,7 +18405,7 @@ type ReportEndpointStatusRequest struct { func (x *ReportEndpointStatusRequest) Reset() { *x = ReportEndpointStatusRequest{} - mi := &file_openshell_proto_msgTypes[238] + mi := &file_openshell_proto_msgTypes[239] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18357,7 +18417,7 @@ func (x *ReportEndpointStatusRequest) String() string { func (*ReportEndpointStatusRequest) ProtoMessage() {} func (x *ReportEndpointStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[238] + mi := &file_openshell_proto_msgTypes[239] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18370,7 +18430,7 @@ func (x *ReportEndpointStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportEndpointStatusRequest.ProtoReflect.Descriptor instead. func (*ReportEndpointStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{238} + return file_openshell_proto_rawDescGZIP(), []int{239} } func (x *ReportEndpointStatusRequest) GetSandboxId() string { @@ -18431,7 +18491,7 @@ type ReportEndpointStatusResponse struct { func (x *ReportEndpointStatusResponse) Reset() { *x = ReportEndpointStatusResponse{} - mi := &file_openshell_proto_msgTypes[239] + mi := &file_openshell_proto_msgTypes[240] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18443,7 +18503,7 @@ func (x *ReportEndpointStatusResponse) String() string { func (*ReportEndpointStatusResponse) ProtoMessage() {} func (x *ReportEndpointStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[239] + mi := &file_openshell_proto_msgTypes[240] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18456,7 +18516,7 @@ func (x *ReportEndpointStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportEndpointStatusResponse.ProtoReflect.Descriptor instead. func (*ReportEndpointStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{239} + return file_openshell_proto_rawDescGZIP(), []int{240} } // A configured endpoint and its last accepted network result in one record. @@ -18485,7 +18545,7 @@ type EndpointStatus struct { func (x *EndpointStatus) Reset() { *x = EndpointStatus{} - mi := &file_openshell_proto_msgTypes[240] + mi := &file_openshell_proto_msgTypes[241] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18497,7 +18557,7 @@ func (x *EndpointStatus) String() string { func (*EndpointStatus) ProtoMessage() {} func (x *EndpointStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[240] + mi := &file_openshell_proto_msgTypes[241] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18510,7 +18570,7 @@ func (x *EndpointStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointStatus.ProtoReflect.Descriptor instead. func (*EndpointStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{240} + return file_openshell_proto_rawDescGZIP(), []int{241} } func (x *EndpointStatus) GetEndpointId() string { @@ -18580,7 +18640,7 @@ type SandboxProvisioning struct { func (x *SandboxProvisioning) Reset() { *x = SandboxProvisioning{} - mi := &file_openshell_proto_msgTypes[241] + mi := &file_openshell_proto_msgTypes[242] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18592,7 +18652,7 @@ func (x *SandboxProvisioning) String() string { func (*SandboxProvisioning) ProtoMessage() {} func (x *SandboxProvisioning) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[241] + mi := &file_openshell_proto_msgTypes[242] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18605,7 +18665,7 @@ func (x *SandboxProvisioning) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxProvisioning.ProtoReflect.Descriptor instead. func (*SandboxProvisioning) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{241} + return file_openshell_proto_rawDescGZIP(), []int{242} } func (x *SandboxProvisioning) GetAttemptId() string { @@ -18700,7 +18760,7 @@ type SandboxServiceExposure struct { func (x *SandboxServiceExposure) Reset() { *x = SandboxServiceExposure{} - mi := &file_openshell_proto_msgTypes[242] + mi := &file_openshell_proto_msgTypes[243] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18712,7 +18772,7 @@ func (x *SandboxServiceExposure) String() string { func (*SandboxServiceExposure) ProtoMessage() {} func (x *SandboxServiceExposure) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[242] + mi := &file_openshell_proto_msgTypes[243] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18725,7 +18785,7 @@ func (x *SandboxServiceExposure) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxServiceExposure.ProtoReflect.Descriptor instead. func (*SandboxServiceExposure) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{242} + return file_openshell_proto_rawDescGZIP(), []int{243} } func (x *SandboxServiceExposure) GetService() string { @@ -19862,19 +19922,21 @@ const file_openshell_proto_rawDesc = "" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1d\n" + "\n" + - "session_id\x18\x02 \x01(\tR\tsessionId\"\x8a\x02\n" + + "session_id\x18\x02 \x01(\tR\tsessionId\"L\n" + + "\x18PeerConfigProviderTarget\x12\x1c\n" + + "\tworkspace\x18\x01 \x01(\tR\tworkspace\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\"\xd0\x02\n" + "\x1bPeerConfigUpdateHintRequest\x12A\n" + "\asandbox\x18\x01 \x01(\v2%.openshell.v1.PeerConfigSandboxTargetH\x00R\asandbox\x12\x1e\n" + "\tworkspace\x18\x02 \x01(\tH\x00R\tworkspace\x12%\n" + - "\rall_connected\x18\x03 \x01(\bH\x00R\fallConnected\x12%\n" + + "\rall_connected\x18\x03 \x01(\bH\x00R\fallConnected\x12D\n" + + "\bprovider\x18\x06 \x01(\v2&.openshell.v1.PeerConfigProviderTargetH\x00R\bprovider\x12%\n" + "\x0esandbox_config\x18\x04 \x01(\bR\rsandboxConfig\x121\n" + "\x14provider_environment\x18\x05 \x01(\bR\x13providerEnvironmentB\a\n" + - "\x05scope\"^\n" + + "\x05scope\"?\n" + "\x1cPeerConfigUpdateHintResponse\x12\x1f\n" + "\vstale_owner\x18\x01 \x01(\bR\n" + - "staleOwner\x12\x1d\n" + - "\n" + - "queue_full\x18\x02 \x01(\bR\tqueueFull\"`\n" + + "staleOwner\"`\n" + "\x0fRelayOpenResult\x12\x1d\n" + "\n" + "channel_id\x18\x01 \x01(\tR\tchannelId\x12\x18\n" + @@ -20493,7 +20555,7 @@ func file_openshell_proto_rawDescGZIP() []byte { } var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 21) -var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 268) +var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 269) var file_openshell_proto_goTypes = []any{ (ExtensionKind)(0), // 0: openshell.v1.ExtensionKind (SandboxPhase)(0), // 1: openshell.v1.SandboxPhase @@ -20707,109 +20769,110 @@ var file_openshell_proto_goTypes = []any{ (*PeerRelayInit)(nil), // 209: openshell.v1.PeerRelayInit (*PeerRelayFrame)(nil), // 210: openshell.v1.PeerRelayFrame (*PeerConfigSandboxTarget)(nil), // 211: openshell.v1.PeerConfigSandboxTarget - (*PeerConfigUpdateHintRequest)(nil), // 212: openshell.v1.PeerConfigUpdateHintRequest - (*PeerConfigUpdateHintResponse)(nil), // 213: openshell.v1.PeerConfigUpdateHintResponse - (*RelayOpenResult)(nil), // 214: openshell.v1.RelayOpenResult - (*RelayClose)(nil), // 215: openshell.v1.RelayClose - (*L7RequestSample)(nil), // 216: openshell.v1.L7RequestSample - (*DenialSummary)(nil), // 217: openshell.v1.DenialSummary - (*DenialGroupCount)(nil), // 218: openshell.v1.DenialGroupCount - (*NetworkActivitySummary)(nil), // 219: openshell.v1.NetworkActivitySummary - (*PolicyChunk)(nil), // 220: openshell.v1.PolicyChunk - (*DraftPolicyUpdate)(nil), // 221: openshell.v1.DraftPolicyUpdate - (*SubmitPolicyAnalysisRequest)(nil), // 222: openshell.v1.SubmitPolicyAnalysisRequest - (*SubmitPolicyAnalysisResponse)(nil), // 223: openshell.v1.SubmitPolicyAnalysisResponse - (*GetDraftPolicyRequest)(nil), // 224: openshell.v1.GetDraftPolicyRequest - (*GetDraftPolicyResponse)(nil), // 225: openshell.v1.GetDraftPolicyResponse - (*ApproveDraftChunkRequest)(nil), // 226: openshell.v1.ApproveDraftChunkRequest - (*ApproveDraftChunkResponse)(nil), // 227: openshell.v1.ApproveDraftChunkResponse - (*RejectDraftChunkRequest)(nil), // 228: openshell.v1.RejectDraftChunkRequest - (*RejectDraftChunkResponse)(nil), // 229: openshell.v1.RejectDraftChunkResponse - (*DraftChunkApproval)(nil), // 230: openshell.v1.DraftChunkApproval - (*ApproveAllDraftChunksRequest)(nil), // 231: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 232: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 233: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 234: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 235: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 236: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 237: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 238: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 239: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 240: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 241: openshell.v1.GetDraftHistoryResponse - (*CreateWorkspaceRequest)(nil), // 242: openshell.v1.CreateWorkspaceRequest - (*CreateWorkspaceResponse)(nil), // 243: openshell.v1.CreateWorkspaceResponse - (*GetWorkspaceRequest)(nil), // 244: openshell.v1.GetWorkspaceRequest - (*GetWorkspaceResponse)(nil), // 245: openshell.v1.GetWorkspaceResponse - (*ListWorkspacesRequest)(nil), // 246: openshell.v1.ListWorkspacesRequest - (*ListWorkspacesResponse)(nil), // 247: openshell.v1.ListWorkspacesResponse - (*DeleteWorkspaceRequest)(nil), // 248: openshell.v1.DeleteWorkspaceRequest - (*DeleteWorkspaceResponse)(nil), // 249: openshell.v1.DeleteWorkspaceResponse - (*WorkspaceMember)(nil), // 250: openshell.v1.WorkspaceMember - (*AddWorkspaceMemberRequest)(nil), // 251: openshell.v1.AddWorkspaceMemberRequest - (*AddWorkspaceMemberResponse)(nil), // 252: openshell.v1.AddWorkspaceMemberResponse - (*RemoveWorkspaceMemberRequest)(nil), // 253: openshell.v1.RemoveWorkspaceMemberRequest - (*RemoveWorkspaceMemberResponse)(nil), // 254: openshell.v1.RemoveWorkspaceMemberResponse - (*ListWorkspaceMembersRequest)(nil), // 255: openshell.v1.ListWorkspaceMembersRequest - (*ListWorkspaceMembersResponse)(nil), // 256: openshell.v1.ListWorkspaceMembersResponse - (*ExtensionServiceCredential)(nil), // 257: openshell.v1.ExtensionServiceCredential - (*EndpointObservation)(nil), // 258: openshell.v1.EndpointObservation - (*ReportEndpointStatusRequest)(nil), // 259: openshell.v1.ReportEndpointStatusRequest - (*ReportEndpointStatusResponse)(nil), // 260: openshell.v1.ReportEndpointStatusResponse - (*EndpointStatus)(nil), // 261: openshell.v1.EndpointStatus - (*SandboxProvisioning)(nil), // 262: openshell.v1.SandboxProvisioning - (*SandboxServiceExposure)(nil), // 263: openshell.v1.SandboxServiceExposure - nil, // 264: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 265: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 266: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 267: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 268: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry - nil, // 269: openshell.v1.PlatformEvent.MetadataEntry - nil, // 270: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 271: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 272: openshell.v1.SandboxResponse.ServiceUrlsEntry - nil, // 273: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 274: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 275: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry - nil, // 276: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 277: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 278: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 279: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry - nil, // 280: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 281: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 282: openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntry - nil, // 283: openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry - nil, // 284: openshell.v1.ProviderEnvironmentSnapshot.FilesEntry - nil, // 285: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 286: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 287: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 288: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*timestamppb.Timestamp)(nil), // 289: google.protobuf.Timestamp - (*datamodelv1.ObjectMeta)(nil), // 290: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 291: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 292: google.protobuf.Struct - (*durationpb.Duration)(nil), // 293: google.protobuf.Duration - (*datamodelv1.WorkspaceSelector)(nil), // 294: openshell.datamodel.v1.WorkspaceSelector - (*datamodelv1.Provider)(nil), // 295: openshell.datamodel.v1.Provider - (sandboxv1.PolicySource)(0), // 296: openshell.sandbox.v1.PolicySource - (*sandboxv1.NetworkEndpoint)(nil), // 297: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 298: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 299: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 300: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 301: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 302: openshell.sandbox.v1.L7Rule - (*sandboxv1.SandboxConfigSnapshot)(nil), // 303: openshell.sandbox.v1.SandboxConfigSnapshot - (*datamodelv1.Workspace)(nil), // 304: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 305: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 306: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 307: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 308: openshell.sandbox.v1.GetGatewayConfigResponse + (*PeerConfigProviderTarget)(nil), // 212: openshell.v1.PeerConfigProviderTarget + (*PeerConfigUpdateHintRequest)(nil), // 213: openshell.v1.PeerConfigUpdateHintRequest + (*PeerConfigUpdateHintResponse)(nil), // 214: openshell.v1.PeerConfigUpdateHintResponse + (*RelayOpenResult)(nil), // 215: openshell.v1.RelayOpenResult + (*RelayClose)(nil), // 216: openshell.v1.RelayClose + (*L7RequestSample)(nil), // 217: openshell.v1.L7RequestSample + (*DenialSummary)(nil), // 218: openshell.v1.DenialSummary + (*DenialGroupCount)(nil), // 219: openshell.v1.DenialGroupCount + (*NetworkActivitySummary)(nil), // 220: openshell.v1.NetworkActivitySummary + (*PolicyChunk)(nil), // 221: openshell.v1.PolicyChunk + (*DraftPolicyUpdate)(nil), // 222: openshell.v1.DraftPolicyUpdate + (*SubmitPolicyAnalysisRequest)(nil), // 223: openshell.v1.SubmitPolicyAnalysisRequest + (*SubmitPolicyAnalysisResponse)(nil), // 224: openshell.v1.SubmitPolicyAnalysisResponse + (*GetDraftPolicyRequest)(nil), // 225: openshell.v1.GetDraftPolicyRequest + (*GetDraftPolicyResponse)(nil), // 226: openshell.v1.GetDraftPolicyResponse + (*ApproveDraftChunkRequest)(nil), // 227: openshell.v1.ApproveDraftChunkRequest + (*ApproveDraftChunkResponse)(nil), // 228: openshell.v1.ApproveDraftChunkResponse + (*RejectDraftChunkRequest)(nil), // 229: openshell.v1.RejectDraftChunkRequest + (*RejectDraftChunkResponse)(nil), // 230: openshell.v1.RejectDraftChunkResponse + (*DraftChunkApproval)(nil), // 231: openshell.v1.DraftChunkApproval + (*ApproveAllDraftChunksRequest)(nil), // 232: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 233: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 234: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 235: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 236: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 237: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 238: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 239: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 240: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 241: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 242: openshell.v1.GetDraftHistoryResponse + (*CreateWorkspaceRequest)(nil), // 243: openshell.v1.CreateWorkspaceRequest + (*CreateWorkspaceResponse)(nil), // 244: openshell.v1.CreateWorkspaceResponse + (*GetWorkspaceRequest)(nil), // 245: openshell.v1.GetWorkspaceRequest + (*GetWorkspaceResponse)(nil), // 246: openshell.v1.GetWorkspaceResponse + (*ListWorkspacesRequest)(nil), // 247: openshell.v1.ListWorkspacesRequest + (*ListWorkspacesResponse)(nil), // 248: openshell.v1.ListWorkspacesResponse + (*DeleteWorkspaceRequest)(nil), // 249: openshell.v1.DeleteWorkspaceRequest + (*DeleteWorkspaceResponse)(nil), // 250: openshell.v1.DeleteWorkspaceResponse + (*WorkspaceMember)(nil), // 251: openshell.v1.WorkspaceMember + (*AddWorkspaceMemberRequest)(nil), // 252: openshell.v1.AddWorkspaceMemberRequest + (*AddWorkspaceMemberResponse)(nil), // 253: openshell.v1.AddWorkspaceMemberResponse + (*RemoveWorkspaceMemberRequest)(nil), // 254: openshell.v1.RemoveWorkspaceMemberRequest + (*RemoveWorkspaceMemberResponse)(nil), // 255: openshell.v1.RemoveWorkspaceMemberResponse + (*ListWorkspaceMembersRequest)(nil), // 256: openshell.v1.ListWorkspaceMembersRequest + (*ListWorkspaceMembersResponse)(nil), // 257: openshell.v1.ListWorkspaceMembersResponse + (*ExtensionServiceCredential)(nil), // 258: openshell.v1.ExtensionServiceCredential + (*EndpointObservation)(nil), // 259: openshell.v1.EndpointObservation + (*ReportEndpointStatusRequest)(nil), // 260: openshell.v1.ReportEndpointStatusRequest + (*ReportEndpointStatusResponse)(nil), // 261: openshell.v1.ReportEndpointStatusResponse + (*EndpointStatus)(nil), // 262: openshell.v1.EndpointStatus + (*SandboxProvisioning)(nil), // 263: openshell.v1.SandboxProvisioning + (*SandboxServiceExposure)(nil), // 264: openshell.v1.SandboxServiceExposure + nil, // 265: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 266: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 267: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 268: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 269: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + nil, // 270: openshell.v1.PlatformEvent.MetadataEntry + nil, // 271: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 272: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 273: openshell.v1.SandboxResponse.ServiceUrlsEntry + nil, // 274: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 275: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 276: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + nil, // 277: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 278: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 279: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 280: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + nil, // 281: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 282: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 283: openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntry + nil, // 284: openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry + nil, // 285: openshell.v1.ProviderEnvironmentSnapshot.FilesEntry + nil, // 286: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 287: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 288: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 289: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*timestamppb.Timestamp)(nil), // 290: google.protobuf.Timestamp + (*datamodelv1.ObjectMeta)(nil), // 291: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 292: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 293: google.protobuf.Struct + (*durationpb.Duration)(nil), // 294: google.protobuf.Duration + (*datamodelv1.WorkspaceSelector)(nil), // 295: openshell.datamodel.v1.WorkspaceSelector + (*datamodelv1.Provider)(nil), // 296: openshell.datamodel.v1.Provider + (sandboxv1.PolicySource)(0), // 297: openshell.sandbox.v1.PolicySource + (*sandboxv1.NetworkEndpoint)(nil), // 298: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 299: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 300: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 301: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 302: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 303: openshell.sandbox.v1.L7Rule + (*sandboxv1.SandboxConfigSnapshot)(nil), // 304: openshell.sandbox.v1.SandboxConfigSnapshot + (*datamodelv1.Workspace)(nil), // 305: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 306: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 307: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 308: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 309: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 289, // 0: openshell.v1.IssueSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp - 289, // 1: openshell.v1.RefreshSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp - 257, // 2: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential - 289, // 3: openshell.v1.RefreshSandboxTokenResponse.sandbox_expiration_time:type_name -> google.protobuf.Timestamp + 290, // 0: openshell.v1.IssueSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp + 290, // 1: openshell.v1.RefreshSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp + 258, // 2: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential + 290, // 3: openshell.v1.RefreshSandboxTokenResponse.sandbox_expiration_time:type_name -> google.protobuf.Timestamp 14, // 4: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus 14, // 5: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus 32, // 6: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo @@ -20820,115 +20883,115 @@ var file_openshell_proto_depIdxs = []int32{ 35, // 11: openshell.v1.ResourceCapabilities.cpu:type_name -> openshell.v1.CpuResourceCapabilities 36, // 12: openshell.v1.ResourceCapabilities.memory:type_name -> openshell.v1.MemoryResourceCapabilities 37, // 13: openshell.v1.ResourceCapabilities.gpu:type_name -> openshell.v1.GpuResourceCapabilities - 290, // 14: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 291, // 14: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 39, // 15: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec 50, // 16: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus 49, // 17: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance - 264, // 18: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 265, // 18: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry 42, // 19: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 291, // 20: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 292, // 20: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy 40, // 21: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements 17, // 22: openshell.v1.SandboxSpec.restart_policy:type_name -> openshell.v1.SandboxRestartPolicy 41, // 23: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 265, // 24: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 266, // 25: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 267, // 26: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 292, // 27: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 292, // 28: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 290, // 29: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 266, // 24: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 267, // 25: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 268, // 26: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 293, // 27: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 293, // 28: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 291, // 29: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 44, // 30: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec 45, // 31: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig - 292, // 32: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct + 293, // 32: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct 47, // 33: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel - 268, // 34: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + 269, // 34: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry 46, // 35: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources 41, // 36: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements 48, // 37: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup - 293, // 38: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration + 294, // 38: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration 51, // 39: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition 1, // 40: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 261, // 41: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus + 262, // 41: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus 179, // 42: openshell.v1.SandboxStatus.configuration_admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 262, // 43: openshell.v1.SandboxStatus.provisioning:type_name -> openshell.v1.SandboxProvisioning - 289, // 44: openshell.v1.SandboxStatus.next_restart_time:type_name -> google.protobuf.Timestamp - 289, // 45: openshell.v1.SandboxStatus.main_process_started_time:type_name -> google.protobuf.Timestamp - 289, // 46: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp - 289, // 47: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp - 269, // 48: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 294, // 49: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 263, // 43: openshell.v1.SandboxStatus.provisioning:type_name -> openshell.v1.SandboxProvisioning + 290, // 44: openshell.v1.SandboxStatus.next_restart_time:type_name -> google.protobuf.Timestamp + 290, // 45: openshell.v1.SandboxStatus.main_process_started_time:type_name -> google.protobuf.Timestamp + 290, // 46: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp + 290, // 47: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp + 270, // 48: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 295, // 49: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 39, // 50: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 270, // 51: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 271, // 52: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 263, // 53: openshell.v1.CreateSandboxRequest.service_exposures:type_name -> openshell.v1.SandboxServiceExposure - 294, // 54: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 271, // 51: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 272, // 52: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 264, // 53: openshell.v1.CreateSandboxRequest.service_exposures:type_name -> openshell.v1.SandboxServiceExposure + 295, // 54: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 43, // 55: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 294, // 56: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 57: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 58: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 56: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 57: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 58: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 43, // 59: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate 43, // 60: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate 18, // 61: openshell.v1.DeleteSandboxTemplateResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 294, // 62: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 289, // 63: openshell.v1.BeginRootfsTarStagingResponse.expiration_time:type_name -> google.protobuf.Timestamp - 294, // 64: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 65: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 66: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 67: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 68: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 69: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 70: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 71: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 62: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 290, // 63: openshell.v1.BeginRootfsTarStagingResponse.expiration_time:type_name -> google.protobuf.Timestamp + 295, // 64: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 65: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 66: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 67: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 68: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 69: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 70: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 71: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 38, // 72: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 272, // 73: openshell.v1.SandboxResponse.service_urls:type_name -> openshell.v1.SandboxResponse.ServiceUrlsEntry + 273, // 73: openshell.v1.SandboxResponse.service_urls:type_name -> openshell.v1.SandboxResponse.ServiceUrlsEntry 38, // 74: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 295, // 75: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 296, // 75: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider 38, // 76: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox 80, // 77: openshell.v1.AttachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt 38, // 78: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox 80, // 79: openshell.v1.DetachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt 78, // 80: openshell.v1.ConfigSnapshotRevision.sandbox_config:type_name -> openshell.v1.SandboxConfigRevision 76, // 81: openshell.v1.ConfigSnapshotRevision.provider_target:type_name -> openshell.v1.ProviderDesiredIdentity - 296, // 82: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource + 297, // 82: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource 5, // 83: openshell.v1.ConfigUpdateOperation.component:type_name -> openshell.v1.ConfigComponent 77, // 84: openshell.v1.ConfigUpdateOperation.target_revision:type_name -> openshell.v1.ConfigSnapshotRevision 7, // 85: openshell.v1.ConfigUpdateOperation.state:type_name -> openshell.v1.ConfigUpdateOperationState 6, // 86: openshell.v1.ConfigUpdateOperation.outcome:type_name -> openshell.v1.ConfigApplyOutcome - 289, // 87: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp - 289, // 88: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp - 289, // 89: openshell.v1.ConfigUpdateOperation.completed_time:type_name -> google.protobuf.Timestamp + 290, // 87: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp + 290, // 88: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp + 290, // 89: openshell.v1.ConfigUpdateOperation.completed_time:type_name -> google.protobuf.Timestamp 2, // 90: openshell.v1.ProviderMutationReceipt.kind:type_name -> openshell.v1.ProviderMutationKind 76, // 91: openshell.v1.ProviderMutationReceipt.desired:type_name -> openshell.v1.ProviderDesiredIdentity - 289, // 92: openshell.v1.ProviderMutationReceipt.persisted_time:type_name -> google.protobuf.Timestamp + 290, // 92: openshell.v1.ProviderMutationReceipt.persisted_time:type_name -> google.protobuf.Timestamp 4, // 93: openshell.v1.ProviderReadinessObservation.reason:type_name -> openshell.v1.ProviderReadinessReason 80, // 94: openshell.v1.ProviderReadinessStatus.receipt:type_name -> openshell.v1.ProviderMutationReceipt 3, // 95: openshell.v1.ProviderReadinessStatus.state:type_name -> openshell.v1.ProviderReadinessState 4, // 96: openshell.v1.ProviderReadinessStatus.reason:type_name -> openshell.v1.ProviderReadinessReason 81, // 97: openshell.v1.ProviderReadinessStatus.observed:type_name -> openshell.v1.ProviderReadinessObservation - 289, // 98: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp - 289, // 99: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp + 290, // 98: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp + 290, // 99: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp 79, // 100: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation - 294, // 101: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 101: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 82, // 102: openshell.v1.GetSandboxProviderStatusResponse.status:type_name -> openshell.v1.ProviderReadinessStatus 81, // 103: openshell.v1.ReportProviderReadinessRequest.observation:type_name -> openshell.v1.ProviderReadinessObservation - 293, // 104: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration - 293, // 105: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration + 294, // 104: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration + 294, // 105: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration 18, // 106: openshell.v1.DeleteSandboxResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 294, // 107: openshell.v1.CreateSshSessionRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 289, // 108: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp - 294, // 109: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 107: openshell.v1.CreateSshSessionRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 290, // 108: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp + 295, // 109: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 20, // 110: openshell.v1.ExposeServiceRequest.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode - 294, // 111: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 112: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 111: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 112: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 97, // 113: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 294, // 114: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 114: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 18, // 115: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 290, // 116: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 291, // 116: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 20, // 117: openshell.v1.ServiceEndpoint.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode 96, // 118: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint 18, // 119: openshell.v1.RevokeSshSessionResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 294, // 120: openshell.v1.ExecSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 273, // 121: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 293, // 122: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration + 295, // 120: openshell.v1.ExecSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 274, // 121: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 294, // 122: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration 101, // 123: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout 102, // 124: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr 103, // 125: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit @@ -20937,148 +21000,148 @@ var file_openshell_proto_depIdxs = []int32{ 105, // 128: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit 100, // 129: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest 108, // 130: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 290, // 131: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 289, // 132: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp - 294, // 133: openshell.v1.WatchSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 289, // 134: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp + 291, // 131: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 290, // 132: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp + 295, // 133: openshell.v1.WatchSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 290, // 134: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp 38, // 135: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox 112, // 136: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine 52, // 137: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent 113, // 138: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 221, // 139: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 289, // 140: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp - 274, // 141: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 294, // 142: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 295, // 143: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 294, // 144: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 145: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 146: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 295, // 147: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 275, // 148: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry - 294, // 149: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 295, // 150: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 222, // 139: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 290, // 140: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp + 275, // 141: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 295, // 142: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 296, // 143: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 295, // 144: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 145: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 146: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 296, // 147: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 276, // 148: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + 295, // 149: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 296, // 150: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider 80, // 151: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt - 295, // 152: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 294, // 153: openshell.v1.ListProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 154: openshell.v1.GetProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 296, // 152: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 295, // 153: openshell.v1.ListProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 154: openshell.v1.GetProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 142, // 155: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 293, // 156: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration + 294, // 156: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration 125, // 157: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride 8, // 158: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType 126, // 159: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken 131, // 160: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh 127, // 161: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant 9, // 162: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 293, // 163: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration - 293, // 164: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration + 294, // 163: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration + 294, // 164: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration 129, // 165: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial 130, // 166: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput 9, // 167: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 289, // 168: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp - 289, // 169: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp - 289, // 170: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp + 290, // 168: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp + 290, // 169: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp + 290, // 170: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp 16, // 171: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 289, // 172: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp - 294, // 173: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 290, // 172: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp + 295, // 173: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 132, // 174: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 294, // 175: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 175: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 9, // 176: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 276, // 177: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 289, // 178: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp + 277, // 177: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 290, // 178: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp 132, // 179: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 294, // 180: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 180: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 132, // 181: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 294, // 182: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 182: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 18, // 183: openshell.v1.DeleteProviderRefreshResponse.outcome:type_name -> openshell.v1.DeletionOutcome 10, // 184: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory 128, // 185: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 297, // 186: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 298, // 187: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 298, // 186: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 299, // 187: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary 133, // 188: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 277, // 189: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 278, // 189: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry 143, // 190: openshell.v1.ProviderProfile.files:type_name -> openshell.v1.ProviderProfileFile 142, // 191: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile 142, // 192: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 294, // 193: openshell.v1.ImportProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 193: openshell.v1.ImportProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 123, // 194: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem 124, // 195: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic 142, // 196: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 294, // 197: openshell.v1.UpdateProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 197: openshell.v1.UpdateProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 123, // 198: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem 124, // 199: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic 142, // 200: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 294, // 201: openshell.v1.LintProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 201: openshell.v1.LintProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 123, // 202: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem 124, // 203: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic 18, // 204: openshell.v1.DeleteProviderResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 294, // 205: openshell.v1.DeleteProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 205: openshell.v1.DeleteProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 18, // 206: openshell.v1.DeleteProviderProfileResponse.outcome:type_name -> openshell.v1.DeletionOutcome 156, // 207: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 278, // 208: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 279, // 209: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry - 280, // 210: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 281, // 211: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 279, // 208: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 280, // 209: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + 281, // 210: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 282, // 211: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry 4, // 212: openshell.v1.GetSandboxProviderEnvironmentResponse.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason - 282, // 213: openshell.v1.GetSandboxProviderEnvironmentResponse.files:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntry + 283, // 213: openshell.v1.GetSandboxProviderEnvironmentResponse.files:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntry 11, // 214: openshell.v1.ProviderEnvironmentValue.classification:type_name -> openshell.v1.ProviderEnvironmentValueClassification 157, // 215: openshell.v1.ProviderEnvironmentValue.static_credential_binding:type_name -> openshell.v1.StaticCredentialBinding - 289, // 216: openshell.v1.ProviderEnvironmentValue.expiration_time:type_name -> google.protobuf.Timestamp + 290, // 216: openshell.v1.ProviderEnvironmentValue.expiration_time:type_name -> google.protobuf.Timestamp 159, // 217: openshell.v1.ProviderEnvironmentSnapshot.values:type_name -> openshell.v1.ProviderEnvironmentValue - 283, // 218: openshell.v1.ProviderEnvironmentSnapshot.dynamic_credentials:type_name -> openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry + 284, // 218: openshell.v1.ProviderEnvironmentSnapshot.dynamic_credentials:type_name -> openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry 4, // 219: openshell.v1.ProviderEnvironmentSnapshot.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason - 284, // 220: openshell.v1.ProviderEnvironmentSnapshot.files:type_name -> openshell.v1.ProviderEnvironmentSnapshot.FilesEntry - 293, // 221: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration - 294, // 222: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 291, // 223: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 299, // 224: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 285, // 220: openshell.v1.ProviderEnvironmentSnapshot.files:type_name -> openshell.v1.ProviderEnvironmentSnapshot.FilesEntry + 294, // 221: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration + 295, // 222: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 292, // 223: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 300, // 224: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue 164, // 225: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 285, // 226: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 286, // 226: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry 165, // 227: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule 166, // 228: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint 167, // 229: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule 169, // 230: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules 170, // 231: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules 171, // 232: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 300, // 233: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 298, // 234: openshell.v1.L7RuleTarget.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 301, // 235: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 301, // 233: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 299, // 234: openshell.v1.L7RuleTarget.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 302, // 235: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule 168, // 236: openshell.v1.AddDenyRules.target:type_name -> openshell.v1.L7RuleTarget - 302, // 237: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 303, // 237: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule 168, // 238: openshell.v1.AddAllowRules.target:type_name -> openshell.v1.L7RuleTarget - 286, // 239: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 294, // 240: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 287, // 239: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 295, // 240: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 182, // 241: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 294, // 242: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 242: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 182, // 243: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision 13, // 244: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus 12, // 245: openshell.v1.SandboxConfigurationAdmission.state:type_name -> openshell.v1.ConfigurationAdmissionState 179, // 246: openshell.v1.ReportSandboxConfigurationRequest.admission:type_name -> openshell.v1.SandboxConfigurationAdmission 13, // 247: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 289, // 248: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp - 289, // 249: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp - 291, // 250: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 287, // 251: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 294, // 252: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 289, // 253: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp + 290, // 248: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp + 290, // 249: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp + 292, // 250: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 288, // 251: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 295, // 252: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 290, // 253: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp 112, // 254: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine 112, // 255: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine 189, // 256: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello 198, // 257: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 214, // 258: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 215, // 259: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 215, // 258: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 216, // 259: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose 195, // 260: openshell.v1.SupervisorMessage.config_update_result:type_name -> openshell.v1.ConfigUpdateResult 196, // 261: openshell.v1.SupervisorMessage.config_bootstrap_result:type_name -> openshell.v1.ConfigBootstrapResult 190, // 262: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted 197, // 263: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected 199, // 264: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat 204, // 265: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 215, // 266: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 216, // 266: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose 192, // 267: openshell.v1.GatewayMessage.config_update:type_name -> openshell.v1.ConfigUpdate 191, // 268: openshell.v1.SessionAccepted.bootstrap:type_name -> openshell.v1.ConfigBootstrap - 293, // 269: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration - 303, // 270: openshell.v1.ConfigBootstrap.sandbox_config:type_name -> openshell.sandbox.v1.SandboxConfigSnapshot + 294, // 269: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration + 304, // 270: openshell.v1.ConfigBootstrap.sandbox_config:type_name -> openshell.sandbox.v1.SandboxConfigSnapshot 160, // 271: openshell.v1.ConfigBootstrap.provider_environment:type_name -> openshell.v1.ProviderEnvironmentSnapshot - 303, // 272: openshell.v1.ConfigUpdate.sandbox_config:type_name -> openshell.sandbox.v1.SandboxConfigSnapshot + 304, // 272: openshell.v1.ConfigUpdate.sandbox_config:type_name -> openshell.sandbox.v1.SandboxConfigSnapshot 160, // 273: openshell.v1.ConfigUpdate.provider_environment:type_name -> openshell.v1.ProviderEnvironmentSnapshot 5, // 274: openshell.v1.ConfigComponentApplyResult.component:type_name -> openshell.v1.ConfigComponent 77, // 275: openshell.v1.ConfigComponentApplyResult.requested_revision:type_name -> openshell.v1.ConfigSnapshotRevision @@ -21093,238 +21156,239 @@ var file_openshell_proto_depIdxs = []int32{ 204, // 284: openshell.v1.PeerRelayInit.relay_open:type_name -> openshell.v1.RelayOpen 209, // 285: openshell.v1.PeerRelayFrame.init:type_name -> openshell.v1.PeerRelayInit 211, // 286: openshell.v1.PeerConfigUpdateHintRequest.sandbox:type_name -> openshell.v1.PeerConfigSandboxTarget - 289, // 287: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp - 289, // 288: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp - 216, // 289: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 218, // 290: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 300, // 291: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 289, // 292: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp - 289, // 293: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp - 289, // 294: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp - 289, // 295: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp - 291, // 296: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 291, // 297: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 294, // 298: openshell.v1.SubmitPolicyAnalysisRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 217, // 299: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 220, // 300: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 219, // 301: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 294, // 302: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 220, // 303: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 289, // 304: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp - 294, // 305: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 306: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 307: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 230, // 308: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 294, // 309: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 300, // 310: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 294, // 311: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 312: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 294, // 313: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 289, // 314: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp - 240, // 315: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 288, // 316: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 304, // 317: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 304, // 318: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 304, // 319: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 18, // 320: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 290, // 321: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 15, // 322: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 294, // 323: openshell.v1.AddWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 15, // 324: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 250, // 325: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 294, // 326: openshell.v1.RemoveWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 18, // 327: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 294, // 328: openshell.v1.ListWorkspaceMembersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 250, // 329: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 289, // 330: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp - 19, // 331: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult - 258, // 332: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation - 19, // 333: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult - 289, // 334: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp - 289, // 335: openshell.v1.SandboxProvisioning.configuration_change_time:type_name -> google.protobuf.Timestamp - 289, // 336: openshell.v1.SandboxProvisioning.first_rejection_time:type_name -> google.protobuf.Timestamp - 289, // 337: openshell.v1.SandboxProvisioning.deadline:type_name -> google.protobuf.Timestamp - 289, // 338: openshell.v1.SandboxProvisioning.timeout_time:type_name -> google.protobuf.Timestamp - 289, // 339: openshell.v1.SandboxProvisioning.cleanup_completed_time:type_name -> google.protobuf.Timestamp - 289, // 340: openshell.v1.SandboxProvisioning.cleanup_retry_time:type_name -> google.protobuf.Timestamp - 289, // 341: openshell.v1.SandboxProvisioning.attachment_change_time:type_name -> google.protobuf.Timestamp - 20, // 342: openshell.v1.SandboxServiceExposure.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode - 289, // 343: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 289, // 344: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 128, // 345: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 157, // 346: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 128, // 347: openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 25, // 348: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 27, // 349: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 29, // 350: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 53, // 351: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 61, // 352: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest - 63, // 353: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 64, // 354: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 54, // 355: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest - 55, // 356: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest - 56, // 357: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest - 57, // 358: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest - 65, // 359: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 66, // 360: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 67, // 361: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 83, // 362: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest - 68, // 363: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 69, // 364: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 70, // 365: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 88, // 366: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 90, // 367: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 91, // 368: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 92, // 369: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 94, // 370: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 98, // 371: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 100, // 372: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 106, // 373: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 107, // 374: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 114, // 375: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 115, // 376: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 116, // 377: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 121, // 378: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 122, // 379: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 146, // 380: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 148, // 381: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 150, // 382: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 117, // 383: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 134, // 384: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 136, // 385: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 138, // 386: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 140, // 387: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 118, // 388: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 153, // 389: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 305, // 390: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 306, // 391: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 163, // 392: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 173, // 393: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 175, // 394: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 177, // 395: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 259, // 396: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest - 85, // 397: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest - 180, // 398: openshell.v1.OpenShell.ReportSandboxConfiguration:input_type -> openshell.v1.ReportSandboxConfigurationRequest - 155, // 399: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 161, // 400: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 183, // 401: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 184, // 402: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 187, // 403: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 200, // 404: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 202, // 405: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest - 208, // 406: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 210, // 407: openshell.v1.OpenShell.PeerRelay:input_type -> openshell.v1.PeerRelayFrame - 85, // 408: openshell.v1.OpenShell.PeerReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest - 259, // 409: openshell.v1.OpenShell.PeerReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest - 83, // 410: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest - 212, // 411: openshell.v1.OpenShell.PeerNotifyConfigUpdate:input_type -> openshell.v1.PeerConfigUpdateHintRequest - 110, // 412: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 222, // 413: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 224, // 414: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 226, // 415: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 228, // 416: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 231, // 417: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 233, // 418: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 235, // 419: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 237, // 420: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 239, // 421: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 21, // 422: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 23, // 423: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 242, // 424: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 244, // 425: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 246, // 426: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 248, // 427: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 251, // 428: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 253, // 429: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 255, // 430: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 26, // 431: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 28, // 432: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 30, // 433: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 71, // 434: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 62, // 435: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse - 71, // 436: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 72, // 437: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 58, // 438: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 58, // 439: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 59, // 440: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse - 60, // 441: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse - 73, // 442: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 74, // 443: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 75, // 444: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 84, // 445: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse - 87, // 446: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 71, // 447: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 71, // 448: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 89, // 449: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 97, // 450: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 97, // 451: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 93, // 452: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 95, // 453: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 99, // 454: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 104, // 455: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 106, // 456: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 104, // 457: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 119, // 458: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 119, // 459: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 120, // 460: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 145, // 461: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 144, // 462: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 147, // 463: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 149, // 464: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 151, // 465: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 119, // 466: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 135, // 467: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 137, // 468: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 139, // 469: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 141, // 470: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 152, // 471: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 154, // 472: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 307, // 473: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 308, // 474: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 172, // 475: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 174, // 476: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 176, // 477: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 178, // 478: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 260, // 479: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse - 86, // 480: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse - 181, // 481: openshell.v1.OpenShell.ReportSandboxConfiguration:output_type -> openshell.v1.ReportSandboxConfigurationResponse - 158, // 482: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 162, // 483: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 186, // 484: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 185, // 485: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 188, // 486: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 201, // 487: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 203, // 488: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse - 208, // 489: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 210, // 490: openshell.v1.OpenShell.PeerRelay:output_type -> openshell.v1.PeerRelayFrame - 86, // 491: openshell.v1.OpenShell.PeerReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse - 260, // 492: openshell.v1.OpenShell.PeerReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse - 84, // 493: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse - 213, // 494: openshell.v1.OpenShell.PeerNotifyConfigUpdate:output_type -> openshell.v1.PeerConfigUpdateHintResponse - 111, // 495: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 223, // 496: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 225, // 497: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 227, // 498: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 229, // 499: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 232, // 500: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 234, // 501: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 236, // 502: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 238, // 503: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 241, // 504: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 22, // 505: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 24, // 506: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 243, // 507: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 245, // 508: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 247, // 509: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 249, // 510: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 252, // 511: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 254, // 512: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 256, // 513: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 431, // [431:514] is the sub-list for method output_type - 348, // [348:431] is the sub-list for method input_type - 348, // [348:348] is the sub-list for extension type_name - 348, // [348:348] is the sub-list for extension extendee - 0, // [0:348] is the sub-list for field type_name + 212, // 287: openshell.v1.PeerConfigUpdateHintRequest.provider:type_name -> openshell.v1.PeerConfigProviderTarget + 290, // 288: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp + 290, // 289: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp + 217, // 290: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 219, // 291: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 301, // 292: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 290, // 293: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp + 290, // 294: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp + 290, // 295: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp + 290, // 296: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp + 292, // 297: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 292, // 298: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 295, // 299: openshell.v1.SubmitPolicyAnalysisRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 218, // 300: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 221, // 301: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 220, // 302: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 295, // 303: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 221, // 304: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 290, // 305: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp + 295, // 306: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 307: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 308: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 231, // 309: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 295, // 310: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 301, // 311: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 295, // 312: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 313: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 295, // 314: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 290, // 315: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp + 241, // 316: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 289, // 317: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 305, // 318: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 305, // 319: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 305, // 320: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 18, // 321: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 291, // 322: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 15, // 323: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 295, // 324: openshell.v1.AddWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 15, // 325: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 251, // 326: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 295, // 327: openshell.v1.RemoveWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 18, // 328: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 295, // 329: openshell.v1.ListWorkspaceMembersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 251, // 330: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 290, // 331: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp + 19, // 332: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult + 259, // 333: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation + 19, // 334: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult + 290, // 335: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp + 290, // 336: openshell.v1.SandboxProvisioning.configuration_change_time:type_name -> google.protobuf.Timestamp + 290, // 337: openshell.v1.SandboxProvisioning.first_rejection_time:type_name -> google.protobuf.Timestamp + 290, // 338: openshell.v1.SandboxProvisioning.deadline:type_name -> google.protobuf.Timestamp + 290, // 339: openshell.v1.SandboxProvisioning.timeout_time:type_name -> google.protobuf.Timestamp + 290, // 340: openshell.v1.SandboxProvisioning.cleanup_completed_time:type_name -> google.protobuf.Timestamp + 290, // 341: openshell.v1.SandboxProvisioning.cleanup_retry_time:type_name -> google.protobuf.Timestamp + 290, // 342: openshell.v1.SandboxProvisioning.attachment_change_time:type_name -> google.protobuf.Timestamp + 20, // 343: openshell.v1.SandboxServiceExposure.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode + 290, // 344: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 290, // 345: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 128, // 346: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 157, // 347: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 128, // 348: openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 25, // 349: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 27, // 350: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 29, // 351: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 53, // 352: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 61, // 353: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest + 63, // 354: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 64, // 355: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 54, // 356: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest + 55, // 357: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest + 56, // 358: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest + 57, // 359: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest + 65, // 360: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 66, // 361: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 67, // 362: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 83, // 363: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest + 68, // 364: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 69, // 365: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 70, // 366: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 88, // 367: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 90, // 368: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 91, // 369: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 92, // 370: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 94, // 371: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 98, // 372: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 100, // 373: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 106, // 374: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 107, // 375: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 114, // 376: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 115, // 377: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 116, // 378: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 121, // 379: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 122, // 380: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 146, // 381: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 148, // 382: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 150, // 383: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 117, // 384: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 134, // 385: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 136, // 386: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 138, // 387: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 140, // 388: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 118, // 389: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 153, // 390: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 306, // 391: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 307, // 392: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 163, // 393: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 173, // 394: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 175, // 395: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 177, // 396: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 260, // 397: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest + 85, // 398: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest + 180, // 399: openshell.v1.OpenShell.ReportSandboxConfiguration:input_type -> openshell.v1.ReportSandboxConfigurationRequest + 155, // 400: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 161, // 401: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 183, // 402: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 184, // 403: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 187, // 404: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 200, // 405: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 202, // 406: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest + 208, // 407: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 210, // 408: openshell.v1.OpenShell.PeerRelay:input_type -> openshell.v1.PeerRelayFrame + 85, // 409: openshell.v1.OpenShell.PeerReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest + 260, // 410: openshell.v1.OpenShell.PeerReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest + 83, // 411: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest + 213, // 412: openshell.v1.OpenShell.PeerNotifyConfigUpdate:input_type -> openshell.v1.PeerConfigUpdateHintRequest + 110, // 413: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 223, // 414: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 225, // 415: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 227, // 416: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 229, // 417: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 232, // 418: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 234, // 419: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 236, // 420: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 238, // 421: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 240, // 422: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 21, // 423: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 23, // 424: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 243, // 425: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 245, // 426: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 247, // 427: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 249, // 428: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 252, // 429: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 254, // 430: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 256, // 431: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 26, // 432: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 28, // 433: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 30, // 434: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 71, // 435: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 62, // 436: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse + 71, // 437: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 72, // 438: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 58, // 439: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 58, // 440: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 59, // 441: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse + 60, // 442: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse + 73, // 443: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 74, // 444: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 75, // 445: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 84, // 446: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse + 87, // 447: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 71, // 448: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 71, // 449: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 89, // 450: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 97, // 451: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 97, // 452: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 93, // 453: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 95, // 454: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 99, // 455: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 104, // 456: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 106, // 457: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 104, // 458: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 119, // 459: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 119, // 460: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 120, // 461: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 145, // 462: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 144, // 463: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 147, // 464: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 149, // 465: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 151, // 466: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 119, // 467: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 135, // 468: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 137, // 469: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 139, // 470: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 141, // 471: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 152, // 472: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 154, // 473: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 308, // 474: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 309, // 475: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 172, // 476: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 174, // 477: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 176, // 478: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 178, // 479: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 261, // 480: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse + 86, // 481: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse + 181, // 482: openshell.v1.OpenShell.ReportSandboxConfiguration:output_type -> openshell.v1.ReportSandboxConfigurationResponse + 158, // 483: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 162, // 484: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 186, // 485: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 185, // 486: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 188, // 487: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 201, // 488: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 203, // 489: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse + 208, // 490: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 210, // 491: openshell.v1.OpenShell.PeerRelay:output_type -> openshell.v1.PeerRelayFrame + 86, // 492: openshell.v1.OpenShell.PeerReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse + 261, // 493: openshell.v1.OpenShell.PeerReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse + 84, // 494: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse + 214, // 495: openshell.v1.OpenShell.PeerNotifyConfigUpdate:output_type -> openshell.v1.PeerConfigUpdateHintResponse + 111, // 496: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 224, // 497: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 226, // 498: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 228, // 499: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 230, // 500: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 233, // 501: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 235, // 502: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 237, // 503: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 239, // 504: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 242, // 505: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 22, // 506: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 24, // 507: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 244, // 508: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 246, // 509: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 248, // 510: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 250, // 511: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 253, // 512: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 255, // 513: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 257, // 514: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 432, // [432:515] is the sub-list for method output_type + 349, // [349:432] is the sub-list for method input_type + 349, // [349:349] is the sub-list for extension type_name + 349, // [349:349] is the sub-list for extension extendee + 0, // [0:349] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -21406,10 +21470,11 @@ func file_openshell_proto_init() { (*PeerRelayFrame_Init)(nil), (*PeerRelayFrame_Data)(nil), } - file_openshell_proto_msgTypes[191].OneofWrappers = []any{ + file_openshell_proto_msgTypes[192].OneofWrappers = []any{ (*PeerConfigUpdateHintRequest_Sandbox)(nil), (*PeerConfigUpdateHintRequest_Workspace)(nil), (*PeerConfigUpdateHintRequest_AllConnected)(nil), + (*PeerConfigUpdateHintRequest_Provider)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -21417,7 +21482,7 @@ func file_openshell_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_openshell_proto_rawDesc), len(file_openshell_proto_rawDesc)), NumEnums: 21, - NumMessages: 268, + NumMessages: 269, NumExtensions: 0, NumServices: 1, },