diff --git a/.agents/skills/helm-dev-environment/SKILL.md b/.agents/skills/helm-dev-environment/SKILL.md index 47df909f32..df8189ad71 100644 --- a/.agents/skills/helm-dev-environment/SKILL.md +++ b/.agents/skills/helm-dev-environment/SKILL.md @@ -421,6 +421,30 @@ above) instead of relying only on `OPENSHELL_GATEWAY_ENDPOINT`; some tests call ### Kubernetes E2E Notes Use `mise run e2e:kubernetes` for the standard Helm-backed Kubernetes suite. +Set `OPENSHELL_E2E_KUBE_TEST=kubernetes_image_update` to scope the Rust suite to +combined image, command, environment, and metadata updates with retained sandbox, +SSH, and PVC identities. The next start verifies the new argv and environment. This target is +included in both existing Agent Sandbox API-version lanes; unlike the Python +Docker suite, it exercises the driver that supports image updates. + +Image updates default to disabled outside test fixtures. The e2e wrapper opts +in only for a fresh gateway fixture that it owns; set +`OPENSHELL_E2E_KUBE_IMAGE_UPDATES=0` to prevent that opt-in. Do not propagate +activation to an existing shared gateway or enable it during a mixed-version +rollout. For manual testing, follow the retirement and activation checklist in +`docs/kubernetes/high-availability.mdx` before setting +`gatewayConfig: {"openshell.gateway": {enable_sandbox_updates: true}}`. +The gateway process equivalents are `--enable-sandbox-updates true` and +`OPENSHELL_ENABLE_SANDBOX_UPDATES=true`. Retire every old process and Job, +settle outstanding driver work, prevent old binaries returning, and do not +treat PostgreSQL connection markers or membership TTL expiry as retirement +proof. SQLite testing supports one gateway process only. Image-update clients +must call the separate `SandboxUpdates` service on the existing gateway +listener, not add a method to the baseline `OpenShell` service or inspect new +public `SandboxStatus` image fields. Resume a pending operation with +`SandboxUpdates.UpdateSandbox`, the current resource version, and no selected +fields, following the documented request-ID semantics. + The kube e2e wrapper creates only one port-forward, to `svc/openshell`; it no longer forwards the unauthenticated health listener or runs a `/readyz` e2e target. `/readyz` remains covered by server unit/integration tests. @@ -611,3 +635,13 @@ for dependencies still declared in `Chart.yaml`. | `deploy/kube/manifests/envoy-gateway-openshell.yaml` | GatewayClass and BackendTrafficPolicy for Envoy Gateway (`mise run helm:gateway:apply`) | | `tasks/scripts/helm-k3s-local.sh` | k3d cluster create/delete/start/stop/status | | `tasks/scripts/keycloak-k8s-setup.sh` | Keycloak deploy, realm import, and development TLS trust anchor | + +## Sandbox update regression lane + +Run `mise run e2e:kubernetes:updates` for the testkit update suite against both +supported Agent Sandbox APIs with the combined Helm chart and its bundled +workspace resources. It verifies stopped-only updates, retained CR/PVC +identity and contents, command/environment changes, and request replay. +`OPENSHELL_E2E_UPDATE_ORIGINAL_IMAGE` and `OPENSHELL_E2E_UPDATE_TARGET_IMAGE` can +select mirrored Alpine 3.21 and 3.22 manifests. Defaults use the public registry +cache; keep immutable digests when overriding them. diff --git a/Cargo.lock b/Cargo.lock index 781481d94e..073efa4e1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4547,6 +4547,7 @@ dependencies = [ "prost", "prost-types", "rcgen", + "regex", "rustls", "serde", "serde_json", diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index ba31f35d87..eb224685ed 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -4116,6 +4116,14 @@ pub async fn sandbox_stop( .into_inner() .sandbox .ok_or_else(|| miette!("gateway returned no sandbox after stop"))?; + // A successful stop can preserve a canonical result when the transient + // compute resource is already gone. There is no Stopped event to wait for. + if sandbox.phase() == SandboxPhase::Completed as i32 + || (sandbox.phase() == SandboxPhase::Error as i32 && has_main_process_result(&sandbox)) + { + println!("{} Sandbox {name} already finished", "✓".green().bold()); + return Ok(()); + } wait_for_lifecycle_phase(&mut client, sandbox, SandboxPhase::Stopped).await?; println!("{} Stopped sandbox {name}", "✓".green().bold()); Ok(()) diff --git a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs index db9c02ffaf..587910fbaf 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -67,6 +67,7 @@ struct SandboxState { create_requests: Arc>>, expose_service_requests: Arc>>, fail_delete_sandbox_message: Arc>>, + stop_response: Arc>>, vm_error_after_started: Arc, vm_error_with_observed_exit: Arc, vm_slow_progress_before_ready: Arc, @@ -231,7 +232,17 @@ impl OpenShell for TestOpenShell { &self, _request: tonic::Request, ) -> Result, Status> { - Err(Status::unimplemented("unused")) + let sandbox = self + .state + .stop_response + .lock() + .await + .clone() + .ok_or_else(|| Status::unimplemented("unused"))?; + Ok(Response::new(SandboxResponse { + sandbox: Some(sandbox), + ..Default::default() + })) } async fn start_sandbox( @@ -1709,6 +1720,57 @@ fn test_config() -> run::SandboxCreateConfig<'static> { } } +#[tokio::test] +async fn sandbox_stop_accepts_terminal_results_without_waiting_for_stopped() { + let server = run_server().await; + let tls = test_tls(&server); + let config_dir = helpers::tempdir().unwrap(); + for (phase, exit_code, reason, succeeds) in [ + ( + SandboxPhase::Completed, + Some(0), + "MainProcessCompleted", + true, + ), + (SandboxPhase::Error, Some(17), "MainProcessFailed", true), + (SandboxPhase::Stopped, None, "Stopped", true), + ( + SandboxPhase::Error, + Some(17), + "ComputeResourceMissing", + false, + ), + ] { + let sandbox = Sandbox { + status: Some(SandboxStatus { + phase: phase as i32, + exit_code, + conditions: vec![SandboxCondition { + r#type: "Ready".into(), + status: "False".into(), + reason: reason.into(), + ..Default::default() + }], + ..Default::default() + }), + ..Default::default() + }; + *server.openshell.state.stop_response.lock().await = Some(sandbox); + Environment::new() + .set("XDG_CONFIG_HOME", config_dir.path()) + .run_async(async { + let result = tokio::time::timeout( + Duration::from_secs(5), + run::sandbox_stop(&server.endpoint, "terminal-stop-test", "default", &tls), + ) + .await + .expect("stop must finish without watching for another phase"); + assert_eq!(result.is_ok(), succeeds, "{reason}: {result:?}"); + }) + .await; + } +} + #[tokio::test] async fn sandbox_delete_continues_after_entry_failure() { let server = run_server().await; diff --git a/crates/openshell-core/README.md b/crates/openshell-core/README.md index 1cf1b7e70e..d1ce03d653 100644 --- a/crates/openshell-core/README.md +++ b/crates/openshell-core/README.md @@ -65,6 +65,20 @@ configuration validation live in `openshell_core::middleware`. Policy admission and the supervisor runtime consume the same contract without introducing a dependency from the policy crate to the supervisor implementation. +## Optional Compute Updates + +The generated `proto::compute::v1::compute_driver_server::ComputeDriver` trait +retains its baseline methods and streaming associated type. Supporting external +drivers additionally serve `ComputeDriverConfigurationUpdates` on the same +endpoint and advertise `openshell.compute.update-sandbox-configuration`. +The request carries image and lifecycle preconditions plus optional command and +environment replacements. Drivers without the capability need no new method. + +Linked drivers opt in through `openshell_core::compute::ComputeDriver` and +`ComputeDriverFactory::build_with_configuration_updates`. The default factory +hook calls `build` without an updater. Registration and capability negotiation +are both required. Metadata-only updates are handled by the gateway. + ## Protobuf Struct Conversion Use `openshell_core::proto_struct` when crossing between `serde_json` values and diff --git a/crates/openshell-core/src/compute.rs b/crates/openshell-core/src/compute.rs new file mode 100644 index 0000000000..3e2a9118cf --- /dev/null +++ b/crates/openshell-core/src/compute.rs @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Opt-in in-process sandbox updates, separate from the baseline driver contract. + +use crate::proto::compute::v1::compute_driver_server; + +/// Optional in-process configuration updater, registered separately by a driver factory. +/// +/// Existing drivers need only the generated baseline trait, not this extension. +/// Supporting factories opt in through `ComputeDriverFactory::build_with_configuration_updates` +/// and advertise the configuration-update capability. External servers use the +/// separate generated `ComputeDriverConfigurationUpdates` service on the same endpoint. +#[tonic::async_trait] +pub trait ComputeDriver: compute_driver_server::ComputeDriver { + async fn update_sandbox_configuration( + &self, + _request: tonic::Request, + ) -> Result< + tonic::Response, + tonic::Status, + > { + Err(tonic::Status::unimplemented( + "sandbox configuration updates are not supported by this driver", + )) + } +} diff --git a/crates/openshell-core/src/config.rs b/crates/openshell-core/src/config.rs index 27e5b80dd9..308b9edb61 100644 --- a/crates/openshell-core/src/config.rs +++ b/crates/openshell-core/src/config.rs @@ -252,6 +252,11 @@ pub struct Config { /// Disabled for local gateways by default. pub enable_websocket_tunnel: bool, + /// Operator activation after all gateways and outstanding legacy driver + /// operations have been drained onto the configuration-update-compatible protocol. + /// Disabled by default; not a license to run mixed-version writers. + pub enable_sandbox_updates: bool, + /// Disabled-by-default gateway interceptor service configs. pub gateway_interceptors: Vec, @@ -999,6 +1004,7 @@ impl Config { oidc: None, auth: GatewayAuthConfig::default(), enable_websocket_tunnel: false, + enable_sandbox_updates: false, gateway_interceptors: Vec::new(), provider_profile_sources: vec![GatewayProviderProfileSourceConfig::User], mtls_auth: MtlsAuthConfig::default(), diff --git a/crates/openshell-core/src/extension_protocol.rs b/crates/openshell-core/src/extension_protocol.rs index 0776cd0f2c..d6d93418d8 100644 --- a/crates/openshell-core/src/extension_protocol.rs +++ b/crates/openshell-core/src/extension_protocol.rs @@ -25,6 +25,10 @@ pub const COMPUTE_ISOLATION_BACKEND: &str = "openshell.compute.isolation-backend /// Driver implements `SuspendSandbox` and resuming through `StartSandbox`. pub const COMPUTE_SUSPEND_RESUME: &str = "openshell.compute.suspend-resume"; +/// Optional stopped-state workload configuration updates retaining the resource and storage. +pub const COMPUTE_UPDATE_SANDBOX_CONFIGURATION: &str = + "openshell.compute.update-sandbox-configuration"; + const MAX_IMPLEMENTATION_NAME_BYTES: usize = 128; const MAX_IMPLEMENTATION_VERSION_BYTES: usize = 128; const MAX_CAPABILITY_BYTES: usize = 128; @@ -128,6 +132,7 @@ pub fn gateway_metadata(family: ExtensionFamily) -> PeerMetadata { if family == ExtensionFamily::Compute { supported_capabilities.push(COMPUTE_LAUNCH_AUTHENTICATION.to_string()); supported_capabilities.push(COMPUTE_ISOLATION_BACKEND.to_string()); + supported_capabilities.push(COMPUTE_UPDATE_SANDBOX_CONFIGURATION.to_string()); } PeerMetadata { protocol_version: Some(ProtocolVersion { @@ -399,6 +404,23 @@ fn valid_capability(value: &str) -> bool { mod tests { use super::*; + #[test] + fn compute_gateway_advertises_both_optional_extensions() { + let gateway = gateway_metadata(ExtensionFamily::Compute); + assert!( + gateway + .supported_capabilities + .iter() + .any(|c| c == COMPUTE_ISOLATION_BACKEND) + ); + assert!( + gateway + .supported_capabilities + .iter() + .any(|c| c == COMPUTE_UPDATE_SANDBOX_CONFIGURATION) + ); + } + fn compatible() -> (PeerMetadata, PeerMetadata) { let gateway = gateway_metadata(ExtensionFamily::Compute); let extension = extension_metadata( diff --git a/crates/openshell-core/src/lib.rs b/crates/openshell-core/src/lib.rs index 0588a9974d..f16eb7c130 100644 --- a/crates/openshell-core/src/lib.rs +++ b/crates/openshell-core/src/lib.rs @@ -11,6 +11,7 @@ pub mod activity; pub mod auth; +pub mod compute; pub mod config; pub mod container_paths; pub mod denial; @@ -58,6 +59,7 @@ pub mod rpc_error; pub mod sandbox_env; pub mod sandbox_generation; pub mod sandbox_session; +pub mod sandbox_update; pub mod secrets; pub mod settings; pub mod shell; diff --git a/crates/openshell-core/src/sandbox_update.rs b/crates/openshell-core/src/sandbox_update.rs new file mode 100644 index 0000000000..2b9557c829 --- /dev/null +++ b/crates/openshell-core/src/sandbox_update.rs @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Shared projections for selected stopped-sandbox configuration changes. +use crate::proto::{Sandbox, SandboxCommandUpdate, SandboxMapUpdate}; +use std::collections::HashMap; + +/// Gateway-owned identity and lifecycle metadata is never caller-replaceable. +/// Other keys, including user keys under `OpenShell` domains, remain editable. +#[must_use] +pub fn is_system_metadata_key(key: &str) -> bool { + key.starts_with("internal.openshell.ai/") + || matches!( + key, + "openshell.nvidia.com/retention" + | "openshell.ai/managed-by" + | "openshell.ai/sandbox-id" + | "openshell.ai/sandbox-name" + | "openshell.ai/sandbox-namespace" + | "openshell.ai/sandbox-workspace" + | "openshell.ai/gateway-id" + | "openshell.ai/caller-driver-config-used" + | "openshell.ai/resource-admission-identities" + ) +} + +fn replace_user_metadata(current: &mut HashMap, update: &SandboxMapUpdate) { + current.retain(|key, _| is_system_metadata_key(key)); + current.extend(update.values.clone()); +} + +/// Caller must validate syntax and forbid system keys before projecting. +pub fn apply_configuration( + sandbox: &mut Sandbox, + command: Option<&SandboxCommandUpdate>, + environment: Option<&SandboxMapUpdate>, + labels: Option<&SandboxMapUpdate>, + annotations: Option<&SandboxMapUpdate>, +) { + if let Some(spec) = sandbox.spec.as_mut() { + if let Some(command) = command { + spec.command.clone_from(&command.argv); + } + if let Some(environment) = environment { + spec.environment.clone_from(&environment.values); + } + } + if let Some(metadata) = sandbox.metadata.as_mut() { + if let Some(labels) = labels { + replace_user_metadata(&mut metadata.labels, labels); + } + if let Some(annotations) = annotations { + replace_user_metadata(&mut metadata.annotations, annotations); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn replacement_preserves_owned_keys_without_freezing_user_domains() { + let mut metadata = HashMap::from([ + ("internal.openshell.ai/auth-epoch".into(), "trusted".into()), + ("openshell.nvidia.com/retention".into(), "ephemeral".into()), + ("openshell.ai/user-note".into(), "old".into()), + ("agents.x-k8s.io/user-note".into(), "old".into()), + ]); + replace_user_metadata(&mut metadata, &SandboxMapUpdate::default()); + assert_eq!(metadata.len(), 2); + assert_eq!(metadata["openshell.nvidia.com/retention"], "ephemeral"); + assert!(!is_system_metadata_key("openshell.ai/user-note")); + assert!(!is_system_metadata_key("agents.x-k8s.io/user-note")); + } +} diff --git a/crates/openshell-core/tests/compute_driver_compat.rs b/crates/openshell-core/tests/compute_driver_compat.rs new file mode 100644 index 0000000000..01baa27a45 --- /dev/null +++ b/crates/openshell-core/tests/compute_driver_compat.rs @@ -0,0 +1,306 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Baseline server source fixture adapted for the upstream suspend RPC. +//! It deliberately implements only the baseline generated `ComputeDriver` methods, +//! including its concrete streaming associated type, with no image-update method +//! or in-process extension trait. New optional RPCs must not break this source. + +use openshell_core::proto::compute::v1::{ + AuthenticateSandboxRequest, AuthenticateSandboxResponse, CreateSandboxRequest, + CreateSandboxResponse, DeleteSandboxRequest, DeleteSandboxResponse, DeleteWorkspaceRequest, + DeleteWorkspaceResponse, EnsureWorkspaceRequest, EnsureWorkspaceResponse, + GetCapabilitiesRequest, GetCapabilitiesResponse, GetSandboxRequest, GetSandboxResponse, + ListSandboxesRequest, ListSandboxesResponse, StartSandboxRequest, StartSandboxResponse, + StopSandboxRequest, StopSandboxResponse, SuspendSandboxRequest, SuspendSandboxResponse, + UpdateSandboxConfigurationRequest, UpdateSandboxConfigurationResponse, + ValidateSandboxCreateRequest, ValidateSandboxCreateResponse, WatchSandboxesEvent, + WatchSandboxesRequest, + compute_driver_client::ComputeDriverClient, + compute_driver_configuration_updates_client::ComputeDriverConfigurationUpdatesClient, + compute_driver_configuration_updates_server::{ + ComputeDriverConfigurationUpdates, ComputeDriverConfigurationUpdatesServer, + }, + compute_driver_server::{ComputeDriver, ComputeDriverServer}, +}; +use prost::Message; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll}; +use tokio::net::{TcpListener, TcpStream}; +use tokio_stream::Stream; +use tonic::{Request, Response, Status}; + +#[derive(Debug)] +struct BaselineDriver; + +#[tonic::async_trait] +impl ComputeDriver for BaselineDriver { + async fn get_capabilities( + &self, + _request: Request, + ) -> Result, Status> { + Ok(Response::new(GetCapabilitiesResponse { + driver_name: "baseline-source-fixture".into(), + ..Default::default() + })) + } + + async fn authenticate_sandbox( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("fixture")) + } + + async fn validate_sandbox_create( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("fixture")) + } + + async fn get_sandbox( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("fixture")) + } + + async fn list_sandboxes( + &self, + _request: Request, + ) -> Result, Status> { + Ok(Response::new(ListSandboxesResponse { sandboxes: vec![] })) + } + + async fn create_sandbox( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("fixture")) + } + + async fn stop_sandbox( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("fixture")) + } + + async fn suspend_sandbox( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("fixture")) + } + + async fn start_sandbox( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("fixture")) + } + + async fn delete_sandbox( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("fixture")) + } + + type WatchSandboxesStream = tokio_stream::Empty>; + + async fn watch_sandboxes( + &self, + _request: Request, + ) -> Result, Status> { + Ok(Response::new(tokio_stream::empty())) + } + + async fn ensure_workspace( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("fixture")) + } + + async fn delete_workspace( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("fixture")) + } +} + +#[derive(Debug, Clone, Default)] +struct ImageUpdates { + calls: Arc>>, +} + +#[tonic::async_trait] +impl ComputeDriverConfigurationUpdates for ImageUpdates { + async fn update_sandbox_configuration( + &self, + request: Request, + ) -> Result, Status> { + self.calls.lock().unwrap().push(request.into_inner()); + Ok(Response::new(UpdateSandboxConfigurationResponse {})) + } +} + +struct TcpIncoming(TcpListener); + +impl Stream for TcpIncoming { + type Item = std::io::Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.0.poll_accept(cx).map(|result| { + Some(result.map(|(stream, _)| { + openshell_core::net::set_tcp_nodelay_best_effort(&stream); + stream + })) + }) + } +} + +async fn roundtrip(serve_image_updates: bool) { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let extension = ImageUpdates::default(); + let optional_service = serve_image_updates + .then(|| ComputeDriverConfigurationUpdatesServer::new(extension.clone())); + let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel(); + let server = tokio::spawn(async move { + tonic::transport::Server::builder() + .add_service(ComputeDriverServer::new(BaselineDriver)) + .add_optional_service(optional_service) + .serve_with_incoming_shutdown(TcpIncoming(listener), async { + let _ = shutdown_rx.await; + }) + .await + .unwrap(); + }); + let channel = tonic::transport::Endpoint::from_shared(format!("http://{address}")) + .unwrap() + .connect() + .await + .unwrap(); + let mut baseline = ComputeDriverClient::new(channel.clone()); + assert_eq!( + baseline + .get_capabilities(GetCapabilitiesRequest::default()) + .await + .unwrap() + .into_inner() + .driver_name, + "baseline-source-fixture" + ); + assert!( + baseline + .list_sandboxes(ListSandboxesRequest {}) + .await + .unwrap() + .into_inner() + .sandboxes + .is_empty() + ); + assert!( + baseline + .watch_sandboxes(WatchSandboxesRequest {}) + .await + .unwrap() + .into_inner() + .message() + .await + .unwrap() + .is_none() + ); + + let request = UpdateSandboxConfigurationRequest { + preconditions: Some( + openshell_core::proto::compute::v1::SandboxUpdatePreconditions { + sandbox_id: "sandbox-id".into(), + image: "example/workload:v2".into(), + ..Default::default() + }, + ), + ..Default::default() + }; + let result = ComputeDriverConfigurationUpdatesClient::new(channel) + .update_sandbox_configuration(request.clone()) + .await; + if serve_image_updates { + result.unwrap(); + assert_eq!(*extension.calls.lock().unwrap(), vec![request]); + } else { + assert_eq!(result.unwrap_err().code(), tonic::Code::Unimplemented); + assert!(extension.calls.lock().unwrap().is_empty()); + } + // Absence of the optional service must not impair the original service. + baseline + .list_sandboxes(ListSandboxesRequest {}) + .await + .unwrap(); + drop(baseline); + shutdown_tx.send(()).unwrap(); + server.await.unwrap(); +} + +#[tokio::test] +async fn baseline_generated_server_source_and_transport_remain_compatible() { + roundtrip(false).await; +} + +#[tokio::test] +async fn optional_image_service_shares_the_baseline_channel() { + roundtrip(true).await; +} + +#[test] +fn baseline_compute_service_methods_are_frozen() { + let descriptor = + prost_types::FileDescriptorSet::decode(openshell_core::FILE_DESCRIPTOR_SET).unwrap(); + let compute = descriptor + .file + .iter() + .find(|file| file.package.as_deref() == Some("openshell.compute.v1")) + .unwrap(); + let baseline = compute + .service + .iter() + .find(|service| service.name.as_deref() == Some("ComputeDriver")) + .unwrap(); + assert_eq!( + baseline + .method + .iter() + .map(|method| method.name.as_deref().unwrap()) + .collect::>(), + vec![ + "GetCapabilities", + "AuthenticateSandbox", + "ValidateSandboxCreate", + "GetSandbox", + "ListSandboxes", + "CreateSandbox", + "StopSandbox", + "StartSandbox", + "SuspendSandbox", + "DeleteSandbox", + "WatchSandboxes", + "EnsureWorkspace", + "DeleteWorkspace", + ] + ); + let image_updates = compute + .service + .iter() + .find(|service| service.name.as_deref() == Some("ComputeDriverConfigurationUpdates")) + .unwrap(); + assert_eq!(image_updates.method.len(), 1); + assert_eq!( + image_updates.method[0].name.as_deref(), + Some("UpdateSandboxConfiguration") + ); +} diff --git a/crates/openshell-driver-docker/src/lib.rs b/crates/openshell-driver-docker/src/lib.rs index 453c5c3afa..8bf5df4e22 100644 --- a/crates/openshell-driver-docker/src/lib.rs +++ b/crates/openshell-driver-docker/src/lib.rs @@ -3156,6 +3156,9 @@ fn validate_docker_outer_fence( } Ok(()) } +#[tonic::async_trait] +impl openshell_core::compute::ComputeDriver for ComputeDriverService {} + #[tonic::async_trait] impl ComputeDriver for ComputeDriverService { type WatchSandboxesStream = WatchStream; @@ -3318,6 +3321,9 @@ impl ComputeDriver for ComputeDriverService { } } +#[tonic::async_trait] +impl openshell_core::compute::ComputeDriver for DockerComputeDriver {} + #[tonic::async_trait] impl ComputeDriver for DockerComputeDriver { async fn authenticate_sandbox( diff --git a/crates/openshell-driver-kubernetes/Cargo.toml b/crates/openshell-driver-kubernetes/Cargo.toml index 1fc7c7d00a..c84713f2bf 100644 --- a/crates/openshell-driver-kubernetes/Cargo.toml +++ b/crates/openshell-driver-kubernetes/Cargo.toml @@ -43,6 +43,7 @@ tracing-subscriber = { workspace = true } thiserror = { workspace = true } miette = { workspace = true } notify = "8" +regex = { workspace = true } rcgen = { workspace = true } bytes = { workspace = true } http = { workspace = true } diff --git a/crates/openshell-driver-kubernetes/README.md b/crates/openshell-driver-kubernetes/README.md index dc5abcaf48..b95fb6bf3f 100644 --- a/crates/openshell-driver-kubernetes/README.md +++ b/crates/openshell-driver-kubernetes/README.md @@ -302,6 +302,36 @@ storage. The driver confirms the stop from both the published `Suspended` condition and deletion of the backing pod. Legacy `v1alpha1` controllers omit a usable stopped condition, so pod deletion alone confirms their stop. +The optional `openshell.compute.update-sandbox-configuration` capability supports +image, command, and environment updates while stopped. External drivers serve +`ComputeDriverConfigurationUpdates` separately from the baseline driver service; +linked factories opt in through `build_with_configuration_updates`. +The request identifies the sandbox, original resource UID, expected image, +operation ID, and monotonic revision. The driver checks the suspended state, +completed bootstrap, current resource UID, and absence of workload and supervisor +Pods by directly reading the controller-recorded workload name, the Sandbox +resource name, and the stable dedicated-supervisor name. Duplicate names are +read once, and terminating Pods block updates. These checks make at most three +Pod reads regardless of namespace size; they do not discover unexpectedly named +Pods. The driver revalidates +external-resource admission before patching the existing resource with a +`resourceVersion` condition. The patch includes the verified UID; Kubernetes +immutable-UID validation also rejects a same-name replacement. No resource or +PVC is recreated. Existing workspace contents are not reseeded. + +A retry verifies durable operation evidence and the live selected targets before +returning success without another patch. Different parameters or older revisions +fail closed, including when later operations return to an earlier image. + +The service reconciles workload argv and effective user environment with the image +in the same conditional patch. The +main-process annotation and agent environment supply the next runtime generation; +the trusted container launcher and runtime environment remain intact. Retry +records identify selected fields and verify their live target values without +copying environment values into operation evidence. Top-level public sandbox +labels and annotations remain gateway metadata; this operation does not change +Kubernetes pod metadata. + The workspace PVC size defaults to `workspace_default_storage_size`. Set `workspace_storage_class` to pin the PVC to a specific `StorageClass`; an empty value omits `storageClassName` so the cluster's default `StorageClass` applies. diff --git a/crates/openshell-driver-kubernetes/src/driver.rs b/crates/openshell-driver-kubernetes/src/driver.rs index 65be627dc9..b893632f3f 100644 --- a/crates/openshell-driver-kubernetes/src/driver.rs +++ b/crates/openshell-driver-kubernetes/src/driver.rs @@ -90,6 +90,9 @@ use tokio::sync::{OnceCell, mpsc}; use tokio_stream::wrappers::ReceiverStream; use tracing::{debug, info, warn}; +#[path = "image_update.rs"] +mod image_update; + pub type WatchStream = Pin> + Send>>; @@ -1341,7 +1344,10 @@ impl KubernetesComputeDriver { openshell_core::extension_protocol::ExtensionFamily::Compute, "openshell/kubernetes", openshell_core::VERSION, - [], + [ + openshell_core::extension_protocol::COMPUTE_UPDATE_SANDBOX_CONFIGURATION + .to_string(), + ], )), }) } diff --git a/crates/openshell-driver-kubernetes/src/grpc.rs b/crates/openshell-driver-kubernetes/src/grpc.rs index ddfaac8b10..c9adffcbf5 100644 --- a/crates/openshell-driver-kubernetes/src/grpc.rs +++ b/crates/openshell-driver-kubernetes/src/grpc.rs @@ -337,6 +337,38 @@ impl ComputeDriver for ComputeDriverService { } } +#[tonic::async_trait] +impl openshell_core::compute::ComputeDriver for ComputeDriverService { + async fn update_sandbox_configuration( + &self, + request: Request, + ) -> Result< + Response, + Status, + > { + self.rpc_tracer + .trace(openshell_otel::rpc::UPDATE_SANDBOX_CONFIGURATION, async { + self.driver + .update_sandbox_configuration(&request.into_inner()) + .await + .map_err(|error| { + Status::from(openshell_core::ComputeDriverError::from(error)) + })?; + Ok(Response::new( + openshell_core::proto::compute::v1::UpdateSandboxConfigurationResponse {}, + )) + }) + .await + } +} + +#[tonic::async_trait] +impl openshell_core::proto::compute::v1::compute_driver_configuration_updates_server::ComputeDriverConfigurationUpdates for ComputeDriverService { + async fn update_sandbox_configuration(&self, request: Request) -> Result, Status> { + openshell_core::compute::ComputeDriver::update_sandbox_configuration(self, request).await + } +} + fn workspace_delete_requires_namespace_access(mode: WorkspaceMode) -> bool { matches!(mode, WorkspaceMode::Managed) } diff --git a/crates/openshell-driver-kubernetes/src/image_update.rs b/crates/openshell-driver-kubernetes/src/image_update.rs new file mode 100644 index 0000000000..7340943400 --- /dev/null +++ b/crates/openshell-driver-kubernetes/src/image_update.rs @@ -0,0 +1,614 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Storage-preserving, stopped-only workload image mutation. + +use super::{ + KUBE_API_TIMEOUT, KubernetesComputeDriver, KubernetesDriverError, LABEL_SANDBOX_NAME, + SANDBOX_POD_NAME_ANNOTATION, SANDBOX_VERSION_V1BETA1, WORKSPACE_INIT_CONTAINER_NAME, + accepts_auth_namespace, admission_error, annotation_or_label, + is_kube_resource_version_conflict, parse_kubernetes_runtime_identity, sandbox_id_from_object, + sandbox_runtime_bootstrap_in_progress, select_expected_sandbox_runtime, +}; +use crate::sandbox_runtime::SandboxRuntimeNames; +use k8s_openapi::api::core::v1::Pod; +use kube::Api; +use kube::api::{DynamicObject, ListParams, Patch, PatchParams}; +use openshell_core::proto::compute::v1::SandboxUpdatePreconditions; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; +use std::sync::LazyLock; + +// Deliberately separate from the runtime bootstrap operation annotations. Those +// annotations drive reconciliation and only describe create/start/stop. +const IMAGE_UPDATE_ANNOTATION: &str = "openshell.ai/sandbox-image-update"; +// Kubernetes limits the sum of all annotation keys and values, in bytes. +const MAX_ANNOTATION_BYTES: usize = 256 * 1024; + +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] +struct ImageUpdateRecord { + operation_id: String, + expected_image: String, + image: String, + revision: u64, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + command_updated: bool, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + environment_updated: bool, +} + +impl KubernetesComputeDriver { + /// Update only a suspended Sandbox's workload and workspace seed images. + /// The resource, claim templates, existing claims, and runtime bootstrap + /// annotations are never replaced. A retry is recognized from durable + /// operation evidence on the same immutable Sandbox resource. + #[tracing::instrument( + name = "kubernetes.update_sandbox_image", + skip_all, + fields( + otel.name = "kubernetes.update_sandbox_image", + otel.status_code = tracing::field::Empty, + sandbox.id = %request.sandbox_id, + ) + )] + #[cfg(test)] + pub async fn update_sandbox_image( + &self, + request: &SandboxUpdatePreconditions, + ) -> Result<(), KubernetesDriverError> { + self.update_sandbox_configuration( + &openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest { + preconditions: Some(request.clone()), + ..Default::default() + }, + ) + .await + } + + pub async fn update_sandbox_configuration( + &self, + request: &openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest, + ) -> Result<(), KubernetesDriverError> { + let image = request.preconditions.as_ref().ok_or_else(|| { + KubernetesDriverError::InvalidArgument("preconditions are required".into()) + })?; + validate_configuration(request)?; + self.update_workload(image, Some(request)).await + } + + async fn update_workload( + &self, + request: &SandboxUpdatePreconditions, + configuration: Option< + &openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest, + >, + ) -> Result<(), KubernetesDriverError> { + let span_status = openshell_otel::ErrorStatusGuard::current(); + validate_image_update_request(request)?; + let _guard = self + .lifecycle_gates + .gate_for(&request.sandbox_id) + .lock_owned() + .await; + // Include discovery, admission and Pod reads in a single bound. + let result = tokio::time::timeout( + KUBE_API_TIMEOUT, + self.update_sandbox_image_inner(request, configuration), + ) + .await + .unwrap_or_else(|_| { + Err(KubernetesDriverError::Message( + "timed out updating Kubernetes sandbox image; retry the same operation".into(), + )) + }); + span_status.finish(result) + } + + async fn update_sandbox_image_inner( + &self, + request: &SandboxUpdatePreconditions, + configuration: Option< + &openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest, + >, + ) -> Result<(), KubernetesDriverError> { + let expected = parse_kubernetes_runtime_identity(&request.expected_runtime_identity)?; + self.wait_for_operator_allowlist() + .await + .map_err(admission_error)?; + if !accepts_auth_namespace( + &self.config, + self.operator_allowlist.as_ref(), + &expected.namespace, + ) { + return Err(KubernetesDriverError::Precondition( + "persisted runtime namespace is outside the driver's configured scope".into(), + )); + } + let lookup = self + .supported_sandbox_api_for_lookup(self.client.clone()) + .await + .map_err(KubernetesDriverError::Message)?; + let objects = lookup + .api + .list(&ListParams::default().labels(&self.sandbox_lookup_selector(&request.sandbox_id))) + .await + .map_err(KubernetesDriverError::from_kube)? + .items; + let selected = select_expected_sandbox_runtime(objects, &expected)?; + let name = selected.metadata.name.as_deref().ok_or_else(|| { + KubernetesDriverError::Precondition("Sandbox resource has no name".into()) + })?; + let api = Self::agent_sandbox_api( + self.client.clone(), + &lookup.resource.version, + &expected.namespace, + ); + // Refresh after selection, then use exactly this snapshot's version in + // the patch. Never retry a resourceVersion conflict with a stale image, + // attachment inventory or operating-state decision. + let object = api + .api + .get(name) + .await + .map_err(KubernetesDriverError::from_kube)?; + let object = select_expected_sandbox_runtime(vec![object], &expected)?; + if sandbox_id_from_object(&object).as_deref() != Ok(request.sandbox_id.as_str()) + || annotation_or_label(&object, LABEL_SANDBOX_NAME).as_deref() + != Some(request.name.as_str()) + { + return Err(KubernetesDriverError::Precondition( + "Sandbox resource does not match the requested sandbox identity".into(), + )); + } + require_stopped_for_image_update(&object, &lookup.resource.version)?; + let mut candidate = object.clone(); + let repeat = prepare_update(&mut candidate, request, configuration)?; + if let Some(configuration) = configuration { + prepare_configuration(&mut candidate, configuration, repeat)?; + } + // Check the encoded candidate before preflight succeeds and the gateway + // durably accepts an intent. Raw argv limits do not account for JSON + // escaping, operation evidence, or existing annotations. + let annotation_bytes: usize = candidate + .metadata + .annotations + .iter() + .flat_map(|annotations| annotations.iter()) + .map(|(key, value)| key.len() + value.len()) + .sum(); + if annotation_bytes > MAX_ANNOTATION_BYTES { + return Err(KubernetesDriverError::InvalidArgument(format!( + "sandbox update exceeds Kubernetes annotation size limit ({annotation_bytes} > {MAX_ANNOTATION_BYTES} bytes)" + ))); + } + // Revalidate the saved provenance and attachment UIDs against current + // operator policy, even for retries. Do not manufacture new provenance + // or re-render driver_config from a caller's request. + self.admit_stored_resources(&candidate) + .await + .map_err(admission_error)?; + self.require_image_update_pods_absent(&object, &request.sandbox_id, &expected.namespace) + .await?; + if request.validate_only || repeat { + return Ok(()); + } + let version = object + .metadata + .resource_version + .as_deref() + .filter(|version| !version.is_empty()) + .ok_or_else(|| { + KubernetesDriverError::Precondition( + "Sandbox resource has no resourceVersion".into(), + ) + })?; + let record = candidate + .metadata + .annotations + .as_ref() + .and_then(|annotations| annotations.get(IMAGE_UPDATE_ANNOTATION)) + .expect("prepared image update has operation evidence"); + let mut patch = serde_json::json!({ + "metadata": { + "uid": expected.resource_uid, + "resourceVersion": version, + "annotations": {IMAGE_UPDATE_ANNOTATION: record} + }, + "spec": {"podTemplate": {"spec": { + "containers": candidate.data["spec"]["podTemplate"]["spec"]["containers"] + }}} + }); + if configuration.is_some_and(|configuration| configuration.main_process.is_some()) { + let key = super::ANNOTATION_SANDBOX_RUNTIME_MAIN_PROCESS_SPEC; + patch["metadata"]["annotations"][key] = serde_json::json!( + candidate + .metadata + .annotations + .as_ref() + .and_then(|annotations| annotations.get(key)) + ); + } + if let Some(init) = candidate + .data + .pointer("/spec/podTemplate/spec/initContainers") + { + patch["spec"]["podTemplate"]["spec"]["initContainers"] = init.clone(); + } + api.api + .patch(name, &PatchParams::default(), &Patch::Merge(&patch)) + .await + .map_err(|error| { + if is_kube_resource_version_conflict(&error) { + KubernetesDriverError::Precondition( + "Sandbox changed during image update; retry the same operation".into(), + ) + } else { + KubernetesDriverError::from_kube(error) + } + })?; + Ok(()) + } + + async fn require_image_update_pods_absent( + &self, + object: &DynamicObject, + sandbox_id: &str, + namespace: &str, + ) -> Result<(), KubernetesDriverError> { + let pods = Api::::namespaced(self.client.clone(), namespace); + let names = SandboxRuntimeNames::new(sandbox_id); + // Check both the controller-recorded and default workload names. The + // dedicated supervisor name is stable across generations; check it even + // with shared supervision to catch a leftover dedicated Pod. Direct + // reads keep preflight independent of the number of namespace Pods. + let pod_names: BTreeSet<_> = [ + object.metadata.name.as_deref(), + object + .metadata + .annotations + .as_ref() + .and_then(|annotations| annotations.get(SANDBOX_POD_NAME_ANNOTATION)) + .map(String::as_str), + Some(names.supervisor_pod.as_str()), + ] + .into_iter() + .flatten() + .filter(|name| !name.is_empty()) + .collect(); + for name in pod_names { + // Terminating Pods still block updates. Only a 404 proves absence; + // authorization, transport and other API failures must propagate. + if pods + .get_opt(name) + .await + .map_err(KubernetesDriverError::from_kube)? + .is_some() + { + return Err(KubernetesDriverError::Precondition( + "cannot update image while sandbox workload or supervisor Pods still exist" + .into(), + )); + } + } + Ok(()) + } +} + +fn validate_image_update_request( + request: &SandboxUpdatePreconditions, +) -> Result<(), KubernetesDriverError> { + for (field, value) in [ + ("sandbox_id", request.sandbox_id.as_str()), + ("name", request.name.as_str()), + ("operation_id", request.operation_id.as_str()), + ] { + if value.is_empty() + || value.len() > 1024 + || value.trim() != value + || value.chars().any(char::is_control) + { + return Err(KubernetesDriverError::InvalidArgument(format!( + "{field} must be a nonempty bounded identifier" + ))); + } + } + if request.revision == 0 { + return Err(KubernetesDriverError::InvalidArgument( + "image update revision must be nonzero".into(), + )); + } + validate_workload_image(&request.image)?; + // The old reference is only a compare-and-swap precondition, not a new + // launch input. Legacy create accepted references that kubelet cannot pull; + // validating their syntax here would prevent repairing those sandboxes. + if request.expected_image.is_empty() || request.expected_image.len() > 2048 { + return Err(KubernetesDriverError::InvalidArgument( + "expected_image must be a nonempty bounded reference".into(), + )); + } + Ok(()) +} + +fn validate_workload_image(image: &str) -> Result<(), KubernetesDriverError> { + // Distribution reference grammar: registry (including a port or bracketed + // IPv6), lowercase repository path, optional tag and/or algorithm:digest. + // Validation is syntax only: resolving/pulling the image remains kubelet's + // job, using the existing operator-selected image-pull Secret inventory. + static REFERENCE: LazyLock = LazyLock::new(|| { + let host_component = r"[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?"; + let host = format!(r"(?:{host_component}(?:\.{host_component})*|\[[a-fA-F0-9:]+\])"); + let path = r"[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*"; + regex::Regex::new(&format!( + r"^(?:{host}(?::[0-9]+)?/)?{path}(?:/{path})*(?::[a-zA-Z0-9_][a-zA-Z0-9_.-]{{0,127}})?(?:@[a-zA-Z][a-zA-Z0-9]*(?:[-_+.][a-zA-Z][a-zA-Z0-9]*)*:[a-fA-F0-9]{{32,}})?$" + )).expect("valid distribution reference grammar") + }); + let named = image.split('@').next().unwrap_or_default(); + let repository = + named.rsplit_once(':').map_or( + named, + |(prefix, suffix)| { + if suffix.contains('/') { named } else { prefix } + }, + ); + let digest_length_valid = image.rsplit_once('@').is_none_or(|(_, digest)| { + digest + .split_once(':') + .is_some_and(|(algorithm, encoded)| match algorithm { + "sha256" => encoded.len() == 64, + "sha384" => encoded.len() == 96, + "sha512" => encoded.len() == 128, + _ => true, + }) + }); + if image.len() > 2048 + || repository.len() > 255 + || !digest_length_valid + || !REFERENCE.is_match(image) + { + return Err(KubernetesDriverError::InvalidArgument( + "image must be a valid container image reference of at most 2048 bytes".into(), + )); + } + Ok(()) +} + +fn require_stopped_for_image_update( + object: &DynamicObject, + version: &str, +) -> Result<(), KubernetesDriverError> { + let suspended = if version == SANDBOX_VERSION_V1BETA1 { + object + .data + .pointer("/spec/operatingMode") + .and_then(serde_json::Value::as_str) + == Some("Suspended") + } else { + object + .data + .pointer("/spec/replicas") + .and_then(serde_json::Value::as_i64) + == Some(0) + }; + if !suspended + || object.metadata.deletion_timestamp.is_some() + || sandbox_runtime_bootstrap_in_progress(object) + { + return Err(KubernetesDriverError::Precondition( + "image updates require a stopped, suspended sandbox with no runtime bootstrap in progress".into(), + )); + } + Ok(()) +} + +/// Apply the image-only mutation in memory; return true for an exact retry. +fn prepare_update( + object: &mut DynamicObject, + request: &SandboxUpdatePreconditions, + configuration: Option<&openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest>, +) -> Result { + let evidence = object + .metadata + .annotations + .as_ref() + .and_then(|annotations| annotations.get(IMAGE_UPDATE_ANNOTATION)) + .map(|value| serde_json::from_str::(value)) + .transpose() + .map_err(|_| { + KubernetesDriverError::Precondition("invalid stored image update evidence".into()) + })?; + let record = ImageUpdateRecord { + operation_id: request.operation_id.clone(), + expected_image: request.expected_image.clone(), + image: request.image.clone(), + revision: request.revision, + command_updated: configuration.is_some_and(|value| value.main_process.is_some()), + environment_updated: configuration.is_some_and(|value| value.environment.is_some()), + }; + if let Some(saved) = &evidence { + if request.revision < saved.revision { + return Err(KubernetesDriverError::Precondition( + "image update revision is older than the stored image revision".into(), + )); + } + if request.revision == saved.revision && saved != &record { + return Err(KubernetesDriverError::Precondition( + "image update revision was already used with different operation parameters".into(), + )); + } + if saved.operation_id == request.operation_id && saved != &record { + return Err(KubernetesDriverError::Precondition( + "image update operation_id was already used with different parameters".into(), + )); + } + } + let repeat = evidence.as_ref() == Some(&record); + let containers = object + .data + .pointer_mut("/spec/podTemplate/spec/containers") + .and_then(serde_json::Value::as_array_mut) + .ok_or_else(|| { + KubernetesDriverError::Precondition("Sandbox has no workload containers".into()) + })?; + let mut agents = containers + .iter_mut() + .filter(|container| container["name"] == "agent"); + let agent = agents.next().ok_or_else(|| { + KubernetesDriverError::Precondition("Sandbox has no agent container".into()) + })?; + if agents.next().is_some() { + return Err(KubernetesDriverError::Precondition( + "Sandbox has multiple agent containers".into(), + )); + } + let expected: &str = if repeat { + &request.image + } else { + &request.expected_image + }; + if agent["image"].as_str() != Some(expected) { + return Err(KubernetesDriverError::Precondition( + "Sandbox workload image does not match expected_image or the recorded operation target" + .into(), + )); + } + agent["image"] = serde_json::json!(request.image); + if let Some(init) = object + .data + .pointer_mut("/spec/podTemplate/spec/initContainers") + .and_then(serde_json::Value::as_array_mut) + { + for container in init + .iter_mut() + .filter(|container| container["name"] == WORKSPACE_INIT_CONTAINER_NAME) + { + if container["image"].as_str() != Some(expected) { + return Err(KubernetesDriverError::Precondition( + "Sandbox workspace seed image does not match its workload image".into(), + )); + } + container["image"] = serde_json::json!(request.image); + } + } + object + .metadata + .annotations + .get_or_insert_with(BTreeMap::new) + .insert( + IMAGE_UPDATE_ANNOTATION.into(), + serde_json::to_string(&record).expect("image update record serializes"), + ); + Ok(repeat) +} + +fn validate_configuration( + request: &openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest, +) -> Result<(), KubernetesDriverError> { + let invalid = |message: &str| KubernetesDriverError::InvalidArgument(message.into()); + if let Some(spec) = &request.main_process + && (spec.command.len() > 256 + || spec.command.iter().map(String::len).sum::() > 256 * 1024 + || spec.command.first().is_some_and(String::is_empty) + || spec.command.iter().any(|arg| arg.contains('\0'))) + { + return Err(invalid("invalid workload argv")); + } + if let Some(environment) = &request.environment { + if environment.values.len() > 256 + || environment + .values + .iter() + .map(|(key, value)| key.len() + value.len()) + .sum::() + > 256 * 1024 + { + return Err(invalid("environment exceeds size limits")); + } + for (key, value) in &environment.values { + if key.is_empty() + || key.starts_with("OPENSHELL_") + || key.as_bytes()[0].is_ascii_digit() + || !key.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'_') + || value.contains(['\0', '\n', '\r']) + { + return Err(invalid("invalid or reserved workload environment entry")); + } + } + } + Ok(()) +} + +fn prepare_configuration( + object: &mut DynamicObject, + request: &openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest, + repeat: bool, +) -> Result<(), KubernetesDriverError> { + if let Some(spec) = &request.main_process { + let key = super::ANNOTATION_SANDBOX_RUNTIME_MAIN_PROCESS_SPEC; + let encoded = super::required_sandbox_annotation(object, key)?; + let mut main = openshell_core::sandbox_env::MainProcessConfig::decode(&encoded) + .map_err(KubernetesDriverError::Precondition)?; + let original = main.clone(); + main.command.clone_from(&spec.command); + main.tty = spec.command.is_empty() || spec.tty; + if repeat && main != original { + return Err(KubernetesDriverError::Precondition( + "recorded update command does not match target".into(), + )); + } + object + .metadata + .annotations + .get_or_insert_with(BTreeMap::new) + .insert( + key.into(), + serde_json::to_string(&main).map_err(|_| { + KubernetesDriverError::InvalidArgument("cannot encode main process".into()) + })?, + ); + } + if let Some(environment) = &request.environment { + let containers = object + .data + .pointer_mut("/spec/podTemplate/spec/containers") + .and_then(serde_json::Value::as_array_mut) + .ok_or_else(|| { + KubernetesDriverError::Precondition("missing workload containers".into()) + })?; + let agent = containers + .iter_mut() + .find(|container| container["name"] == "agent") + .ok_or_else(|| KubernetesDriverError::Precondition("missing agent container".into()))?; + let mut current = std::collections::HashMap::new(); + let mut retained = Vec::new(); + for entry in agent["env"].as_array().into_iter().flatten() { + let name = entry["name"].as_str().ok_or_else(|| { + KubernetesDriverError::Precondition("invalid workload environment".into()) + })?; + if name.starts_with("OPENSHELL_") { + retained.push(entry.clone()); + } else if let Some(value) = entry["value"].as_str() { + current.insert(name.to_owned(), value.to_owned()); + } else { + return Err(KubernetesDriverError::Precondition( + "cannot replace workload environment with external value sources".into(), + )); + } + } + if repeat && current != environment.values { + return Err(KubernetesDriverError::Precondition( + "recorded update environment does not match target".into(), + )); + } + let mut entries: Vec<_> = environment.values.iter().collect(); + entries.sort_unstable_by_key(|(name, _)| *name); + for (name, value) in entries { + retained.push(serde_json::json!({"name": name, "value": value})); + } + agent["env"] = serde_json::json!(retained); + } + Ok(()) +} + +#[cfg(test)] +#[path = "image_update_tests.rs"] +mod tests; diff --git a/crates/openshell-driver-kubernetes/src/image_update_tests.rs b/crates/openshell-driver-kubernetes/src/image_update_tests.rs new file mode 100644 index 0000000000..6a079d9575 --- /dev/null +++ b/crates/openshell-driver-kubernetes/src/image_update_tests.rs @@ -0,0 +1,1006 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::super::{ + ANNOTATION_SANDBOX_RUNTIME_BOOTSTRAP_OPERATION, ANNOTATION_SANDBOX_RUNTIME_BOOTSTRAP_PHASE, + ANNOTATION_SANDBOX_RUNTIME_BOOTSTRAPPING, ANNOTATION_SANDBOX_RUNTIME_LOG_LEVEL, + ANNOTATION_SANDBOX_RUNTIME_MAIN_PROCESS_SPEC, ANNOTATION_SANDBOX_RUNTIME_READINESS, + KubernetesComputeConfig, LABEL_SANDBOX_ID, LABEL_SANDBOX_WORKSPACE, OperatorNamespaceAllowlist, + SANDBOX_BOOTSTRAP_CONTAINER_NAME, SANDBOX_BOOTSTRAP_VOLUME_NAME, SANDBOX_GROUP, + SANDBOX_VERSION_V1ALPHA1, WORKSPACE_INIT_MOUNT_PATH, WorkspaceMode, +}; +use super::*; +use http_body_util::BodyExt as _; +use kube::Client; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tokio::sync::OnceCell; + +struct ApiState { + sandbox: serde_json::Value, + pvc: serde_json::Value, + pods: Vec, + calls: Vec<(http::Method, String)>, + patches: Vec, + conflict: bool, + pod_error: Option<(String, http::StatusCode)>, +} + +fn response( + status: http::StatusCode, + value: serde_json::Value, +) -> http::Response { + http::Response::builder() + .status(status) + .header(http::header::CONTENT_TYPE, "application/json") + .body(kube::client::Body::from(value.to_string().into_bytes())) + .unwrap() +} + +fn merge(target: &mut serde_json::Value, patch: &serde_json::Value) { + if let Some(patch) = patch.as_object() { + for (key, value) in patch { + if value.is_null() { + target.as_object_mut().unwrap().remove(key); + } else { + if target.get(key).is_none() { + target[key] = serde_json::Value::Null; + } + if value.is_object() && !target[key].is_object() { + target[key] = serde_json::json!({}); + } + merge(&mut target[key], value); + } + } + } else { + *target = patch.clone(); + } +} + +fn snapshot(version: &str) -> serde_json::Value { + let mut value = serde_json::json!({ + "apiVersion": format!("{SANDBOX_GROUP}/{version}"), + "kind": "Sandbox", + "metadata": { + "name": "sandbox-cr", "namespace": "openshell", "uid": "resource-uid", + "resourceVersion": "42", "generation": 7, + "labels": {LABEL_SANDBOX_ID: "sandbox-1", LABEL_SANDBOX_NAME: "sandbox-name", LABEL_SANDBOX_WORKSPACE: "team-a"}, + "annotations": { + LABEL_SANDBOX_ID: "sandbox-1", LABEL_SANDBOX_NAME: "sandbox-name", + crate::resource_admission::CONFIG_USED: "true", + crate::resource_admission::IDENTITIES: "{\"PersistentVolumeClaim/openshell/team-data\":\"data-pvc-uid\"}", + "operator.example/bootstrap-provenance": "preserve-me", + ANNOTATION_SANDBOX_RUNTIME_MAIN_PROCESS_SPEC: "stored-launch-spec", + ANNOTATION_SANDBOX_RUNTIME_LOG_LEVEL: "info", + ANNOTATION_SANDBOX_RUNTIME_READINESS: "unavailable", + ANNOTATION_SANDBOX_RUNTIME_BOOTSTRAPPING: "false", + ANNOTATION_SANDBOX_RUNTIME_BOOTSTRAP_OPERATION: "restart", + ANNOTATION_SANDBOX_RUNTIME_BOOTSTRAP_PHASE: "released", + SANDBOX_POD_NAME_ANNOTATION: "recorded-workload" + }, + "ownerReferences": [{"apiVersion":"v1", "kind":"Namespace", "name":"openshell", "uid":"namespace-uid"}] + }, + "spec": { + "volumeClaimTemplates": [{"metadata":{"name":"workspace"}, "spec":{"accessModes":["ReadWriteOnce"], "resources":{"requests":{"storage":"10Gi"}}}}], + "podTemplate": { + "metadata": {"annotations": {"operator.example/pod-injection": "preserved"}}, + "spec": { + "automountServiceAccountToken": false, + "serviceAccountName": "openshell-sandbox", + "imagePullSecrets": [{"name":"operator-registry"}], + "volumes": [ + {"name": SANDBOX_BOOTSTRAP_VOLUME_NAME, "secret":{"secretName":"old-bootstrap-secret"}}, + {"name":"data", "persistentVolumeClaim":{"claimName":"team-data", "readOnly":false}} + ], + "containers": [ + {"name":"agent", "image":"registry.example/agent:v1", "command":["/.openshell/runtime/openshell-sandbox"], "env":[{"name":"UNCHANGED","value":"yes"}], "volumeMounts":[{"name":"workspace", "mountPath":"/sandbox"}, {"name":"data", "mountPath":"/data", "subPath":"agent"}]}, + {"name":"operator-sidecar", "image":"registry.example/agent:v1"} + ], + "initContainers": [ + {"name":SANDBOX_BOOTSTRAP_CONTAINER_NAME, "image":"trusted-runtime:v3"}, + {"name":WORKSPACE_INIT_CONTAINER_NAME, "image":"registry.example/agent:v1", "command":["seed-workspace"], "volumeMounts":[{"name":"workspace", "mountPath":WORKSPACE_INIT_MOUNT_PATH}]}, + {"name":"operator-init", "image":"registry.example/agent:v1"} + ] + } + } + }, + "status":{"conditions":[{"type":"Suspended", "status":"True", "observedGeneration":7}]} + }); + if version == SANDBOX_VERSION_V1BETA1 { + value["spec"]["operatingMode"] = serde_json::json!("Suspended"); + } else { + value["spec"]["replicas"] = serde_json::json!(0); + } + value +} + +fn fixture( + version: &'static str, + operator: bool, +) -> (KubernetesComputeDriver, Arc>) { + let state = Arc::new(Mutex::new(ApiState { + sandbox: snapshot(version), + pvc: serde_json::json!({ + "apiVersion":"meta.k8s.io/v1", "kind":"PartialObjectMetadata", + "metadata": {"name":"team-data", "namespace":"openshell", "uid":"data-pvc-uid", + "labels": {"openshell.ai/sandbox-attachable":"true", "openshell.ai/sandbox-attachable-workspace":"team-a"}} + }), + pods: Vec::new(), + calls: Vec::new(), + patches: Vec::new(), + conflict: false, + pod_error: None, + })); + let service_state = state.clone(); + let service = tower::service_fn(move |request: http::Request| { + let state = service_state.clone(); + async move { + let path = request.uri().path().to_string(); + let method = request.method().clone(); + let body = request.into_body().collect().await.unwrap().to_bytes(); + let mut state = state.lock().unwrap(); + state.calls.push((method.clone(), path.clone())); + let value = match (method, path.as_str()) { + (http::Method::GET, path) if path.ends_with("/sandboxes") => { + serde_json::json!({"apiVersion":format!("{SANDBOX_GROUP}/{version}"), "kind":"SandboxList", "metadata":{}, "items":[state.sandbox]}) + } + (http::Method::GET, path) if path.ends_with("/sandboxes/sandbox-cr") => { + state.sandbox.clone() + } + ( + http::Method::GET, + "/api/v1/namespaces/openshell/persistentvolumeclaims/team-data", + ) => state.pvc.clone(), + (http::Method::GET, path) + if path.starts_with("/api/v1/namespaces/openshell/pods/") => + { + let name = path + .strip_prefix("/api/v1/namespaces/openshell/pods/") + .unwrap(); + if let Some((error_name, status)) = &state.pod_error + && error_name == name + { + return Ok(response( + *status, + serde_json::json!({ + "apiVersion":"v1", "kind":"Status", "status":"Failure", + "reason":status.canonical_reason().unwrap(), + "message":"pod read failed", "code":status.as_u16() + }), + )); + } + let Some(pod) = state + .pods + .iter() + .find(|pod| pod["metadata"]["name"] == name) + else { + return Ok(response( + http::StatusCode::NOT_FOUND, + serde_json::json!({ + "apiVersion":"v1", "kind":"Status", "status":"Failure", + "reason":"NotFound", "message":"pod not found", "code":404 + }), + )); + }; + pod.clone() + } + (http::Method::PATCH, path) if path.ends_with("/sandboxes/sandbox-cr") => { + let mutation: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_eq!( + mutation["metadata"]["uid"], + state.sandbox["metadata"]["uid"] + ); + assert_eq!( + mutation["metadata"]["resourceVersion"], + state.sandbox["metadata"]["resourceVersion"] + ); + state.patches.push(mutation.clone()); + if state.conflict { + return Ok::<_, std::convert::Infallible>(response( + http::StatusCode::CONFLICT, + serde_json::json!({"apiVersion":"v1", "kind":"Status", "status":"Failure", "reason":"Conflict", "message":"concurrent start or edit", "code":409}), + )); + } + let next_version = state.sandbox["metadata"]["resourceVersion"] + .as_str() + .unwrap() + .parse::() + .unwrap() + + 1; + let next_generation = + state.sandbox["metadata"]["generation"].as_u64().unwrap() + 1; + merge(&mut state.sandbox, &mutation); + state.sandbox["metadata"]["resourceVersion"] = + serde_json::json!(next_version.to_string()); + state.sandbox["metadata"]["generation"] = serde_json::json!(next_generation); + state.sandbox.clone() + } + (method, path) => { + panic!("unexpected image update Kubernetes request {method} {path}") + } + }; + Ok::<_, std::convert::Infallible>(response(http::StatusCode::OK, value)) + } + }); + let client = Client::new(service, "openshell"); + let api_version = OnceCell::new(); + api_version.set(version).unwrap(); + let driver = KubernetesComputeDriver { + client: client.clone(), + watch_client: client, + sandbox_api_version: Arc::new(api_version), + lifecycle_gates: Arc::default(), + shared_supervisor: None, + network_trust_bundle: None, + config: KubernetesComputeConfig { + allow_driver_config: true, + workspace_mode: if operator { + WorkspaceMode::Operator + } else { + WorkspaceMode::Shared + }, + ..Default::default() + }, + operator_allowlist: operator.then(|| { + OperatorNamespaceAllowlist::from_set(std::iter::once("openshell".to_string()).collect()) + }), + }; + (driver, state) +} + +fn request() -> SandboxUpdatePreconditions { + SandboxUpdatePreconditions { + sandbox_id: "sandbox-1".into(), + name: "sandbox-name".into(), + image: "registry.example/agent:v2".into(), + expected_image: "registry.example/agent:v1".into(), + expected_runtime_identity: "kubernetes://openshell/resource-uid/old-supervisor-uid".into(), + operation_id: "stable-operation-1".into(), + validate_only: false, + revision: 1, + } +} + +#[tokio::test] +async fn image_update_preflight_validates_without_writes_and_apply_rechecks() { + let (driver, state) = fixture(SANDBOX_VERSION_V1BETA1, false); + let before = state.lock().unwrap().sandbox.clone(); + let mut preflight = request(); + preflight.validate_only = true; + driver.update_sandbox_image(&preflight).await.unwrap(); + { + let state = state.lock().unwrap(); + assert!(state.patches.is_empty()); + assert_eq!(state.sandbox, before); + assert!( + state + .calls + .iter() + .any(|(_, path)| path.ends_with("/persistentvolumeclaims/team-data")) + ); + assert_eq!(pod_read_names(&state).len(), 3); + } + // Operator approval can change between preflight and apply. Both requests + // independently enforce the entire stopped/admission check. + state.lock().unwrap().pvc["metadata"]["labels"] = serde_json::json!({}); + assert!(matches!( + driver.update_sandbox_image(&request()).await, + Err(KubernetesDriverError::Precondition(_)) + )); + assert!(matches!( + driver.update_sandbox_image(&preflight).await, + Err(KubernetesDriverError::Precondition(_)) + )); + assert!(state.lock().unwrap().patches.is_empty()); +} + +#[tokio::test] +async fn image_update_capability_is_optional() { + let (driver, _) = fixture(SANDBOX_VERSION_V1BETA1, false); + let metadata = driver.capabilities().unwrap().extension.unwrap(); + let capability = openshell_core::extension_protocol::COMPUTE_UPDATE_SANDBOX_CONFIGURATION; + assert!( + metadata + .supported_capabilities + .iter() + .any(|value| value == capability) + ); + assert!( + !metadata + .required_capabilities + .iter() + .any(|value| value == capability) + ); +} + +#[tokio::test] +async fn image_update_preserves_resource_storage_and_operator_bootstrap_annotations() { + for version in [SANDBOX_VERSION_V1BETA1, SANDBOX_VERSION_V1ALPHA1] { + for operator in [false, true] { + let (driver, state) = fixture(version, operator); + if operator { + // Operator-mode identity can live in bootstrap annotations + // rather than a caller-visible workload-name label. + state.lock().unwrap().sandbox["metadata"]["labels"] + .as_object_mut() + .unwrap() + .remove(LABEL_SANDBOX_NAME); + } + let before = state.lock().unwrap().sandbox.clone(); + let pvc_before = state.lock().unwrap().pvc.clone(); + driver.update_sandbox_image(&request()).await.unwrap(); + let state = state.lock().unwrap(); + let mut expected = before; + expected["metadata"]["resourceVersion"] = serde_json::json!("43"); + expected["metadata"]["generation"] = serde_json::json!(8); + expected["metadata"]["annotations"][IMAGE_UPDATE_ANNOTATION] = serde_json::json!( + serde_json::to_string(&ImageUpdateRecord { + command_updated: false, + environment_updated: false, + operation_id: request().operation_id, + expected_image: request().expected_image, + image: request().image, + revision: request().revision, + }) + .unwrap() + ); + expected["spec"]["podTemplate"]["spec"]["containers"][0]["image"] = + serde_json::json!(request().image); + expected["spec"]["podTemplate"]["spec"]["initContainers"][1]["image"] = + serde_json::json!(request().image); + // Whole-object equality proves claim templates, mounts, owner UID, + // launch state, operator annotations, and trusted/sidecar images + // were preserved. The API rejects any unexpected DELETE/POST. + assert_eq!(state.sandbox, expected); + assert_eq!(state.pvc, pvc_before); + assert_eq!(state.patches.len(), 1); + assert_eq!( + state.patches[0]["metadata"]["annotations"] + .as_object() + .unwrap() + .len(), + 1 + ); + assert!( + state.patches[0]["spec"] + .get("volumeClaimTemplates") + .is_none() + ); + assert!( + state.patches[0]["spec"]["podTemplate"]["spec"] + .get("volumes") + .is_none() + ); + } + } +} + +#[tokio::test] +async fn image_update_retry_is_durable_and_does_not_patch_twice() { + let (driver, state) = fixture(SANDBOX_VERSION_V1BETA1, false); + driver.update_sandbox_image(&request()).await.unwrap(); + // Simulate a process restart by dropping the original in-memory gate state. + let mut restarted = driver.clone(); + restarted.lifecycle_gates = Arc::default(); + restarted.update_sandbox_image(&request()).await.unwrap(); + assert_eq!(state.lock().unwrap().patches.len(), 1); + let mut other_operation = request(); + other_operation.operation_id = "other-operation".into(); + other_operation.revision += 1; + assert!(matches!( + driver.update_sandbox_image(&other_operation).await, + Err(KubernetesDriverError::Precondition(_)) + )); + let mut reused_operation = request(); + reused_operation.image = "registry.example/agent:v3".into(); + assert!(matches!( + driver.update_sandbox_image(&reused_operation).await, + Err(KubernetesDriverError::Precondition(_)) + )); + assert_eq!(state.lock().unwrap().patches.len(), 1); +} + +#[tokio::test] +async fn image_update_monotonic_revision_rejects_late_replay_after_a_tag_round_trip() { + let (driver, state) = fixture(SANDBOX_VERSION_V1BETA1, false); + let a = request(); + driver.update_sandbox_image(&a).await.unwrap(); + let mut b = a.clone(); + b.expected_image = a.image.clone(); + b.image = a.expected_image.clone(); + b.operation_id = "operation-b".into(); + b.revision = 2; + driver.update_sandbox_image(&b).await.unwrap(); + let before = state.lock().unwrap().sandbox.clone(); + // The current tag again matches A's expected image, but its revision is + // stale. The latest evidence must reject A rather than reapply it. + assert!(matches!(driver.update_sandbox_image(&a).await, + Err(KubernetesDriverError::Precondition(message)) if message.contains("older"))); + let mut conflicting = a.clone(); + conflicting.operation_id = "different-operation-at-revision-2".into(); + conflicting.revision = b.revision; + assert!(matches!(driver.update_sandbox_image(&conflicting).await, + Err(KubernetesDriverError::Precondition(message)) if message.contains("revision was already used"))); + assert_eq!(state.lock().unwrap().patches.len(), 2); + assert_eq!(state.lock().unwrap().sandbox, before); + driver.update_sandbox_image(&b).await.unwrap(); + assert_eq!(state.lock().unwrap().patches.len(), 2); + conflicting.revision = 3; + driver.update_sandbox_image(&conflicting).await.unwrap(); + assert_eq!(state.lock().unwrap().patches.len(), 3); +} + +#[tokio::test] +async fn image_update_rejects_zero_revision_before_kubernetes_io() { + let (driver, state) = fixture(SANDBOX_VERSION_V1BETA1, false); + let mut request = request(); + request.revision = 0; + assert!(matches!( + driver.update_sandbox_image(&request).await, + Err(KubernetesDriverError::InvalidArgument(_)) + )); + assert!(state.lock().unwrap().calls.is_empty()); +} + +#[tokio::test] +async fn image_update_rejects_running_bootstrapping_and_unspecified_operating_state() { + for version in [SANDBOX_VERSION_V1BETA1, SANDBOX_VERSION_V1ALPHA1] { + for scenario in ["running", "bootstrap", "missing", "deleting"] { + let (driver, state) = fixture(version, false); + { + let mut state = state.lock().unwrap(); + match scenario { + "running" if version == SANDBOX_VERSION_V1BETA1 => { + state.sandbox["spec"]["operatingMode"] = serde_json::json!("Running"); + } + "running" => state.sandbox["spec"]["replicas"] = serde_json::json!(1), + "bootstrap" => { + state.sandbox["metadata"]["annotations"] + [ANNOTATION_SANDBOX_RUNTIME_BOOTSTRAPPING] = serde_json::json!("true"); + } + "missing" => { + state.sandbox["spec"].as_object_mut().unwrap().remove( + if version == SANDBOX_VERSION_V1BETA1 { + "operatingMode" + } else { + "replicas" + }, + ); + } + "deleting" => { + state.sandbox["metadata"]["deletionTimestamp"] = + serde_json::json!("2026-10-15T18:00:00Z"); + } + _ => unreachable!(), + } + } + assert!( + matches!( + driver.update_sandbox_image(&request()).await, + Err(KubernetesDriverError::Precondition(_)) + ), + "{scenario} {version}" + ); + assert!(state.lock().unwrap().patches.is_empty()); + } + } +} + +fn pod_read_names(state: &ApiState) -> Vec<&str> { + state + .calls + .iter() + .filter_map(|(method, path)| { + assert_ne!( + path, "/api/v1/namespaces/openshell/pods", + "must not list Pods" + ); + let name = path.strip_prefix("/api/v1/namespaces/openshell/pods/")?; + assert_eq!(method, http::Method::GET); + Some(name) + }) + .collect() +} + +#[tokio::test] +async fn image_update_rejects_existing_known_pods_including_terminating_pods() { + for version in [SANDBOX_VERSION_V1ALPHA1, SANDBOX_VERSION_V1BETA1] { + for name in ["sandbox-cr", "recorded-workload", "os-supervisor-sandbox-1"] { + for terminating in [false, true] { + let (driver, state) = fixture(version, false); + let mut metadata = serde_json::json!({"name":name}); + if terminating { + metadata["deletionTimestamp"] = "2026-10-15T18:00:00Z".into(); + } + state.lock().unwrap().pods.push(serde_json::json!({ + "apiVersion":"v1", "kind":"Pod", "metadata":metadata + })); + for validate_only in [true, false] { + let result = driver + .update_sandbox_image(&SandboxUpdatePreconditions { + validate_only, + ..request() + }) + .await; + assert!( + matches!(result, Err(KubernetesDriverError::Precondition(message)) if message.contains("Pods still exist")), + "{version}: {name}, terminating={terminating}, validate_only={validate_only}" + ); + assert!(state.lock().unwrap().patches.is_empty()); + } + } + } + } +} + +#[tokio::test] +async fn image_update_reads_only_distinct_known_pod_names() { + for version in [SANDBOX_VERSION_V1ALPHA1, SANDBOX_VERSION_V1BETA1] { + for recorded_name in [ + None, + Some(""), + Some("sandbox-cr"), + Some("recorded-workload"), + Some("os-supervisor-sandbox-1"), + ] { + let (driver, state) = fixture(version, false); + { + let mut state = state.lock().unwrap(); + let annotations = state.sandbox["metadata"]["annotations"] + .as_object_mut() + .unwrap(); + annotations.remove(SANDBOX_POD_NAME_ANNOTATION); + if let Some(name) = recorded_name { + annotations.insert(SANDBOX_POD_NAME_ANNOTATION.into(), name.into()); + } + state.pods.push(serde_json::json!({ + "apiVersion":"v1", "kind":"Pod", "metadata":{"name":"unrelated-workload"} + })); + } + for validate_only in [true, false] { + state.lock().unwrap().calls.clear(); + driver + .update_sandbox_image(&SandboxUpdatePreconditions { + validate_only, + ..request() + }) + .await + .unwrap(); + let state = state.lock().unwrap(); + let mut actual = pod_read_names(&state); + actual.sort_unstable(); + let mut expected = vec!["os-supervisor-sandbox-1", "sandbox-cr"]; + if recorded_name == Some("recorded-workload") { + expected.insert(1, "recorded-workload"); + } + assert_eq!(actual, expected, "{version}: {recorded_name:?}"); + assert_eq!(state.patches.len(), usize::from(!validate_only)); + } + } + } +} + +#[tokio::test] +async fn image_update_propagates_known_pod_read_errors_without_writes() { + for version in [SANDBOX_VERSION_V1ALPHA1, SANDBOX_VERSION_V1BETA1] { + for name in ["sandbox-cr", "recorded-workload", "os-supervisor-sandbox-1"] { + for status in [ + http::StatusCode::FORBIDDEN, + http::StatusCode::INTERNAL_SERVER_ERROR, + ] { + let (driver, state) = fixture(version, false); + state.lock().unwrap().pod_error = Some((name.into(), status)); + for validate_only in [true, false] { + let error = driver + .update_sandbox_image(&SandboxUpdatePreconditions { + validate_only, + ..request() + }) + .await + .unwrap_err(); + assert!(error.to_string().contains("pod read failed"), "{error}"); + assert!(state.lock().unwrap().patches.is_empty()); + } + } + } + } +} + +#[tokio::test] +async fn image_update_rejects_image_name_and_runtime_identity_conflicts() { + for scenario in ["image", "name", "runtime", "seed", "missing-provenance"] { + let (driver, state) = fixture(SANDBOX_VERSION_V1BETA1, false); + let mut request = request(); + match scenario { + "image" => request.expected_image = "registry.example/agent:other".into(), + "name" => request.name = "different-name".into(), + "runtime" => { + request.expected_runtime_identity = + "kubernetes://openshell/replacement-uid/old-supervisor".into(); + } + "seed" => { + state.lock().unwrap().sandbox["spec"]["podTemplate"]["spec"]["initContainers"][1] + ["image"] = serde_json::json!("different-seed:v1"); + } + "missing-provenance" => { + state.lock().unwrap().sandbox["metadata"]["annotations"] + .as_object_mut() + .unwrap() + .remove(crate::resource_admission::CONFIG_USED); + } + _ => unreachable!(), + } + assert!( + matches!( + driver.update_sandbox_image(&request).await, + Err(KubernetesDriverError::Precondition(_)) + ), + "{scenario}" + ); + assert!(state.lock().unwrap().patches.is_empty()); + } +} + +#[tokio::test] +async fn image_update_revalidates_attachment_admission_and_uid_even_on_retry() { + for retry in [false, true] { + for scenario in ["labels", "uid", "config-disabled"] { + let (mut driver, state) = fixture(SANDBOX_VERSION_V1BETA1, false); + if retry { + driver.update_sandbox_image(&request()).await.unwrap(); + } + match scenario { + "labels" => state.lock().unwrap().pvc["metadata"]["labels"] = serde_json::json!({}), + "uid" => { + state.lock().unwrap().pvc["metadata"]["uid"] = + serde_json::json!("replacement-pvc-uid"); + } + "config-disabled" => driver.config.allow_driver_config = false, + _ => unreachable!(), + } + assert!( + matches!( + driver.update_sandbox_image(&request()).await, + Err(KubernetesDriverError::Precondition(_)) + ), + "{scenario} retry={retry}" + ); + assert_eq!(state.lock().unwrap().patches.len(), usize::from(retry)); + } + } +} + +#[tokio::test] +async fn image_update_conflict_does_not_blindly_retry_or_recreate() { + let (driver, state) = fixture(SANDBOX_VERSION_V1BETA1, false); + state.lock().unwrap().conflict = true; + let before = state.lock().unwrap().sandbox.clone(); + assert!( + matches!(driver.update_sandbox_image(&request()).await, Err(KubernetesDriverError::Precondition(message)) if message.contains("changed during image update")) + ); + let state = state.lock().unwrap(); + assert_eq!(state.patches.len(), 1); + assert_eq!(state.sandbox, before); +} + +#[tokio::test] +async fn image_update_grpc_maps_validation_and_precondition_errors() { + use openshell_core::proto::compute::v1::compute_driver_configuration_updates_server::ComputeDriverConfigurationUpdates; + let (driver, state) = fixture(SANDBOX_VERSION_V1BETA1, false); + let service = crate::ComputeDriverService::new(driver); + let mut invalid = request(); + invalid.image = "https://registry.example/image:v2".into(); + let error = service + .update_sandbox_configuration(tonic::Request::new( + openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest { + preconditions: Some(invalid), + ..Default::default() + }, + )) + .await + .unwrap_err(); + assert_eq!(error.code(), tonic::Code::InvalidArgument); + assert!(state.lock().unwrap().calls.is_empty()); + let mut conflict = request(); + conflict.expected_image = "other:v1".into(); + let error = service + .update_sandbox_configuration(tonic::Request::new( + openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest { + preconditions: Some(conflict), + ..Default::default() + }, + )) + .await + .unwrap_err(); + assert_eq!(error.code(), tonic::Code::FailedPrecondition); +} + +#[tokio::test] +async fn image_update_can_repair_a_legacy_invalid_image_reference() { + let (driver, state) = fixture(SANDBOX_VERSION_V1BETA1, false); + let invalid_image = "UpperCaseRepository"; + { + let mut state = state.lock().unwrap(); + state.sandbox["spec"]["podTemplate"]["spec"]["containers"][0]["image"] = + serde_json::json!(invalid_image); + state.sandbox["spec"]["podTemplate"]["spec"]["initContainers"][1]["image"] = + serde_json::json!(invalid_image); + } + let mut update = request(); + update.expected_image = invalid_image.into(); + driver.update_sandbox_image(&update).await.unwrap(); + let state = state.lock().unwrap(); + assert_eq!(state.patches.len(), 1); + assert_eq!( + state.sandbox["spec"]["podTemplate"]["spec"]["containers"][0]["image"], + update.image + ); + assert_eq!(state.sandbox["metadata"]["uid"], "resource-uid"); +} + +#[test] +fn image_update_validates_container_references() { + for image in [ + "agent", + "registry.example:5000/team/agent:v2", + "localhost/agent:Tag_1", + "[::1]:5000/team/agent", + "team/image__name:v2", + &format!("registry.example/agent@sha256:{}", "a".repeat(64)), + ] { + validate_workload_image(image).unwrap(); + } + for image in [ + "", + " image:v2", + "image:v2\n", + "https://host/image", + "host//image", + "UpperCaseRepository", + "agent:", + "agent@sha256:invalid", + "agent@sha256:abc", + &format!("agent@sha256:{}", "a".repeat(32)), + &format!("agent@sha256:{}", "a".repeat(65)), + &format!("{}:v1", "a".repeat(256)), + "../agent", + &"a".repeat(2049), + ] { + assert!(validate_workload_image(image).is_err(), "{image:?}"); + } +} + +#[tokio::test] +async fn image_update_serializes_with_other_lifecycle_operations() { + let (driver, state) = fixture(SANDBOX_VERSION_V1BETA1, false); + let guard = driver + .lifecycle_gates + .gate_for("sandbox-1") + .lock_owned() + .await; + let request = request(); + let pending = driver.update_sandbox_image(&request); + tokio::pin!(pending); + assert!( + tokio::time::timeout(Duration::from_millis(10), &mut pending) + .await + .is_err() + ); + assert!(state.lock().unwrap().calls.is_empty()); + drop(guard); + pending.await.unwrap(); +} + +#[tokio::test] +async fn configuration_update_atomically_changes_restart_inputs_and_retries_without_writes() { + use openshell_core::proto::compute::v1::{ + SandboxEnvironmentUpdate, SandboxMainProcessUpdate, UpdateSandboxConfigurationRequest, + }; + for version in [SANDBOX_VERSION_V1ALPHA1, SANDBOX_VERSION_V1BETA1] { + let (driver, state) = fixture(version, false); + { + let mut state = state.lock().unwrap(); + state.sandbox["metadata"]["annotations"] + [ANNOTATION_SANDBOX_RUNTIME_MAIN_PROCESS_SPEC] = serde_json::json!( + openshell_core::sandbox_env::MainProcessConfig::encode_driver_spec(None).unwrap() + ); + state.sandbox["spec"]["podTemplate"]["spec"]["containers"][0]["env"] + .as_array_mut() + .unwrap() + .push(serde_json::json!({"name":"OPENSHELL_TEST_RUNTIME", "value":"protected"})); + } + let before = state.lock().unwrap().sandbox.clone(); + let mut update = UpdateSandboxConfigurationRequest { + preconditions: Some(request()), + main_process: Some(SandboxMainProcessUpdate { + command: vec!["/usr/local/bin/start-agent".into(), "--new".into()], + ..Default::default() + }), + environment: Some(SandboxEnvironmentUpdate { + values: std::collections::HashMap::from([( + "AGENT_MODE".into(), + "sensitive-environment-value".into(), + )]), + }), + }; + update.preconditions.as_mut().unwrap().validate_only = true; + driver.update_sandbox_configuration(&update).await.unwrap(); + assert_eq!(state.lock().unwrap().sandbox, before); + update.preconditions.as_mut().unwrap().validate_only = false; + driver.update_sandbox_configuration(&update).await.unwrap(); + { + let state = state.lock().unwrap(); + assert_eq!(state.patches.len(), 1); + assert_eq!( + state.sandbox["spec"]["volumeClaimTemplates"], + before["spec"]["volumeClaimTemplates"] + ); + let agent = &state.sandbox["spec"]["podTemplate"]["spec"]["containers"][0]; + assert_eq!( + agent["command"], + before["spec"]["podTemplate"]["spec"]["containers"][0]["command"] + ); + assert_eq!(agent["image"], update.preconditions.as_ref().unwrap().image); + let env = agent["env"].as_array().unwrap(); + assert!(env.iter().any(|entry| entry["name"] == "AGENT_MODE" + && entry["value"] == "sensitive-environment-value")); + assert!( + env.iter() + .any(|entry| entry["name"] == "OPENSHELL_TEST_RUNTIME" + && entry["value"] == "protected") + ); + assert!(!env.iter().any(|entry| entry["name"] == "UNCHANGED")); + let encoded = state.sandbox["metadata"]["annotations"] + [ANNOTATION_SANDBOX_RUNTIME_MAIN_PROCESS_SPEC] + .as_str() + .unwrap(); + assert_eq!( + openshell_core::sandbox_env::MainProcessConfig::decode(encoded) + .unwrap() + .command, + update.main_process.as_ref().unwrap().command + ); + let evidence = state.sandbox["metadata"]["annotations"][IMAGE_UPDATE_ANNOTATION] + .as_str() + .unwrap(); + assert!( + !evidence.contains("sensitive-environment-value"), + "operation evidence must not contain environment values" + ); + } + driver.update_sandbox_configuration(&update).await.unwrap(); + assert_eq!(state.lock().unwrap().patches.len(), 1); + let mut changed = update.clone(); + changed + .environment + .as_mut() + .unwrap() + .values + .insert("AGENT_MODE".into(), "different".into()); + assert!(driver.update_sandbox_configuration(&changed).await.is_err()); + changed = update.clone(); + changed + .main_process + .as_mut() + .unwrap() + .command + .push("--different".into()); + assert!(driver.update_sandbox_configuration(&changed).await.is_err()); + update.preconditions.as_mut().unwrap().expected_image = + update.preconditions.as_ref().unwrap().image.clone(); + update.preconditions.as_mut().unwrap().revision += 1; + update.preconditions.as_mut().unwrap().operation_id = "clear-configuration".into(); + update.main_process.as_mut().unwrap().command.clear(); + update.environment.as_mut().unwrap().values.clear(); + driver.update_sandbox_configuration(&update).await.unwrap(); + let state = state.lock().unwrap(); + let encoded = state.sandbox["metadata"]["annotations"] + [ANNOTATION_SANDBOX_RUNTIME_MAIN_PROCESS_SPEC] + .as_str() + .unwrap(); + let main = openshell_core::sandbox_env::MainProcessConfig::decode(encoded).unwrap(); + assert!(main.command.is_empty() && main.tty); + assert_eq!( + state.sandbox["spec"]["podTemplate"]["spec"]["containers"][0]["env"] + .as_array() + .unwrap() + .len(), + 1 + ); + } +} + +#[tokio::test] +async fn configuration_update_rejects_oversized_encoded_annotations_without_writes() { + use openshell_core::proto::compute::v1::{ + SandboxMainProcessUpdate, UpdateSandboxConfigurationRequest, + }; + // Both commands fit the gateway's count, per-argument, and total byte + // limits. The second exceeds the annotation budget only after JSON escaping. + let mut full_command = vec!["/bin/echo".to_string()]; + full_command.extend((0..7).map(|_| "a".repeat(32 * 1024))); + full_command.push("a".repeat(32 * 1024 - "/bin/echo".len())); + let mut escaped_command = vec!["/bin/echo".to_string()]; + escaped_command.extend((0..8).map(|_| "\"".repeat(16 * 1024))); + for version in [SANDBOX_VERSION_V1ALPHA1, SANDBOX_VERSION_V1BETA1] { + for command in [&full_command, &escaped_command] { + let (driver, state) = fixture(version, false); + state.lock().unwrap().sandbox["metadata"]["annotations"] + [ANNOTATION_SANDBOX_RUNTIME_MAIN_PROCESS_SPEC] = serde_json::json!( + openshell_core::sandbox_env::MainProcessConfig::encode_driver_spec(None).unwrap() + ); + let before = state.lock().unwrap().sandbox.clone(); + let mut update = UpdateSandboxConfigurationRequest { + preconditions: Some(request()), + main_process: Some(SandboxMainProcessUpdate { + command: command.clone(), + tty: false, + }), + environment: None, + }; + for validate_only in [true, false] { + update.preconditions.as_mut().unwrap().validate_only = validate_only; + let error = driver + .update_sandbox_configuration(&update) + .await + .unwrap_err(); + assert!(matches!(error, KubernetesDriverError::InvalidArgument(_))); + assert!(error.to_string().contains("annotation size limit")); + let state = state.lock().unwrap(); + assert_eq!(state.sandbox, before); + assert!(state.patches.is_empty()); + } + } + } +} + +#[tokio::test] +async fn image_update_annotation_budget_includes_existing_keys_and_values() { + for version in [SANDBOX_VERSION_V1ALPHA1, SANDBOX_VERSION_V1BETA1] { + let (driver, state) = fixture(version, false); + let mut request = request(); + request.validate_only = true; + let mut candidate: DynamicObject = + serde_json::from_value(state.lock().unwrap().sandbox.clone()).unwrap(); + prepare_update(&mut candidate, &request, None).unwrap(); + let existing_bytes: usize = candidate + .metadata + .annotations + .unwrap() + .iter() + .map(|(key, value)| key.len() + value.len()) + .sum(); + let key = "operator.example/padding"; + let padding = "a".repeat(MAX_ANNOTATION_BYTES - existing_bytes - key.len()); + state.lock().unwrap().sandbox["metadata"]["annotations"][key] = serde_json::json!(padding); + driver.update_sandbox_image(&request).await.unwrap(); + state.lock().unwrap().sandbox["metadata"]["annotations"][key] = + serde_json::json!(format!("{padding}a")); + let error = driver.update_sandbox_image(&request).await.unwrap_err(); + assert!(matches!(error, KubernetesDriverError::InvalidArgument(_))); + assert!(state.lock().unwrap().patches.is_empty()); + } +} + +#[tokio::test] +async fn configuration_update_rejects_reserved_environment_without_writes() { + use openshell_core::proto::compute::v1::{ + SandboxEnvironmentUpdate, UpdateSandboxConfigurationRequest, + }; + let (driver, state) = fixture(SANDBOX_VERSION_V1BETA1, false); + let update = UpdateSandboxConfigurationRequest { + preconditions: Some(request()), + main_process: None, + environment: Some(SandboxEnvironmentUpdate { + values: std::collections::HashMap::from([( + "OPENSHELL_SESSION".into(), + "forged".into(), + )]), + }), + }; + assert!(matches!( + driver.update_sandbox_configuration(&update).await, + Err(KubernetesDriverError::InvalidArgument(_)) + )); + assert!(state.lock().unwrap().calls.is_empty()); +} diff --git a/crates/openshell-driver-kubernetes/src/main.rs b/crates/openshell-driver-kubernetes/src/main.rs index 7803d1561a..e7b9722e17 100644 --- a/crates/openshell-driver-kubernetes/src/main.rs +++ b/crates/openshell-driver-kubernetes/src/main.rs @@ -340,7 +340,9 @@ async fn main() -> Result<()> { .await .into_diagnostic()?; - let service = ComputeDriverServer::new(ComputeDriverService::new(driver)); + let driver_service = ComputeDriverService::new(driver); + let configuration_updates_service = openshell_core::proto::compute::v1::compute_driver_configuration_updates_server::ComputeDriverConfigurationUpdatesServer::new(driver_service.clone()); + let service = ComputeDriverServer::new(driver_service); let shutdown = async move { shutdown_signal().await; let _ = shutdown_tx.send(true); @@ -354,6 +356,7 @@ async fn main() -> Result<()> { tonic::transport::Server::builder() .layer(openshell_otel::compute_driver_rpc_layer()) .add_service(service) + .add_service(configuration_updates_service) .serve_with_incoming_shutdown( openshell_core::external_driver_socket::SameUidUnixIncoming::new(listener), shutdown, @@ -365,6 +368,7 @@ async fn main() -> Result<()> { tonic::transport::Server::builder() .layer(openshell_otel::compute_driver_rpc_layer()) .add_service(service) + .add_service(configuration_updates_service) .serve_with_shutdown(args.bind_address, shutdown) .await .into_diagnostic() diff --git a/crates/openshell-driver-mxc/src/grpc.rs b/crates/openshell-driver-mxc/src/grpc.rs index 91c61d2088..ca834987cb 100644 --- a/crates/openshell-driver-mxc/src/grpc.rs +++ b/crates/openshell-driver-mxc/src/grpc.rs @@ -32,6 +32,9 @@ impl ComputeDriverService { } } +#[tonic::async_trait] +impl openshell_core::compute::ComputeDriver for ComputeDriverService {} + #[tonic::async_trait] impl ComputeDriver for ComputeDriverService { async fn get_capabilities( diff --git a/crates/openshell-driver-podman/src/grpc.rs b/crates/openshell-driver-podman/src/grpc.rs index 59b2fe1f60..dff752db24 100644 --- a/crates/openshell-driver-podman/src/grpc.rs +++ b/crates/openshell-driver-podman/src/grpc.rs @@ -48,6 +48,9 @@ impl ComputeDriverService { } } +#[tonic::async_trait] +impl openshell_core::compute::ComputeDriver for ComputeDriverService {} + #[tonic::async_trait] impl ComputeDriver for ComputeDriverService { async fn authenticate_sandbox( diff --git a/crates/openshell-driver-vm/src/driver.rs b/crates/openshell-driver-vm/src/driver.rs index 304c95bc32..6260a52842 100644 --- a/crates/openshell-driver-vm/src/driver.rs +++ b/crates/openshell-driver-vm/src/driver.rs @@ -5323,6 +5323,9 @@ fn configure_main_exit_marker(command: &mut Command, state_dir: &Path) { .arg(state_dir.join(MAIN_PROCESS_EXITED_FILE)); } +#[tonic::async_trait] +impl openshell_core::compute::ComputeDriver for VmDriver {} + #[tonic::async_trait] impl ComputeDriver for VmDriver { async fn authenticate_sandbox( diff --git a/crates/openshell-gateway-interceptors/src/routes.rs b/crates/openshell-gateway-interceptors/src/routes.rs index c47f42e40d..30e053e323 100644 --- a/crates/openshell-gateway-interceptors/src/routes.rs +++ b/crates/openshell-gateway-interceptors/src/routes.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Interceptable `OpenShell` route classification. +//! Interceptable public gateway route classification. use std::collections::{BTreeMap, BTreeSet}; @@ -10,6 +10,10 @@ use prost_reflect::DescriptorPool; use crate::{InterceptorError, Result}; const SERVICE_OPEN_SHELL: &str = "openshell.v1.OpenShell"; +const SERVICE_SANDBOX_UPDATES: &str = "openshell.v1.SandboxUpdates"; + +/// Unary sandbox-update methods that may be targeted by gateway interceptors. +pub const INTERCEPTABLE_SANDBOX_UPDATE_METHODS: &[&str] = &["UpdateSandbox"]; /// Unary `openshell.v1.OpenShell` methods that may be targeted by gateway /// interceptors. New methods are non-interceptable until deliberately added @@ -58,25 +62,31 @@ impl OpenShellRouteIndex { } pub(crate) fn from_descriptor_pool(pool: &DescriptorPool) -> Result { - let service = pool + let open_shell = pool .get_service_by_name(SERVICE_OPEN_SHELL) .ok_or_else(|| { InterceptorError::Config(format!( "descriptor set does not contain service '{SERVICE_OPEN_SHELL}'" )) })?; + // SandboxUpdates is an optional extension: baseline descriptor sets + // still need only OpenShell, but a present extension must be valid. + let sandbox_updates = pool.get_service_by_name(SERVICE_SANDBOX_UPDATES); let mut all_methods = BTreeSet::new(); let mut unary_methods = BTreeSet::new(); let mut input_types = BTreeMap::new(); let mut output_types = BTreeMap::new(); - for method in service.methods() { - let name = method.name().to_string(); - all_methods.insert(name.clone()); - if !method.is_client_streaming() && !method.is_server_streaming() { - unary_methods.insert(name.clone()); - input_types.insert(name.clone(), method.input().full_name().to_string()); - output_types.insert(name, method.output().full_name().to_string()); + for service in std::iter::once(&open_shell).chain(sandbox_updates.as_ref()) { + let service_name = service.full_name(); + for method in service.methods() { + let route = format!("{service_name}/{}", method.name()); + all_methods.insert(route.clone()); + if !method.is_client_streaming() && !method.is_server_streaming() { + unary_methods.insert(route.clone()); + input_types.insert(route.clone(), method.input().full_name().to_string()); + output_types.insert(route, method.output().full_name().to_string()); + } } } @@ -86,43 +96,49 @@ impl OpenShellRouteIndex { input_types, output_types, }; - index.validate_interceptable_list()?; + index.validate_interceptable_list(SERVICE_OPEN_SHELL, INTERCEPTABLE_METHODS)?; + if sandbox_updates.is_some() { + index.validate_interceptable_list( + SERVICE_SANDBOX_UPDATES, + INTERCEPTABLE_SANDBOX_UPDATE_METHODS, + )?; + } Ok(index) } #[must_use] pub fn is_interceptable(&self, service: &str, method: &str) -> bool { - service == SERVICE_OPEN_SHELL - && self.unary_methods.contains(method) - && INTERCEPTABLE_METHODS.contains(&method) + let allowed = match service { + SERVICE_OPEN_SHELL => INTERCEPTABLE_METHODS, + SERVICE_SANDBOX_UPDATES => INTERCEPTABLE_SANDBOX_UPDATE_METHODS, + _ => return false, + }; + allowed.contains(&method) && self.unary_methods.contains(&format!("{service}/{method}")) } #[must_use] pub fn input_type(&self, service: &str, method: &str) -> Option<&str> { - if service == SERVICE_OPEN_SHELL && self.unary_methods.contains(method) { - self.input_types.get(method).map(String::as_str) - } else { - None - } + self.input_types + .get(&format!("{service}/{method}")) + .map(String::as_str) } #[must_use] pub fn output_type(&self, service: &str, method: &str) -> Option<&str> { - if service == SERVICE_OPEN_SHELL && self.unary_methods.contains(method) { - self.output_types.get(method).map(String::as_str) - } else { - None - } + self.output_types + .get(&format!("{service}/{method}")) + .map(String::as_str) } - fn validate_interceptable_list(&self) -> Result<()> { + fn validate_interceptable_list(&self, service: &str, methods: &[&str]) -> Result<()> { let mut stale = Vec::new(); let mut streaming = Vec::new(); - for method in INTERCEPTABLE_METHODS { - if !self.all_methods.contains(*method) { - stale.push((*method).to_string()); - } else if !self.unary_methods.contains(*method) { - streaming.push((*method).to_string()); + for method in methods { + let route = format!("{service}/{method}"); + if !self.all_methods.contains(&route) { + stale.push(route); + } else if !self.unary_methods.contains(&route) { + streaming.push(route); } } if !stale.is_empty() { @@ -141,8 +157,158 @@ impl OpenShellRouteIndex { #[cfg(test)] mod tests { + use prost::Message; + use prost_types::{FileDescriptorSet, ServiceDescriptorProto}; + use super::*; + fn descriptor_set() -> FileDescriptorSet { + FileDescriptorSet::decode(openshell_core::FILE_DESCRIPTOR_SET).unwrap() + } + + fn service_mut<'a>( + descriptors: &'a mut FileDescriptorSet, + name: &str, + ) -> &'a mut ServiceDescriptorProto { + descriptors + .file + .iter_mut() + .filter(|file| file.package.as_deref() == Some("openshell.v1")) + .flat_map(|file| &mut file.service) + .find(|service| service.name.as_deref() == Some(name)) + .unwrap() + } + + #[test] + fn open_shell_only_descriptor_set_is_accepted() { + let mut descriptors = descriptor_set(); + for file in &mut descriptors.file { + file.service.retain(|service| { + file.package.as_deref() == Some("openshell.v1") + && service.name.as_deref() == Some("OpenShell") + }); + } + + let index = OpenShellRouteIndex::from_descriptor_set(&descriptors.encode_to_vec()).unwrap(); + for method in INTERCEPTABLE_METHODS { + assert!(index.is_interceptable(SERVICE_OPEN_SHELL, method)); + assert!(index.input_type(SERVICE_OPEN_SHELL, method).is_some()); + assert!(index.output_type(SERVICE_OPEN_SHELL, method).is_some()); + } + assert!(!index.is_interceptable(SERVICE_SANDBOX_UPDATES, "UpdateSandbox")); + assert!( + index + .input_type(SERVICE_SANDBOX_UPDATES, "UpdateSandbox") + .is_none() + ); + assert!( + index + .output_type(SERVICE_SANDBOX_UPDATES, "UpdateSandbox") + .is_none() + ); + } + + #[test] + fn missing_open_shell_service_is_rejected() { + let mut descriptors = descriptor_set(); + for file in &mut descriptors.file { + file.service + .retain(|service| service.name.as_deref() != Some("OpenShell")); + } + + let error = + OpenShellRouteIndex::from_descriptor_set(&descriptors.encode_to_vec()).unwrap_err(); + assert!(error.to_string().contains(&format!( + "descriptor set does not contain service '{SERVICE_OPEN_SHELL}'" + ))); + } + + #[test] + fn present_sandbox_updates_service_requires_update_sandbox() { + let mut descriptors = descriptor_set(); + service_mut(&mut descriptors, "SandboxUpdates") + .method + .clear(); + + let error = + OpenShellRouteIndex::from_descriptor_set(&descriptors.encode_to_vec()).unwrap_err(); + assert!(error.to_string().contains("stale methods")); + assert!( + error + .to_string() + .contains("openshell.v1.SandboxUpdates/UpdateSandbox") + ); + } + + #[test] + fn streaming_update_sandbox_is_rejected() { + for (client_streaming, server_streaming) in [(true, false), (false, true), (true, true)] { + let mut descriptors = descriptor_set(); + let method = &mut service_mut(&mut descriptors, "SandboxUpdates").method[0]; + assert_eq!(method.name.as_deref(), Some("UpdateSandbox")); + method.client_streaming = Some(client_streaming); + method.server_streaming = Some(server_streaming); + + let error = + OpenShellRouteIndex::from_descriptor_set(&descriptors.encode_to_vec()).unwrap_err(); + assert!(error.to_string().contains("streaming methods")); + assert!( + error + .to_string() + .contains("openshell.v1.SandboxUpdates/UpdateSandbox") + ); + } + } + + #[test] + fn unlisted_sandbox_update_methods_are_not_interceptable() { + let mut descriptors = descriptor_set(); + let service = service_mut(&mut descriptors, "SandboxUpdates"); + let mut future_method = service.method[0].clone(); + future_method.name = Some("FutureUnaryMethod".to_string()); + service.method.push(future_method); + + let index = OpenShellRouteIndex::from_descriptor_set(&descriptors.encode_to_vec()).unwrap(); + assert!(index.is_interceptable(SERVICE_SANDBOX_UPDATES, "UpdateSandbox")); + assert!(!index.is_interceptable(SERVICE_SANDBOX_UPDATES, "FutureUnaryMethod")); + assert_eq!( + index.input_type(SERVICE_SANDBOX_UPDATES, "FutureUnaryMethod"), + Some("openshell.v1.UpdateSandboxRequest") + ); + } + + #[test] + fn open_shell_allowlist_validation_remains_required() { + let mut missing_method = descriptor_set(); + service_mut(&mut missing_method, "OpenShell") + .method + .retain(|method| method.name.as_deref() != Some("CreateSandbox")); + let error = + OpenShellRouteIndex::from_descriptor_set(&missing_method.encode_to_vec()).unwrap_err(); + assert!(error.to_string().contains("stale methods")); + assert!( + error + .to_string() + .contains("openshell.v1.OpenShell/CreateSandbox") + ); + + let mut streaming_method = descriptor_set(); + let method = service_mut(&mut streaming_method, "OpenShell") + .method + .iter_mut() + .find(|method| method.name.as_deref() == Some("CreateSandbox")) + .unwrap(); + method.server_streaming = Some(true); + let error = OpenShellRouteIndex::from_descriptor_set(&streaming_method.encode_to_vec()) + .unwrap_err(); + assert!(error.to_string().contains("streaming methods")); + assert!( + error + .to_string() + .contains("openshell.v1.OpenShell/CreateSandbox") + ); + } + #[test] fn interceptable_entries_match_real_unary_methods() { OpenShellRouteIndex::from_descriptor_set(openshell_core::FILE_DESCRIPTOR_SET).unwrap(); @@ -153,6 +319,26 @@ mod tests { let index = OpenShellRouteIndex::from_descriptor_set(openshell_core::FILE_DESCRIPTOR_SET).unwrap(); assert!(index.is_interceptable("openshell.v1.OpenShell", "CreateSandbox")); + assert!(index.is_interceptable(SERVICE_SANDBOX_UPDATES, "UpdateSandbox")); + assert!(!index.is_interceptable(SERVICE_OPEN_SHELL, "UpdateSandbox")); + assert!(!index.is_interceptable(SERVICE_SANDBOX_UPDATES, "CreateSandbox")); + assert_eq!( + index.input_type(SERVICE_OPEN_SHELL, "CreateSandbox"), + Some("openshell.v1.CreateSandboxRequest") + ); + assert_eq!( + index.input_type(SERVICE_SANDBOX_UPDATES, "UpdateSandbox"), + Some("openshell.v1.UpdateSandboxRequest") + ); + assert_eq!( + index.output_type(SERVICE_SANDBOX_UPDATES, "UpdateSandbox"), + Some("openshell.v1.SandboxResponse") + ); + assert!( + index + .input_type(SERVICE_OPEN_SHELL, "UpdateSandbox") + .is_none() + ); assert!(index.is_interceptable("openshell.v1.OpenShell", "UpdateConfig")); assert!(index.is_interceptable("openshell.v1.OpenShell", "SubmitPolicyAnalysis")); assert!(!index.is_interceptable("openshell.v1.OpenShell", "Health")); diff --git a/crates/openshell-gateway/src/lib.rs b/crates/openshell-gateway/src/lib.rs index a6a50774b6..eda99c9b33 100644 --- a/crates/openshell-gateway/src/lib.rs +++ b/crates/openshell-gateway/src/lib.rs @@ -289,6 +289,18 @@ impl openshell_server::ComputeDriverFactory for KubernetesFactory { &self, context: openshell_server::ComputeDriverBuildContext<'_>, ) -> openshell_core::Result { + self.build_with_configuration_updates(context) + .await + .map(|(instance, _)| instance) + } + + async fn build_with_configuration_updates( + &self, + context: openshell_server::ComputeDriverBuildContext<'_>, + ) -> openshell_core::Result<( + openshell_server::ComputeDriverInstance, + Option, + )> { let mut config = kubernetes_config(context.config_context())?; config.supervisor_otlp_endpoint = context .otlp_config() @@ -330,9 +342,12 @@ impl openshell_server::ComputeDriverFactory for KubernetesFactory { .await } .map_err(|error| openshell_core::Error::execution(error.to_string()))?; - let driver = openshell_driver_kubernetes::ComputeDriverService::new_in_process(driver); - Ok(openshell_server::ComputeDriverInstance::InProcess( - std::sync::Arc::new(driver), + let driver = std::sync::Arc::new( + openshell_driver_kubernetes::ComputeDriverService::new_in_process(driver), + ); + Ok(( + openshell_server::ComputeDriverInstance::InProcess(driver.clone()), + Some(driver), )) } } diff --git a/crates/openshell-otel/src/grpc.rs b/crates/openshell-otel/src/grpc.rs index 8184caae35..9edb630318 100644 --- a/crates/openshell-otel/src/grpc.rs +++ b/crates/openshell-otel/src/grpc.rs @@ -102,6 +102,11 @@ pub mod rpc { "CreateSandbox", "openshell.compute.v1.ComputeDriver/CreateSandbox", ); + pub const UPDATE_SANDBOX_CONFIGURATION: ComputeDriverRpc = ComputeDriverRpc { + service: "openshell.compute.v1.ComputeDriverConfigurationUpdates", + method: "UpdateSandboxConfiguration", + operation: "openshell.compute.v1.ComputeDriverConfigurationUpdates/UpdateSandboxConfiguration", + }; pub const GET_SANDBOX: ComputeDriverRpc = ComputeDriverRpc::new( "GetSandbox", "openshell.compute.v1.ComputeDriver/GetSandbox", @@ -194,6 +199,7 @@ pub fn compute_driver_rpc_operation(path: &str) -> Option { Some("GetCapabilities") => Some(rpc::GET_CAPABILITIES), Some("ValidateSandboxCreate") => Some(rpc::VALIDATE_SANDBOX_CREATE), Some("CreateSandbox") => Some(rpc::CREATE_SANDBOX), + Some("UpdateSandboxConfiguration") => Some(rpc::UPDATE_SANDBOX_CONFIGURATION), Some("GetSandbox") => Some(rpc::GET_SANDBOX), Some("ListSandboxes") => Some(rpc::LIST_SANDBOXES), Some("StopSandbox") => Some(rpc::STOP_SANDBOX), @@ -317,6 +323,7 @@ mod tests { rpc::GET_CAPABILITIES, rpc::VALIDATE_SANDBOX_CREATE, rpc::CREATE_SANDBOX, + rpc::UPDATE_SANDBOX_CONFIGURATION, rpc::GET_SANDBOX, rpc::LIST_SANDBOXES, rpc::STOP_SANDBOX, diff --git a/crates/openshell-sdk/src/client.rs b/crates/openshell-sdk/src/client.rs index 59f0dacb74..1f03916ef2 100644 --- a/crates/openshell-sdk/src/client.rs +++ b/crates/openshell-sdk/src/client.rs @@ -12,7 +12,7 @@ use crate::auth::{BearerSlot, EdgeAuthInterceptor, bearer_metadata}; use crate::config::{AuthConfig, ClientConfig}; use crate::error::{Result, SdkError}; use crate::pagination::{Page, Pager}; -use crate::raw::AuthedGrpcClient; +use crate::raw::{AuthedGrpcClient, AuthedSandboxUpdatesClient}; use crate::refresh::{RefreshedToken, TokenSource}; use crate::routing::OwnerCache; use crate::types::{ @@ -122,6 +122,23 @@ impl OpenShellClient { Ok(self.raw_grpc()) } + /// Authenticated client for the optional `SandboxUpdates` service, sharing + /// this client's channel and live bearer slot. Like [`Self::raw_grpc`], this + /// does not refresh or retry OIDC credentials by itself. + pub fn raw_sandbox_updates(&self) -> AuthedSandboxUpdatesClient { + proto::sandbox_updates_client::SandboxUpdatesClient::with_interceptor( + self.channel.clone(), + self.interceptor.clone(), + ) + } + + /// Like [`Self::raw_sandbox_updates`], but refreshes the bearer token first + /// when needed. Reactive retry remains the raw caller's responsibility. + pub async fn raw_sandbox_updates_fresh(&self) -> Result { + self.ensure_fresh().await?; + Ok(self.raw_sandbox_updates()) + } + /// Force an OIDC refresh and write the new token into the live bearer /// slot, regardless of expiry. Returns `true` when a refresher is wired /// and a fresh token was minted, `false` for static auth. Use after a diff --git a/crates/openshell-sdk/src/raw.rs b/crates/openshell-sdk/src/raw.rs index faca71fb59..15e26d52cc 100644 --- a/crates/openshell-sdk/src/raw.rs +++ b/crates/openshell-sdk/src/raw.rs @@ -20,6 +20,7 @@ pub use openshell_core::proto; pub use openshell_core::proto::open_shell_client::OpenShellClient as GrpcClient; +pub use openshell_core::proto::sandbox_updates_client::SandboxUpdatesClient; pub use openshell_core::proto::{ CreateSandboxRequest, CreateSandboxTemplateRequest, CreateWorkspaceRequest, DeleteSandboxRequest, DeleteSandboxTemplateRequest, DeleteWorkspaceRequest, ExecSandboxRequest, @@ -28,12 +29,12 @@ pub use openshell_core::proto::{ ListSandboxTemplatesRequest, ListSandboxesRequest, ListWorkspacesRequest, ProviderDesiredIdentity, ProviderMutationKind, ProviderMutationReceipt, ProviderReadinessObservation, ProviderReadinessReason, ProviderReadinessState, - ProviderReadinessStatus, ResumeSandboxRequest, Sandbox, SandboxPhase as ProtoSandboxPhase, - SandboxResources, SandboxServiceLevel, SandboxSpec as ProtoSandboxSpec, SandboxStartup, - SandboxTemplate, SandboxTemplateResponse, SandboxWorkloadConfig, SandboxWorkloadTemplate, - SandboxWorkloadTemplateProvenance, SandboxWorkloadTemplateSpec, - ServiceStatus as ProtoServiceStatus, StartSandboxRequest, StopSandboxRequest, - SuspendSandboxRequest, Workspace, + ProviderReadinessStatus, ResumeSandboxRequest, Sandbox, SandboxCommandUpdate, SandboxMapUpdate, + SandboxPhase as ProtoSandboxPhase, SandboxResources, SandboxServiceLevel, + SandboxSpec as ProtoSandboxSpec, SandboxStartup, SandboxTemplate, SandboxTemplateResponse, + SandboxWorkloadConfig, SandboxWorkloadTemplate, SandboxWorkloadTemplateProvenance, + SandboxWorkloadTemplateSpec, ServiceStatus as ProtoServiceStatus, StartSandboxRequest, + StopSandboxRequest, SuspendSandboxRequest, UpdateSandboxRequest, Workspace, }; /// Type alias for the gRPC client wrapped in the SDK's auth interceptor. @@ -43,3 +44,12 @@ pub type AuthedGrpcClient = GrpcClient< crate::EdgeAuthInterceptor, >, >; + +/// Sandbox-updates client using the same live auth slot and transport as the +/// main service client. +pub type AuthedSandboxUpdatesClient = SandboxUpdatesClient< + tonic::service::interceptor::InterceptedService< + tonic::transport::Channel, + crate::EdgeAuthInterceptor, + >, +>; diff --git a/crates/openshell-sdk/tests/client_mock.rs b/crates/openshell-sdk/tests/client_mock.rs index b730c41d16..1f3f791a08 100644 --- a/crates/openshell-sdk/tests/client_mock.rs +++ b/crates/openshell-sdk/tests/client_mock.rs @@ -10,6 +10,7 @@ use openshell_core::proto; use openshell_core::proto::open_shell_server::{OpenShell, OpenShellServer}; +use openshell_core::proto::sandbox_updates_server::{SandboxUpdates, SandboxUpdatesServer}; use openshell_core::replica_routing; use openshell_sdk::{ AuthConfig, ClientConfig, ExecOptions, ListOptions, OpenShellClient, Refresh, RefreshError, @@ -60,6 +61,8 @@ struct MockState { last_start: Mutex>, last_suspend: Mutex>, last_resume: Mutex>, + + last_update: Mutex>, last_list_request: Mutex>, list_requests: Mutex>, last_exec_request: Mutex>, @@ -1172,16 +1175,40 @@ impl OpenShell for TestOpenShell { } } +#[tonic::async_trait] +impl SandboxUpdates for TestOpenShell { + async fn update_sandbox( + &self, + request: tonic::Request, + ) -> Result, Status> { + if let Some(expected) = &self.state.require_bearer { + let got = request + .metadata() + .get("authorization") + .and_then(|value| value.to_str().ok()); + if got != Some(expected.as_str()) { + return Err(Status::unauthenticated("invalid or missing bearer token")); + } + } + *self.state.last_update.lock().await = Some(request.into_inner()); + Ok(Response::new(proto::SandboxResponse::default())) + } +} + /// Spin up the mock gateway, return its endpoint URL. async fn start_mock(state: Arc) -> String { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let endpoint = format!("http://{addr}"); let stream = TcpListenerStream::new(listener); + let updates = SandboxUpdatesServer::new(TestOpenShell { + state: state.clone(), + }); let svc = OpenShellServer::new(TestOpenShell { state }); tokio::spawn(async move { let _ = tonic::transport::Server::builder() .add_service(svc) + .add_service(updates) .serve_with_incoming(stream) .await; }); @@ -2371,6 +2398,65 @@ async fn raw_grpc_fresh_refreshes_before_raw_call() { ); } +#[tokio::test] +async fn raw_sandbox_updates_shares_transport_and_live_auth_slot() { + let state = Arc::new(MockState { + require_bearer: Some("Bearer fresh-token".to_string()), + ..Default::default() + }); + let endpoint = start_mock(state.clone()).await; + let calls = Arc::new(AtomicU32::new(0)); + let mut config = ClientConfig::new(&endpoint); + config.auth = Some(AuthConfig::Oidc { + token: "stale-token".to_string(), + expires_at: Some( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + + 5, + ), + refresh: Some(Arc::new(OneShotRefresher { + calls: calls.clone(), + })), + }); + let client = OpenShellClient::connect(config).await.unwrap(); + let mut updates = client.raw_sandbox_updates(); + let request = proto::UpdateSandboxRequest { + workspace_scope: Some(proto::workspace_selector("team")), + name: "sandbox".into(), + image: Some("example/workload:new".into()), + expected_resource_version: 42, + request_id: "550e8400-e29b-41d4-a716-446655440000".into(), + ..Default::default() + }; + assert_eq!( + updates + .update_sandbox(request.clone()) + .await + .unwrap_err() + .code(), + tonic::Code::Unauthenticated + ); + assert_eq!(calls.load(Ordering::SeqCst), 0); + client + .raw_sandbox_updates_fresh() + .await + .unwrap() + .update_sandbox(request.clone()) + .await + .unwrap(); + assert_eq!(calls.load(Ordering::SeqCst), 1); + // Previously created raw clients read the same refreshed bearer slot. + updates.update_sandbox(request.clone()).await.unwrap(); + client + .raw_grpc() + .health(proto::HealthRequest {}) + .await + .unwrap(); + assert_eq!(*state.last_update.lock().await, Some(request)); +} + #[tokio::test] async fn watch_logs_forwards_logs_and_warnings() { let state = Arc::new(MockState { diff --git a/crates/openshell-sdk/tests/sandbox_status_compatibility.rs b/crates/openshell-sdk/tests/sandbox_status_compatibility.rs new file mode 100644 index 0000000000..7a080a381f --- /dev/null +++ b/crates/openshell-sdk/tests/sandbox_status_compatibility.rs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Compile-time source-compatibility fixture for the v0.1.5 public status shape. + +use openshell_sdk::raw::proto::SandboxStatus; + +#[test] +fn baseline_exhaustive_sandbox_status_literal_still_compiles() { + // Intentionally no `..Default::default()`: adding even an optional public + // field breaks existing downstream Rust exhaustive struct literals. + let status = SandboxStatus { + agent_pod: String::new(), + agent_fd: String::new(), + sandbox_fd: String::new(), + conditions: Vec::new(), + phase: 0, + current_policy_version: 0, + main_process_instance_id: String::new(), + exit_code: None, + endpoint_statuses: Vec::new(), + configuration_admission: None, + configuration_activated: None, + provisioning: None, + restart_count: 0, + next_restart_time: None, + main_process_started_time: None, + last_token_grant_failure: None, + }; + assert_eq!(status, SandboxStatus::default()); +} diff --git a/crates/openshell-server/README.md b/crates/openshell-server/README.md index 3af2d80f6a..ef7b0546b5 100644 --- a/crates/openshell-server/README.md +++ b/crates/openshell-server/README.md @@ -123,6 +123,50 @@ or two minutes after the withdrawal, after their owner was told to stop them. Pool members still authenticate with the one shared supervisor token, so a member identity is asserted by its hello rather than bound to its Pod. +## Optional in-process sandbox updates + +`SharedComputeDriver` retains the generated baseline compute-driver trait, and +`ComputeDriverInstance` retains its existing variants. Existing compiled drivers +and factories need no new trait implementation or registration method. + +A supporting factory overrides `ComputeDriverFactory::build_with_configuration_updates` +to return its ordinary instance plus a separately registered updater. The default +calls `build` and returns no updater. External drivers serve +`ComputeDriverConfigurationUpdates` on their existing endpoint. Workload updates +require `openshell.compute.update-sandbox-configuration` for image, command, or +environment changes. Metadata-only replacements use one database CAS and update +the label index without invoking a driver. + +New updates require operator activation and compatible gateway membership. +Accepted updates recover from private durable intent at startup and in periodic +reconciliation, including when activation is subsequently disabled. Recovery +pages through a partial database index of pending private update records instead +of scanning sandboxes or completed receipts. Acceptance and completion maintain +the indexed status in the same transaction as the intent and lifecycle interlock. +Each gateway keeps automatic recovery backoff in memory. Failed attempts wait +1, 2, 4, 8, then 15 minutes; each failure log includes the attempt and retry delay. +Replicas retry independently, and a gateway restart resets its backoff. A new +operation starts without delay. Completed or deleted updates are removed from +the backoff map as recovery observes them. +An authorized `UpdateSandbox` with no selected fields and the current resource +version resumes immediately, bypassing backoff without reconstructing its selections. Completion retains the +exact operation receipt for idempotent completion across replicas; a newer update +invalidates it. Ambiguous results retain the baseline provisioning interlock. +Explicit deletion can supersede a pending update: the driver only patches the +original immutable resource and never creates a replacement. The Deleting CAS +prevents delayed completion. Cleanup bypasses the pending-driver guard only when +the pending operation ID matches the persisted update intent. Final removal +deletes the sandbox and private intent atomically, retaining that evidence if +cleanup fails or loses a resource-version race. + +Metadata replacement preserves gateway-owned keys, including retention, while +other user keys under OpenShell domains remain editable. Completion preserves +later annotation merges accepted through `UpdateConfig`. + +A provisioning timeout can be repaired by Stop after its original driver work +settles and cleanup completes. Stop exposes Stopped without discarding the timeout +diagnosis or storage. This lifecycle behavior also applies when updates are disabled. + ## Operator credential export `GetProviderCredentials` exports selected profile-declared runtime environment diff --git a/crates/openshell-server/build.rs b/crates/openshell-server/build.rs index c061cb0b6d..6230f8d259 100644 --- a/crates/openshell-server/build.rs +++ b/crates/openshell-server/build.rs @@ -30,8 +30,10 @@ fn main() -> Result<(), Box> { let storage_proto_dir = manifest_dir.join("proto"); let public_proto_dir = manifest_dir.join("../../proto"); let storage_proto = storage_proto_dir.join("storage.proto"); + let image_proto = storage_proto_dir.join("sandbox_image.proto"); println!("cargo:rerun-if-changed={}", storage_proto.display()); + println!("cargo:rerun-if-changed={}", image_proto.display()); for imported_proto in [ "datamodel.proto", "openshell.proto", @@ -71,7 +73,7 @@ fn main() -> Result<(), Box> { "::openshell_core::proto::sandbox::v1", ) .file_descriptor_set_path(&descriptor_path) - .compile_with_config(proto_config, &[storage_proto], &include_dirs)?; + .compile_with_config(proto_config, &[storage_proto, image_proto], &include_dirs)?; Ok(()) } diff --git a/crates/openshell-server/migrations/postgres/009_add_pending_sandbox_updates_index.sql b/crates/openshell-server/migrations/postgres/009_add_pending_sandbox_updates_index.sql new file mode 100644 index 0000000000..bdd068f86d --- /dev/null +++ b/crates/openshell-server/migrations/postgres/009_add_pending_sandbox_updates_index.sql @@ -0,0 +1,3 @@ +CREATE INDEX IF NOT EXISTS sandbox_pending_updates_idx + ON objects (object_type, id) + WHERE object_type = 'sandbox_image_state' AND status = 'pending'; diff --git a/crates/openshell-server/migrations/sqlite/009_add_pending_sandbox_updates_index.sql b/crates/openshell-server/migrations/sqlite/009_add_pending_sandbox_updates_index.sql new file mode 100644 index 0000000000..bdd068f86d --- /dev/null +++ b/crates/openshell-server/migrations/sqlite/009_add_pending_sandbox_updates_index.sql @@ -0,0 +1,3 @@ +CREATE INDEX IF NOT EXISTS sandbox_pending_updates_idx + ON objects (object_type, id) + WHERE object_type = 'sandbox_image_state' AND status = 'pending'; diff --git a/crates/openshell-server/proto/sandbox_image.proto b/crates/openshell-server/proto/sandbox_image.proto new file mode 100644 index 0000000000..a8b012e293 --- /dev/null +++ b/crates/openshell-server/proto/sandbox_image.proto @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +syntax = "proto3"; + +// A new private format; frozen openshell.storage.v1 remains unchanged. +package openshell.storage.image.v1; +import "openshell.proto"; + +// Gateway-private image mutation state. Never embed this in public SandboxStatus: +// older gateways may rewrite public protobufs without retaining unknown fields. +message StoredSandboxImageUpdate { + string operation_id = 1; + string previous_image = 2; + string image = 3; + uint64 revision = 4; + // Presence retains selection semantics. + openshell.v1.SandboxCommandUpdate command = 5; + openshell.v1.SandboxMapUpdate environment = 6; + openshell.v1.SandboxMapUpdate labels = 7; + openshell.v1.SandboxMapUpdate annotations = 8; +} + +message StoredSandboxImageState { + string sandbox_id = 1; + uint64 revision = 2; + StoredSandboxImageUpdate pending = 3; + // Original presence and attempt identity while the interlock is held. + // Completion preserves newer diagnostics and clock edits on the live record. + openshell.v1.SandboxProvisioning previous_provisioning = 4; + // Exact last successful intent, for idempotent completion across replicas. + StoredSandboxImageUpdate completed = 5; +} diff --git a/crates/openshell-server/src/cli.rs b/crates/openshell-server/src/cli.rs index 9b16b53914..dfbbf4e7bb 100644 --- a/crates/openshell-server/src/cli.rs +++ b/crates/openshell-server/src/cli.rs @@ -313,6 +313,11 @@ struct RunArgs { action = ArgAction::Set )] enable_websocket_tunnel: bool, + + /// Activate stopped-sandbox configuration updates only after retiring every incompatible gateway + /// and settling its outstanding driver operations. See HA upgrade guidance. + #[arg(long, env = "OPENSHELL_ENABLE_SANDBOX_UPDATES", default_value_t = false, action = ArgAction::Set)] + enable_sandbox_updates: bool, } pub fn command() -> Command { @@ -594,6 +599,8 @@ fn prepare_server_config_with_drivers( .service_routing .validate(&config.name) .map_err(|error| miette::miette!("invalid service routing configuration: {error}"))?; + + config.enable_sandbox_updates = args.enable_sandbox_updates; if let Some(sources) = file .as_ref() .and_then(|file| file.openshell.gateway.provider_profile_sources.clone()) @@ -1379,6 +1386,11 @@ fn merge_file_into_args(args: &mut RunArgs, file: &GatewayFileSection, matches: { args.enable_websocket_tunnel = enabled; } + if let Some(enabled) = file.enable_sandbox_updates + && arg_defaulted(matches, "enable_sandbox_updates") + { + args.enable_sandbox_updates = enabled; + } if let Some(mtls_auth) = &file.mtls_auth && arg_defaulted(matches, "enable_mtls_auth") { @@ -3095,6 +3107,40 @@ mod tests { toml::from_str(toml).expect("valid TOML in test fixture") } + #[test] + fn sandbox_update_activation_defaults_off_and_file_requires_explicit_opt_in() { + Environment::new().remove("OPENSHELL_ENABLE_SANDBOX_UPDATES").run(|| { + let (mut args, matches) = parse_with_args(&["openshell-gateway"]); + assert!(!args.enable_sandbox_updates); + assert!(!openshell_core::Config::new(None).enable_sandbox_updates); + let file = config_file_from_toml("[openshell]\nversion = 2\n[openshell.gateway]\nenable_sandbox_updates = true\n"); + merge_file_into_args(&mut args, &file.openshell.gateway, &matches); + assert!(args.enable_sandbox_updates); + }); + } + + #[test] + fn explicit_cli_or_environment_disable_overrides_file_activation() { + let file = config_file_from_toml( + "[openshell]\nversion = 2\n[openshell.gateway]\nenable_sandbox_updates = true\n", + ); + Environment::new() + .remove("OPENSHELL_ENABLE_SANDBOX_UPDATES") + .run(|| { + let (mut args, matches) = + parse_with_args(&["openshell-gateway", "--enable-sandbox-updates", "false"]); + merge_file_into_args(&mut args, &file.openshell.gateway, &matches); + assert!(!args.enable_sandbox_updates); + }); + Environment::new() + .set("OPENSHELL_ENABLE_SANDBOX_UPDATES", "false") + .run(|| { + let (mut args, matches) = parse_with_args(&["openshell-gateway"]); + merge_file_into_args(&mut args, &file.openshell.gateway, &matches); + assert!(!args.enable_sandbox_updates); + }); + } + #[test] fn rejects_legacy_drivers_flag() { let error = command() diff --git a/crates/openshell-server/src/compute/image_update.rs b/crates/openshell-server/src/compute/image_update.rs new file mode 100644 index 0000000000..4a19ee2a53 --- /dev/null +++ b/crates/openshell-server/src/compute/image_update.rs @@ -0,0 +1,2826 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Stopped-only configuration changes with private durable intent and an operation +//! interlock understood by supported older gateways. + +use super::{ComputeRuntime, provisioning_operation, sandbox_compute_runtime_identity}; +use crate::storage_proto::StoredSandboxImageUpdate as SandboxImageUpdate; +use openshell_core::extension_protocol::COMPUTE_UPDATE_SANDBOX_CONFIGURATION; +use openshell_core::proto::compute::v1::SandboxUpdatePreconditions; +use openshell_core::proto::{Sandbox, SandboxPhase}; +use openshell_core::{GetResourceVersion, ObjectId, ObjectName}; +use tonic::{Request, Status}; +use tracing::info; + +const UPDATE_RECOVERY_PAGE_SIZE: u32 = 128; +const UPDATE_ATTEMPT_TIMEOUT: std::time::Duration = std::time::Duration::from_mins(1); + +#[derive(Clone)] +pub(super) struct UpdateRecoveryBackoff { + operation_id: String, + attempts: u32, + next_retry: tokio::time::Instant, +} + +impl ComputeRuntime { + pub(crate) fn with_sandbox_updates(mut self, enabled: bool) -> Self { + self.sandbox_updates_enabled = enabled; + self + } + + #[cfg(test)] + pub(crate) async fn update_sandbox_image( + &self, + sandbox_id: &str, + image: String, + expected_resource_version: u64, + ) -> Result { + self.update_sandbox_configuration( + sandbox_id, + &openshell_core::proto::UpdateSandboxRequest { + image: Some(image), + expected_resource_version, + ..Default::default() + }, + ) + .await + } + + pub(crate) async fn update_sandbox_configuration( + &self, + sandbox_id: &str, + update: &openshell_core::proto::UpdateSandboxRequest, + ) -> Result { + let expected_resource_version = update.expected_resource_version; + let lifecycle_guard = self.lifecycle_gates.lock_for(sandbox_id).await; + let mut sandbox_guard = self.lock_sandbox_for_lifecycle(&lifecycle_guard).await; + let current = self + .store + .get_message::(sandbox_id) + .await + .map_err(|_| Status::internal("fetch sandbox failed"))? + .ok_or_else(|| Status::not_found("sandbox not found"))?; + ensure_image_update_stopped(¤t)?; + if current.get_resource_version() != expected_resource_version { + return Err(crate::grpc::persistence_error_to_status( + crate::persistence::PersistenceError::Conflict { + current_resource_version: Some(current.get_resource_version()), + }, + "update sandbox image", + )); + } + let state = self + .store + .get_sandbox_image_state(sandbox_id) + .await + .map_err(|_| Status::internal("fetch private sandbox image state failed"))?; + let pending = state.as_ref().and_then(|state| state.pending.clone()); + let resume = update.image.is_none() + && update.command.is_none() + && update.environment.is_none() + && update.labels.is_none() + && update.annotations.is_none(); + if pending.is_none() { + if resume { + return Err(Status::failed_precondition( + "sandbox has no pending update to resume", + )); + } + if !self.sandbox_updates_enabled { + return Err(Status::failed_precondition( + "sandbox updates are disabled; complete the compatible deployment rollout before activating them", + )); + } + provisioning_operation::ensure_operation_settled(¤t)?; + // Metadata lives only in the gateway. CAS fences lifecycle changes + // and writes the label index in the same transaction as the object. + if update.image.is_none() && update.command.is_none() && update.environment.is_none() { + let mut candidate = current.clone(); + openshell_core::sandbox_update::apply_configuration( + &mut candidate, + None, + None, + update.labels.as_ref(), + update.annotations.as_ref(), + ); + if candidate == current { + return Ok(current); + } + let updated = self + .store + .update_message_cas::( + sandbox_id, + expected_resource_version, + |sandbox| { + openshell_core::sandbox_update::apply_configuration( + sandbox, + None, + None, + update.labels.as_ref(), + update.annotations.as_ref(), + ); + }, + ) + .await + .map_err(|error| { + crate::grpc::persistence_error_to_status(error, "update sandbox metadata") + })?; + self.sandbox_index.update_from_sandbox(&updated); + self.sandbox_watch_bus.notify(sandbox_id); + return Ok(updated); + } + self.store + .ensure_image_update_deployment_compatible() + .await + .map_err(|error| match error { + crate::persistence::PersistenceError::Config(message) => { + Status::failed_precondition(message) + } + _ => { + Status::unavailable("cannot verify sandbox-update deployment compatibility") + } + })?; + } + self.ensure_configuration_update_supported()?; + let image = update + .image + .clone() + .or_else(|| { + current + .spec + .as_ref() + .and_then(|spec| spec.template.as_ref()) + .map(|template| template.image.clone()) + }) + .ok_or_else(|| Status::failed_precondition("sandbox has no workload image"))?; + if let Some(intent) = &pending { + ensure_image_update_interlock(¤t, intent)?; + if !resume + && (intent.image != image + || intent.command != update.command + || intent.environment != update.environment + || intent.labels != update.labels + || intent.annotations != update.annotations) + { + return Err(Status::failed_precondition( + "another sandbox update is pending; resume with the current resource version and no selected fields", + )); + } + } else { + provisioning_operation::ensure_operation_settled(¤t)?; + } + let old_image = current + .spec + .as_ref() + .and_then(|spec| spec.template.as_ref()) + .map(|template| template.image.clone()) + .filter(|image| !image.is_empty()) + .ok_or_else(|| Status::failed_precondition("sandbox has no workload image"))?; + let mut candidate = current.clone(); + openshell_core::sandbox_update::apply_configuration( + &mut candidate, + update.command.as_ref(), + update.environment.as_ref(), + update.labels.as_ref(), + update.annotations.as_ref(), + ); + if pending.is_none() && old_image == image && candidate == current { + return Ok(current); + } + let revision = state.as_ref().map_or(0, |state| state.revision); + let intent = pending.clone().unwrap_or_else(|| SandboxImageUpdate { + operation_id: uuid::Uuid::new_v4().to_string(), + previous_image: old_image, + image, + revision: revision.saturating_add(1), + command: update.command.clone(), + environment: update.environment.clone(), + labels: update.labels.clone(), + annotations: update.annotations.clone(), + }); + if pending.is_none() && revision == u64::MAX { + return Err(Status::resource_exhausted( + "sandbox image revision exhausted", + )); + } + let accepted = if pending.is_some() { + current + } else { + // Keep lifecycle operations serialized, but let driver watch events + // and reconciliation progress while preflight waits on the driver. + // The acceptance CAS below rejects any intervening record changes. + drop(sandbox_guard); + tokio::time::timeout( + UPDATE_ATTEMPT_TIMEOUT, + self.reconcile_sandbox_update(¤t, &intent, true), + ) + .await + .map_err(|_| Status::deadline_exceeded("sandbox update preflight timed out"))??; + sandbox_guard = self.lock_sandbox_for_lifecycle(&lifecycle_guard).await; + // The private intent, selected configuration, and OLD-understood pending + // operation flag commit in one version-fenced transaction. + self.store + .begin_sandbox_image_update( + sandbox_id, + expected_resource_version, + revision, + &intent, + ) + .await + .map_err(|error| { + crate::grpc::persistence_error_to_status(error, "record sandbox image update") + })? + }; + self.sandbox_index.update_from_sandbox(&accepted); + self.sandbox_watch_bus.notify(sandbox_id); + drop(sandbox_guard); + let runtime = self.clone(); + tokio::spawn(async move { + let _lifecycle_guard = lifecycle_guard; + // Client cancellation must not cancel accepted work, but a stalled + // driver must release the gate for recovery, resume, or deletion. + // Timeout is ambiguous: retain any pending intent and its interlock. + tokio::time::timeout( + UPDATE_ATTEMPT_TIMEOUT, + runtime.complete_image_update(accepted, intent), + ) + .await + .unwrap_or_else(|_| { + Err(Status::deadline_exceeded( + "sandbox update timed out; recovery will retry any pending intent", + )) + }) + }) + .await + .map_err(|_| { + Status::internal("sandbox image update worker failed; intent remains pending") + })? + } + + async fn complete_image_update( + &self, + pending: Sandbox, + intent: SandboxImageUpdate, + ) -> Result { + ensure_image_update_interlock(&pending, &intent)?; + self.reconcile_sandbox_update(&pending, &intent, false) + .await?; + let _guard = self.lock_sandbox_local(pending.object_id()).await; + // An ambiguous error NEVER clears either the private intent or the + // public operation interlock. Only exact successful reconciliation can. + let updated = self + .store + .complete_sandbox_image_update(pending.object_id(), &intent) + .await + .map_err(|error| { + crate::grpc::persistence_error_to_status(error, "complete sandbox image update") + })?; + self.update_recovery_backoff + .lock() + .await + .remove(pending.object_id()); + self.sandbox_index.update_from_sandbox(&updated); + self.sandbox_watch_bus.notify(updated.object_id()); + info!(sandbox_id = %updated.object_id(), operation_id = %intent.operation_id, + "Sandbox configuration updated; sandbox remains stopped"); + Ok(updated) + } + + async fn reconcile_sandbox_update( + &self, + sandbox: &Sandbox, + intent: &SandboxImageUpdate, + validate_only: bool, + ) -> Result<(), Status> { + let image_update = image_update_driver_request(sandbox, intent, validate_only); + let mut candidate = sandbox.clone(); + openshell_core::sandbox_update::apply_configuration( + &mut candidate, + intent.command.as_ref(), + intent.environment.as_ref(), + intent.labels.as_ref(), + intent.annotations.as_ref(), + ); + let spec = candidate + .spec + .as_ref() + .ok_or_else(|| Status::failed_precondition("sandbox has no spec"))?; + let main_process = intent.command.as_ref().map(|_| { + openshell_core::proto::compute::v1::SandboxMainProcessUpdate { + command: spec.command.clone(), + tty: spec.tty, + } + }); + let environment = intent.environment.as_ref().map(|_| { + let mut values = spec + .template + .as_ref() + .map_or_else(Default::default, |template| template.environment.clone()); + values.extend(spec.environment.clone()); + openshell_core::proto::compute::v1::SandboxEnvironmentUpdate { values } + }); + let request = openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest { + preconditions: Some(image_update), + main_process, + environment, + }; + let driver = self.image_update_driver.clone().ok_or_else(|| { + Status::unimplemented("compute driver has no registered configuration updater") + })?; + self.driver + .call_owned( + openshell_otel::rpc::UPDATE_SANDBOX_CONFIGURATION, + Some(sandbox.object_id()), + move |_| async move { + driver + .update_sandbox_configuration(Request::new(request)) + .await + }, + ) + .await?; + Ok(()) + } + + fn ensure_configuration_update_supported(&self) -> Result<(), Status> { + let capability = COMPUTE_UPDATE_SANDBOX_CONFIGURATION; + if self.image_update_driver.is_none() + || !self + .driver_info + .negotiated_extension + .supported_capabilities + .iter() + .any(|value| value == capability) + { + return Err(Status::unimplemented(format!( + "compute driver does not support {capability}; restore a compatible driver to resume pending updates" + ))); + } + Ok(()) + } + + pub(super) async fn recover_pending_sandbox_updates(&self) -> Result<(), String> { + let Ok(permit) = self.update_recovery_sweep.clone().try_acquire_owned() else { + return Ok(()); + }; + let ids = self + .store + .list_pending_sandbox_update_ids(None, UPDATE_RECOVERY_PAGE_SIZE) + .await + .map_err(|error| format!("list pending sandbox updates: {error}"))?; + if ids.is_empty() { + self.update_recovery_backoff.lock().await.clear(); + return Ok(()); + } + let runtime = self.clone(); + // Recovery must not hold readiness or unrelated reconciliation hostage + // to a slow remote driver. One bounded sweep per runtime, at most eight + // operations in flight, with the normal per-sandbox lifecycle gate. + tokio::spawn(async move { + let _permit = permit; + if let Err(error) = runtime.recover_pending_sandbox_update_pages(ids).await { + tracing::warn!(%error, "Sandbox update recovery sweep failed"); + } + }); + Ok(()) + } + + async fn recover_pending_sandbox_update_pages( + &self, + mut ids: Vec, + ) -> Result<(), String> { + use futures::StreamExt as _; + let mut pending_ids = std::collections::HashSet::new(); + while let Some(after) = ids.last().cloned() { + pending_ids.extend(ids.iter().cloned()); + futures::stream::iter(ids) + .for_each_concurrent(8, |id| { + // Each sandbox owns independent locks even though these + // futures are polled concurrently in the same Tokio task. + crate::persistence::lock_order::branch(async move { + if let Err(error) = self.recover_one_sandbox_update(&id).await { + tracing::warn!(sandbox_id = %id, %error, + "Cannot read pending sandbox update for recovery"); + } + }) + }) + .await; + ids = self + .store + .list_pending_sandbox_update_ids(Some(&after), UPDATE_RECOVERY_PAGE_SIZE) + .await + .map_err(|error| format!("list pending sandbox updates: {error}"))?; + } + // Forget sandboxes completed or deleted by another replica. Only prune + // after a successful full scan, so a failed page does not reset backoff. + self.update_recovery_backoff + .lock() + .await + .retain(|id, _| pending_ids.contains(id)); + Ok(()) + } + + async fn recover_one_sandbox_update(&self, id: &str) -> Result<(), Status> { + self.recover_one_sandbox_update_at(id, tokio::time::Instant::now()) + .await + } + + async fn recover_one_sandbox_update_at( + &self, + id: &str, + now: tokio::time::Instant, + ) -> Result<(), Status> { + let Some(_gate) = self.lifecycle_gates.try_lock_for(id) else { + return Ok(()); + }; + let deadline = tokio::time::Instant::now() + UPDATE_ATTEMPT_TIMEOUT; + let read = async { + let sandbox = self + .store + .get_message::(id) + .await + .map_err(|_| Status::unavailable("cannot read sandbox for update recovery"))?; + let state = self + .store + .get_sandbox_image_state(id) + .await + .map_err(|_| Status::unavailable("cannot read pending sandbox update"))?; + Ok::<_, Status>((sandbox, state.and_then(|state| state.pending))) + }; + let (Some(sandbox), Some(intent)) = tokio::time::timeout_at(deadline, read) + .await + .map_err(|_| Status::deadline_exceeded("reading pending sandbox update timed out"))?? + else { + self.update_recovery_backoff.lock().await.remove(id); + return Ok(()); + }; + if sandbox.phase() == SandboxPhase::Deleting as i32 { + self.update_recovery_backoff.lock().await.remove(id); + return Ok(()); + } + let previous = self + .update_recovery_backoff + .lock() + .await + .get(id) + .cloned() + .filter(|backoff| backoff.operation_id == intent.operation_id); + if previous + .as_ref() + .is_some_and(|backoff| backoff.next_retry > now) + { + return Ok(()); + } + let recover = async { + ensure_image_update_stopped(&sandbox)?; + self.ensure_configuration_update_supported()?; + self.complete_image_update(sandbox, intent.clone()).await?; + Ok(()) + }; + let result = tokio::time::timeout_at(deadline, recover) + .await + .unwrap_or_else(|_| { + Err(Status::deadline_exceeded( + "sandbox update recovery timed out", + )) + }); + if let Err(error) = result { + let attempts = previous.map_or(1, |backoff| backoff.attempts.saturating_add(1)); + let delay = std::time::Duration::from_secs( + 60 * (1_u64 << attempts.saturating_sub(1).min(4)).min(15), + ); + self.update_recovery_backoff.lock().await.insert( + id.to_owned(), + UpdateRecoveryBackoff { + operation_id: intent.operation_id.clone(), + attempts, + next_retry: tokio::time::Instant::now() + delay, + }, + ); + tracing::warn!(sandbox_id = %id, operation_id = %intent.operation_id, + attempt = attempts, retry_after_secs = delay.as_secs(), %error, + "Sandbox update remains pending; recovery deferred"); + } + Ok(()) + } + + /// Only explicit deletion of the matching update may bypass the driver + /// interlock. Deleting alone says nothing about an outstanding create/start. + pub(super) async fn pending_driver_operation_blocks_cleanup( + &self, + sandbox: &Sandbox, + ) -> Result { + if !super::provisioning_deadline::driver_operation_pending(sandbox) { + return Ok(false); + } + if sandbox.phase() != SandboxPhase::Deleting as i32 { + return Ok(true); + } + let state = self + .store + .get_sandbox_image_state(sandbox.object_id()) + .await + .map_err(|error| format!("read pending sandbox update for cleanup: {error}"))?; + Ok(state + .and_then(|state| state.pending) + .is_none_or(|intent| ensure_image_update_interlock(sandbox, &intent).is_err())) + } + + pub(super) async fn ensure_no_pending_image_update( + &self, + sandbox: &Sandbox, + ) -> Result<(), Status> { + let state = self + .store + .get_sandbox_image_state(sandbox.object_id()) + .await + .map_err(|_| Status::unavailable("cannot read private sandbox image state"))?; + if state.as_ref().is_some_and(|state| state.pending.is_some()) { + return Err(Status::failed_precondition( + "sandbox update is pending; recovery will retry automatically, or resume UpdateSandbox with the current resource version and no selected fields", + )); + } + Ok(()) + } +} + +pub(super) fn ensure_image_update_interlock( + sandbox: &Sandbox, + intent: &SandboxImageUpdate, +) -> Result<(), Status> { + if sandbox + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + .is_none_or(|record| { + !record.driver_operation_pending || record.driver_operation_id != intent.operation_id + }) + { + return Err(Status::aborted( + "sandbox image-update operation interlock changed", + )); + } + Ok(()) +} + +fn image_update_driver_request( + sandbox: &Sandbox, + intent: &SandboxImageUpdate, + validate_only: bool, +) -> SandboxUpdatePreconditions { + SandboxUpdatePreconditions { + sandbox_id: sandbox.object_id().to_string(), + name: sandbox.object_name().to_string(), + image: intent.image.clone(), + expected_image: intent.previous_image.clone(), + expected_runtime_identity: sandbox_compute_runtime_identity(sandbox), + operation_id: intent.operation_id.clone(), + validate_only, + revision: intent.revision, + } +} + +fn ensure_image_update_stopped(sandbox: &Sandbox) -> Result<(), Status> { + if sandbox.phase() != SandboxPhase::Stopped as i32 + || sandbox + .metadata + .as_ref() + .is_some_and(|metadata| metadata.deletion_time.is_some()) + { + return Err(Status::failed_precondition( + "sandbox must be Stopped before updating its configuration", + )); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::compute::{ + COMPUTE_RUNTIME_IDENTITY_ANNOTATION, apply_driver_snapshot, provisioning_deadline, + }; + use crate::persistence::ObjectType; + use crate::sandbox_index::SandboxIndex; + use crate::sandbox_watch::SandboxWatchBus; + use crate::supervisor_session::SupervisorSessionRegistry; + use crate::test_support::{FakeComputeDriver, FakeComputeDriverCall}; + use crate::tracing_bus::TracingLogBus; + use openshell_core::proto::compute::v1::{DriverSandbox, DriverSandboxStatus}; + use openshell_core::proto::datamodel::v1::ObjectMeta; + use openshell_core::proto::{SandboxSpec, SandboxTemplate}; + use std::collections::HashMap; + use std::sync::Arc; + use tonic::Code; + + async fn fixture(supported: bool) -> (ComputeRuntime, Arc, Sandbox) { + let store = Arc::new(crate::persistence::test_store().await); + crate::ensure_default_workspace(&store).await.unwrap(); + let driver = Arc::new(FakeComputeDriver::new()); + let mut runtime = ComputeRuntime::from_driver( + "test".into(), + driver.clone(), + None, + store.clone(), + SandboxIndex::new(), + SandboxWatchBus::new(), + TracingLogBus::new(), + Arc::new(SupervisorSessionRegistry::new()), + ) + .await + .unwrap(); + runtime.sandbox_updates_enabled = true; + if supported { + runtime.image_update_driver = Some(driver.clone()); + runtime + .driver_info + .negotiated_extension + .supported_capabilities + .extend([COMPUTE_UPDATE_SANDBOX_CONFIGURATION.into()]); + } + let mut sandbox = Sandbox { + metadata: Some(ObjectMeta { + id: "image-sandbox-id".into(), + name: "image-sandbox".into(), + workspace: "default".into(), + labels: HashMap::from([("app".into(), "retained".into())]), + annotations: HashMap::from([ + ( + COMPUTE_RUNTIME_IDENTITY_ANNOTATION.into(), + "stable-runtime".into(), + ), + ( + crate::compute::ISOLATION_BACKEND_ANNOTATION.into(), + openshell_core::isolation_registration::BUILTIN_BACKEND_NAME.into(), + ), + ]), + ..Default::default() + }), + spec: Some(SandboxSpec { + template: Some(SandboxTemplate { + image: "example/workload:old".into(), + ..Default::default() + }), + environment: HashMap::from([("KEEP".into(), "value".into())]), + command: vec!["/bin/sh".into()], + ..Default::default() + }), + host_key_fingerprint: "SHA256:stable-key".into(), + ..Default::default() + }; + sandbox.set_phase(SandboxPhase::Stopped as i32); + store.put_message(&sandbox).await.unwrap(); + let sandbox = store + .get_message::(sandbox.object_id()) + .await + .unwrap() + .unwrap(); + (runtime, driver, sandbox) + } + + fn image_calls(driver: &FakeComputeDriver) -> Vec { + driver + .calls() + .into_iter() + .filter_map(|call| match call { + FakeComputeDriverCall::UpdateSandboxImage { request } if !request.validate_only => { + Some(request) + } + _ => None, + }) + .collect() + } + + #[tokio::test] + async fn recovery_drains_multiple_pending_pages_with_updates_disabled() { + let (mut runtime, driver, original) = fixture(true).await; + runtime.sandbox_updates_enabled = false; + let mut expected_ids = Vec::new(); + for index in 0..129 { + let id = format!("pending-{index:03}"); + let mut sandbox = original.clone(); + let metadata = sandbox.metadata.as_mut().unwrap(); + metadata.id.clone_from(&id); + metadata.name.clone_from(&id); + runtime.store.put_message(&sandbox).await.unwrap(); + let sandbox = runtime + .store + .get_message::(&id) + .await + .unwrap() + .unwrap(); + runtime + .store + .begin_sandbox_image_update( + &id, + sandbox.get_resource_version(), + 0, + &SandboxImageUpdate { + operation_id: format!("operation-{index}"), + previous_image: "example/workload:old".into(), + image: "example/workload:new".into(), + revision: 1, + ..Default::default() + }, + ) + .await + .unwrap(); + expected_ids.push(id); + } + runtime.recover_pending_sandbox_updates().await.unwrap(); + drop(runtime.update_recovery_sweep.acquire().await.unwrap()); + let mut recovered_ids: Vec<_> = image_calls(&driver) + .into_iter() + .map(|call| call.sandbox_id) + .collect(); + recovered_ids.sort(); + assert_eq!(recovered_ids, expected_ids); + assert!( + runtime + .store + .list_pending_sandbox_update_ids(None, 128) + .await + .unwrap() + .is_empty() + ); + // A subsequent empty sweep must not revisit completed updates. + runtime.recover_pending_sandbox_updates().await.unwrap(); + drop(runtime.update_recovery_sweep.acquire().await.unwrap()); + assert_eq!(image_calls(&driver).len(), expected_ids.len()); + } + + #[tokio::test] + async fn startup_does_not_wait_for_a_blocked_update_driver() { + use std::time::Duration; + for manages_lifecycle in [false, true] { + let (mut runtime, driver, original) = fixture(true).await; + runtime.driver_info.gateway_manages_lifecycle = manages_lifecycle; + driver.set_image_update_error(Some(Code::Unavailable)); + runtime + .update_sandbox_image( + original.object_id(), + "new-image".into(), + original.get_resource_version(), + ) + .await + .unwrap_err(); + driver.set_image_update_error(None); + let (entered, release) = driver.pause_image_updates(); + tokio::time::timeout(Duration::from_secs(2), runtime.start_persisted_sandboxes()) + .await + .expect("startup must not wait on update recovery") + .unwrap(); + tokio::time::timeout(Duration::from_secs(2), entered.notified()) + .await + .expect("startup must schedule update recovery"); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .unwrap() + .pending + .is_some() + ); + release.add_permits(1); + drop(runtime.update_recovery_sweep.acquire().await.unwrap()); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .unwrap() + .pending + .is_none() + ); + } + } + + #[tokio::test] + async fn recovery_uses_stored_selection_even_when_admission_is_disabled() { + use openshell_core::proto::{SandboxCommandUpdate, SandboxMapUpdate, UpdateSandboxRequest}; + for automatic in [false, true] { + let (mut runtime, driver, original) = fixture(true).await; + driver.set_image_update_error(Some(Code::Unavailable)); + let update = UpdateSandboxRequest { + command: Some(SandboxCommandUpdate { + argv: vec!["sleep".into(), "infinity".into()], + }), + environment: Some(SandboxMapUpdate::default()), + expected_resource_version: original.get_resource_version(), + ..Default::default() + }; + runtime + .update_sandbox_configuration(original.object_id(), &update) + .await + .unwrap_err(); + runtime.sandbox_updates_enabled = false; + // Missing driver support keeps the intent; restoring it is enough + // for recovery, without re-enabling admission or reconstructing fields. + let updater = runtime.image_update_driver.take(); + runtime.recover_pending_sandbox_updates().await.unwrap(); + drop(runtime.update_recovery_sweep.acquire().await.unwrap()); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .unwrap() + .pending + .is_some() + ); + runtime.image_update_driver = updater; + driver.set_image_update_error(None); + // Repeated scans respect this gateway's delay even after the + // driver has been repaired. + runtime.recover_pending_sandbox_updates().await.unwrap(); + drop(runtime.update_recovery_sweep.acquire().await.unwrap()); + assert_eq!(image_calls(&driver).len(), 1); + let pending = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + if automatic { + let next = + runtime.update_recovery_backoff.lock().await[original.object_id()].next_retry; + runtime + .recover_one_sandbox_update_at(original.object_id(), next) + .await + .unwrap(); + } else { + runtime + .update_sandbox_configuration( + original.object_id(), + &UpdateSandboxRequest { + expected_resource_version: pending.get_resource_version(), + ..Default::default() + }, + ) + .await + .unwrap(); + } + let complete = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + assert_eq!(complete.spec, pending.spec); + assert!(provisioning_operation::ensure_operation_settled(&complete).is_ok()); + let calls = image_calls(&driver); + assert_eq!(calls.len(), 2); + assert_eq!(calls[0], calls[1]); + } + } + + #[tokio::test] + async fn failed_automatic_updates_back_off_but_explicit_resume_is_immediate() { + let (runtime, driver, original) = fixture(true).await; + driver.set_image_update_error(Some(Code::Unavailable)); + runtime + .update_sandbox_image( + original.object_id(), + "new-image".into(), + original.get_resource_version(), + ) + .await + .unwrap_err(); + let pending = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + for (attempt, delay) in [ + (1, 60_000), + (2, 120_000), + (3, 240_000), + (4, 480_000), + (5, 900_000), + (6, 900_000), + ] { + let due = runtime + .update_recovery_backoff + .lock() + .await + .get(original.object_id()) + .map_or_else(tokio::time::Instant::now, |backoff| backoff.next_retry); + let before = tokio::time::Instant::now(); + runtime + .recover_one_sandbox_update_at(original.object_id(), due) + .await + .unwrap(); + let after = tokio::time::Instant::now(); + let backoff = + runtime.update_recovery_backoff.lock().await[original.object_id()].clone(); + assert_eq!(backoff.attempts, attempt); + let delay = std::time::Duration::from_millis(delay); + assert!((before + delay..=after + delay).contains(&backoff.next_retry)); + runtime + .recover_one_sandbox_update_at( + original.object_id(), + backoff.next_retry - std::time::Duration::from_millis(1), + ) + .await + .unwrap(); + assert_eq!(image_calls(&driver).len(), attempt as usize + 1); + } + driver.set_image_update_error(None); + // Scheduling never changes the public version; a fresh version is not + // required merely because another automatic attempt failed. + runtime + .update_sandbox_configuration( + original.object_id(), + &openshell_core::proto::UpdateSandboxRequest { + expected_resource_version: pending.get_resource_version(), + ..Default::default() + }, + ) + .await + .unwrap(); + let state = runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .unwrap(); + assert!(state.pending.is_none()); + assert!(runtime.update_recovery_backoff.lock().await.is_empty()); + } + + #[tokio::test] + async fn recovery_backoff_is_local_and_does_not_delay_a_new_operation() { + let (runtime, driver, original) = fixture(true).await; + driver.set_image_update_error(Some(Code::Unavailable)); + runtime + .update_sandbox_image( + original.object_id(), + "new-image".into(), + original.get_resource_version(), + ) + .await + .unwrap_err(); + let state = runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap(); + runtime + .recover_one_sandbox_update(original.object_id()) + .await + .unwrap(); + runtime + .clone() + .recover_one_sandbox_update(original.object_id()) + .await + .unwrap(); + assert_eq!(image_calls(&driver).len(), 2); + assert_eq!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap(), + state + ); + + // A fresh runtime (restart or another replica) has no backoff, even + // though it reads the same durable pending intent. + let mut restarted = runtime.clone(); + restarted.update_recovery_backoff = Arc::default(); + driver.set_image_update_error(None); + restarted + .recover_one_sandbox_update(original.object_id()) + .await + .unwrap(); + assert_eq!(image_calls(&driver).len(), 3); + assert!(!runtime.update_recovery_backoff.lock().await.is_empty()); + + let completed = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + driver.set_image_update_error(Some(Code::Unavailable)); + restarted + .update_sandbox_image( + original.object_id(), + "third-image".into(), + completed.get_resource_version(), + ) + .await + .unwrap_err(); + // The original runtime still remembers the previous operation's delay, + // but the new operation must be eligible immediately. + runtime + .recover_one_sandbox_update(original.object_id()) + .await + .unwrap(); + assert_eq!(image_calls(&driver).len(), 5); + assert_eq!( + runtime.update_recovery_backoff.lock().await[original.object_id()].attempts, + 1 + ); + + driver.set_image_update_error(None); + restarted + .recover_one_sandbox_update(original.object_id()) + .await + .unwrap(); + runtime.recover_pending_sandbox_updates().await.unwrap(); + assert!(runtime.update_recovery_backoff.lock().await.is_empty()); + } + + #[tokio::test] + async fn timed_out_explicit_update_releases_gate_and_preserves_recovery() { + use std::time::Duration; + + for cancel_request in [false, true] { + for recovery in ["automatic", "resume", "delete"] { + let (runtime, driver, original) = fixture(true).await; + let (entered, release) = driver.pause_image_updates(); + let update = { + let runtime = runtime.clone(); + let original = original.clone(); + tokio::spawn(async move { + runtime + .update_sandbox_image( + original.object_id(), + "new-image".into(), + original.get_resource_version(), + ) + .await + }) + }; + entered.notified().await; + release.add_permits(1); // Let preflight pass; stall the accepted update. + entered.notified().await; + let intent = runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .unwrap() + .pending + .unwrap(); + if cancel_request { + update.abort(); + } + tokio::time::pause(); + tokio::time::advance(UPDATE_ATTEMPT_TIMEOUT).await; + tokio::task::yield_now().await; + tokio::time::resume(); + let result = tokio::time::timeout(Duration::from_secs(2), update) + .await + .expect("explicit update must have a bounded lifetime"); + if cancel_request { + assert!(result.unwrap_err().is_cancelled()); + } else { + assert_eq!(result.unwrap().unwrap_err().code(), Code::DeadlineExceeded); + } + drop( + tokio::time::timeout( + Duration::from_secs(2), + runtime.lifecycle_gates.lock_for(original.object_id()), + ) + .await + .expect("the detached worker must release its lifecycle gate"), + ); + let pending = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + ensure_image_update_interlock(&pending, &intent).unwrap(); + assert_eq!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .unwrap() + .pending, + Some(intent.clone()) + ); + + tokio::time::timeout(Duration::from_secs(2), async { + match recovery { + "automatic" => { + release.add_permits(1); + runtime.recover_pending_sandbox_updates().await.unwrap(); + drop(runtime.update_recovery_sweep.acquire().await.unwrap()); + } + "resume" => { + release.add_permits(1); + runtime + .update_sandbox_configuration( + original.object_id(), + &openshell_core::proto::UpdateSandboxRequest { + expected_resource_version: pending.get_resource_version(), + ..Default::default() + }, + ) + .await + .unwrap(); + } + "delete" => { + // Deletion must work while the updater remains stalled. + runtime + .delete_sandbox("default", original.object_name()) + .await + .unwrap(); + } + _ => unreachable!(), + } + }) + .await + .expect("a timed-out update must permit recovery or deletion"); + let state = runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap(); + if recovery == "delete" { + assert!(state.is_none()); + assert!( + runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .is_none() + ); + assert!( + runtime + .store + .complete_sandbox_image_update(original.object_id(), &intent) + .await + .is_err(), + "late completion cannot resurrect a deleted sandbox" + ); + } else { + assert!(state.unwrap().pending.is_none()); + let calls = image_calls(&driver); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].operation_id, intent.operation_id); + } + } + } + } + + #[tokio::test] + async fn timed_out_update_preflight_leaves_sandbox_available_for_deletion() { + use std::time::Duration; + + let (runtime, driver, original) = fixture(true).await; + let (entered, _release) = driver.pause_image_updates(); + let update = { + let runtime = runtime.clone(); + let original = original.clone(); + tokio::spawn(async move { + runtime + .update_sandbox_image( + original.object_id(), + "new-image".into(), + original.get_resource_version(), + ) + .await + }) + }; + entered.notified().await; + tokio::time::pause(); + tokio::time::advance(UPDATE_ATTEMPT_TIMEOUT).await; + tokio::task::yield_now().await; + tokio::time::resume(); + let error = tokio::time::timeout(Duration::from_secs(2), update) + .await + .expect("preflight must time out before accepting an update") + .unwrap() + .unwrap_err(); + assert_eq!(error.code(), Code::DeadlineExceeded); + assert_eq!( + runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(), + original + ); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .is_none() + ); + tokio::time::timeout( + Duration::from_secs(2), + runtime.delete_sandbox("default", original.object_name()), + ) + .await + .expect("preflight timeout must release the lifecycle gate") + .unwrap(); + } + + #[tokio::test] + async fn timed_out_automatic_update_retains_intent_and_defers_recovery() { + let (runtime, driver, original) = fixture(true).await; + driver.set_image_update_error(Some(Code::Unavailable)); + runtime + .update_sandbox_image( + original.object_id(), + "new-image".into(), + original.get_resource_version(), + ) + .await + .unwrap_err(); + driver.set_image_update_error(None); + let (entered, release) = driver.pause_image_updates(); + runtime.recover_pending_sandbox_updates().await.unwrap(); + entered.notified().await; + tokio::time::pause(); + tokio::time::advance(UPDATE_ATTEMPT_TIMEOUT).await; + tokio::task::yield_now().await; + tokio::time::resume(); + drop(runtime.update_recovery_sweep.acquire().await.unwrap()); + let state = runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .unwrap(); + assert!(state.pending.is_some()); + let backoff = runtime.update_recovery_backoff.lock().await[original.object_id()].clone(); + assert_eq!(backoff.attempts, 1); + assert!(backoff.next_retry > tokio::time::Instant::now()); + runtime.recover_pending_sandbox_updates().await.unwrap(); + drop(runtime.update_recovery_sweep.acquire().await.unwrap()); + // The fake records calls only after the pause, so the timed-out call + // has not reached its recording point. No second attempt may enter. + assert_eq!(image_calls(&driver).len(), 1); + assert!( + tokio::time::timeout(std::time::Duration::from_millis(10), entered.notified()) + .await + .is_err() + ); + release.add_permits(1); + } + + #[tokio::test] + async fn metadata_only_update_preserves_retention_and_needs_no_driver_capability() { + use openshell_core::proto::{SandboxMapUpdate, UpdateSandboxRequest}; + let (runtime, driver, mut original) = fixture(false).await; + original.metadata.as_mut().unwrap().annotations.extend([ + ("openshell.nvidia.com/retention".into(), "ephemeral".into()), + ("openshell.ai/user-note".into(), "old".into()), + ]); + runtime.store.put_message(&original).await.unwrap(); + let original = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + let updated = runtime + .update_sandbox_configuration( + original.object_id(), + &UpdateSandboxRequest { + labels: Some(SandboxMapUpdate { + values: HashMap::from([("team".into(), "new".into())]), + }), + annotations: Some(SandboxMapUpdate { + values: HashMap::from([("openshell.ai/user-note".into(), "new".into())]), + }), + expected_resource_version: original.get_resource_version(), + ..Default::default() + }, + ) + .await + .unwrap(); + assert_eq!( + updated.metadata.as_ref().unwrap().annotations["openshell.nvidia.com/retention"], + "ephemeral" + ); + assert_eq!( + updated.metadata.as_ref().unwrap().annotations["openshell.ai/user-note"], + "new" + ); + let noop = runtime + .update_sandbox_configuration( + original.object_id(), + &UpdateSandboxRequest { + labels: Some(SandboxMapUpdate { + values: updated.metadata.as_ref().unwrap().labels.clone(), + }), + expected_resource_version: updated.get_resource_version(), + ..Default::default() + }, + ) + .await + .unwrap(); + assert_eq!(noop, updated); + assert_eq!(updated.spec, original.spec); + assert_eq!(updated.status, original.status); + assert!(image_calls(&driver).is_empty()); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .is_none() + ); + let record = runtime + .store + .get("sandbox", original.object_id()) + .await + .unwrap() + .unwrap(); + let labels: serde_json::Value = + serde_json::from_str(record.labels.as_ref().unwrap()).unwrap(); + assert_eq!(labels["team"], "new"); + assert_eq!( + runtime + .update_sandbox_configuration( + original.object_id(), + &UpdateSandboxRequest { + labels: Some(SandboxMapUpdate::default()), + expected_resource_version: original.get_resource_version(), + ..Default::default() + } + ) + .await + .unwrap_err() + .code(), + Code::Aborted + ); + } + + #[tokio::test] + async fn deleting_does_not_bypass_unrelated_pending_driver_operations() { + for missing_intent in [false, true] { + let (runtime, driver, original) = fixture(true).await; + driver.set_image_update_error(Some(Code::Unavailable)); + runtime + .update_sandbox_image( + original.object_id(), + "new-image".into(), + original.get_resource_version(), + ) + .await + .unwrap_err(); + let mut deleting = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + deleting.set_phase(SandboxPhase::Deleting as i32); + if missing_intent { + runtime + .store + .delete_sandbox_image_state(original.object_id()) + .await + .unwrap(); + } else { + deleting + .status + .as_mut() + .unwrap() + .provisioning + .as_mut() + .unwrap() + .driver_operation_id = "unrelated-create-or-start".into(); + } + runtime.store.put_message(&deleting).await.unwrap(); + let deleting = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + let version = deleting.get_resource_version(); + driver.clear_calls(); + + runtime + .recover_persisted_lifecycle_transitions() + .await + .unwrap(); + runtime.apply_deleted(original.object_id()).await.unwrap(); + { + let _guard = runtime.lock_sandbox_local(original.object_id()).await; + runtime + .apply_deleted_if_version_locked(&deleting, version) + .await + .unwrap(); + assert!( + !runtime + .remove_sandbox_record_if_version_locked(original.object_id(), version) + .await + .unwrap() + ); + } + let record = runtime + .store + .get(Sandbox::object_type(), original.object_id()) + .await + .unwrap() + .unwrap(); + runtime + .prune_missing_sandbox(record, i64::MAX, 0) + .await + .unwrap(); + assert_eq!( + runtime + .delete_sandbox("default", original.object_name()) + .await + .unwrap_err() + .code(), + if missing_intent { + Code::FailedPrecondition + } else { + Code::Aborted + } + ); + assert_eq!( + runtime + .store + .get_message::(original.object_id()) + .await + .unwrap(), + Some(deleting) + ); + assert!(driver.calls().is_empty()); + } + } + + #[tokio::test] + async fn deleting_pending_update_can_retry_failed_record_removal() { + let (mut runtime, driver, original) = fixture(true).await; + let directory = tempfile::tempdir().unwrap(); + let database_url = format!("sqlite://{}", directory.path().join("delete.db").display()); + runtime.store = Arc::new( + crate::persistence::Store::connect(&database_url) + .await + .unwrap(), + ); + crate::ensure_default_workspace(&runtime.store) + .await + .unwrap(); + runtime.store.put_message(&original).await.unwrap(); + driver.set_image_update_error(Some(Code::Unavailable)); + runtime + .update_sandbox_image( + original.object_id(), + "new-image".into(), + original.get_resource_version(), + ) + .await + .unwrap_err(); + let mut deleting = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + deleting.set_phase(SandboxPhase::Deleting as i32); + runtime.store.put_message(&deleting).await.unwrap(); + let pending = runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap(); + let pool = sqlx::SqlitePool::connect(&database_url).await.unwrap(); + sqlx::query( + "CREATE TRIGGER fail_sandbox_delete BEFORE DELETE ON objects +WHEN OLD.object_type = 'sandbox' +BEGIN SELECT RAISE(ABORT, 'injected sandbox deletion failure'); END", + ) + .execute(&pool) + .await + .unwrap(); + assert!(runtime.apply_deleted(original.object_id()).await.is_err()); + let retained = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + assert_eq!(retained.phase(), SandboxPhase::Deleting as i32); + assert_eq!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap(), + pending + ); + assert!( + !runtime + .pending_driver_operation_blocks_cleanup(&retained) + .await + .unwrap() + ); + + sqlx::query("DROP TRIGGER fail_sandbox_delete") + .execute(&pool) + .await + .unwrap(); + runtime.apply_deleted(original.object_id()).await.unwrap(); + assert!( + runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .is_none() + ); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .is_none() + ); + pool.close().await; + } + + #[tokio::test] + async fn explicit_delete_supersedes_pending_update_without_restoring_the_driver() { + let (mut runtime, driver, original) = fixture(true).await; + driver.set_image_update_error(Some(Code::Unavailable)); + runtime + .update_sandbox_image( + original.object_id(), + "new-image".into(), + original.get_resource_version(), + ) + .await + .unwrap_err(); + let intent = runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .unwrap() + .pending + .unwrap(); + runtime.image_update_driver = None; + runtime.sandbox_updates_enabled = false; + runtime + .delete_sandbox("default", original.object_name()) + .await + .unwrap(); + assert!( + runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .is_none() + ); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .is_none() + ); + assert!( + runtime + .store + .complete_sandbox_image_update(original.object_id(), &intent) + .await + .is_err() + ); + } + + #[tokio::test] + async fn configuration_update_reconciles_all_fields_and_recovers_exact_pending_target() { + use openshell_core::proto::{SandboxCommandUpdate, SandboxMapUpdate, UpdateSandboxRequest}; + let (runtime, driver, original) = fixture(true).await; + let request = UpdateSandboxRequest { + image: Some("example/workload:new".into()), + command: Some(SandboxCommandUpdate { + argv: vec!["/usr/local/bin/start-agent".into(), "--new".into()], + }), + environment: Some(SandboxMapUpdate { + values: HashMap::from([("MODE".into(), "new".into())]), + }), + labels: Some(SandboxMapUpdate { + values: HashMap::from([("team".into(), "agents".into())]), + }), + annotations: Some(SandboxMapUpdate { + values: HashMap::from([("example.com/revision".into(), "2".into())]), + }), + expected_resource_version: original.get_resource_version(), + ..Default::default() + }; + driver.set_image_update_error(Some(Code::Unavailable)); + assert_eq!( + runtime + .update_sandbox_configuration(original.object_id(), &request) + .await + .unwrap_err() + .code(), + Code::Unavailable + ); + let pending = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + assert!( + pending + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap() + .driver_operation_pending + ); + let mut retry = request.clone(); + retry.expected_resource_version = pending.get_resource_version(); + retry + .command + .as_mut() + .unwrap() + .argv + .push("--different".into()); + assert_eq!( + runtime + .update_sandbox_configuration(original.object_id(), &retry) + .await + .unwrap_err() + .code(), + Code::FailedPrecondition + ); + retry.command = request.command.clone(); + driver.set_image_update_error(None); + let updated = runtime + .update_sandbox_configuration(original.object_id(), &retry) + .await + .unwrap(); + assert_eq!(updated.phase(), SandboxPhase::Stopped as i32); + assert_eq!( + updated.spec.as_ref().unwrap().command, + request.command.unwrap().argv + ); + assert_eq!( + updated.spec.as_ref().unwrap().environment, + request.environment.unwrap().values + ); + for (key, value) in &original.metadata.as_ref().unwrap().annotations { + if openshell_core::sandbox_update::is_system_metadata_key(key) { + assert_eq!( + updated.metadata.as_ref().unwrap().annotations.get(key), + Some(value) + ); + } + } + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .unwrap() + .pending + .is_none() + ); + let clear = UpdateSandboxRequest { + command: Some(SandboxCommandUpdate::default()), + environment: Some(SandboxMapUpdate::default()), + labels: Some(SandboxMapUpdate::default()), + annotations: Some(SandboxMapUpdate::default()), + expected_resource_version: updated.get_resource_version(), + ..Default::default() + }; + let cleared = runtime + .update_sandbox_configuration(original.object_id(), &clear) + .await + .unwrap(); + assert!(cleared.spec.as_ref().unwrap().command.is_empty()); + assert!(cleared.spec.as_ref().unwrap().environment.is_empty()); + assert!(cleared.metadata.as_ref().unwrap().labels.is_empty()); + assert_eq!( + cleared + .spec + .as_ref() + .unwrap() + .template + .as_ref() + .unwrap() + .image, + "example/workload:new" + ); + } + + #[tokio::test] + async fn configuration_update_requires_capability_before_mutating() { + use openshell_core::proto::SandboxCommandUpdate; + let (mut runtime, _, original) = fixture(true).await; + runtime + .driver_info + .negotiated_extension + .supported_capabilities + .retain(|capability| capability != COMPUTE_UPDATE_SANDBOX_CONFIGURATION); + let update = openshell_core::proto::UpdateSandboxRequest { + command: Some(SandboxCommandUpdate::default()), + expected_resource_version: original.get_resource_version(), + ..Default::default() + }; + assert_eq!( + runtime + .update_sandbox_configuration(original.object_id(), &update) + .await + .unwrap_err() + .code(), + Code::Unimplemented + ); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .is_none() + ); + assert_eq!( + runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(), + original + ); + } + + #[tokio::test] + async fn image_update_preserves_identity_and_every_other_spec_field() { + let (runtime, driver, mut original) = fixture(true).await; + // Image edits must preserve the upstream backend pin and launch identity, + // including generation/session and authorization epoch annotations. + crate::auth::sandbox_session::PersistedSandboxIdentity::new() + .unwrap() + .write(&mut original.metadata.as_mut().unwrap().annotations); + runtime.store.put_message(&original).await.unwrap(); + let original = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + let updated = runtime + .update_sandbox_image( + original.object_id(), + "example/workload:new".into(), + original.get_resource_version(), + ) + .await + .unwrap(); + let mut expected = original.spec.clone().unwrap(); + expected.template.as_mut().unwrap().image = "example/workload:new".into(); + assert_eq!(updated.spec, Some(expected)); + assert_eq!(updated.object_id(), original.object_id()); + assert_eq!(updated.object_name(), original.object_name()); + assert_eq!(updated.host_key_fingerprint, original.host_key_fingerprint); + assert_eq!( + updated.metadata.as_ref().unwrap().annotations, + original.metadata.as_ref().unwrap().annotations + ); + assert_eq!( + updated.metadata.as_ref().unwrap().labels, + original.metadata.as_ref().unwrap().labels + ); + assert_eq!(updated.phase(), SandboxPhase::Stopped as i32); + let state = runtime + .store + .get_sandbox_image_state(updated.object_id()) + .await + .unwrap() + .unwrap(); + assert!(state.pending.is_none()); + assert_eq!(state.revision, 1); + assert_eq!(updated.status, original.status); + let calls = image_calls(&driver); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].expected_image, "example/workload:old"); + assert_eq!(calls[0].image, "example/workload:new"); + assert_eq!(calls[0].revision, 1); + assert_eq!(calls[0].expected_runtime_identity, "stable-runtime"); + assert!(uuid::Uuid::parse_str(&calls[0].operation_id).is_ok()); + assert!(!driver.calls().iter().any(|call| matches!( + call, + FakeComputeDriverCall::CreateSandbox { .. } + | FakeComputeDriverCall::DeleteSandbox { .. } + | FakeComputeDriverCall::StartSandbox { .. } + ))); + let unchanged = runtime + .update_sandbox_image( + updated.object_id(), + "example/workload:new".into(), + updated.get_resource_version(), + ) + .await + .unwrap(); + assert_eq!( + unchanged.get_resource_version(), + updated.get_resource_version() + ); + assert_eq!(image_calls(&driver).len(), 1); + let back = runtime + .update_sandbox_image( + updated.object_id(), + "example/workload:old".into(), + updated.get_resource_version(), + ) + .await + .unwrap(); + assert_eq!( + runtime + .store + .get_sandbox_image_state(back.object_id()) + .await + .unwrap() + .unwrap() + .revision, + 2 + ); + assert_eq!(image_calls(&driver)[1].revision, 2); + } + + #[tokio::test] + async fn image_update_state_is_removed_on_sandbox_deletion() { + for out_of_band in [false, true] { + let (runtime, _, original) = fixture(true).await; + let mut unrelated = original.clone(); + let metadata = unrelated.metadata.as_mut().unwrap(); + metadata.id = "unrelated-sandbox-id".into(); + metadata.name = "unrelated-sandbox".into(); + runtime.store.put_message(&unrelated).await.unwrap(); + let unrelated = runtime + .store + .get_message::(unrelated.object_id()) + .await + .unwrap() + .unwrap(); + for sandbox in [&original, &unrelated] { + runtime + .update_sandbox_image( + sandbox.object_id(), + "example/workload:new".into(), + sandbox.get_resource_version(), + ) + .await + .unwrap(); + } + let unrelated_state = runtime + .store + .get_sandbox_image_state(unrelated.object_id()) + .await + .unwrap() + .unwrap(); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .is_some() + ); + + // Exercise explicit deletion and watch-driven removal, including + // a delayed or repeated watch deletion event. + if out_of_band { + runtime.apply_deleted(original.object_id()).await.unwrap(); + } else { + runtime + .delete_sandbox("default", original.object_name()) + .await + .unwrap(); + } + runtime.apply_deleted(original.object_id()).await.unwrap(); + assert!( + runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .is_none() + ); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .is_none() + ); + assert_eq!( + runtime + .store + .get_sandbox_image_state(unrelated.object_id()) + .await + .unwrap(), + Some(unrelated_state) + ); + } + } + + #[tokio::test] + async fn image_update_rejects_unregistered_updater_without_writes() { + let (mut runtime, driver, original) = fixture(true).await; + runtime.image_update_driver = None; + driver.clear_calls(); + let error = runtime + .update_sandbox_image( + original.object_id(), + "example/workload:new".into(), + original.get_resource_version(), + ) + .await + .unwrap_err(); + assert_eq!(error.code(), Code::Unimplemented); + assert!(driver.calls().is_empty()); + assert_eq!( + runtime + .store + .get_message::(original.object_id()) + .await + .unwrap(), + Some(original.clone()) + ); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .is_none() + ); + } + + #[tokio::test] + async fn image_update_rejects_unsupported_driver_and_stale_version_without_writes() { + let (runtime, driver, original) = fixture(false).await; + assert_eq!( + runtime + .update_sandbox_image( + original.object_id(), + "example/workload:new".into(), + original.get_resource_version() + ) + .await + .unwrap_err() + .code(), + Code::Unimplemented + ); + assert!(image_calls(&driver).is_empty()); + let (runtime, driver, original) = fixture(true).await; + assert_eq!( + runtime + .update_sandbox_image( + original.object_id(), + "example/workload:new".into(), + original.get_resource_version() + 1 + ) + .await + .unwrap_err() + .code(), + Code::Aborted + ); + assert!(image_calls(&driver).is_empty()); + assert_eq!( + runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(), + original + ); + } + + #[tokio::test] + async fn failed_image_preflight_has_no_durable_effects() { + let (runtime, driver, original) = fixture(true).await; + driver.set_image_validation_error(Some(Code::InvalidArgument)); + assert_eq!( + runtime + .update_sandbox_image( + original.object_id(), + "invalid:image".into(), + original.get_resource_version() + ) + .await + .unwrap_err() + .code(), + Code::InvalidArgument + ); + assert!(image_calls(&driver).is_empty()); + assert_eq!( + runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(), + original + ); + } + + #[tokio::test] + async fn image_preflight_allows_watch_progress_and_rejects_intervening_changes() { + use openshell_core::proto::compute::v1::{ + WatchSandboxesEvent, WatchSandboxesSandboxEvent, watch_sandboxes_event, + }; + use std::time::Duration; + + let (runtime, driver, original) = fixture(true).await; + let mut unrelated = original.clone(); + let metadata = unrelated.metadata.as_mut().unwrap(); + metadata.id = "unrelated-sandbox-id".into(); + metadata.name = "unrelated-sandbox".into(); + runtime.store.put_message(&unrelated).await.unwrap(); + let unrelated = runtime + .store + .get_message::(unrelated.object_id()) + .await + .unwrap() + .unwrap(); + let (entered, release) = driver.pause_image_updates(); + let update = { + let runtime = runtime.clone(); + let original = original.clone(); + tokio::spawn(async move { + runtime + .update_sandbox_image( + original.object_id(), + "example/workload:new".into(), + original.get_resource_version(), + ) + .await + }) + }; + tokio::time::timeout(Duration::from_secs(2), entered.notified()) + .await + .expect("update must reach preflight"); + + // Process snapshots in watch-loop order. The event for the sandbox + // being validated must not hold up the following unrelated event. + tokio::time::timeout(Duration::from_secs(2), async { + for sandbox in [&original, &unrelated] { + runtime + .apply_watch_event(WatchSandboxesEvent { + payload: Some(watch_sandboxes_event::Payload::Sandbox( + WatchSandboxesSandboxEvent { + sandbox: Some(DriverSandbox { + id: sandbox.object_id().into(), + name: sandbox.object_name().into(), + ..Default::default() + }), + }, + )), + }) + .await + .unwrap(); + } + }) + .await + .expect("watch events must progress while preflight is paused"); + for sandbox in [&original, &unrelated] { + let observed = runtime + .store + .get_message::(sandbox.object_id()) + .await + .unwrap() + .unwrap(); + assert!(observed.get_resource_version() > sandbox.get_resource_version()); + assert_eq!(observed.spec, sandbox.spec); + } + + // The original conditional request is now stale. Preflight success + // must not commit intent or issue a mutating driver call. + release.add_permits(1); + let result = tokio::time::timeout(Duration::from_secs(2), update) + .await + .expect("stale update must finish without applying") + .unwrap(); + assert_eq!(result.unwrap_err().code(), Code::Aborted); + assert!(image_calls(&driver).is_empty()); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .is_none() + ); + } + + #[tokio::test] + async fn concurrent_image_updates_cannot_overwrite_each_other() { + let (runtime, driver, original) = fixture(true).await; + // Real RPCs are separate tasks. Polling both futures with join! in one + // task would share the debug task-local lock-order tracker. + let spawn_update = |image: &str| { + let runtime = runtime.clone(); + let sandbox_id = original.object_id().to_string(); + let version = original.get_resource_version(); + let image = image.to_string(); + tokio::spawn(async move { + runtime + .update_sandbox_image(&sandbox_id, image, version) + .await + }) + }; + let first = spawn_update("example/workload:first"); + let second = spawn_update("example/workload:second"); + let (first, second) = tokio::join!(first, second); + let (first, second) = (first.unwrap(), second.unwrap()); + assert_ne!(first.is_ok(), second.is_ok()); + assert_eq!( + first.err().or_else(|| second.err()).unwrap().code(), + Code::Aborted + ); + assert_eq!(image_calls(&driver).len(), 1); + } + + #[tokio::test] + async fn image_update_rejects_non_stopped_states() { + for phase in [ + SandboxPhase::Ready, + SandboxPhase::Provisioning, + SandboxPhase::Starting, + SandboxPhase::Stopping, + SandboxPhase::Completed, + SandboxPhase::Error, + SandboxPhase::Deleting, + ] { + let (runtime, driver, mut sandbox) = fixture(true).await; + sandbox.set_phase(phase as i32); + runtime.store.put_message(&sandbox).await.unwrap(); + let sandbox = runtime + .store + .get_message::(sandbox.object_id()) + .await + .unwrap() + .unwrap(); + assert_eq!( + runtime + .update_sandbox_image( + sandbox.object_id(), + "example/workload:new".into(), + sandbox.get_resource_version() + ) + .await + .unwrap_err() + .code(), + Code::FailedPrecondition, + "{phase:?}" + ); + assert!(image_calls(&driver).is_empty()); + } + } + + #[tokio::test] + async fn terminal_workload_can_stop_update_command_and_start() { + use openshell_core::proto::{SandboxCommandUpdate, UpdateSandboxRequest}; + + for exit_code in [0, 1, 137] { + let (runtime, driver, mut original) = fixture(true).await; + crate::auth::sandbox_session::PersistedSandboxIdentity::new() + .unwrap() + .write(&mut original.metadata.as_mut().unwrap().annotations); + original.spec.as_mut().unwrap().command = + vec!["/bin/sh".into(), "-c".into(), format!("exit {exit_code}")]; + super::super::apply_main_process_exit(&mut original, "exited-instance", exit_code); + runtime.store.put_message(&original).await.unwrap(); + + let stopped = runtime + .stop_sandbox("default", original.object_name()) + .await + .unwrap(); + assert_eq!(stopped.phase(), SandboxPhase::Stopped as i32); + assert_eq!(stopped.status.as_ref().unwrap().exit_code, Some(exit_code)); + assert_eq!( + stopped.status.as_ref().unwrap().main_process_instance_id, + "exited-instance" + ); + assert_eq!(stopped.spec, original.spec); + assert_eq!(stopped.object_id(), original.object_id()); + assert_eq!(stopped.host_key_fingerprint, original.host_key_fingerprint); + let repeated = runtime + .stop_sandbox("default", original.object_name()) + .await + .unwrap(); + assert_eq!(repeated, stopped); + + let command = vec!["sleep".into(), "infinity".into()]; + let updated = runtime + .update_sandbox_configuration( + original.object_id(), + &UpdateSandboxRequest { + command: Some(SandboxCommandUpdate { + argv: command.clone(), + }), + expected_resource_version: stopped.get_resource_version(), + ..Default::default() + }, + ) + .await + .unwrap(); + assert_eq!(updated.phase(), SandboxPhase::Stopped as i32); + assert_eq!(updated.spec.as_ref().unwrap().command, command); + assert_eq!(updated.status.as_ref().unwrap().exit_code, Some(exit_code)); + let starting = runtime + .start_sandbox("default", original.object_name()) + .await + .unwrap(); + assert_eq!(starting.phase(), SandboxPhase::Starting as i32); + assert_eq!(starting.spec, updated.spec); + assert_eq!(starting.status.as_ref().unwrap().exit_code, None); + let calls = driver.calls(); + assert_eq!( + calls + .iter() + .filter(|call| matches!(call, FakeComputeDriverCall::StopSandbox { .. })) + .count(), + 1, + "terminal compute must be stopped once before updating" + ); + assert!(!calls.iter().any(|call| matches!( + call, + FakeComputeDriverCall::DeleteSandbox { .. } + | FakeComputeDriverCall::CreateSandbox { .. } + ))); + } + } + + #[tokio::test] + async fn image_update_can_roll_back_after_start_timeout_cleanup() { + let (runtime, driver, mut original) = fixture(true).await; + crate::auth::sandbox_session::PersistedSandboxIdentity::new() + .unwrap() + .write(&mut original.metadata.as_mut().unwrap().annotations); + runtime.store.put_message(&original).await.unwrap(); + let original = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + let upgraded = runtime + .update_sandbox_image( + original.object_id(), + "example/workload:missing".into(), + original.get_resource_version(), + ) + .await + .unwrap(); + let upgraded_revision = runtime + .store + .get_sandbox_image_state(upgraded.object_id()) + .await + .unwrap() + .unwrap() + .revision; + let starting = runtime + .start_sandbox("default", original.object_name()) + .await + .unwrap(); + assert_eq!(starting.phase(), SandboxPhase::Starting as i32); + // Backdate the owned preparation clock rather than waiting for the + // production budget. Keep its attempt and settled operation identities. + let overdue = runtime + .store + .update_message_cas::( + starting.object_id(), + starting.get_resource_version(), + |sandbox| { + let record = sandbox + .status + .as_mut() + .unwrap() + .provisioning + .as_mut() + .unwrap(); + record.preparation_deadline = + openshell_core::time::timestamp_from_millis(1).ok(); + record.deadline = record.preparation_deadline; + }, + ) + .await + .unwrap(); + let expired = { + let _guard = runtime.lock_sandbox_local(overdue.object_id()).await; + runtime + .claim_provisioning_timeout(&overdue, openshell_core::time::now_ms()) + .await + .unwrap() + .unwrap() + }; + assert_eq!(expired.phase(), SandboxPhase::Error as i32); + assert!( + runtime + .stop_sandbox("default", original.object_name()) + .await + .is_err() + ); + assert!( + runtime + .update_sandbox_image( + original.object_id(), + "example/workload:old".into(), + expired.get_resource_version() + ) + .await + .is_err() + ); + { + let gate = runtime.lifecycle_gates.lock_for(expired.object_id()).await; + runtime + .reclaim_provisioning_timeout(&expired, &gate) + .await + .unwrap(); + } + let reclaimed = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + assert!( + reclaimed + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap() + .cleanup_completed_time + .is_some() + ); + let stopped = runtime + .stop_sandbox("default", original.object_name()) + .await + .unwrap(); + assert_eq!(stopped.phase(), SandboxPhase::Stopped as i32); + assert_eq!( + stopped.status.as_ref().unwrap().provisioning, + reclaimed.status.as_ref().unwrap().provisioning + ); + let corrected = runtime + .update_sandbox_image( + original.object_id(), + "example/workload:old".into(), + stopped.get_resource_version(), + ) + .await + .unwrap(); + assert_eq!( + runtime + .store + .get_sandbox_image_state(corrected.object_id()) + .await + .unwrap() + .unwrap() + .revision, + upgraded_revision + 1 + ); + assert_eq!(corrected.object_id(), original.object_id()); + assert_eq!( + corrected.host_key_fingerprint, + original.host_key_fingerprint + ); + assert_eq!(corrected.spec, original.spec); + let retried = runtime + .start_sandbox("default", original.object_name()) + .await + .unwrap(); + assert_eq!(retried.phase(), SandboxPhase::Starting as i32); + assert!(!provisioning_deadline::timed_out(&retried)); + let calls = driver.calls(); + assert_eq!( + calls + .iter() + .filter(|call| matches!(call, FakeComputeDriverCall::StopSandbox { .. })) + .count(), + 1, + "only timeout reclamation stops the driver" + ); + assert_eq!( + calls + .iter() + .filter(|call| matches!(call, FakeComputeDriverCall::StartSandbox { .. })) + .count(), + 2 + ); + assert!(!calls.iter().any(|call| matches!( + call, + FakeComputeDriverCall::CreateSandbox { .. } + | FakeComputeDriverCall::DeleteSandbox { .. } + ))); + } + + #[tokio::test] + async fn interrupted_image_update_blocks_start_and_retries_the_same_operation() { + let (runtime, driver, original) = fixture(true).await; + driver.set_image_update_error(Some(Code::Unavailable)); + assert_eq!( + runtime + .update_sandbox_image( + original.object_id(), + "example/workload:new".into(), + original.get_resource_version() + ) + .await + .unwrap_err() + .code(), + Code::Unavailable + ); + let pending = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + assert!( + runtime + .store + .get_sandbox_image_state(pending.object_id()) + .await + .unwrap() + .unwrap() + .pending + .is_some() + ); + assert_eq!( + runtime + .start_sandbox("default", original.object_name()) + .await + .unwrap_err() + .code(), + Code::FailedPrecondition + ); + assert_eq!( + runtime + .update_sandbox_image( + original.object_id(), + "example/workload:other".into(), + pending.get_resource_version() + ) + .await + .unwrap_err() + .code(), + Code::FailedPrecondition + ); + driver.set_image_update_error(None); + let complete = runtime + .update_sandbox_image( + original.object_id(), + "example/workload:new".into(), + pending.get_resource_version(), + ) + .await + .unwrap(); + assert!( + runtime + .store + .get_sandbox_image_state(complete.object_id()) + .await + .unwrap() + .unwrap() + .pending + .is_none() + ); + let calls = image_calls(&driver); + assert_eq!(calls.len(), 2); + assert_eq!(calls[0], calls[1]); + } + + #[tokio::test] + async fn image_update_intent_survives_driver_status_replacement() { + let (runtime, driver, original) = fixture(true).await; + driver.set_image_update_error(Some(Code::Unavailable)); + runtime + .update_sandbox_image( + original.object_id(), + "example/workload:new".into(), + original.get_resource_version(), + ) + .await + .unwrap_err(); + let expected = runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .unwrap(); + let mut sandbox = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + let grant_failure = Box::new(openshell_core::proto::TokenGrantFailureObservation { + destination_host: "api.example.com".into(), + ..Default::default() + }); + sandbox.status.as_mut().unwrap().last_token_grant_failure = Some(grant_failure.clone()); + let incoming = DriverSandbox { + id: original.object_id().into(), + name: original.object_name().into(), + status: Some(DriverSandboxStatus::default()), + ..Default::default() + }; + apply_driver_snapshot(&mut sandbox, &incoming, false, false); + runtime.store.put_message(&sandbox).await.unwrap(); + assert_eq!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .unwrap(), + expected + ); + assert!( + sandbox + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap() + .driver_operation_pending + ); + assert_eq!( + sandbox.status.as_ref().unwrap().last_token_grant_failure, + Some(grant_failure) + ); + assert_eq!( + runtime + .start_sandbox("default", original.object_name()) + .await + .unwrap_err() + .code(), + Code::FailedPrecondition + ); + } + + #[tokio::test] + async fn sandbox_update_activation_is_disabled_by_default_without_effects() { + let (mut runtime, driver, original) = fixture(true).await; + runtime.sandbox_updates_enabled = false; + assert_eq!( + runtime + .update_sandbox_image( + original.object_id(), + "example/workload:new".into(), + original.get_resource_version() + ) + .await + .unwrap_err() + .code(), + Code::FailedPrecondition + ); + assert!( + !driver + .calls() + .iter() + .any(|call| matches!(call, FakeComputeDriverCall::UpdateSandboxImage { .. })) + ); + assert_eq!( + runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(), + original + ); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .is_none() + ); + } + // These are the relevant pre-feature wire fields: no image-update fields + // exist in public status. Older readers already understand provisioning. + #[derive(Clone, PartialEq, prost::Message)] + struct LegacyStatus014 { + #[prost(int32, tag = "6")] + phase: i32, + #[prost(message, optional, tag = "13")] + provisioning: Option, + } + + #[tokio::test] + async fn legacy_status_roundtrip_cannot_erase_intent_or_bypass_lifecycle_interlock() { + use prost::Message; + let (runtime, driver, original) = fixture(true).await; + driver.set_image_update_error(Some(Code::Unavailable)); + runtime + .update_sandbox_image( + original.object_id(), + "example/workload:new".into(), + original.get_resource_version(), + ) + .await + .unwrap_err(); + let private_before = runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .unwrap(); + let mut pending = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + let legacy = + LegacyStatus014::decode(pending.status.as_ref().unwrap().encode_to_vec().as_slice()) + .unwrap(); + assert!( + legacy + .provisioning + .as_ref() + .unwrap() + .driver_operation_pending + ); + pending.status = Some( + openshell_core::proto::SandboxStatus::decode(legacy.encode_to_vec().as_slice()) + .unwrap(), + ); + runtime.store.put_message(&pending).await.unwrap(); + let rewritten = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + assert_eq!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .unwrap(), + private_before + ); + // This existing guard is also present in v0.1.4, unlike our private check. + assert_eq!( + provisioning_operation::ensure_operation_settled(&rewritten) + .unwrap_err() + .code(), + Code::FailedPrecondition + ); + assert_eq!( + runtime + .start_sandbox("default", original.object_name()) + .await + .unwrap_err() + .code(), + Code::FailedPrecondition + ); + assert_eq!( + runtime + .stop_sandbox("default", original.object_name()) + .await + .unwrap_err() + .code(), + Code::FailedPrecondition + ); + runtime + .recover_persisted_lifecycle_transitions() + .await + .unwrap(); + assert!(!driver.calls().iter().any(|call| matches!( + call, + FakeComputeDriverCall::StartSandbox { .. } + | FakeComputeDriverCall::StopSandbox { .. } + | FakeComputeDriverCall::DeleteSandbox { .. } + ))); + driver.set_image_update_error(None); + let complete = runtime + .update_sandbox_image( + original.object_id(), + "example/workload:new".into(), + rewritten.get_resource_version(), + ) + .await + .unwrap(); + assert!(provisioning_operation::ensure_operation_settled(&complete).is_ok()); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .unwrap() + .pending + .is_none() + ); + } + + #[tokio::test] + async fn stale_legacy_start_cas_loses_to_atomic_image_acceptance() { + let (runtime, driver, original) = fixture(true).await; + // Model an old Start paused after reading Stopped but before its CAS. + driver.set_image_update_error(Some(Code::Unavailable)); + runtime + .update_sandbox_image( + original.object_id(), + "example/workload:new".into(), + original.get_resource_version(), + ) + .await + .unwrap_err(); + let stale_start = runtime + .store + .update_message_cas::( + original.object_id(), + original.get_resource_version(), + |sandbox| sandbox.set_phase(SandboxPhase::Starting as i32), + ) + .await; + assert!(matches!( + stale_start, + Err(crate::persistence::PersistenceError::Conflict { .. }) + )); + let current = runtime + .store + .get_message::(original.object_id()) + .await + .unwrap() + .unwrap(); + assert!(provisioning_operation::ensure_operation_settled(¤t).is_err()); + assert!( + !driver + .calls() + .iter() + .any(|call| matches!(call, FakeComputeDriverCall::StartSandbox { .. })) + ); + } + + #[tokio::test] + async fn image_acceptance_loses_if_legacy_start_already_claimed_starting() { + let (runtime, driver, original) = fixture(true).await; + let starting = runtime + .store + .update_message_cas::( + original.object_id(), + original.get_resource_version(), + |sandbox| sandbox.set_phase(SandboxPhase::Starting as i32), + ) + .await + .unwrap(); + assert_eq!( + runtime + .update_sandbox_image( + original.object_id(), + "example/workload:new".into(), + starting.get_resource_version() + ) + .await + .unwrap_err() + .code(), + Code::FailedPrecondition + ); + assert!( + runtime + .store + .get_sandbox_image_state(original.object_id()) + .await + .unwrap() + .is_none() + ); + assert!( + !driver + .calls() + .iter() + .any(|call| matches!(call, FakeComputeDriverCall::UpdateSandboxImage { .. })) + ); + } +} diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 215b864e4b..2039ef4f7f 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -4,6 +4,7 @@ //! Gateway-owned compute orchestration over a pluggable compute backend. pub mod driver_config; +mod image_update; pub mod lease; mod mutation_guard; pub mod provisioning_deadline; @@ -86,6 +87,13 @@ pub type DriverWatchStream = pub type SharedComputeDriver = Arc + Send + Sync>; +/// Optional configuration-update registration, independent of the baseline driver object. +pub type SharedComputeDriverConfigurationUpdates = Arc< + dyn openshell_core::compute::ComputeDriver + + Send + + Sync, +>; + fn supports_isolation_registration(capabilities: &[String]) -> bool { capabilities.iter().any(|capability| { capability == openshell_core::extension_protocol::COMPUTE_ISOLATION_BACKEND @@ -429,21 +437,26 @@ impl QuiesceOperation { let phase = SandboxPhase::try_from(sandbox.phase()).unwrap_or(SandboxPhase::Unknown); match self { Self::Stop => { - if matches!(phase, SandboxPhase::Stopped | SandboxPhase::Completed) - || is_failed_main_process_result(sandbox) - { + if phase == SandboxPhase::Stopped { return Ok(QuiesceAdmission::Settled); } - // Timeout cleanup owns reclamation while the record remains - // Error. Its later start may recreate missing compute; an - // ordinary stop must not replace that recovery state with - // Stopping or Stopped. - if phase == SandboxPhase::Error && provisioning_deadline::timed_out(sandbox) { + // Timeout cleanup owns reclamation until it completes. Once + // reclaimed, explicit stop can expose Stopped for image repair + // while preserving the timeout record for a later start. + if phase == SandboxPhase::Error + && provisioning_deadline::timed_out(sandbox) + && sandbox + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + .is_none_or(|record| record.cleanup_completed_time.is_none()) + { return Err(Status::failed_precondition( - "sandbox provisioning timed out; retry start after timeout cleanup completes", + "sandbox provisioning timed out; wait for timeout cleanup before stop or start", )); } - // Error does not prove compute has stopped. Use the normal + // Neither a main-process exit nor an infrastructure Error + // proves compute has stopped. Use the normal // driver stop and session cleanup before a subsequent start // renews authentication and repeats configuration admission // for the retained sandbox. Stopping a suspended sandbox @@ -452,6 +465,7 @@ impl QuiesceOperation { if !matches!( phase, SandboxPhase::Ready + | SandboxPhase::Completed | SandboxPhase::Stopping | SandboxPhase::Error | SandboxPhase::Suspending @@ -459,7 +473,7 @@ impl QuiesceOperation { ) && !is_automatic_restart_transition(sandbox) { return Err(Status::failed_precondition(format!( - "sandbox must be Ready, Suspended, Error, or in an automatic restart to stop (current phase: {phase:?})" + "sandbox must be Ready, Completed, Suspended, Error, or in an automatic restart to stop (current phase: {phase:?})" ))); } Ok(if phase == SandboxPhase::Stopping { @@ -670,6 +684,7 @@ impl AcquiredRemoteDriverEndpoint { #[derive(Debug, Clone)] struct RemoteComputeDriver { client: RemoteComputeDriverClient, + configuration_updates_client: openshell_core::proto::compute::v1::compute_driver_configuration_updates_client::ComputeDriverConfigurationUpdatesClient>, admission_acknowledgement: Arc>>, } @@ -680,7 +695,8 @@ type RemoteComputeDriverClient = ComputeDriverClient< impl RemoteComputeDriver { fn new(channel: Channel) -> Self { Self { - client: ComputeDriverClient::with_interceptor(channel, TraceContextInterceptor), + client: ComputeDriverClient::with_interceptor(channel.clone(), TraceContextInterceptor), + configuration_updates_client: openshell_core::proto::compute::v1::compute_driver_configuration_updates_client::ComputeDriverConfigurationUpdatesClient::with_interceptor(channel, TraceContextInterceptor), admission_acknowledgement: Arc::new(StdMutex::new(None)), } } @@ -721,6 +737,23 @@ impl RemoteComputeDriver { } } +#[tonic::async_trait] +impl openshell_core::compute::ComputeDriver for RemoteComputeDriver { + async fn update_sandbox_configuration( + &self, + request: Request, + ) -> Result< + tonic::Response, + Status, + > { + self.verify_admission_policy(false).await?; + self.configuration_updates_client + .clone() + .update_sandbox_configuration(request) + .await + } +} + #[tonic::async_trait] impl ComputeDriver for RemoteComputeDriver { type WatchSandboxesStream = DriverWatchStream; @@ -877,11 +910,15 @@ pub struct ComputeRuntime { admission: openshell_core::resource_admission::DriverAdmissionConfig, admission_acknowledgement: String, driver: TracedDriver, + image_update_driver: Option, driver_info: ComputeDriverInfoSnapshot, telemetry_compute_driver: TelemetryComputeDriver, driver_process: Option>, default_image: String, image_preparation_timeout_seconds: u32, + sandbox_updates_enabled: bool, + update_recovery_sweep: Arc, + update_recovery_backoff: Arc>>, store: Arc, sandbox_index: SandboxIndex, sandbox_watch_bus: SandboxWatchBus, @@ -991,6 +1028,7 @@ impl ComputeRuntime { Ok(Self { isolation_backend: None, driver: TracedDriver::new(driver, driver_name), + image_update_driver: None, admission: openshell_core::resource_admission::DriverAdmissionConfig::default(), admission_acknowledgement: capabilities.resource_admission_policy, driver_info, @@ -998,6 +1036,9 @@ impl ComputeRuntime { driver_process, default_image, image_preparation_timeout_seconds: 1800, + sandbox_updates_enabled: false, + update_recovery_sweep: Arc::new(tokio::sync::Semaphore::new(1)), + update_recovery_backoff: Arc::default(), store, sandbox_index, sandbox_watch_bus, @@ -1129,10 +1170,10 @@ impl ComputeRuntime { tracing_log_bus: TracingLogBus, supervisor_sessions: Arc, ) -> Result { - let driver: SharedComputeDriver = Arc::new(RemoteComputeDriver::new(endpoint.channel)); + let driver = Arc::new(RemoteComputeDriver::new(endpoint.channel)); Self::from_driver( endpoint.name, - driver, + driver.clone(), endpoint.driver_process, store, sandbox_index, @@ -1141,6 +1182,15 @@ impl ComputeRuntime { supervisor_sessions, ) .await + .map(|runtime| runtime.with_image_update_driver(Some(driver))) + } + + pub(crate) fn with_image_update_driver( + mut self, + driver: Option, + ) -> Self { + self.image_update_driver = driver; + self } #[must_use] @@ -1808,14 +1858,15 @@ impl ComputeRuntime { Some(guard) => guard, None => runtime.lock_sandbox_for_lifecycle(&lifecycle_guard).await, }; - runtime - .compensate_failed_create_inner( - &created, - lifecycle_guard, - sandbox_guard, - original, - ) - .await + // Keep compensation's future bounded as the persisted sandbox + // status grows (including image-update reconciliation state). + Box::pin(runtime.compensate_failed_create_inner( + &created, + lifecycle_guard, + sandbox_guard, + original, + )) + .await } .instrument(request_span), ) @@ -2042,14 +2093,32 @@ impl ComputeRuntime { QuiesceAdmission::Begin => {} } - let (reason, message) = operation.requested_condition(); + let reclaimed_timeout = operation == QuiesceOperation::Stop + && current.phase() == SandboxPhase::Error as i32 + && provisioning_deadline::timed_out(¤t); + let (next_phase, reason, message) = if reclaimed_timeout { + ( + SandboxPhase::Stopped, + "StoppedAfterProvisioningTimeout", + "Timed-out compute has been reclaimed; sandbox stopped for repair", + ) + } else { + let (reason, message) = operation.requested_condition(); + (operation.in_flight_phase(), reason, message) + }; match self - .write_lifecycle_phase(¤t, operation.in_flight_phase(), reason, message) + .write_lifecycle_phase(¤t, next_phase, reason, message) .await { Ok(transition) => { self.sandbox_index.update_from_sandbox(&transition); self.sandbox_watch_bus.notify(&sandbox_id); + if reclaimed_timeout { + self.cleanup_stopped_sandbox_sessions(&transition) + .await + .map_err(Status::internal)?; + return Ok(transition); + } break (current, transition); } Err(crate::persistence::PersistenceError::Conflict { .. }) @@ -2134,8 +2203,12 @@ impl ComputeRuntime { Ok(settled) } Err(err) => { - self.recover_failed_lifecycle(&lifecycle_guard, &transition, &previous, true) - .await; + if let Some(terminal) = self + .recover_failed_lifecycle(&lifecycle_guard, &transition, &previous, true) + .await + { + return Ok(terminal); + } Err(Status::new( err.code(), format!("{} sandbox failed: {}", operation.verb(), err.message()), @@ -2264,6 +2337,7 @@ impl ComputeRuntime { self.validate_launch_signer_configured(authority.is_some())?; } provisioning_operation::ensure_operation_settled(&candidate)?; + self.ensure_no_pending_image_update(&candidate).await?; if provisioning_deadline::timed_out(&candidate) && candidate .status @@ -2301,6 +2375,7 @@ impl ComputeRuntime { let mut attempts = 0; let (previous, starting, launch_authentication) = loop { provisioning_operation::ensure_operation_settled(¤t)?; + self.ensure_no_pending_image_update(¤t).await?; let phase = SandboxPhase::try_from(current.phase()).unwrap_or(SandboxPhase::Unknown); if let StartIntent::Resume { .. } = intent { // Resume restores only retained state. A Starting record may @@ -2610,7 +2685,8 @@ impl ComputeRuntime { settled, }) => { provisioning_operation::ensure_current_result(&settled, &settled)?; - self.recover_failed_lifecycle(&lifecycle_guard, &settled, &previous, false) + let _ = self + .recover_failed_lifecycle(&lifecycle_guard, &settled, &previous, false) .await; Err(Status::new( err.code(), @@ -2863,7 +2939,7 @@ impl ComputeRuntime { transition: &Sandbox, previous: &Sandbox, expected_stopped: bool, - ) { + ) -> Option { let sandbox_id = transition.object_id(); let sandbox_name = transition.object_name(); let observed = tokio::time::timeout( @@ -2875,6 +2951,14 @@ impl ComputeRuntime { let _sandbox_guard = self.lock_sandbox_for_lifecycle(lifecycle_guard).await; match observed { + Ok(None) if terminal_stop_exit_code(transition).is_some() => { + match self.restore_missing_terminal_stop(transition).await { + Ok(terminal) => return Some(terminal), + Err(error) => { + warn!(sandbox_id, %error, "Failed to restore terminal result after stop"); + } + } + } Ok(Some(snapshot)) if snapshot.id == sandbox_id && snapshot.status.is_some() => { let backend_phase = derive_phase(snapshot.status.as_ref()); let observed_stopped = backend_phase == SandboxPhase::Stopped @@ -2943,6 +3027,38 @@ impl ComputeRuntime { ); } } + None + } + + /// Called under the sandbox mutation lock after an authoritative lookup + /// confirms absent compute. The persisted exit code survives a gateway + /// restart; Stopping only retains it when stop began from a terminal result. + async fn restore_missing_terminal_stop(&self, transition: &Sandbox) -> Result { + let exit_code = terminal_stop_exit_code(transition) + .ok_or_else(|| Status::failed_precondition("stop has no terminal result to restore"))?; + let instance_id = transition + .status + .as_ref() + .unwrap() + .main_process_instance_id + .clone(); + let restored = self + .store + .update_message_cas::( + transition.object_id(), + sandbox_resource_version(transition), + |sandbox| apply_main_process_exit(sandbox, &instance_id, exit_code), + ) + .await + .map_err(|error| { + crate::grpc::persistence_error_to_status(error, "restore terminal sandbox result") + })?; + self.sandbox_index.update_from_sandbox(&restored); + self.sandbox_watch_bus.notify(restored.object_id()); + self.cleanup_stopped_sandbox_sessions(&restored) + .await + .map_err(Status::internal)?; + Ok(restored) } async fn reconcile_lifecycle_snapshot( @@ -3406,7 +3522,23 @@ impl ComputeRuntime { .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? .ok_or_else(|| Status::not_found("sandbox not found"))?, }; - provisioning_operation::ensure_operation_settled(&sandbox)?; + // Explicit deletion can supersede a configuration update: the + // driver only patches an existing immutable resource and cannot + // recreate it after deletion. Keep the interlock until removal so + // a failed delete can safely restore the pending update. + let pending_update = self + .store + .get_sandbox_image_state(sandbox_id) + .await + .map_err(|_| Status::unavailable("cannot read pending sandbox update"))? + .and_then(|state| state.pending); + if failed_create.is_none() + && let Some(intent) = pending_update + { + image_update::ensure_image_update_interlock(&sandbox, &intent)?; + } else { + provisioning_operation::ensure_operation_settled(&sandbox)?; + } // Failed-create compensation owns only its original attempt. // Preserve its timeout diagnosis and any replacement attempt on @@ -3562,7 +3694,10 @@ impl ComputeRuntime { let mut delete_resource_version = record.resource_version; let mut sandbox = decode_sandbox_record(&record)?; - if provisioning_deadline::driver_operation_pending(&sandbox) { + if self + .pending_driver_operation_blocks_cleanup(&sandbox) + .await? + { return Ok(false); } if sandbox.phase() != SandboxPhase::Deleting as i32 { @@ -3588,10 +3723,11 @@ impl ComputeRuntime { return Err(error); } - match self - .store - .delete_if(Sandbox::object_type(), sandbox_id, delete_resource_version) - .await + match Box::pin( + self.store + .delete_sandbox_with_image_state_if(sandbox_id, delete_resource_version), + ) + .await { Ok(true) => { self.cleanup_removed_sandbox_state(sandbox_id); @@ -4077,7 +4213,7 @@ impl ComputeRuntime { { Box::pin(self.recover_persisted_lifecycle_transitions()).await?; if !self.driver_info.gateway_manages_lifecycle { - return Ok(()); + return self.recover_pending_sandbox_updates().await; } let sandbox_ids = self.list_persisted_sandbox_ids("gateway startup").await?; @@ -4337,7 +4473,7 @@ impl ComputeRuntime { "Sandbox start sweep complete" ); } - Ok(()) + self.recover_pending_sandbox_updates().await } async fn recover_persisted_lifecycle_transitions(&self) -> Result<(), String> { @@ -4358,7 +4494,10 @@ impl ComputeRuntime { continue; } }; - if provisioning_deadline::driver_operation_pending(&sandbox) { + if self + .pending_driver_operation_blocks_cleanup(&sandbox) + .await? + { warn!( sandbox_id, "Retaining pending driver operation during gateway recovery" @@ -4378,12 +4517,20 @@ impl ComputeRuntime { } } SandboxPhase::Stopping => { - self.recover_quiesce_transition(QuiesceOperation::Stop, &sandbox) - .await; + self.recover_quiesce_transition( + QuiesceOperation::Stop, + &sandbox, + &lifecycle_guard, + ) + .await; } SandboxPhase::Suspending => { - self.recover_quiesce_transition(QuiesceOperation::Suspend, &sandbox) - .await; + self.recover_quiesce_transition( + QuiesceOperation::Suspend, + &sandbox, + &lifecycle_guard, + ) + .await; } SandboxPhase::Starting => { if let Err(error) = self.validate_isolation_backend_binding(&sandbox) { @@ -4458,7 +4605,7 @@ impl ComputeRuntime { if let ProvisioningOperationError::Driver { settled, .. } = err && in_flight_resume_intent(&settled).is_some() { - Box::pin(self.recover_failed_lifecycle( + let _ = Box::pin(self.recover_failed_lifecycle( &lifecycle_guard, &settled, &sandbox, @@ -4477,12 +4624,24 @@ impl ComputeRuntime { /// Re-issue the idempotent driver call of a stop or suspend interrupted /// by a gateway restart, then commit its settled phase. - async fn recover_quiesce_transition(&self, operation: QuiesceOperation, sandbox: &Sandbox) { + async fn recover_quiesce_transition( + &self, + operation: QuiesceOperation, + sandbox: &Sandbox, + lifecycle_guard: &SandboxLifecycleGuard, + ) { let sandbox_id = sandbox.object_id(); if let Err(err) = self .call_quiesce_driver(operation, sandbox_id, sandbox.object_name()) .await { + if terminal_stop_exit_code(sandbox).is_some() + && Box::pin(self.recover_failed_lifecycle(lifecycle_guard, sandbox, sandbox, true)) + .await + .is_some() + { + return; + } warn!(sandbox_id, operation = operation.verb(), error = %err, "Failed to recover sandbox lifecycle transition"); return; } @@ -4767,6 +4926,9 @@ impl ComputeRuntime { async fn reconcile_loop(self: Arc, mut cancel: watch::Receiver) { loop { + if let Err(error) = self.recover_pending_sandbox_updates().await { + warn!(%error, "Sandbox update recovery sweep failed"); + } if let Err(err) = self.reconcile_store_with_backend(ORPHAN_GRACE_PERIOD).await { warn!(error = %err, "Store reconciliation sweep failed"); } @@ -5314,7 +5476,7 @@ impl ComputeRuntime { continue; } - self.prune_missing_sandbox(record, sweep_started_at_ms, grace_ms) + Box::pin(self.prune_missing_sandbox(record, sweep_started_at_ms, grace_ms)) .await .inspect_err(|_| crate::otel_tracing::mark_error(&tracing::Span::current()))?; } @@ -6083,9 +6245,17 @@ impl ComputeRuntime { .await .map_err(|e| e.to_string())?; if let Some(sandbox) = sandbox.as_ref() { - if provisioning_deadline::driver_operation_pending(sandbox) + // Timeout cleanup can emit Deleted before an explicit stop exposes + // Stopped for repair. A delayed (or repeated) event must retain that + // recovery record too; explicit deletion still transitions to Deleting. + if self + .pending_driver_operation_blocks_cleanup(sandbox) + .await? || (provisioning_deadline::timed_out(sandbox) - && sandbox.phase() == i32::from(SandboxPhase::Error)) + && matches!( + SandboxPhase::try_from(sandbox.phase()), + Ok(SandboxPhase::Error | SandboxPhase::Stopped) + )) { return Ok(()); } @@ -6121,7 +6291,10 @@ impl ComputeRuntime { sandbox: &Sandbox, expected_resource_version: u64, ) -> Result<(), String> { - if provisioning_deadline::driver_operation_pending(sandbox) { + if self + .pending_driver_operation_blocks_cleanup(sandbox) + .await? + { return Ok(()); } let sandbox_id = sandbox.object_id(); @@ -6131,6 +6304,8 @@ impl ComputeRuntime { } async fn cleanup_sandbox_owned_records(&self, sandbox: &Sandbox) -> Result<(), String> { + // Keep private update intent until atomic sandbox removal: a cleanup + // failure or CAS conflict must leave evidence for the deletion retry. self.cleanup_ssh_identity(sandbox) .await .map_err(|error| error.to_string())?; @@ -6446,7 +6621,10 @@ impl ComputeRuntime { } let sandbox = decode_sandbox_record(¤t_record)?; - if provisioning_deadline::driver_operation_pending(&sandbox) { + if self + .pending_driver_operation_blocks_cleanup(&sandbox) + .await? + { return Ok(()); } let age_ms = openshell_core::time::now_ms() @@ -6489,6 +6667,13 @@ impl ComputeRuntime { let sandbox = decode_sandbox_record(¤t_record)?; let phase = SandboxPhase::try_from(sandbox.phase()).unwrap_or(SandboxPhase::Unknown); + if terminal_stop_exit_code(&sandbox).is_some() { + return self + .restore_missing_terminal_stop(&sandbox) + .await + .map(|_| ()) + .map_err(|error| error.to_string()); + } if phase == SandboxPhase::Completed || is_failed_main_process_result(&sandbox) || provisioning_deadline::timed_out(&sandbox) @@ -6684,6 +6869,16 @@ fn has_specific_infrastructure_error(sandbox: &Sandbox) -> bool { }) } +fn terminal_stop_exit_code(sandbox: &Sandbox) -> Option { + if sandbox.phase() != SandboxPhase::Stopping as i32 + || provisioning_deadline::timed_out(sandbox) + || provisioning_deadline::driver_operation_pending(sandbox) + { + return None; + } + sandbox.status.as_ref()?.exit_code +} + fn is_failed_main_process_result(sandbox: &Sandbox) -> bool { sandbox.phase() == SandboxPhase::Error as i32 && sandbox.status.as_ref().is_some_and(|status| { @@ -7951,6 +8146,9 @@ fn apply_lifecycle_phase( message: &str, image_preparation_timeout_seconds: u32, ) { + let preserve_exit_code = phase == SandboxPhase::Stopping + && (sandbox.phase() == SandboxPhase::Completed as i32 + || is_failed_main_process_result(sandbox)); sandbox.set_phase(phase as i32); // Every explicit lifecycle write ends the previous transition's resume // intent. A resuming start records a new one after this write. @@ -7966,7 +8164,11 @@ fn apply_lifecycle_phase( let status = sandbox.status.get_or_insert_with(Default::default); // Retain the previous instance id as a tombstone until the restarted // supervisor registers its new id. - status.exit_code = None; + // Explicit stop preserves an already-recorded terminal result for + // diagnosis while the sandbox is repaired. A fresh start clears it. + if !preserve_exit_code { + status.exit_code = None; + } status.restart_count = 0; set_next_restart_at_ms(status, 0); set_main_process_started_at_ms(status, 0); @@ -8382,6 +8584,7 @@ pub fn new_test_runtime_with_driver( ComputeRuntime { isolation_backend: None, driver: TracedDriver::new(driver, "test".to_string()), + image_update_driver: None, admission: openshell_core::resource_admission::DriverAdmissionConfig { allow_driver_config: true, ..Default::default() @@ -8403,6 +8606,9 @@ pub fn new_test_runtime_with_driver( driver_process: None, default_image: "openshell/sandbox:test".to_string(), image_preparation_timeout_seconds: 1800, + sandbox_updates_enabled: false, + update_recovery_sweep: Arc::new(tokio::sync::Semaphore::new(1)), + update_recovery_backoff: Arc::default(), store, sandbox_index: SandboxIndex::new(), sandbox_watch_bus: SandboxWatchBus::new(), @@ -9262,6 +9468,9 @@ mod tests { } } + #[tonic::async_trait] + impl openshell_core::compute::ComputeDriver for ControlledDriver {} + #[tonic::async_trait] impl ComputeDriver for ControlledDriver { async fn authenticate_sandbox( @@ -9578,6 +9787,7 @@ mod tests { ComputeRuntime { isolation_backend: None, driver: TracedDriver::new(driver, "test-driver".to_string()), + image_update_driver: None, admission: openshell_core::resource_admission::DriverAdmissionConfig { allow_driver_config: true, ..Default::default() @@ -9599,6 +9809,9 @@ mod tests { driver_process: None, default_image: "openshell/sandbox:test".to_string(), image_preparation_timeout_seconds: 1800, + sandbox_updates_enabled: false, + update_recovery_sweep: Arc::new(Semaphore::new(1)), + update_recovery_backoff: Arc::default(), store, sandbox_index: SandboxIndex::new(), sandbox_watch_bus: SandboxWatchBus::new(), @@ -12555,6 +12768,9 @@ mod tests { #[derive(Debug, Default)] struct FailingDriver(TestDriver); + #[tonic::async_trait] + impl openshell_core::compute::ComputeDriver for FailingDriver {} + #[tonic::async_trait] impl ComputeDriver for FailingDriver { async fn authenticate_sandbox( @@ -15120,7 +15336,7 @@ mod tests { tokio::time::timeout( Duration::from_secs(1), - runtime.reconcile_store_with_backend(Duration::ZERO), + Box::pin(runtime.reconcile_store_with_backend(Duration::ZERO)), ) .await .expect("prune sweep blocked on the stuck driver delete call") @@ -17538,10 +17754,11 @@ mod tests { async fn remote_compute_driver_interceptor_propagates_every_rpc() { use crate::otel_tracing::test_exporter; use crate::test_support::FakeComputeDriver; + use openshell_core::compute::ComputeDriver as _; let dir = tempfile::tempdir().unwrap(); let socket_path = dir.path().join("compute-driver.sock"); - let driver = FakeComputeDriver::new(); + let driver = FakeComputeDriver::new().with_sandbox_image_updates(); let _server = driver.serve_uds(&socket_path).unwrap(); let endpoint = connect_remote_compute_driver("external-test", &socket_path) .await @@ -17595,6 +17812,21 @@ mod tests { .watch_sandboxes(Request::new(WatchSandboxesRequest {})) .await .unwrap(); + remote + .update_sandbox_configuration(Request::new( + openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest { + preconditions: Some( + openshell_core::proto::compute::v1::SandboxUpdatePreconditions { + sandbox_id: sandbox.id.clone(), + image: "example/workload:v2".into(), + ..Default::default() + }, + ), + ..Default::default() + }, + )) + .await + .unwrap(); remote .delete_sandbox(Request::new(DeleteSandboxRequest { sandbox_id: sandbox.id, @@ -17612,7 +17844,7 @@ mod tests { let traceparents = driver.traceparents(); assert_eq!( traceparents.len(), - 10, + 12, "the client interceptor should cover every RPC" ); assert!( @@ -18223,12 +18455,175 @@ endpoint = { kind = "unix", socket_path = "/run/agent-substrate/isolation.sock" .await .is_err() ); + let status = openshell_core::compute::ComputeDriver::update_sandbox_configuration( + &remote, + Request::new( + openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest::default(), + ), + ) + .await + .unwrap_err(); + assert_eq!(status.code(), Code::FailedPrecondition); assert_eq!( driver.calls(), - vec![FakeComputeDriverCall::GetCapabilities; 3] + vec![FakeComputeDriverCall::GetCapabilities; 4] ); } + #[tokio::test] + #[cfg(unix)] + async fn remote_compute_driver_negotiates_optional_image_service() { + use crate::test_support::{FakeComputeDriver, FakeComputeDriverCall}; + use openshell_core::GetResourceVersion as _; + use openshell_core::extension_protocol::COMPUTE_UPDATE_SANDBOX_CONFIGURATION; + + for supported in [false, true] { + let dir = tempfile::tempdir().unwrap(); + let socket_path = dir.path().join("image-updates.sock"); + let driver = FakeComputeDriver::new(); + let driver = if supported { + driver.with_sandbox_image_updates() + } else { + driver + }; + let _server = driver.serve_uds(&socket_path).unwrap(); + let endpoint = connect_remote_compute_driver("external-test", &socket_path) + .await + .unwrap(); + let store = Arc::new(crate::persistence::test_store().await); + crate::ensure_default_workspace(&store).await.unwrap(); + let mut runtime = ComputeRuntime::new_remote_driver( + endpoint, + store.clone(), + SandboxIndex::new(), + SandboxWatchBus::new(), + TracingLogBus::new(), + Arc::new(SupervisorSessionRegistry::new()), + ) + .await + .unwrap(); + runtime.sandbox_updates_enabled = true; + assert!( + !runtime + .driver_info + .negotiated_extension + .supported_capabilities + .iter() + .any(|capability| capability + == openshell_core::extension_protocol::COMPUTE_ISOLATION_BACKEND), + "image-only updates do not require external isolation registration" + ); + assert_eq!( + runtime + .driver_info + .negotiated_extension + .supported_capabilities + .iter() + .any(|capability| capability == COMPUTE_UPDATE_SANDBOX_CONFIGURATION), + supported, + ); + let mut sandbox = sandbox_record("sb-image", "image-sandbox", SandboxPhase::Stopped); + sandbox.metadata.as_mut().unwrap().annotations.insert( + COMPUTE_RUNTIME_IDENTITY_ANNOTATION.into(), + "stable-runtime".into(), + ); + sandbox.spec = Some(SandboxSpec { + template: Some(SandboxTemplate { + image: "example/workload:v1".into(), + ..Default::default() + }), + ..Default::default() + }); + store.put_message(&sandbox).await.unwrap(); + let sandbox = store + .get_message::("sb-image") + .await + .unwrap() + .unwrap(); + driver.clear_calls(); + let result = runtime + .update_sandbox_image( + "sb-image", + "example/workload:v2".into(), + sandbox.get_resource_version(), + ) + .await; + let requests = driver + .calls() + .into_iter() + .filter_map(|call| match call { + FakeComputeDriverCall::UpdateSandboxImage { request } => Some(request), + _ => None, + }) + .collect::>(); + if supported { + let updated = result.unwrap(); + assert_eq!( + updated.spec.unwrap().template.unwrap().image, + "example/workload:v2" + ); + assert_eq!( + requests.len(), + 2, + "preflight and mutation use the optional service" + ); + assert!(requests[0].validate_only); + assert!(!requests[1].validate_only); + for request in &requests { + assert_eq!(request.sandbox_id, "sb-image"); + assert_eq!(request.expected_runtime_identity, "stable-runtime"); + assert_eq!(request.expected_image, "example/workload:v1"); + assert_eq!(request.image, "example/workload:v2"); + } + assert_eq!(requests[0].operation_id, requests[1].operation_id); + } else { + assert_eq!(result.unwrap_err().code(), Code::Unimplemented); + assert!( + requests.is_empty(), + "capability admission must precede extension calls" + ); + let unchanged = store + .get_message::("sb-image") + .await + .unwrap() + .unwrap(); + assert_eq!(unchanged, sandbox); + } + } + } + + #[tokio::test] + async fn in_process_driver_defaults_image_updates_to_unimplemented() { + // NoopTestDriver implements only the baseline generated trait. + let mut runtime = test_runtime(Arc::new(NoopTestDriver::default())) + .await + .with_sandbox_updates(true); + runtime + .driver_info + .negotiated_extension + .supported_capabilities + .push(openshell_core::extension_protocol::COMPUTE_UPDATE_SANDBOX_CONFIGURATION.into()); + let mut sandbox = sandbox_record( + "unsupported-update", + "unsupported-update", + SandboxPhase::Stopped, + ); + sandbox.spec = Some(SandboxSpec { + template: Some(SandboxTemplate { + image: "example/workload:v1".into(), + ..Default::default() + }), + ..Default::default() + }); + runtime.store.put_message(&sandbox).await.unwrap(); + let error = runtime + .update_sandbox_image("unsupported-update", "example/workload:v2".into(), 1) + .await + .unwrap_err(); + assert_eq!(error.code(), Code::Unimplemented); + assert!(runtime.image_update_driver.is_none()); + } + #[tokio::test] async fn negotiated_launch_requirement_rejects_create_before_driver_or_store() { let driver = Arc::new(TestDriver { diff --git a/crates/openshell-server/src/compute/tests/error_recovery.rs b/crates/openshell-server/src/compute/tests/error_recovery.rs index c6c2d45aee..8d269d1804 100644 --- a/crates/openshell-server/src/compute/tests/error_recovery.rs +++ b/crates/openshell-server/src/compute/tests/error_recovery.rs @@ -16,6 +16,226 @@ async fn stored(runtime: &ComputeRuntime, id: &str) -> Sandbox { .unwrap() } +#[tokio::test] +async fn terminal_stop_preserves_result_when_compute_is_missing() { + for exit_code in [0, 1, 137] { + let driver = ControlledDriver::new(); + driver.set_stop_outcome(ControlledLifecycleOutcome::NotFound); + let runtime = test_runtime(driver.clone()).await; + let mut sandbox = sandbox_record("terminal-id", "terminal", SandboxPhase::Ready); + apply_main_process_exit(&mut sandbox, "exited-instance", exit_code); + runtime.store.put_message(&sandbox).await.unwrap(); + let session = ssh_session_record("old-session", sandbox.object_id()); + runtime.store.put_message(&session).await.unwrap(); + register_test_supervisor_session(&runtime, sandbox.object_id()); + + for _ in 0..2 { + let result = runtime.stop_sandbox("default", "terminal").await.unwrap(); + assert_eq!(result.phase(), sandbox.phase()); + assert_eq!(result.status.as_ref().unwrap().exit_code, Some(exit_code)); + assert_eq!( + result.status.as_ref().unwrap().main_process_instance_id, + "exited-instance" + ); + assert_eq!( + ready_condition(&result).unwrap().reason, + ready_condition(&sandbox).unwrap().reason + ); + assert_eq!(result.spec, sandbox.spec); + assert!(!runtime.supervisor_sessions.has_session(sandbox.object_id())); + assert!( + runtime + .store + .get_message::(session.object_id()) + .await + .unwrap() + .is_none() + ); + + let record = runtime + .store + .get(Sandbox::object_type(), sandbox.object_id()) + .await + .unwrap() + .unwrap(); + runtime + .prune_missing_sandbox(record, openshell_core::time::now_ms(), 0) + .await + .unwrap(); + assert_eq!(stored(&runtime, sandbox.object_id()).await, result); + } + assert_eq!(driver.start_calls(), 0); + assert_eq!(driver.delete_calls(), 0); + } +} + +#[tokio::test] +async fn terminal_stop_recovers_from_persisted_result_after_restart_or_pruning() { + for exit_code in [0, 1, 137] { + for startup in [false, true] { + let driver = ControlledDriver::new(); + driver.set_stop_outcome(ControlledLifecycleOutcome::NotFound); + let runtime = test_runtime(driver).await; + let mut sandbox = sandbox_record("terminal-id", "terminal", SandboxPhase::Ready); + apply_main_process_exit(&mut sandbox, "exited-instance", exit_code); + runtime.store.put_message(&sandbox).await.unwrap(); + let terminal = stored(&runtime, sandbox.object_id()).await; + // Simulate a gateway dying after persisting Stopping, with no + // in-memory pre-stop snapshot available to either recovery path. + runtime + .write_lifecycle_phase( + &terminal, + SandboxPhase::Stopping, + "StopRequested", + "Stop requested", + ) + .await + .unwrap(); + let session = ssh_session_record("old-session", sandbox.object_id()); + runtime.store.put_message(&session).await.unwrap(); + register_test_supervisor_session(&runtime, sandbox.object_id()); + + if startup { + runtime + .recover_persisted_lifecycle_transitions() + .await + .unwrap(); + } else { + let record = runtime + .store + .get(Sandbox::object_type(), sandbox.object_id()) + .await + .unwrap() + .unwrap(); + runtime + .prune_missing_sandbox(record, openshell_core::time::now_ms(), 0) + .await + .unwrap(); + } + let result = stored(&runtime, sandbox.object_id()).await; + assert_eq!(result.phase(), terminal.phase()); + assert_eq!(result.status.as_ref().unwrap().exit_code, Some(exit_code)); + assert_eq!( + ready_condition(&result).unwrap().reason, + ready_condition(&terminal).unwrap().reason + ); + assert!(!runtime.supervisor_sessions.has_session(sandbox.object_id())); + assert!( + runtime + .store + .get_message::(session.object_id()) + .await + .unwrap() + .is_none() + ); + } + } +} + +#[tokio::test] +async fn terminal_stop_keeps_uncertain_compute_pending_until_explicit_retry() { + let driver = ControlledDriver::new(); + driver.set_stop_outcome(ControlledLifecycleOutcome::NotFound); + driver.set_get_outcome(ControlledGetOutcome::Error("lookup unavailable")); + let runtime = test_runtime(driver.clone()).await; + let mut sandbox = sandbox_record("terminal-id", "terminal", SandboxPhase::Ready); + apply_main_process_exit(&mut sandbox, "exited-instance", 0); + runtime.store.put_message(&sandbox).await.unwrap(); + + assert_eq!( + runtime + .stop_sandbox("default", "terminal") + .await + .unwrap_err() + .code(), + Code::NotFound + ); + let pending = stored(&runtime, sandbox.object_id()).await; + assert_eq!(pending.phase(), SandboxPhase::Stopping as i32); + assert_eq!(pending.status.as_ref().unwrap().exit_code, Some(0)); + runtime + .recover_persisted_lifecycle_transitions() + .await + .unwrap(); + assert_eq!(stored(&runtime, sandbox.object_id()).await, pending); + + driver.set_get_outcome(ControlledGetOutcome::Missing); + let result = runtime.stop_sandbox("default", "terminal").await.unwrap(); + assert_eq!(result.phase(), SandboxPhase::Completed as i32); + assert_eq!(result.status.as_ref().unwrap().exit_code, Some(0)); +} + +#[tokio::test] +async fn terminal_stop_recovery_does_not_overwrite_concurrent_changes() { + let driver = ControlledDriver::new(); + driver.set_stop_outcome(ControlledLifecycleOutcome::NotFound); + driver.block_get(); + let runtime = test_runtime(driver.clone()).await; + let mut sandbox = sandbox_record("terminal-id", "terminal", SandboxPhase::Ready); + apply_main_process_exit(&mut sandbox, "exited-instance", 0); + runtime.store.put_message(&sandbox).await.unwrap(); + let stop = { + let runtime = runtime.clone(); + tokio::spawn(async move { runtime.stop_sandbox("default", "terminal").await }) + }; + driver.get_started.notified().await; + let pending = stored(&runtime, sandbox.object_id()).await; + let deleting = runtime + .write_lifecycle_phase( + &pending, + SandboxPhase::Deleting, + "DeleteRequested", + "Delete requested", + ) + .await + .unwrap(); + driver.release_get(); + + assert!(stop.await.unwrap().is_err()); + assert_eq!(stored(&runtime, sandbox.object_id()).await, deleting); +} + +#[tokio::test] +async fn stop_without_terminal_result_still_reports_missing_compute() { + let driver = ControlledDriver::new(); + driver.set_stop_outcome(ControlledLifecycleOutcome::NotFound); + let runtime = test_runtime(driver).await; + let sandbox = sandbox_record("running-id", "running", SandboxPhase::Ready); + runtime.store.put_message(&sandbox).await.unwrap(); + assert_eq!( + runtime + .stop_sandbox("default", "running") + .await + .unwrap_err() + .code(), + Code::NotFound + ); + let pending = stored(&runtime, sandbox.object_id()).await; + assert_eq!(pending.phase(), SandboxPhase::Stopping as i32); + assert_eq!(pending.status.as_ref().unwrap().exit_code, None); + runtime + .recover_persisted_lifecycle_transitions() + .await + .unwrap(); + assert_eq!(stored(&runtime, sandbox.object_id()).await, pending); + let record = runtime + .store + .get(Sandbox::object_type(), sandbox.object_id()) + .await + .unwrap() + .unwrap(); + runtime + .prune_missing_sandbox(record, openshell_core::time::now_ms(), 0) + .await + .unwrap(); + let failed = stored(&runtime, sandbox.object_id()).await; + assert_eq!(failed.phase(), SandboxPhase::Error as i32); + assert_eq!( + ready_condition(&failed).unwrap().reason, + "ComputeResourceMissing" + ); +} + #[tokio::test] async fn failed_gateway_start_can_be_stopped_and_retried() { let driver = ControlledDriver::new(); @@ -152,6 +372,10 @@ async fn pending_driver_operation_blocks_error_recovery_without_mutation() { let runtime = test_runtime(driver.clone()).await; let mut sandbox = error_sandbox_record("pending-id", "pending", "StartFailed"); let mut operation = provisioning_deadline::new_record(openshell_core::time::now_ms()); + // Even a completion timestamp cannot override an unsettled original driver + // operation. This also fences malformed/stale timeout cleanup evidence. + operation.timeout_time = openshell_core::time::timestamp_from_millis(300_000).ok(); + operation.cleanup_completed_time = openshell_core::time::timestamp_from_millis(320_000).ok(); operation.driver_operation_pending = true; operation.driver_operation_id = "owned-operation".into(); sandbox.status.as_mut().unwrap().provisioning = Some(operation); @@ -188,14 +412,39 @@ async fn timeout_stop_preserves_cleanup_and_direct_start_recovery() { runtime.store.put_message(&sandbox).await.unwrap(); let before = stored(&runtime, sandbox.object_id()).await; - let error = runtime - .stop_sandbox("default", "timeout") - .await - .unwrap_err(); - assert_eq!(error.code(), Code::FailedPrecondition); - assert!(error.message().contains("start")); - assert_eq!(stored(&runtime, sandbox.object_id()).await, before); - assert_eq!(driver.stop_calls(), 0, "timeout cleanup owns reclamation"); + let stop = runtime.stop_sandbox("default", "timeout").await; + if cleanup_complete { + let stopped = stop.expect( + "completed timeout cleanup permits an explicit stopped state for image repair", + ); + assert_eq!(stopped.phase(), SandboxPhase::Stopped as i32); + assert_eq!(stopped.object_id(), before.object_id()); + assert_eq!(stopped.spec, before.spec); + assert_eq!( + stopped.status.as_ref().unwrap().provisioning, + before.status.as_ref().unwrap().provisioning + ); + let repeated = runtime.stop_sandbox("default", "timeout").await.unwrap(); + assert_eq!( + repeated, stopped, + "the retained stopped transition is idempotent" + ); + // Docker cleanup and its snapshot poll can both report deletion of + // the cancelled preparation after explicit stop has returned. + for _ in 0..2 { + runtime.apply_deleted(sandbox.object_id()).await.unwrap(); + assert_eq!(stored(&runtime, sandbox.object_id()).await, stopped); + } + } else { + let error = stop.unwrap_err(); + assert_eq!(error.code(), Code::FailedPrecondition); + assert_eq!(stored(&runtime, sandbox.object_id()).await, before); + } + assert_eq!( + driver.stop_calls(), + 0, + "timeout cleanup already owns reclamation" + ); let result = runtime.start_sandbox("default", "timeout").await; if cleanup_complete { @@ -214,6 +463,35 @@ async fn timeout_stop_preserves_cleanup_and_direct_start_recovery() { } } +#[tokio::test] +async fn reclaimed_timeout_can_still_start_directly_without_stop() { + let driver = ControlledDriver::new(); + driver.set_start_outcome(ControlledLifecycleOutcome::NotFound); + driver.track_compute.store(true, Ordering::SeqCst); + let runtime = test_runtime(driver.clone()).await; + let mut sandbox = error_sandbox_record( + "direct-timeout-id", + "direct-timeout", + "ImagePreparationTimedOut", + ); + let mut operation = provisioning_deadline::new_record(0); + operation.timeout_time = openshell_core::time::timestamp_from_millis(300_000).ok(); + operation.cleanup_completed_time = openshell_core::time::timestamp_from_millis(320_000).ok(); + sandbox.status.as_mut().unwrap().provisioning = Some(operation); + runtime.store.put_message(&sandbox).await.unwrap(); + + let starting = runtime + .start_sandbox("default", "direct-timeout") + .await + .unwrap(); + assert_eq!(starting.phase(), SandboxPhase::Starting as i32); + assert_eq!(starting.object_id(), sandbox.object_id()); + assert!(driver.compute_exists.load(Ordering::SeqCst)); + assert_eq!(driver.start_calls(), 1); + assert_eq!(driver.stop_calls(), 0); + assert_eq!(driver.delete_calls(), 0); +} + #[tokio::test] async fn failed_error_stop_retains_retryable_state_without_claiming_success() { for observed_error in [false, true] { diff --git a/crates/openshell-server/src/compute_factory_compatibility.rs b/crates/openshell-server/src/compute_factory_compatibility.rs new file mode 100644 index 0000000000..57552446de --- /dev/null +++ b/crates/openshell-server/src/compute_factory_compatibility.rs @@ -0,0 +1,193 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Baseline factory source fixture: no image-update trait or new hook required. + +use std::collections::BTreeMap; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriver; + +use crate::test_support::FakeComputeDriver; +use crate::{ + ComputeDriverBuildContext, ComputeDriverFactory, ComputeDriverInstance, + ComputeDriverRegistration, ComputeDriverRegistry, DriverWatchStream, SharedComputeDriver, + SharedComputeDriverConfigurationUpdates, +}; + +// Written using only the baseline generated trait, not the new optional trait. +type BaselineDriver = + Arc + Send + Sync>; + +struct BaselineFactory { + driver: BaselineDriver, + calls: Arc, +} + +#[async_trait::async_trait] +impl ComputeDriverFactory for BaselineFactory { + async fn build( + &self, + _context: ComputeDriverBuildContext<'_>, + ) -> openshell_core::Result { + self.calls.fetch_add(1, Ordering::SeqCst); + // This unchanged baseline expression must compile without a new trait. + Ok(ComputeDriverInstance::InProcess(self.driver.clone())) + } +} + +struct OptInFactory { + driver: Arc, + legacy_build_calls: Arc, + opt_in_calls: Arc, +} + +#[async_trait::async_trait] +impl ComputeDriverFactory for OptInFactory { + async fn build( + &self, + _context: ComputeDriverBuildContext<'_>, + ) -> openshell_core::Result { + self.legacy_build_calls.fetch_add(1, Ordering::SeqCst); + Ok(ComputeDriverInstance::InProcess(self.driver.clone())) + } + + async fn build_with_configuration_updates( + &self, + _context: ComputeDriverBuildContext<'_>, + ) -> openshell_core::Result<( + ComputeDriverInstance, + Option, + )> { + self.opt_in_calls.fetch_add(1, Ordering::SeqCst); + Ok(( + ComputeDriverInstance::InProcess(self.driver.clone()), + Some(self.driver.clone()), + )) + } +} + +async fn registered_runtime(factory: impl ComputeDriverFactory + 'static) -> crate::ComputeRuntime { + let mut registry = ComputeDriverRegistry::new(); + registry + .install(ComputeDriverRegistration::new("fixture", 0, None, factory).unwrap()) + .unwrap(); + let selection = registry.select(Some("fixture")).unwrap(); + let config = openshell_core::Config::new(None); + let overrides = BTreeMap::new(); + let startup = crate::compute::driver_config::DriverStartupContext { + file: None, + guest_tls: None, + network_trust: None, + network_trust_configured: false, + gateway_port: 8080, + gateway_tls_enabled: false, + endpoint_overrides: &overrides, + }; + let store = Arc::new(crate::persistence::test_store().await); + crate::ensure_default_workspace(&store).await.unwrap(); + let mut sandbox = openshell_core::proto::Sandbox { + metadata: Some(openshell_core::proto::ObjectMeta { + id: "fixture-sandbox".into(), + name: "fixture-sandbox".into(), + workspace: "default".into(), + ..Default::default() + }), + spec: Some(openshell_core::proto::SandboxSpec { + template: Some(openshell_core::proto::SandboxTemplate { + image: "example/workload:v1".into(), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }; + sandbox.set_phase(openshell_core::proto::SandboxPhase::Stopped as i32); + store.put_message(&sandbox).await.unwrap(); + + crate::build_compute_runtime( + ®istry, + &selection, + &config, + startup, + store, + crate::sandbox_index::SandboxIndex::new(), + crate::sandbox_watch::SandboxWatchBus::new(), + crate::tracing_bus::TracingLogBus::new(), + Arc::new(crate::supervisor_session::SupervisorSessionRegistry::new()), + tokio::sync::watch::channel(false).1, + ) + .await + .unwrap() + .with_sandbox_updates(true) +} + +#[tokio::test] +async fn baseline_factory_and_trait_object_still_build_without_opt_in() { + // This concrete fixture implements only the baseline generated trait. + let driver: BaselineDriver = Arc::new(crate::compute::NoopTestDriver::default()); + let shared: SharedComputeDriver = driver.clone(); + // Exhaustive baseline enum match also prevents adding a required new variant. + match ComputeDriverInstance::InProcess(shared) { + ComputeDriverInstance::InProcess(actual) => assert!(Arc::ptr_eq(&actual, &driver)), + ComputeDriverInstance::ManagedRemote(_) => panic!("expected in-process driver"), + } + let calls = Arc::new(AtomicUsize::new(0)); + let runtime = registered_runtime(BaselineFactory { + driver, + calls: calls.clone(), + }) + .await; + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert_eq!( + runtime + .update_sandbox_image("fixture-sandbox", "example/workload:v2".into(), 1) + .await + .unwrap_err() + .code(), + tonic::Code::Unimplemented + ); +} + +#[tokio::test] +async fn capability_alone_does_not_register_an_in_process_image_updater() { + let driver = Arc::new(FakeComputeDriver::new().with_sandbox_image_updates()); + let runtime = registered_runtime(BaselineFactory { + driver: driver.clone(), + calls: Arc::new(AtomicUsize::new(0)), + }) + .await; + driver.clear_calls(); + assert_eq!( + runtime + .update_sandbox_image("fixture-sandbox", "example/workload:v2".into(), 1) + .await + .unwrap_err() + .code(), + tonic::Code::Unimplemented + ); + assert!(driver.calls().is_empty()); +} + +#[tokio::test] +async fn registered_opt_in_hook_supplies_the_optional_updater() { + let legacy_calls = Arc::new(AtomicUsize::new(0)); + let opt_in_calls = Arc::new(AtomicUsize::new(0)); + let runtime = registered_runtime(OptInFactory { + driver: Arc::new(FakeComputeDriver::new().with_sandbox_image_updates()), + legacy_build_calls: legacy_calls.clone(), + opt_in_calls: opt_in_calls.clone(), + }) + .await; + assert_eq!(legacy_calls.load(Ordering::SeqCst), 0); + assert_eq!(opt_in_calls.load(Ordering::SeqCst), 1); + let updated = runtime + .update_sandbox_image("fixture-sandbox", "example/workload:v2".into(), 1) + .await + .unwrap(); + assert_eq!( + updated.spec.unwrap().template.unwrap().image, + "example/workload:v2" + ); +} diff --git a/crates/openshell-server/src/config_file.rs b/crates/openshell-server/src/config_file.rs index cee598a992..a956bca9cd 100644 --- a/crates/openshell-server/src/config_file.rs +++ b/crates/openshell-server/src/config_file.rs @@ -143,6 +143,9 @@ pub struct GatewayFileSection { /// Absolute preparation budget for new attempts; existing deadlines persist. #[serde(default)] pub image_preparation_timeout_seconds: Option, + /// Explicit activation only after the compatible deployment rollout ends. + #[serde(default)] + pub enable_sandbox_updates: Option, #[serde(default)] pub grpc_rate_limit_requests: Option, #[serde(default)] diff --git a/crates/openshell-server/src/gateway_members.rs b/crates/openshell-server/src/gateway_members.rs index 33d68414df..b70bf5f82f 100644 --- a/crates/openshell-server/src/gateway_members.rs +++ b/crates/openshell-server/src/gateway_members.rs @@ -16,6 +16,9 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +/// Initial stopped-sandbox configuration update protocol advertised by gateways. +pub const SANDBOX_UPDATE_PROTOCOL: u32 = 1; + const MEMBER_OBJECT_TYPE: &str = "gateway_member"; /// How long a membership record stays valid without a refresh. @@ -43,6 +46,9 @@ fn member_object_id(replica_id: &str) -> String { struct MemberPayload { replica_id: String, peer_endpoint: String, + /// Binary capability, not feature activation. Missing denotes old replicas. + #[serde(default)] + sandbox_update_protocol: u32, } /// A gateway replica that was alive as of its last refresh. @@ -71,6 +77,7 @@ impl GatewayMemberIndex { let payload = MemberPayload { replica_id: replica_id.to_string(), peer_endpoint: peer_endpoint.to_string(), + sandbox_update_protocol: SANDBOX_UPDATE_PROTOCOL, }; let bytes = serde_json::to_vec(&payload) .map_err(|err| PersistenceError::Encode(err.to_string()))?; diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index 8af2709134..d066845fa4 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -242,6 +242,16 @@ impl OpenShellService { // Trait impl — thin delegation to submodules // --------------------------------------------------------------------------- +#[tonic::async_trait] +impl openshell_core::proto::sandbox_updates_server::SandboxUpdates for OpenShellService { + async fn update_sandbox( + &self, + request: Request, + ) -> Result, Status> { + mutation_replay::run(&self.state, request).await + } +} + #[tonic::async_trait] impl OpenShell for OpenShellService { async fn health( diff --git a/crates/openshell-server/src/grpc/mutation_replay.rs b/crates/openshell-server/src/grpc/mutation_replay.rs index 1a39b3dea0..837d26466c 100644 --- a/crates/openshell-server/src/grpc/mutation_replay.rs +++ b/crates/openshell-server/src/grpc/mutation_replay.rs @@ -102,6 +102,9 @@ struct AuthorizationBarrier { pub(super) trait Mutation: Message + Default + Send + Sync + 'static { type Output: Send + 'static; const METHOD: &'static str; + /// Preserve legacy replay keys while allowing independent services to use + /// fully qualified identities for newly introduced methods. + const REPLAY_METHOD: &'static str = Self::METHOD; const PROTECTED: bool = false; // Streaming producers, not returned stream handles, confirm completion. const DEFERRED: bool = false; @@ -217,7 +220,7 @@ pub(super) async fn run( let bucket = format!("_mutation/{caller}"); let key = hash_json(&serde_json::json!([ caller, - M::METHOD, + M::REPLAY_METHOD, scope.name, request_id ]))?; diff --git a/crates/openshell-server/src/grpc/mutation_replay/ordinary.rs b/crates/openshell-server/src/grpc/mutation_replay/ordinary.rs index 25410043f6..a88c6abb9d 100644 --- a/crates/openshell-server/src/grpc/mutation_replay/ordinary.rs +++ b/crates/openshell-server/src/grpc/mutation_replay/ordinary.rs @@ -335,10 +335,11 @@ async fn profile_scope( } macro_rules! mutation { - ($req:ty, $resp:ty, $method:literal, $handler:path, $auth:expr, $capture:expr, $restore:expr) => { + ($req:ty, $resp:ty, $method:literal, $handler:path, $auth:expr, $capture:expr, $restore:expr $(, $replay_method:literal)?) => { impl Mutation for $req { type Output = $resp; const METHOD: &'static str = $method; + $(const REPLAY_METHOD: &'static str = $replay_method;)? const PROTECTED: bool = true; fn request_id(&self) -> &str { &self.request_id @@ -389,7 +390,7 @@ macro_rules! scoped_mutation { } macro_rules! sandbox_scoped_mutation { - ($req:ty, $resp:ty, $method:literal, $handler:path, $role:ident, $capture:expr, $restore:expr) => { + ($req:ty, $resp:ty, $method:literal, $handler:path, $role:ident, $capture:expr, $restore:expr $(, $replay_method:literal)?) => { mutation!( $req, $resp, @@ -406,6 +407,7 @@ macro_rules! sandbox_scoped_mutation { }, $capture, $restore + $(, $replay_method)? ); }; } @@ -423,7 +425,7 @@ fn sandbox_receipt( } macro_rules! sandbox_mutation { - ($req:ty, $method:literal, $handler:path) => { + ($req:ty, $method:literal, $handler:path $(, $replay_method:literal)?) => { sandbox_scoped_mutation!( $req, SandboxResponse, @@ -444,6 +446,7 @@ macro_rules! sandbox_mutation { service_urls: HashMap::new(), }) } + $(, $replay_method)? ); }; } @@ -471,6 +474,12 @@ scoped_mutation!( }) } ); +sandbox_mutation!( + openshell_core::proto::UpdateSandboxRequest, + "UpdateSandbox", + sandbox::handle_update_sandbox, + "/openshell.v1.SandboxUpdates/UpdateSandbox" +); sandbox_mutation!( StartSandboxRequest, "StartSandbox", diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 77a54a2bc8..a797da2589 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -315,6 +315,102 @@ pub(super) async fn handle_create_sandbox( result } +/// Only the workload image is mutable in the first update contract. Runtime +/// replacement is an explicit Stop/Update/Start workflow, never Delete/Create. +pub(super) async fn handle_update_sandbox( + state: &Arc, + request: Request, +) -> Result, Status> { + let principal = super::extract_principal(&request)?; + let request = request.into_inner(); + validate_image_update_request(&request)?; + let sandbox = resolve_and_authorize_sandbox_name( + state, + &principal, + &request.name, + crate::auth::workspace_authz::selected_workspace_name(request.workspace_scope.as_ref())?, + MinWorkspaceRole::User, + ) + .await?; + super::workspace::resolve_workspace(state.store.as_ref(), sandbox.object_workspace()) + .await? + .ensure_active()?; + let mut candidate = sandbox.clone(); + let spec = candidate + .spec + .as_mut() + .ok_or_else(|| Status::failed_precondition("sandbox has no spec"))?; + if let Some(image) = &request.image { + spec.template + .get_or_insert_with(Default::default) + .image + .clone_from(image); + } + openshell_core::sandbox_update::apply_configuration( + &mut candidate, + request.command.as_ref(), + request.environment.as_ref(), + request.labels.as_ref(), + request.annotations.as_ref(), + ); + let candidate_spec = candidate.spec.as_ref().expect("spec checked above"); + validate_sandbox_spec(&request.name, candidate_spec)?; + + let sandbox = Box::pin( + state + .compute + .update_sandbox_configuration(sandbox.object_id(), &request), + ) + .await?; + Ok(Response::new(SandboxResponse { + sandbox: Some(sandbox), + service_urls: HashMap::new(), + })) +} + +fn validate_image_update_request( + request: &openshell_core::proto::UpdateSandboxRequest, +) -> Result<(), Status> { + if request.name.is_empty() { + return Err(Status::invalid_argument("name is required")); + } + if request.expected_resource_version == 0 { + return Err(Status::invalid_argument( + "expected_resource_version must be nonzero", + )); + } + if let Some(image) = &request.image + && (image.is_empty() + || image.len() > super::MAX_TEMPLATE_STRING_LEN + || image.chars().any(|c| c.is_whitespace() || c.is_control())) + { + return Err(Status::invalid_argument( + "image must be a nonempty bounded OCI reference without whitespace or control characters", + )); + } + for map in [request.labels.as_ref(), request.annotations.as_ref()] + .into_iter() + .flatten() + { + if map + .values + .keys() + .any(|key| openshell_core::sandbox_update::is_system_metadata_key(key)) + { + return Err(Status::invalid_argument( + "system metadata cannot be updated", + )); + } + } + if let Some(labels) = &request.labels { + super::validation::validate_labels(&labels.values)?; + } + if let Some(annotations) = &request.annotations { + super::validation::validate_annotations(&annotations.values, "annotations")?; + } + Ok(()) +} + /// Allocate a gateway-owned staging slot for a local rootfs tar archive. /// /// The caller writes the archive to the returned path and then names the token @@ -786,10 +882,7 @@ fn validate_create_sandbox_request_pre_io( workload_template_name: &str, ) -> Result<(), Status> { // Validate labels (keys and values must meet Kubernetes requirements). - for (key, value) in &request.labels { - crate::grpc::validation::validate_label_key(key)?; - crate::grpc::validation::validate_label_value(value)?; - } + crate::grpc::validation::validate_labels(&request.labels)?; crate::grpc::validation::validate_caller_annotations(&request.annotations, "annotations")?; if request.service_exposures.len() > MAX_CREATE_SERVICE_EXPOSURES { @@ -7999,6 +8092,21 @@ mod tests { assert_eq!(process.run_as_group, "1234"); } + #[test] + fn create_sandbox_accepts_more_than_128_labels() { + let request = CreateSandboxRequest { + name: "many-labels".into(), + spec: Some(SandboxSpec::default()), + labels: (0..129) + .map(|index| (format!("label-{index}"), "value".into())) + .collect(), + ..Default::default() + }; + for workload_template in ["", "workload-template"] { + validate_create_sandbox_request_pre_io(&request, workload_template).unwrap(); + } + } + #[tokio::test] async fn create_sandbox_still_rejects_long_label_values() { let state = test_server_state().await; @@ -10470,3 +10578,6 @@ mod tests { )); } } + +#[cfg(test)] +mod image_update_tests; diff --git a/crates/openshell-server/src/grpc/sandbox/image_update_tests.rs b/crates/openshell-server/src/grpc/sandbox/image_update_tests.rs new file mode 100644 index 0000000000..e83d786ee0 --- /dev/null +++ b/crates/openshell-server/src/grpc/sandbox/image_update_tests.rs @@ -0,0 +1,459 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use crate::auth::audit::ConnectionSource; +use crate::grpc::test_support::{authed_request, test_server_state}; +use crate::test_support::{FakeComputeDriver, FakeComputeDriverCall}; +use openshell_core::proto::compute::v1::GetCapabilitiesResponse; +use openshell_core::proto::datamodel::v1::ObjectMeta; +use openshell_core::proto::sandbox_updates_client::SandboxUpdatesClient; +use openshell_core::proto::{SandboxCommandUpdate, UpdateSandboxRequest}; +use tonic::Code; + +struct RunningService(tokio::task::JoinHandle<()>); + +impl Drop for RunningService { + fn drop(&mut self) { + self.0.abort(); + } +} + +async fn wire_client( + state: Arc, +) -> ( + SandboxUpdatesClient, + RunningService, +) { + use crate::multiplex::MultiplexService; + use openshell_core::proto::open_shell_client::OpenShellClient; + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + // Use the production gateway multiplexer, not a standalone tonic server: + // this proves registration, routing and principal injection on the same + // listener as the unchanged OpenShell service. + let server = RunningService(tokio::spawn(async move { + let mut connections = tokio::task::JoinSet::new(); + loop { + let (stream, peer) = listener.accept().await.unwrap(); + let service = MultiplexService::new(state.clone()); + connections.spawn(async move { + service + .serve(stream, ConnectionSource::Peer(peer)) + .await + .unwrap(); + }); + } + })); + let channel = tonic::transport::Endpoint::from_shared(format!("http://{address}")) + .unwrap() + .connect() + .await + .unwrap(); + OpenShellClient::new(channel.clone()) + .health(openshell_core::proto::HealthRequest {}) + .await + .unwrap(); + (SandboxUpdatesClient::new(channel), server) +} + +#[test] +fn image_updates_have_their_own_authenticated_workspace_scoped_service() { + use crate::auth::method_authz::{AuthMode, lookup}; + use crate::grpc::mutation_replay::Mutation; + + let path = "/openshell.v1.SandboxUpdates/UpdateSandbox"; + let authorization = lookup(path).expect("new service must participate in gateway auth"); + assert_eq!(authorization.auth_mode, AuthMode::Bearer); + assert_eq!(authorization.scope.as_deref(), Some("sandbox:write")); + assert_eq!(authorization.workspace_role.as_deref(), Some("user")); + assert!(lookup("/openshell.v1.OpenShell/UpdateSandbox").is_none()); + assert_eq!(::REPLAY_METHOD, path); +} + +fn request() -> UpdateSandboxRequest { + UpdateSandboxRequest { + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + name: "image-update".into(), + image: Some("example/workload:new".into()), + expected_resource_version: 1, + request_id: String::new(), + ..Default::default() + } +} + +#[test] +fn image_only_request_requires_target_image_and_conditional_version() { + let valid = request(); + validate_image_update_request(&valid).unwrap(); + for image in ["", " ", "example/workload:tag\n", "bad image", "a\0b"] { + assert_eq!( + validate_image_update_request(&UpdateSandboxRequest { + image: Some(image.into()), + ..valid.clone() + }) + .unwrap_err() + .code(), + Code::InvalidArgument + ); + } + assert!( + validate_image_update_request(&UpdateSandboxRequest { + image: Some("a".repeat(super::super::MAX_TEMPLATE_STRING_LEN + 1)), + ..valid.clone() + }) + .is_err() + ); + assert!( + validate_image_update_request(&UpdateSandboxRequest { + name: String::new(), + ..valid.clone() + }) + .is_err() + ); + assert!( + validate_image_update_request(&UpdateSandboxRequest { + expected_resource_version: 0, + ..valid + }) + .is_err() + ); +} + +async fn state() -> (Arc, Arc, Sandbox) { + let mut state = test_server_state().await; + Arc::get_mut(&mut state) + .unwrap() + .config + .enable_sandbox_updates = true; + let driver = Arc::new( + FakeComputeDriver::new().with_capabilities(GetCapabilitiesResponse { + driver_name: "image-test".into(), + default_image: "example/workload:old".into(), + extension: Some(openshell_core::extension_protocol::extension_metadata( + openshell_core::extension_protocol::ExtensionFamily::Compute, + "openshell/image-test", + "test", + [openshell_core::extension_protocol::COMPUTE_UPDATE_SANDBOX_CONFIGURATION.into()], + )), + ..Default::default() + }), + ); + let compute = crate::compute::ComputeRuntime::from_driver( + "image-test".into(), + driver.clone(), + None, + state.store.clone(), + crate::sandbox_index::SandboxIndex::new(), + crate::sandbox_watch::SandboxWatchBus::new(), + crate::tracing_bus::TracingLogBus::new(), + Arc::new(crate::supervisor_session::SupervisorSessionRegistry::new()), + ) + .await + .unwrap() + .with_image_update_driver(Some(driver.clone())) + .with_sandbox_updates(true); + Arc::get_mut(&mut state).unwrap().compute = compute; + let mut sandbox = Sandbox { + metadata: Some(ObjectMeta { + id: "stable-sandbox-id".into(), + name: "image-update".into(), + workspace: "default".into(), + ..Default::default() + }), + spec: Some(SandboxSpec { + template: Some(SandboxTemplate { + image: "example/workload:old".into(), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }; + sandbox.set_phase(SandboxPhase::Stopped as i32); + state.store.put_message(&sandbox).await.unwrap(); + let sandbox = state + .store + .get_message::(sandbox.object_id()) + .await + .unwrap() + .unwrap(); + (state, driver, sandbox) +} + +#[tokio::test] +async fn public_image_update_checks_workspace_and_missing_resource() { + let (state, driver, sandbox) = state().await; + let mut req = request(); + req.expected_resource_version = sandbox.get_resource_version(); + req.workspace_scope = None; + assert_eq!( + handle_update_sandbox(&state, authed_request(req.clone())) + .await + .unwrap_err() + .code(), + Code::InvalidArgument + ); + req.workspace_scope = Some(openshell_core::proto::workspace_selector("default")); + req.name = "missing-sandbox".into(); + assert_eq!( + handle_update_sandbox(&state, authed_request(req)) + .await + .unwrap_err() + .code(), + Code::NotFound + ); + assert!( + !driver + .calls() + .iter() + .any(|call| matches!(call, FakeComputeDriverCall::UpdateSandboxImage { .. })) + ); +} + +#[tokio::test] +async fn public_image_update_replays_without_reapplying_or_touching_same_name_replacement() { + let (mut state, driver, sandbox) = state().await; + // Exercise production routing with the explicit local developer identity. + Arc::get_mut(&mut state) + .unwrap() + .config + .auth + .allow_unauthenticated_users = true; + let directory = tempfile::tempdir().unwrap(); + let key = directory.path().join("private-key"); + std::fs::write(&key, b"test-only stable private fingerprint material").unwrap(); + Arc::get_mut(&mut state).unwrap().config.gateway_jwt = + Some(openshell_core::config::GatewayJwtConfig { + signing_key_path: key, + public_key_path: directory.path().join("public"), + kid_path: directory.path().join("kid"), + gateway_id: "test".into(), + ttl_secs: None, + }); + let (mut client, _server) = wire_client(state.clone()).await; + let req = UpdateSandboxRequest { + expected_resource_version: sandbox.get_resource_version(), + request_id: uuid::Uuid::new_v4().to_string(), + command: Some(SandboxCommandUpdate { + argv: vec!["/usr/local/bin/start-agent".into(), "--updated".into()], + }), + environment: Some(openshell_core::proto::SandboxMapUpdate { + values: HashMap::from([("AGENT_MODE".into(), "updated".into())]), + }), + labels: Some(openshell_core::proto::SandboxMapUpdate { + values: HashMap::from([("team".into(), "agents".into())]), + }), + annotations: Some(openshell_core::proto::SandboxMapUpdate { + values: HashMap::from([("example.com/revision".into(), "2".into())]), + }), + ..request() + }; + let updated = client + .update_sandbox(req.clone()) + .await + .unwrap() + .into_inner() + .sandbox + .unwrap(); + assert_eq!(updated.object_id(), sandbox.object_id()); + assert_eq!( + updated + .spec + .as_ref() + .unwrap() + .template + .as_ref() + .unwrap() + .image, + req.image.as_ref().unwrap().as_str() + ); + assert_eq!( + updated.spec.as_ref().unwrap().command, + req.command.as_ref().unwrap().argv + ); + assert_eq!( + updated.spec.as_ref().unwrap().environment, + req.environment.as_ref().unwrap().values + ); + assert_eq!( + updated.metadata.as_ref().unwrap().labels, + req.labels.as_ref().unwrap().values + ); + let replay = client.update_sandbox(req.clone()).await.unwrap(); + assert_eq!(replay.metadata().get("openshell-replayed").unwrap(), "true"); + assert_eq!(replay.get_ref().sandbox.as_ref().unwrap(), &updated); + assert_eq!(driver.calls().iter().filter(|call| matches!(call, FakeComputeDriverCall::UpdateSandboxConfiguration { request } if !request.preconditions.as_ref().unwrap().validate_only)).count(), 1); + let changed = UpdateSandboxRequest { + image: Some("example/workload:other".into()), + ..req.clone() + }; + assert_eq!( + client.update_sandbox(changed).await.unwrap_err().code(), + Code::FailedPrecondition + ); + state + .store + .delete(Sandbox::object_type(), sandbox.object_id()) + .await + .unwrap(); + let mut replacement = sandbox.clone(); + let metadata = replacement.metadata.as_mut().unwrap(); + metadata.id = "replacement-sandbox-id".into(); + metadata.resource_version = 0; + state.store.put_message(&replacement).await.unwrap(); + assert_eq!( + client.update_sandbox(req).await.unwrap_err().code(), + Code::FailedPrecondition + ); + let replacement = state + .store + .get_message::(replacement.object_id()) + .await + .unwrap() + .unwrap(); + assert_eq!(replacement.spec, sandbox.spec); + assert_eq!(driver.calls().iter().filter(|call| matches!(call, FakeComputeDriverCall::UpdateSandboxConfiguration { request } if !request.preconditions.as_ref().unwrap().validate_only)).count(), 1); +} + +#[test] +fn configuration_update_accepts_more_than_128_labels() { + let update = UpdateSandboxRequest { + labels: Some(openshell_core::proto::SandboxMapUpdate { + values: (0..129) + .map(|index| (format!("label-{index}"), "value".into())) + .collect(), + }), + ..request() + }; + validate_image_update_request(&update).unwrap(); +} + +#[test] +fn configuration_update_presence_and_reserved_metadata() { + let mut update = request(); + update.image = None; + assert!(validate_image_update_request(&update).is_ok()); + update.command = Some(SandboxCommandUpdate::default()); + validate_image_update_request(&update).unwrap(); + // User metadata under these domains was accepted at creation and remains + // editable; only explicit gateway identity and lifecycle keys are reserved. + for key in ["openshell.ai/user-note", "agents.x-k8s.io/user-note"] { + update.annotations = Some(openshell_core::proto::SandboxMapUpdate { + values: HashMap::from([(key.into(), "new".into())]), + }); + validate_image_update_request(&update).unwrap(); + } + update.annotations = None; + for key in [ + "internal.openshell.ai/auth-epoch", + "openshell.ai/sandbox-id", + "openshell.nvidia.com/retention", + ] { + update.annotations = Some(openshell_core::proto::SandboxMapUpdate { + values: HashMap::from([(key.into(), "forged".into())]), + }); + assert!(validate_image_update_request(&update).is_err()); + update.labels = update.annotations.take(); + assert!(validate_image_update_request(&update).is_err()); + } +} + +#[tokio::test] +async fn pending_update_retry_preserves_later_update_config_annotations() { + use openshell_core::proto::{SandboxMapUpdate, UpdateConfigRequest}; + + let (state, driver, sandbox) = state().await; + let mut update = UpdateSandboxRequest { + expected_resource_version: sandbox.get_resource_version(), + annotations: Some(SandboxMapUpdate { + values: HashMap::from([ + ("example.com/revision".into(), "original".into()), + ("example.com/retained".into(), "value".into()), + ]), + }), + ..request() + }; + driver.set_image_update_error(Some(Code::Unavailable)); + assert_eq!( + handle_update_sandbox(&state, authed_request(update.clone())) + .await + .unwrap_err() + .code(), + Code::Unavailable + ); + let pending = state + .store + .get_message::(sandbox.object_id()) + .await + .unwrap() + .unwrap(); + + // Exercise the existing public annotation writer while the update is + // pending, including both a new key and an overwrite of a selected key. + let annotations = crate::grpc::policy::handle_update_config( + &state, + authed_request(UpdateConfigRequest { + sandbox: sandbox.object_name().into(), + workspace_scope: update.workspace_scope.clone(), + expected_resource_version: pending.get_resource_version(), + setting_key: "log_level".into(), + delete_setting: true, + annotations: HashMap::from([ + ("example.com/revision".into(), "later".into()), + ("example.com/added".into(), "new".into()), + ]), + ..Default::default() + }), + ) + .await + .unwrap() + .into_inner() + .annotations; + assert_eq!(annotations.get("example.com/retained").unwrap(), "value"); + let current = state + .store + .get_message::(sandbox.object_id()) + .await + .unwrap() + .unwrap(); + // Resume from stored intent, without knowing the original selection mask. + update = UpdateSandboxRequest { + workspace_scope: update.workspace_scope, + name: update.name, + expected_resource_version: current.get_resource_version(), + ..Default::default() + }; + driver.set_image_update_error(None); + let completed = handle_update_sandbox(&state, authed_request(update)) + .await + .unwrap() + .into_inner() + .sandbox + .unwrap(); + assert_eq!( + completed.metadata.as_ref().unwrap().annotations, + annotations + ); + assert_eq!(completed.phase(), SandboxPhase::Stopped as i32); + assert!( + completed + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .is_none_or(|record| !record.driver_operation_pending) + ); + assert!( + state + .store + .get_sandbox_image_state(sandbox.object_id()) + .await + .unwrap() + .unwrap() + .pending + .is_none() + ); +} diff --git a/crates/openshell-server/src/grpc/validation.rs b/crates/openshell-server/src/grpc/validation.rs index 8dd62c5c57..d84488bcca 100644 --- a/crates/openshell-server/src/grpc/validation.rs +++ b/crates/openshell-server/src/grpc/validation.rs @@ -459,6 +459,17 @@ pub(super) fn validate_string_map( Ok(()) } +/// Validate label keys and values using Kubernetes syntax. +pub(super) fn validate_labels( + labels: &std::collections::HashMap, +) -> Result<(), Status> { + for (key, value) in labels { + validate_label_key(key)?; + validate_label_value(value)?; + } + Ok(()) +} + /// Validate object annotations. /// /// Annotation keys use the same qualified-key shape as labels. Annotation diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 06bbf42736..12ca9223ea 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -482,6 +482,7 @@ impl ServerState { credentials: credentials::CredentialRuntime, ) -> Self { let replica_id = compute::lease::replica_id(); + let compute = compute.with_sandbox_updates(config.enable_sandbox_updates); compute.configure_ssh_identities(credentials.clone()); let service_route_index = Arc::new(service_routing::ServiceRouteIndex::new( config.service_routing_discriminator().map(str::to_string), @@ -1388,6 +1389,7 @@ async fn terminate_signal() { pub use compute::{ AcquiredRemoteDriverEndpoint, DriverWatchStream, ManagedDriverProcess, SharedComputeDriver, + SharedComputeDriverConfigurationUpdates, }; /// Driver instance returned by a compiled compute-driver factory. @@ -1463,6 +1465,23 @@ pub trait ComputeDriverFactory: Send + Sync { } async fn build(&self, context: ComputeDriverBuildContext<'_>) -> Result; + + /// Build with a separately registered, optional in-process image updater. + /// + /// Existing factories only implement `build`; their baseline driver object + /// remains sufficient and this hook defaults to no configuration-update support. + /// Supporting factories return the same driver instance in both slots and + /// advertise the negotiated configuration-update capability. Remote drivers expose + /// their extension over the existing endpoint instead of using this hook. + async fn build_with_configuration_updates( + &self, + context: ComputeDriverBuildContext<'_>, + ) -> Result<( + ComputeDriverInstance, + Option, + )> { + self.build(context).await.map(|instance| (instance, None)) + } } /// One named compiled-driver registration. @@ -1913,7 +1932,10 @@ async fn build_compute_runtime( supervisor_recovery_timeout_secs: supervisor_pool .and_then(|pool| pool.recovery_timeout_secs), }; - let instance = registration.factory.build(build_context).await?; + let (instance, image_updates) = registration + .factory + .build_with_configuration_updates(build_context) + .await?; let supervisor_host = shared_supervision .into_inner() .map(|supervision| supervision.controller().clone()); @@ -1935,7 +1957,8 @@ async fn build_compute_runtime( .map_err(|error| { Error::execution(format!("failed to create compute runtime: {error}")) })? - .with_supervisor_host(supervisor_host), + .with_supervisor_host(supervisor_host) + .with_image_update_driver(image_updates), ComputeDriverInstance::ManagedRemote(_) if supervisor_host.is_some() => { return Err(Error::config( "only an in-process compute driver can use the gateway's shared supervision", @@ -2210,6 +2233,9 @@ pub(crate) async fn ensure_default_workspace(store: &Store) -> Result<()> { } } +#[cfg(test)] +mod compute_factory_compatibility; + #[cfg(test)] mod tests { use super::{ @@ -2701,7 +2727,7 @@ mod tests { .iter() .map(|service| service.name.as_str()) .collect::>(), - ["openshell.v1.OpenShell"] + ["openshell.v1.OpenShell", "openshell.v1.SandboxUpdates"] ); stop_listener(shutdown, handle).await; diff --git a/crates/openshell-server/src/multiplex.rs b/crates/openshell-server/src/multiplex.rs index d58192fba2..438b7d4bc6 100644 --- a/crates/openshell-server/src/multiplex.rs +++ b/crates/openshell-server/src/multiplex.rs @@ -18,6 +18,7 @@ use hyper_util::{ }; use metrics::{counter, histogram}; use openshell_core::proto::open_shell_server::OpenShellServer; +use openshell_core::proto::sandbox_updates_server::SandboxUpdatesServer; use openshell_core::proto::supervisor_host::v1::supervisor_host_server::SupervisorHostServer; use openshell_core::{ Config, @@ -305,6 +306,13 @@ impl MultiplexService { openshell, ); let reflection = self.state.reflection_service.clone(); + let sandbox_updates = SandboxUpdatesServer::new(OpenShellService::new(self.state.clone())) + .max_decoding_message_size(MAX_GRPC_DECODE_SIZE); + let sandbox_updates = GatewayInterceptorGrpcService::new( + sandbox_updates, + self.state.gateway_interceptors.clone(), + Some(self.state.clone()), + ); let authz_policy = self.state.config.oidc.as_ref().map(|oidc| AuthzPolicy { admin_role: oidc.admin_role.clone(), user_role: oidc.user_role.clone(), @@ -312,7 +320,7 @@ impl MultiplexService { }); let authenticator_chain = build_authenticator_chain(&self.state); let grpc_service = AuthGrpcRouter::with_peer_identity( - GrpcRouter::new(openshell, reflection), + GrpcRouter::new(openshell, reflection).with_sandbox_updates(sandbox_updates), authenticator_chain, authz_policy, self.state @@ -987,11 +995,12 @@ where } } -/// Combined gRPC service that routes between `OpenShell` and reflection. +/// Combined gRPC service for public gateway services and reflection. #[derive(Clone)] -pub struct GrpcRouter { +pub struct GrpcRouter { openshell: N, reflection: R, + sandbox_updates: Option, } impl GrpcRouter { @@ -999,13 +1008,22 @@ impl GrpcRouter { Self { openshell, reflection, + sandbox_updates: None, + } + } + + fn with_sandbox_updates(self, sandbox_updates: U) -> GrpcRouter { + GrpcRouter { + openshell: self.openshell, + reflection: self.reflection, + sandbox_updates: Some(sandbox_updates), } } } pub const REFLECTION_PATH_PREFIX: &str = "/grpc.reflection.v1."; -impl tower::Service> for GrpcRouter +impl tower::Service> for GrpcRouter where N: tower::Service> + Clone + Send + 'static, N::Response: Send, @@ -1016,6 +1034,11 @@ where + Send + 'static, R::Future: Send, + U: tower::Service, Response = N::Response, Error = N::Error> + + Clone + + Send + + 'static, + U::Future: Send, B: Send + 'static, { type Response = N::Response; @@ -1030,6 +1053,13 @@ where if req.uri().path().starts_with(REFLECTION_PATH_PREFIX) { let mut svc = self.reflection.clone(); Box::pin(async move { svc.ready().await?.call(req).await }) + } else if req + .uri() + .path() + .starts_with("/openshell.v1.SandboxUpdates/") + && let Some(mut svc) = self.sandbox_updates.clone() + { + Box::pin(async move { svc.ready().await?.call(req).await }) } else { let mut svc = self.openshell.clone(); Box::pin(async move { svc.ready().await?.call(req).await }) @@ -2836,6 +2866,13 @@ mod tests { otel_span_name(&http::Method::POST, "/openshell.v1.OpenShell/CreateSandbox"), "openshell.v1.OpenShell/CreateSandbox" ); + assert_eq!( + otel_span_name( + &http::Method::POST, + "/openshell.v1.SandboxUpdates/UpdateSandbox" + ), + "openshell.v1.SandboxUpdates/UpdateSandbox" + ); } /// Non-RPC paths use a low-cardinality method-only name because sandbox @@ -2905,10 +2942,12 @@ mod tests { name, calls: calls.clone(), }; - let mut router = GrpcRouter::new(service("openshell"), service("reflection")); + let mut router = GrpcRouter::new(service("openshell"), service("reflection")) + .with_sandbox_updates(service("sandbox_updates")); for path in [ "/openshell.v1.OpenShell/Health", + "/openshell.v1.SandboxUpdates/UpdateSandbox", "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo", ] { router @@ -2922,7 +2961,10 @@ mod tests { .unwrap(); } - assert_eq!(*calls.lock().unwrap(), vec!["openshell", "reflection"]); + assert_eq!( + *calls.lock().unwrap(), + vec!["openshell", "sandbox_updates", "reflection"] + ); } #[tokio::test] @@ -3018,7 +3060,10 @@ mod tests { .collect(); names.sort(); - assert_eq!(names, vec!["openshell.v1.OpenShell"]); + assert_eq!( + names, + vec!["openshell.v1.OpenShell", "openshell.v1.SandboxUpdates"] + ); let descriptor_response = responses.message().await.unwrap().unwrap(); let Some(MessageResponse::FileDescriptorResponse(response)) = @@ -3038,6 +3083,11 @@ mod tests { .is_some(), "a fresh descriptor pool must resolve the advertised service" ); + assert!( + reflected_pool + .get_service_by_name("openshell.v1.SandboxUpdates") + .is_some() + ); assert!( reflected_pool .get_message_by_name("openshell.v1.HealthRequest") diff --git a/crates/openshell-server/src/persistence/image_activation.rs b/crates/openshell-server/src/persistence/image_activation.rs new file mode 100644 index 0000000000..e7024319b0 --- /dev/null +++ b/crates/openshell-server/src/persistence/image_activation.rs @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Additional, fail-closed deployment checks for operator-activated image updates. +//! +//! These snapshots are not a durable fence or proof that old processes have +//! stopped. Membership can expire, gateways need not advertise peer endpoints, +//! and a backend can connect after the check. The explicit operator activation +//! must attest that incompatible gateways have been drained and retired. The +//! old-understood pending-operation interlock, not these observations, protects +//! an accepted image mutation from a subsequent legacy Start. + +use super::{ObjectRecord, PersistenceError, PersistenceResult, Store}; +use crate::gateway_members::{MEMBER_TTL, SANDBOX_UPDATE_PROTOCOL}; +use openshell_core::time::now_ms; + +const MEMBER_OBJECT_TYPE: &str = "gateway_member"; +const MEMBER_LIMIT: u32 = 1024; + +impl Store { + /// Reject observable incompatible or unknown gateway deployment participants. + /// + /// The caller separately enforces the default-off operator activation flag. + /// `SQLite` returns success under its supported single-gateway deployment + /// contract; this does not enforce process exclusion for a shared file. + /// `PostgreSQL` checks fresh gateway membership records. Administrative, + /// monitoring, and backup sessions are not gateway participants. Operator + /// activation attests that unregistered legacy processes have been retired. + /// Recovery of accepted operations does not use this admission check. + pub async fn ensure_image_update_deployment_compatible(&self) -> PersistenceResult<()> { + let Self::Postgres(_) = self else { + return Ok(()); + }; + + // One extra row distinguishes a complete bounded snapshot from a + // truncated listing. Never silently treat uninspected rows as compatible. + let records = self + .list_by_type(MEMBER_OBJECT_TYPE, MEMBER_LIMIT + 1, 0) + .await?; + ensure_live_members_compatible(&records, now_ms()) + } +} + +fn ensure_live_members_compatible(records: &[ObjectRecord], now: i64) -> PersistenceResult<()> { + if records.len() > MEMBER_LIMIT as usize { + return Err(PersistenceError::Config( + "cannot establish image-update deployment compatibility: gateway membership listing exceeds its limit" + .into(), + )); + } + let ttl_ms = i64::try_from(MEMBER_TTL.as_millis()).unwrap_or(i64::MAX); + for record in records { + // Match membership liveness semantics, including clock-skew clamping. + // Ignoring expired records is not evidence that their processes stopped. + let age_ms = now.saturating_sub(record.updated_at_ms).max(0); + if age_ms >= ttl_ms { + continue; + } + let member: serde_json::Map = + serde_json::from_slice(&record.payload).map_err(|_| { + PersistenceError::Config( + "cannot establish image-update deployment compatibility: a live gateway membership record is not a valid JSON object" + .into(), + ) + })?; + let replica_id = member.get("replica_id").and_then(serde_json::Value::as_str); + let protocol = member + .get("sandbox_update_protocol") + .and_then(serde_json::Value::as_u64); + if replica_id.is_none_or(|id| id.trim().is_empty()) + || protocol != Some(u64::from(SANDBOX_UPDATE_PROTOCOL)) + { + return Err(PersistenceError::Config(format!( + "sandbox updates require every live gateway membership record to advertise sandbox-update protocol {SANDBOX_UPDATE_PROTOCOL} and a nonempty replica id" + ))); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn record(payload: &[u8], updated_at_ms: i64) -> ObjectRecord { + ObjectRecord { + object_type: MEMBER_OBJECT_TYPE.into(), + id: "gateway-member:gw-0".into(), + name: "gw-0".into(), + workspace: String::new(), + payload: payload.to_vec(), + created_at_ms: updated_at_ms, + updated_at_ms, + labels: None, + resource_version: 1, + } + } + + #[tokio::test] + async fn sqlite_uses_the_supported_single_gateway_contract() { + let store = super::super::test_store().await; + store + .ensure_image_update_deployment_compatible() + .await + .unwrap(); + } + + #[tokio::test] + #[ignore = "requires OPENSHELL_TEST_POSTGRES_URL; run mise run test:rust:postgres"] + async fn postgres_activation_ignores_administrative_sessions_and_preserves_application_name() { + use super::super::test_postgres::TestSchema; + let schema = TestSchema::create("sandbox_update_activation").await; + let mut url = url::Url::parse(schema.url()).unwrap(); + url.query_pairs_mut() + .append_pair("application_name", "operator-custom-name"); + let store = Store::connect(url.as_str()).await.unwrap(); + let Store::Postgres(pg) = &store else { + unreachable!() + }; + let name: String = sqlx::query_scalar("SHOW application_name") + .fetch_one(&pg.pool) + .await + .unwrap(); + assert_eq!(name, "operator-custom-name"); + // TestSchema's unmarked admin connection remains open in this database. + store + .ensure_image_update_deployment_compatible() + .await + .unwrap(); + let index = crate::gateway_members::GatewayMemberIndex::new( + std::sync::Arc::new(store.clone()), + MEMBER_TTL, + ); + index + .register("compatible", "https://gateway:8443") + .await + .unwrap(); + store + .ensure_image_update_deployment_compatible() + .await + .unwrap(); + store + .put_if( + MEMBER_OBJECT_TYPE, + "gateway-member:legacy", + "legacy", + "", + br#"{"replica_id":"legacy"}"#, + None, + super::super::WriteCondition::MustCreate, + ) + .await + .unwrap(); + assert!(matches!( + store.ensure_image_update_deployment_compatible().await, + Err(PersistenceError::Config(_)) + )); + store.close().await; + schema.drop_schema().await; + } + + #[test] + fn compatible_live_members_and_empty_snapshot_pass() { + assert!(ensure_live_members_compatible(&[], 100).is_ok()); + let member = record( + br#"{"replica_id":"gw-0","peer_endpoint":"https://gw-0:8443","sandbox_update_protocol":1}"#, + 100, + ); + assert!(ensure_live_members_compatible(&[member], 100).is_ok()); + } + + #[test] + fn malformed_missing_unknown_or_ill_typed_protocols_fail_closed() { + let invalid: &[&[u8]] = &[ + b"not JSON", + br"[]", + br#"["gw-0",1]"#, + br"null", + br#"{"replica_id":"gw-0"}"#, + br#"{"replica_id":"gw-0","sandbox_update_protocol":0}"#, + br#"{"replica_id":"gw-0","sandbox_update_protocol":2}"#, + br#"{"replica_id":"gw-0","sandbox_update_protocol":"1"}"#, + br#"{"replica_id":"gw-0","sandbox_update_protocol":true}"#, + br#"{"replica_id":"gw-0","sandbox_update_protocol":1.0}"#, + br#"{"sandbox_update_protocol":1}"#, + br#"{"replica_id":"","sandbox_update_protocol":1}"#, + br#"{"replica_id":" ","sandbox_update_protocol":1}"#, + ]; + for payload in invalid { + assert!(ensure_live_members_compatible(&[record(payload, 100)], 100).is_err()); + } + } + + #[test] + fn expired_records_are_not_considered_live_but_future_timestamps_are() { + let ttl_ms = i64::try_from(MEMBER_TTL.as_millis()).unwrap(); + assert!(ensure_live_members_compatible(&[record(b"legacy", 100)], 100 + ttl_ms).is_ok()); + assert!( + ensure_live_members_compatible(&[record(b"legacy", 100)], 100 + ttl_ms - 1).is_err() + ); + assert!(ensure_live_members_compatible(&[record(b"legacy", 200)], 100).is_err()); + } + + #[test] + fn any_incompatible_live_member_rejects_the_snapshot() { + let compatible = record(br#"{"replica_id":"gw-0","sandbox_update_protocol":1}"#, 100); + let legacy = record(br#"{"replica_id":"gw-1"}"#, 100); + assert!(ensure_live_members_compatible(&[compatible, legacy], 100).is_err()); + } + + #[test] + fn listing_truncation_fails_closed_even_when_extra_rows_are_expired() { + let records = vec![record(b"legacy", 0); MEMBER_LIMIT as usize + 1]; + let now = i64::try_from(MEMBER_TTL.as_millis()).unwrap(); + assert!(ensure_live_members_compatible(&records, now).is_err()); + } +} diff --git a/crates/openshell-server/src/persistence/mod.rs b/crates/openshell-server/src/persistence/mod.rs index f14111b74d..30280b96bb 100644 --- a/crates/openshell-server/src/persistence/mod.rs +++ b/crates/openshell-server/src/persistence/mod.rs @@ -3,12 +3,14 @@ //! Persistence layer for `OpenShell` Server. +mod image_activation; mod legacy_time_wire; pub mod lock_order; pub mod mutation_lock; mod postgres; #[cfg(test)] mod refresh_lock_tests; +mod sandbox_image; mod sqlite; pub use crate::storage_proto::{ diff --git a/crates/openshell-server/src/persistence/postgres.rs b/crates/openshell-server/src/persistence/postgres.rs index a44f2399a7..ca070bedd6 100644 --- a/crates/openshell-server/src/persistence/postgres.rs +++ b/crates/openshell-server/src/persistence/postgres.rs @@ -40,7 +40,7 @@ use super::{DELETE_MANY_BATCH_SIZE, DRAFT_CHUNK_OBJECT_TYPE, POLICY_OBJECT_TYPE} #[derive(Debug, Clone)] pub struct PostgresStore { - pool: PgPool, + pub(super) pool: PgPool, /// Dedicated connections for session-level mutation advisory locks. lock_pool: PgPool, /// How acquisitions and guards use the lock pool's connections. diff --git a/crates/openshell-server/src/persistence/sandbox_image.rs b/crates/openshell-server/src/persistence/sandbox_image.rs new file mode 100644 index 0000000000..386fe78136 --- /dev/null +++ b/crates/openshell-server/src/persistence/sandbox_image.rs @@ -0,0 +1,1961 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Private, atomic image-update intent storage. +//! +//! The intent is deliberately not part of `Sandbox`: an older gateway decoding +//! and rewriting that public message must not erase an accepted update. Every +//! transaction locks the sandbox first, then its independent image-state row. + +use super::{PersistenceError, PersistenceResult, Store, current_time_ms, map_db_error}; +use crate::storage_proto::{StoredSandboxImageState, StoredSandboxImageUpdate}; +use openshell_core::SetResourceVersion; +use openshell_core::proto::{Sandbox, SandboxPhase, SandboxProvisioning}; +use prost::Message; +use sqlx::{Postgres, Sqlite, Transaction}; + +const IMAGE_STATE_OBJECT_TYPE: &str = "sandbox_image_state"; +const IMAGE_STATE_ID_PREFIX: &str = "sandbox-image-state:"; + +fn state_id(sandbox_id: &str) -> String { + format!("{IMAGE_STATE_ID_PREFIX}{sandbox_id}") +} + +fn conflict(current_resource_version: Option) -> PersistenceError { + PersistenceError::Conflict { + current_resource_version, + } +} + +fn decode_state(sandbox_id: &str, payload: &[u8]) -> PersistenceResult { + let state = StoredSandboxImageState::decode(payload) + .map_err(|error| PersistenceError::Decode(error.to_string()))?; + if state.sandbox_id != sandbox_id + || state + .pending + .as_ref() + .is_some_and(|pending| pending.revision == 0 || pending.revision != state.revision) + || (state.pending.is_none() && state.previous_provisioning.is_some()) + || state + .previous_provisioning + .as_ref() + .is_some_and(|previous| previous.driver_operation_pending) + { + return Err(PersistenceError::Database( + "inconsistent stored sandbox image state".into(), + )); + } + Ok(state) +} + +fn decode_sandbox( + sandbox_id: &str, + payload: &[u8], + resource_version: i64, +) -> PersistenceResult { + if resource_version < 1 { + return Err(PersistenceError::Database( + "invalid sandbox resource version".into(), + )); + } + let payload = super::legacy_time_wire::migrate("sandbox", payload)?; + let mut sandbox = Sandbox::decode(payload.as_slice()) + .map_err(|error| PersistenceError::Decode(error.to_string()))?; + if sandbox + .metadata + .as_ref() + .is_none_or(|meta| meta.id != sandbox_id) + { + return Err(PersistenceError::Database( + "inconsistent stored sandbox identity".into(), + )); + } + // The SQL column, not a possibly stale protobuf field, is authoritative. + sandbox.set_resource_version(resource_version.cast_unsigned()); + Ok(sandbox) +} + +fn resource_version(sandbox: &Sandbox) -> u64 { + sandbox + .metadata + .as_ref() + .map_or(0, |meta| meta.resource_version) +} + +fn require_stopped(sandbox: &Sandbox) -> PersistenceResult<()> { + if sandbox + .metadata + .as_ref() + .is_none_or(|meta| meta.deletion_time.is_some()) + || sandbox + .status + .as_ref() + .is_none_or(|status| status.phase != SandboxPhase::Stopped as i32) + { + return Err(conflict(Some(resource_version(sandbox)))); + } + Ok(()) +} + +fn validate_intent(intent: &StoredSandboxImageUpdate) -> PersistenceResult<()> { + for map in [intent.labels.as_ref(), intent.annotations.as_ref()] + .into_iter() + .flatten() + { + if map + .values + .keys() + .any(|key| openshell_core::sandbox_update::is_system_metadata_key(key)) + { + return Err(PersistenceError::Encode( + "system metadata cannot be updated".into(), + )); + } + } + if intent.operation_id.is_empty() + || intent.previous_image.is_empty() + || intent.image.is_empty() + || intent.revision == 0 + { + return Err(PersistenceError::Encode( + "image update requires an operation id, both images, and a nonzero revision".into(), + )); + } + Ok(()) +} + +fn bump_resource_version(sandbox: &mut Sandbox) -> PersistenceResult<()> { + let next = resource_version(sandbox) + .checked_add(1) + .filter(|next| i64::try_from(*next).is_ok()) + .ok_or_else(|| PersistenceError::Encode("sandbox resource version exhausted".into()))?; + sandbox.set_resource_version(next); + Ok(()) +} + +/// Pure projection: all acceptance checks happen before either row is written. +fn project_begin( + mut sandbox: Sandbox, + state: Option, + expected_sandbox_resource_version: u64, + expected_revision: u64, + intent: &StoredSandboxImageUpdate, +) -> PersistenceResult<(Sandbox, StoredSandboxImageState)> { + let current_version = resource_version(&sandbox); + if expected_sandbox_resource_version == 0 + || expected_sandbox_resource_version != current_version + { + return Err(conflict(Some(current_version))); + } + require_stopped(&sandbox)?; + validate_intent(intent)?; + let mut state = state.unwrap_or_else(|| StoredSandboxImageState { + sandbox_id: sandbox + .metadata + .as_ref() + .map_or_else(String::new, |meta| meta.id.clone()), + revision: 0, + pending: None, + previous_provisioning: None, + completed: None, + }); + if state.revision != expected_revision || state.pending.is_some() { + return Err(conflict(Some(current_version))); + } + let next_revision = expected_revision + .checked_add(1) + .ok_or_else(|| PersistenceError::Encode("sandbox image revision exhausted".into()))?; + if intent.revision != next_revision { + return Err(conflict(Some(current_version))); + } + let previous_provisioning = sandbox + .status + .as_ref() + .and_then(|status| status.provisioning.clone()); + if previous_provisioning + .as_ref() + .is_some_and(|previous| previous.driver_operation_pending) + { + return Err(conflict(Some(current_version))); + } + let template = sandbox + .spec + .as_mut() + .and_then(|spec| spec.template.as_mut()) + .ok_or_else(|| conflict(Some(current_version)))?; + if template.image != intent.previous_image { + return Err(conflict(Some(current_version))); + } + template.image.clone_from(&intent.image); + openshell_core::sandbox_update::apply_configuration( + &mut sandbox, + intent.command.as_ref(), + intent.environment.as_ref(), + intent.labels.as_ref(), + intent.annotations.as_ref(), + ); + state.revision = next_revision; + state.pending = Some(intent.clone()); + state.completed = None; + state.previous_provisioning = previous_provisioning; + let status = sandbox + .status + .as_mut() + .ok_or_else(|| conflict(Some(current_version)))?; + let operation = status + .provisioning + .get_or_insert_with(|| SandboxProvisioning { + attempt_id: format!("image-update:{}", intent.operation_id), + ..Default::default() + }); + // This is an existing, baseline-known lifecycle interlock. Old gateways + // retain it on ordinary status writes and refuse Start/Stop/Delete while + // it is pending. Do not settle it on an ambiguous driver error. + operation.driver_operation_pending = true; + operation + .driver_operation_id + .clone_from(&intent.operation_id); + bump_resource_version(&mut sandbox)?; + Ok((sandbox, state)) +} + +fn project_complete( + mut sandbox: Sandbox, + state: Option, + intent: &StoredSandboxImageUpdate, +) -> PersistenceResult<(Sandbox, StoredSandboxImageState)> { + let current_version = resource_version(&sandbox); + validate_intent(intent)?; + let mut state = state.ok_or_else(|| conflict(Some(current_version)))?; + // The exact receipt proves completion even if a subsequent Start changed + // the phase. A newer update clears this receipt, preventing ABA matches. + if state.pending.is_none() + && state.revision == intent.revision + && state.completed.as_ref() == Some(intent) + { + return Ok((sandbox, state)); + } + require_stopped(&sandbox)?; + if state.revision != intent.revision || state.pending.as_ref() != Some(intent) { + return Err(conflict(Some(current_version))); + } + if sandbox + .spec + .as_ref() + .and_then(|spec| spec.template.as_ref()) + .is_none_or(|template| template.image != intent.image) + { + return Err(conflict(Some(current_version))); + } + // Acceptance already committed the annotation replacement. UpdateConfig + // may subsequently merge annotations while driver reconciliation is pending; + // preserve those accepted writes rather than requiring the old map forever. + // Annotations are gateway metadata and are not reconciled by the driver. + let mut target = sandbox.clone(); + openshell_core::sandbox_update::apply_configuration( + &mut target, + intent.command.as_ref(), + intent.environment.as_ref(), + intent.labels.as_ref(), + None, + ); + if target != sandbox { + return Err(conflict(Some(current_version))); + } + let status = sandbox + .status + .as_mut() + .ok_or_else(|| conflict(Some(current_version)))?; + let expected_attempt = state.previous_provisioning.as_ref().map_or_else( + || format!("image-update:{}", intent.operation_id), + |previous| previous.attempt_id.clone(), + ); + let operation = status + .provisioning + .as_mut() + .ok_or_else(|| conflict(Some(current_version)))?; + if !operation.driver_operation_pending + || operation.driver_operation_id != intent.operation_id + || operation.attempt_id != expected_attempt + { + return Err(conflict(Some(current_version))); + } + // Retire only our flag on the CURRENT record. Restoring a saved snapshot + // would erase a concurrent provider attachment's accepted clock edit. + // Keep the newest operation ID to reject delayed older callbacks (ABA). + let unchanged_legacy_record = state.previous_provisioning.is_none() + && *operation + == SandboxProvisioning { + attempt_id: expected_attempt, + driver_operation_id: intent.operation_id.clone(), + driver_operation_pending: true, + ..Default::default() + }; + if unchanged_legacy_record { + status.provisioning = None; + } else { + operation.driver_operation_pending = false; + } + state.previous_provisioning = None; + state.pending = None; + state.completed = Some(intent.clone()); + // Completing an intent invalidates any Start CAS based on pending state. + bump_resource_version(&mut sandbox)?; + Ok((sandbox, state)) +} + +impl Store { + /// Discover only pending updates through the partial index, without reading + /// sandbox records or completed receipts. The cursor is a sandbox ID from + /// the preceding page; callers must recheck current intent before recovery. + pub(crate) async fn list_pending_sandbox_update_ids( + &self, + after: Option<&str>, + limit: u32, + ) -> PersistenceResult> { + let after = after.map_or_else(String::new, state_id); + let ids = match self { + Self::Sqlite(store) => { + sqlx::query_scalar::<_, String>( + "SELECT id FROM objects +WHERE object_type = 'sandbox_image_state' AND status = 'pending' AND id > ?1 +ORDER BY id LIMIT ?2", + ) + .bind(&after) + .bind(i64::from(limit)) + .fetch_all(&store.pool) + .await + } + Self::Postgres(store) => { + sqlx::query_scalar::<_, String>( + "SELECT id FROM objects +WHERE object_type = 'sandbox_image_state' AND status = 'pending' AND id > $1 +ORDER BY id LIMIT $2", + ) + .bind(&after) + .bind(i64::from(limit)) + .fetch_all(&store.pool) + .await + } + } + .map_err(|error| map_db_error(&error))?; + ids.into_iter() + .map(|id| { + id.strip_prefix(IMAGE_STATE_ID_PREFIX) + .filter(|sandbox_id| !sandbox_id.is_empty()) + .map(str::to_owned) + .ok_or_else(|| { + PersistenceError::Database("invalid sandbox image state identity".into()) + }) + }) + .collect() + } + + /// Remove a sandbox and its private update state in the same transaction. + /// Failed cleanup must retain the intent that authorizes deletion to bypass + /// the pending driver operation interlock. + pub(crate) async fn delete_sandbox_with_image_state_if( + &self, + sandbox_id: &str, + expected_resource_version: u64, + ) -> PersistenceResult { + let mut tx = ImageTransaction::begin(self).await?; + let sandbox = match tx.lock_sandbox(sandbox_id).await { + Ok(sandbox) => sandbox, + Err(PersistenceError::Conflict { + current_resource_version: None, + }) => return Ok(false), + Err(error) => return Err(error), + }; + if resource_version(&sandbox) != expected_resource_version { + return Err(conflict(Some(resource_version(&sandbox)))); + } + let image_state_id = state_id(sandbox_id); + for (object_type, id) in [ + ("sandbox", sandbox_id), + (IMAGE_STATE_OBJECT_TYPE, image_state_id.as_str()), + ] { + match &mut tx { + ImageTransaction::Sqlite(tx) => { + sqlx::query("DELETE FROM objects WHERE object_type = ?1 AND id = ?2") + .bind(object_type) + .bind(id) + .execute(&mut **tx) + .await + .map_err(|error| map_db_error(&error))?; + } + ImageTransaction::Postgres(tx) => { + sqlx::query("DELETE FROM objects WHERE object_type = $1 AND id = $2") + .bind(object_type) + .bind(id) + .execute(&mut **tx) + .await + .map_err(|error| map_db_error(&error))?; + } + } + } + tx.commit().await?; + Ok(true) + } + + /// Remove private image state independently to exercise recovery fixtures. + #[cfg(test)] + pub(crate) async fn delete_sandbox_image_state( + &self, + sandbox_id: &str, + ) -> PersistenceResult { + self.delete(IMAGE_STATE_OBJECT_TYPE, &state_id(sandbox_id)) + .await + } + + /// Read gateway-private image state by immutable sandbox identity. + pub async fn get_sandbox_image_state( + &self, + sandbox_id: &str, + ) -> PersistenceResult> { + let id = state_id(sandbox_id); + let payload = match self { + Self::Sqlite(store) => { + sqlx::query_scalar::<_, Vec>( + r#"SELECT "payload" FROM "objects" WHERE "object_type" = ?1 AND "id" = ?2"#, + ) + .bind(IMAGE_STATE_OBJECT_TYPE) + .bind(&id) + .fetch_optional(&store.pool) + .await + } + Self::Postgres(store) => { + sqlx::query_scalar::<_, Vec>( + "SELECT payload FROM objects WHERE object_type = $1 AND id = $2", + ) + .bind(IMAGE_STATE_OBJECT_TYPE) + .bind(&id) + .fetch_optional(&store.pool) + .await + } + } + .map_err(|error| map_db_error(&error))?; + payload + .map(|payload| decode_state(sandbox_id, &payload)) + .transpose() + } + + /// Atomically accept a stopped-sandbox image change and its private intent. + pub async fn begin_sandbox_image_update( + &self, + sandbox_id: &str, + expected_sandbox_resource_version: u64, + expected_revision: u64, + intent: &StoredSandboxImageUpdate, + ) -> PersistenceResult { + let mut tx = ImageTransaction::begin(self).await?; + let sandbox = tx.lock_sandbox(sandbox_id).await?; + let state = tx.lock_state(sandbox_id).await?; + let previous_version = resource_version(&sandbox); + let (sandbox, state) = project_begin( + sandbox, + state, + expected_sandbox_resource_version, + expected_revision, + intent, + )?; + tx.write_projection(sandbox_id, previous_version, &sandbox, &state) + .await?; + tx.commit().await?; + Ok(sandbox) + } + + /// Retire only the exact pending intent, retaining its monotonic revision. + pub async fn complete_sandbox_image_update( + &self, + sandbox_id: &str, + intent: &StoredSandboxImageUpdate, + ) -> PersistenceResult { + let mut tx = ImageTransaction::begin(self).await?; + let sandbox = tx.lock_sandbox(sandbox_id).await?; + let state = tx.lock_state(sandbox_id).await?; + let previous_version = resource_version(&sandbox); + let (sandbox, state) = project_complete(sandbox, state, intent)?; + if resource_version(&sandbox) == previous_version { + tx.commit().await?; + return Ok(sandbox); + } + tx.write_projection(sandbox_id, previous_version, &sandbox, &state) + .await?; + tx.commit().await?; + Ok(sandbox) + } +} + +/// Dropping an unfinished `SQLx` transaction rolls back both projections. +enum ImageTransaction { + Sqlite(Transaction<'static, Sqlite>), + Postgres(Transaction<'static, Postgres>), +} + +impl ImageTransaction { + async fn begin(store: &Store) -> PersistenceResult { + match store { + Store::Sqlite(store) => store + .pool + .begin_with("BEGIN IMMEDIATE") + .await + .map(Self::Sqlite), + Store::Postgres(store) => store.pool.begin().await.map(Self::Postgres), + } + .map_err(|error| map_db_error(&error)) + } + + async fn lock_sandbox(&mut self, sandbox_id: &str) -> PersistenceResult { + let row = match self { + Self::Sqlite(tx) => { + sqlx::query_as::<_, (Vec, i64)>( + r#"SELECT "payload", "resource_version" FROM "objects" +WHERE "object_type" = 'sandbox' AND "id" = ?1"#, + ) + .bind(sandbox_id) + .fetch_optional(&mut **tx) + .await + } + Self::Postgres(tx) => { + sqlx::query_as::<_, (Vec, i64)>( + "SELECT payload, resource_version FROM objects +WHERE object_type = 'sandbox' AND id = $1 FOR UPDATE", + ) + .bind(sandbox_id) + .fetch_optional(&mut **tx) + .await + } + } + .map_err(|error| map_db_error(&error))? + .ok_or_else(|| conflict(None))?; + decode_sandbox(sandbox_id, &row.0, row.1) + } + + async fn lock_state( + &mut self, + sandbox_id: &str, + ) -> PersistenceResult> { + let id = state_id(sandbox_id); + let payload = match self { + Self::Sqlite(tx) => { + sqlx::query_scalar::<_, Vec>( + r#"SELECT "payload" FROM "objects" WHERE "object_type" = ?1 AND "id" = ?2"#, + ) + .bind(IMAGE_STATE_OBJECT_TYPE) + .bind(&id) + .fetch_optional(&mut **tx) + .await + } + Self::Postgres(tx) => { + sqlx::query_scalar::<_, Vec>( + "SELECT payload FROM objects WHERE object_type = $1 AND id = $2 FOR UPDATE", + ) + .bind(IMAGE_STATE_OBJECT_TYPE) + .bind(&id) + .fetch_optional(&mut **tx) + .await + } + } + .map_err(|error| map_db_error(&error))?; + payload + .map(|payload| decode_state(sandbox_id, &payload)) + .transpose() + } + + async fn write_projection( + &mut self, + sandbox_id: &str, + previous_version: u64, + sandbox: &Sandbox, + state: &StoredSandboxImageState, + ) -> PersistenceResult<()> { + let previous_version_sql = i64::try_from(previous_version).map_err(|_| { + PersistenceError::Encode("sandbox resource version exceeds database range".into()) + })?; + let now_ms = current_time_ms(); + let sandbox_payload = sandbox.encode_to_vec(); + let labels = sandbox.metadata.as_ref().map(|metadata| &metadata.labels); + let labels = serde_json::to_value(labels) + .map_err(|error| PersistenceError::Encode(error.to_string()))?; + let affected = match self { + Self::Sqlite(tx) => sqlx::query( + r#"UPDATE "objects" +SET "payload" = ?2, "updated_at_ms" = ?3, "resource_version" = "resource_version" + 1, "labels" = ?5 +WHERE "object_type" = 'sandbox' AND "id" = ?1 AND "resource_version" = ?4"#, + ) + .bind(sandbox_id) + .bind(&sandbox_payload) + .bind(now_ms) + .bind(previous_version_sql) + .bind(labels.to_string()) + .execute(&mut **tx) + .await + .map(|result| result.rows_affected()), + Self::Postgres(tx) => sqlx::query( + "UPDATE objects +SET payload = $2, updated_at_ms = $3, resource_version = resource_version + 1, labels = $5 +WHERE object_type = 'sandbox' AND id = $1 AND resource_version = $4", + ) + .bind(sandbox_id) + .bind(&sandbox_payload) + .bind(now_ms) + .bind(previous_version_sql) + .bind(&labels) + .execute(&mut **tx) + .await + .map(|result| result.rows_affected()), + } + .map_err(|error| map_db_error(&error))?; + if affected != 1 { + return Err(conflict(Some(previous_version))); + } + + let id = state_id(sandbox_id); + let state_payload = state.encode_to_vec(); + // Keep the discovery index in the same transaction as the intent and + // public lifecycle interlock. Completed receipts remain stored but are + // excluded from recovery scans. + let state_status = if state.pending.is_some() { + "pending" + } else { + "completed" + }; + let affected = match self { + Self::Sqlite(tx) => sqlx::query( + r#"INSERT INTO "objects" ( + "object_type", "id", "name", "workspace", "payload", "created_at_ms", "updated_at_ms", "status" +) VALUES (?1, ?2, NULL, '', ?3, ?4, ?4, ?5) +ON CONFLICT ("id") DO UPDATE +SET "payload" = excluded."payload", "updated_at_ms" = excluded."updated_at_ms", "status" = excluded."status", + "resource_version" = "objects"."resource_version" + 1 +WHERE "objects"."object_type" = ?1 AND "objects"."name" IS NULL AND "objects"."workspace" = ''"#, + ) + .bind(IMAGE_STATE_OBJECT_TYPE) + .bind(&id) + .bind(&state_payload) + .bind(now_ms) + .bind(state_status) + .execute(&mut **tx) + .await + .map(|result| result.rows_affected()), + Self::Postgres(tx) => sqlx::query( + "INSERT INTO objects ( + object_type, id, name, workspace, payload, created_at_ms, updated_at_ms, status +) VALUES ($1, $2, NULL, '', $3, $4, $4, $5) +ON CONFLICT (id) DO UPDATE +SET payload = EXCLUDED.payload, updated_at_ms = EXCLUDED.updated_at_ms, status = EXCLUDED.status, + resource_version = objects.resource_version + 1 +WHERE objects.object_type = $1 AND objects.name IS NULL AND objects.workspace = ''", + ) + .bind(IMAGE_STATE_OBJECT_TYPE) + .bind(&id) + .bind(&state_payload) + .bind(now_ms) + .bind(state_status) + .execute(&mut **tx) + .await + .map(|result| result.rows_affected()), + } + .map_err(|error| map_db_error(&error))?; + if affected != 1 { + return Err(PersistenceError::Database( + "sandbox image state identity is already occupied".into(), + )); + } + Ok(()) + } + + async fn commit(self) -> PersistenceResult<()> { + match self { + Self::Sqlite(tx) => tx.commit().await, + Self::Postgres(tx) => tx.commit().await, + } + .map_err(|error| map_db_error(&error)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use openshell_core::proto::{ObjectMeta, SandboxSpec, SandboxStatus, SandboxTemplate}; + + fn sandbox() -> Sandbox { + Sandbox { + metadata: Some(ObjectMeta { + id: "sandbox-id".into(), + name: "sandbox-name".into(), + workspace: "default".into(), + resource_version: 5, + ..Default::default() + }), + spec: Some(SandboxSpec { + template: Some(SandboxTemplate { + image: "old-image".into(), + ..Default::default() + }), + ..Default::default() + }), + status: Some(SandboxStatus { + phase: SandboxPhase::Stopped as i32, + ..Default::default() + }), + ..Default::default() + } + } + + fn intent() -> StoredSandboxImageUpdate { + StoredSandboxImageUpdate { + operation_id: "operation-id".into(), + previous_image: "old-image".into(), + image: "new-image".into(), + revision: 1, + ..Default::default() + } + } + + fn image(sandbox: &Sandbox) -> &str { + &sandbox + .spec + .as_ref() + .unwrap() + .template + .as_ref() + .unwrap() + .image + } + + #[test] + fn exact_completion_is_idempotent_but_another_operation_is_not() { + let (accepted, state) = project_begin(sandbox(), None, 5, 0, &intent()).unwrap(); + let (completed, receipt) = project_complete(accepted, Some(state), &intent()).unwrap(); + assert_eq!( + project_complete(completed.clone(), Some(receipt.clone()), &intent()).unwrap(), + (completed.clone(), receipt.clone()) + ); + let mut other = intent(); + other.operation_id = "another-operation".into(); + assert!(project_complete(completed.clone(), Some(receipt.clone()), &other).is_err()); + let mut started = completed; + started.set_phase(SandboxPhase::Starting as i32); + assert_eq!( + project_complete(started.clone(), Some(receipt), &intent()) + .unwrap() + .0, + started + ); + } + + #[test] + fn begin_and_complete_project_image_and_baseline_known_interlock() { + let original = sandbox(); + let (accepted, state) = project_begin(original.clone(), None, 5, 0, &intent()).unwrap(); + assert_eq!(image(&accepted), "new-image"); + assert_eq!(resource_version(&accepted), 6); + let operation = accepted + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap(); + assert!(operation.driver_operation_pending); + assert_eq!(operation.driver_operation_id, intent().operation_id); + assert_eq!(operation.attempt_id, "image-update:operation-id"); + assert!(operation.deadline.is_none()); + assert!(operation.preparation_deadline.is_none()); + assert!(state.previous_provisioning.is_none()); + assert_eq!(state.pending, Some(intent())); + let (completed, state) = project_complete(accepted, Some(state), &intent()).unwrap(); + assert_eq!(resource_version(&completed), 7); + assert_eq!(completed.status, original.status); + assert_eq!(state.revision, 1); + assert!(state.pending.is_none()); + assert!(state.previous_provisioning.is_none()); + } + + #[test] + fn completion_restores_existing_provisioning_except_last_operation_identity() { + let mut original = sandbox(); + let previous = SandboxProvisioning { + attempt_id: "previous-attempt".into(), + driver_operation_id: "previous-settled-operation".into(), + timeout_time: Some(prost_types::Timestamp { + seconds: 123, + nanos: 456, + }), + cleanup_completed_time: Some(prost_types::Timestamp { + seconds: 124, + nanos: 0, + }), + cleanup_error: "retained diagnostic".into(), + ..Default::default() + }; + original.status.as_mut().unwrap().provisioning = Some(previous.clone()); + let (accepted, state) = project_begin(original.clone(), None, 5, 0, &intent()).unwrap(); + assert_eq!(state.previous_provisioning, Some(previous)); + let (completed, cleared) = project_complete(accepted, Some(state), &intent()).unwrap(); + original + .status + .as_mut() + .unwrap() + .provisioning + .as_mut() + .unwrap() + .driver_operation_id = intent().operation_id; + assert_eq!(completed.status, original.status); + assert!(cleared.previous_provisioning.is_none()); + assert!(cleared.pending.is_none()); + assert_eq!(cleared.revision, 1); + } + + #[test] + fn delayed_previous_operation_cannot_match_completed_image_operation() { + let mut original = sandbox(); + let previous = SandboxProvisioning { + attempt_id: "retained-attempt".into(), + driver_operation_id: "delayed-operation".into(), + timeout_time: Some(prost_types::Timestamp { + seconds: 456, + nanos: 0, + }), + cleanup_completed_time: Some(prost_types::Timestamp { + seconds: 457, + nanos: 0, + }), + ..Default::default() + }; + original.status.as_mut().unwrap().provisioning = Some(previous.clone()); + let (accepted, state) = project_begin(original, None, 5, 0, &intent()).unwrap(); + let (completed, _) = project_complete(accepted, Some(state), &intent()).unwrap(); + let current = completed + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap(); + // Baseline same_operation compares this pair. Preserve the attempt and + // timeout provenance without resurrecting the old callback's identity. + assert_ne!( + (¤t.attempt_id, ¤t.driver_operation_id), + (&previous.attempt_id, &previous.driver_operation_id) + ); + assert_eq!(current.attempt_id, previous.attempt_id); + assert_eq!(current.driver_operation_id, intent().operation_id); + assert!(!current.driver_operation_pending); + assert_eq!(current.timeout_time, previous.timeout_time); + assert_eq!( + current.cleanup_completed_time, + previous.cleanup_completed_time + ); + } + + #[test] + fn image_completion_preserves_concurrent_attachment_clock_edits() { + let mut original = sandbox(); + let mut previous = crate::compute::provisioning_deadline::new_preparation_record(0, 1800); + previous.driver_operation_id = "prior-operation".into(); + previous.attachment_change_id = "before-attachment".into(); + original.status.as_mut().unwrap().provisioning = Some(previous); + let (mut accepted, state) = project_begin(original, None, 5, 0, &intent()).unwrap(); + crate::compute::provisioning_deadline::attachments_changed(&mut accepted, 2_000); + let mut expected = accepted + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap() + .clone(); + assert_ne!(expected.attachment_change_id, "before-attachment"); + expected.driver_operation_pending = false; + let (completed, state) = project_complete(accepted, Some(state), &intent()).unwrap(); + assert_eq!(completed.status.unwrap().provisioning, Some(expected)); + assert!(state.previous_provisioning.is_none()); + } + + #[test] + fn image_completion_cannot_settle_a_replaced_provisioning_attempt() { + let (mut accepted, state) = project_begin(sandbox(), None, 5, 0, &intent()).unwrap(); + accepted + .status + .as_mut() + .unwrap() + .provisioning + .as_mut() + .unwrap() + .attempt_id = "replacement-attempt".into(); + assert!(matches!( + project_complete(accepted, Some(state), &intent()), + Err(PersistenceError::Conflict { .. }) + )); + } + + #[test] + fn pending_driver_operation_is_not_overwritten_and_completion_requires_its_fence() { + let mut occupied = sandbox(); + occupied.status.as_mut().unwrap().provisioning = Some(SandboxProvisioning { + attempt_id: "other-attempt".into(), + driver_operation_pending: true, + driver_operation_id: "other-operation".into(), + ..Default::default() + }); + assert!(project_begin(occupied, None, 5, 0, &intent()).is_err()); + let (accepted, state) = project_begin(sandbox(), None, 5, 0, &intent()).unwrap(); + let mut missing = accepted.clone(); + missing.status.as_mut().unwrap().provisioning = None; + assert!(project_complete(missing, Some(state.clone()), &intent()).is_err()); + let mut cleared = accepted.clone(); + cleared + .status + .as_mut() + .unwrap() + .provisioning + .as_mut() + .unwrap() + .driver_operation_pending = false; + assert!(project_complete(cleared, Some(state.clone()), &intent()).is_err()); + let mut other = accepted; + other + .status + .as_mut() + .unwrap() + .provisioning + .as_mut() + .unwrap() + .driver_operation_id = "other".into(); + assert!(project_complete(other, Some(state), &intent()).is_err()); + } + + #[test] + fn begin_rejects_stale_versions_revisions_and_pending_updates() { + for version in [0, 4, 6] { + assert!(matches!( + project_begin(sandbox(), None, version, 0, &intent()), + Err(PersistenceError::Conflict { .. }) + )); + } + assert!(project_begin(sandbox(), None, 5, 1, &intent()).is_err()); + let mut wrong = intent(); + wrong.revision = 2; + assert!(project_begin(sandbox(), None, 5, 0, &wrong).is_err()); + let (accepted, state) = project_begin(sandbox(), None, 5, 0, &intent()).unwrap(); + let next = StoredSandboxImageUpdate { + operation_id: "next-operation".into(), + previous_image: "new-image".into(), + image: "newer-image".into(), + revision: 2, + ..Default::default() + }; + assert!(project_begin(accepted, Some(state), 6, 1, &next).is_err()); + } + + #[test] + fn lifecycle_and_image_checks_apply_at_acceptance_and_completion() { + let mut running = sandbox(); + running.status.as_mut().unwrap().phase = SandboxPhase::Ready as i32; + assert!(project_begin(running, None, 5, 0, &intent()).is_err()); + let mut deleting = sandbox(); + deleting.metadata.as_mut().unwrap().deletion_time = Some(prost_types::Timestamp::default()); + assert!(project_begin(deleting, None, 5, 0, &intent()).is_err()); + let mut wrong_image = sandbox(); + wrong_image + .spec + .as_mut() + .unwrap() + .template + .as_mut() + .unwrap() + .image = "other".into(); + assert!(project_begin(wrong_image, None, 5, 0, &intent()).is_err()); + let (accepted, state) = project_begin(sandbox(), None, 5, 0, &intent()).unwrap(); + let mut stale_intent = intent(); + stale_intent.operation_id = "other-operation".into(); + assert!(project_complete(accepted.clone(), Some(state.clone()), &stale_intent).is_err()); + let mut changed = accepted.clone(); + changed + .spec + .as_mut() + .unwrap() + .template + .as_mut() + .unwrap() + .image = "other".into(); + assert!(project_complete(changed, Some(state.clone()), &intent()).is_err()); + let mut running = accepted.clone(); + running.status.as_mut().unwrap().phase = SandboxPhase::Starting as i32; + assert!(project_complete(running, Some(state.clone()), &intent()).is_err()); + let mut deleting = accepted.clone(); + deleting.metadata.as_mut().unwrap().deletion_time = Some(prost_types::Timestamp::default()); + assert!(project_complete(deleting, Some(state.clone()), &intent()).is_err()); + let mut wrong_revision = state; + wrong_revision.revision = 2; + assert!(project_complete(accepted.clone(), Some(wrong_revision), &intent()).is_err()); + assert!(project_complete(accepted, None, &intent()).is_err()); + } + + #[test] + fn sandbox_decode_uses_authoritative_column_and_rejects_invalid_identity() { + let original = sandbox(); + let decoded = decode_sandbox("sandbox-id", &original.encode_to_vec(), 12).unwrap(); + assert_eq!(resource_version(&decoded), 12); + assert!(decode_sandbox("other-id", &original.encode_to_vec(), 12).is_err()); + assert!(decode_sandbox("sandbox-id", &original.encode_to_vec(), 0).is_err()); + } + + #[test] + fn resource_version_overflow_and_invalid_intents_do_not_project() { + let mut exhausted = sandbox(); + exhausted.set_resource_version(i64::MAX.cast_unsigned()); + assert!(matches!( + project_begin(exhausted, None, i64::MAX.cast_unsigned(), 0, &intent()), + Err(PersistenceError::Encode(_)) + )); + let mut invalid = intent(); + invalid.operation_id.clear(); + assert!(matches!( + project_begin(sandbox(), None, 5, 0, &invalid), + Err(PersistenceError::Encode(_)) + )); + } + + async fn fixture() -> (Store, Sandbox) { + let store = super::super::test_store().await; + store.put_message(&sandbox()).await.unwrap(); + let stored = store + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(); + (store, stored) + } + + async fn assert_atomic_sandbox_deletion(store: &Store) { + store.put_message(&sandbox()).await.unwrap(); + let original = store + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(); + let accepted = store + .begin_sandbox_image_update("sandbox-id", resource_version(&original), 0, &intent()) + .await + .unwrap(); + let pending = store.get_sandbox_image_state("sandbox-id").await.unwrap(); + assert!(matches!( + store + .delete_sandbox_with_image_state_if("sandbox-id", resource_version(&original)) + .await, + Err(PersistenceError::Conflict { .. }) + )); + assert_eq!( + store.get_message::("sandbox-id").await.unwrap(), + Some(accepted.clone()) + ); + assert_eq!( + store.get_sandbox_image_state("sandbox-id").await.unwrap(), + pending + ); + assert!( + store + .delete_sandbox_with_image_state_if("sandbox-id", resource_version(&accepted)) + .await + .unwrap() + ); + assert!( + store + .get_message::("sandbox-id") + .await + .unwrap() + .is_none() + ); + assert!( + store + .get_sandbox_image_state("sandbox-id") + .await + .unwrap() + .is_none() + ); + assert!( + !store + .delete_sandbox_with_image_state_if("sandbox-id", resource_version(&accepted)) + .await + .unwrap() + ); + assert!( + store + .complete_sandbox_image_update("sandbox-id", &intent()) + .await + .is_err() + ); + } + + #[tokio::test] + async fn sqlite_sandbox_deletion_preserves_intent_on_conflict() { + let store = super::super::test_store().await; + assert_atomic_sandbox_deletion(&store).await; + } + + #[tokio::test] + async fn sqlite_private_state_deletion_failure_rolls_back_sandbox_removal() { + let (store, original) = fixture().await; + let accepted = store + .begin_sandbox_image_update("sandbox-id", resource_version(&original), 0, &intent()) + .await + .unwrap(); + let pending = store.get_sandbox_image_state("sandbox-id").await.unwrap(); + let Store::Sqlite(sqlite) = &store else { + panic!("expected SQLite"); + }; + sqlx::query( + "CREATE TRIGGER fail_image_delete BEFORE DELETE ON objects +WHEN OLD.object_type = 'sandbox_image_state' +BEGIN SELECT RAISE(ABORT, 'injected image state deletion failure'); END", + ) + .execute(&sqlite.pool) + .await + .unwrap(); + assert!( + store + .delete_sandbox_with_image_state_if("sandbox-id", resource_version(&accepted)) + .await + .is_err() + ); + assert_eq!( + store.get_message::("sandbox-id").await.unwrap(), + Some(accepted.clone()) + ); + assert_eq!( + store.get_sandbox_image_state("sandbox-id").await.unwrap(), + pending + ); + sqlx::query("DROP TRIGGER fail_image_delete") + .execute(&sqlite.pool) + .await + .unwrap(); + assert!( + store + .delete_sandbox_with_image_state_if("sandbox-id", resource_version(&accepted)) + .await + .unwrap() + ); + assert!( + store + .get_sandbox_image_state("sandbox-id") + .await + .unwrap() + .is_none() + ); + } + + #[tokio::test] + #[ignore = "requires PostgreSQL; run mise run test:rust:postgres"] + async fn postgres_sandbox_deletion_preserves_intent_on_conflict() { + let schema = + super::super::test_postgres::TestSchema::create("sandbox_update_deletion").await; + let store = schema.connect_store().await; + assert_atomic_sandbox_deletion(&store).await; + store.close().await; + schema.drop_schema().await; + } + + async fn assert_pending_update_discovery(store: &Store) { + assert!( + store + .list_pending_sandbox_update_ids(None, 2) + .await + .unwrap() + .is_empty() + ); + for id in ["d", "b", "e", "a", "c"] { + let mut original = sandbox(); + let metadata = original.metadata.as_mut().unwrap(); + metadata.id = id.into(); + metadata.name = id.into(); + store.put_message(&original).await.unwrap(); + if id == "e" { + continue; // An ordinary sandbox has no update state. + } + let original = store.get_message::(id).await.unwrap().unwrap(); + // Rejected acceptance must not add a discovery entry. + assert!( + store + .begin_sandbox_image_update(id, resource_version(&original) + 1, 0, &intent()) + .await + .is_err() + ); + store + .begin_sandbox_image_update(id, resource_version(&original), 0, &intent()) + .await + .unwrap(); + } + let completed = store + .complete_sandbox_image_update("c", &intent()) + .await + .unwrap(); + let receipt = store.get_sandbox_image_state("c").await.unwrap().unwrap(); + assert_eq!(receipt.completed, Some(intent())); + + let first = store + .list_pending_sandbox_update_ids(None, 2) + .await + .unwrap(); + assert_eq!(first, ["a", "b"]); + // Removing the first page must not shift the next page past any work. + store + .complete_sandbox_image_update("a", &intent()) + .await + .unwrap(); + store.delete_sandbox_image_state("b").await.unwrap(); + assert_eq!( + store + .list_pending_sandbox_update_ids(first.last().map(String::as_str), 2) + .await + .unwrap(), + ["d"] + ); + assert!( + store + .list_pending_sandbox_update_ids(Some("d"), 2) + .await + .unwrap() + .is_empty() + ); + + let next = StoredSandboxImageUpdate { + operation_id: "next-operation".into(), + previous_image: "new-image".into(), + image: "newer-image".into(), + revision: 2, + ..Default::default() + }; + store + .begin_sandbox_image_update("c", resource_version(&completed), 1, &next) + .await + .unwrap(); + // A stale completion must not remove the newer pending entry. + assert!( + store + .complete_sandbox_image_update("c", &intent()) + .await + .is_err() + ); + assert_eq!( + store + .list_pending_sandbox_update_ids(None, 2) + .await + .unwrap(), + ["c", "d"] + ); + store.delete_sandbox_image_state("d").await.unwrap(); + store + .complete_sandbox_image_update("c", &next) + .await + .unwrap(); + // Idempotent completion leaves the entry absent and the receipt intact. + store + .complete_sandbox_image_update("c", &next) + .await + .unwrap(); + assert!( + store + .list_pending_sandbox_update_ids(None, 2) + .await + .unwrap() + .is_empty() + ); + assert_eq!( + store + .get_sandbox_image_state("c") + .await + .unwrap() + .unwrap() + .completed, + Some(next) + ); + } + + #[tokio::test] + async fn sqlite_pending_update_discovery_tracks_transactions_and_pages() { + let store = super::super::test_store().await; + assert_pending_update_discovery(&store).await; + let Store::Sqlite(sqlite) = &store else { + unreachable!() + }; + let plan = sqlx::query_as::<_, (i64, i64, i64, String)>( + "EXPLAIN QUERY PLAN SELECT id FROM objects +WHERE object_type = 'sandbox_image_state' AND status = 'pending' AND id > ?1 +ORDER BY id LIMIT ?2", + ) + .bind("") + .bind(2_i64) + .fetch_all(&sqlite.pool) + .await + .unwrap(); + assert!( + plan.iter() + .any(|row| row.3.contains("sandbox_pending_updates_idx")), + "{plan:?}" + ); + } + + #[tokio::test] + #[ignore = "requires PostgreSQL; run mise run test:rust:postgres"] + async fn postgres_pending_update_discovery_tracks_transactions_and_pages() { + let schema = super::super::test_postgres::TestSchema::create("pending_updates").await; + let store = schema.connect_store().await; + assert_pending_update_discovery(&store).await; + store.close().await; + schema.drop_schema().await; + } + + #[test] + fn legacy_image_intent_decodes_without_configuration_selections() { + let decoded = + StoredSandboxImageUpdate::decode(&b"\x0a\x02op\x12\x01a\x1a\x01b\x20\x01"[..]).unwrap(); + assert_eq!(decoded.operation_id, "op"); + assert_eq!(decoded.previous_image, "a"); + assert_eq!(decoded.image, "b"); + assert_eq!(decoded.revision, 1); + assert!( + decoded.command.is_none() + && decoded.environment.is_none() + && decoded.labels.is_none() + && decoded.annotations.is_none() + ); + } + + #[tokio::test] + async fn sqlite_configuration_update_reindexes_labels_atomically() { + let (store, original) = fixture().await; + assert_configuration_update_reindexes_labels(&store, original).await; + } + + #[tokio::test] + #[ignore = "requires a disposable PostgreSQL database; run mise run test:rust:postgres"] + async fn postgres_sandbox_image_configuration_reindexes_labels_atomically() { + let schema = + super::super::test_postgres::TestSchema::create("sandbox_configuration_labels").await; + let store = schema.connect_store().await; + crate::ensure_default_workspace(&store).await.unwrap(); + store.put_message(&sandbox()).await.unwrap(); + let original = store + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(); + assert_configuration_update_reindexes_labels(&store, original).await; + store.close().await; + schema.drop_schema().await; + } + + async fn assert_configuration_update_reindexes_labels(store: &Store, mut original: Sandbox) { + use openshell_core::proto::SandboxMapUpdate; + original + .metadata + .as_mut() + .unwrap() + .labels + .insert("team".into(), "old".into()); + store.put_message(&original).await.unwrap(); + let original = store + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(); + let update = StoredSandboxImageUpdate { + labels: Some(SandboxMapUpdate { + values: std::collections::HashMap::from([("team".into(), "new".into())]), + }), + ..intent() + }; + store + .begin_sandbox_image_update("sandbox-id", resource_version(&original), 0, &update) + .await + .unwrap(); + assert!( + store + .list_with_selector("sandbox", "default", "team=old", 10, 0) + .await + .unwrap() + .is_empty() + ); + assert_eq!( + store + .list_with_selector("sandbox", "default", "team=new", 10, 0) + .await + .unwrap() + .len(), + 1 + ); + store + .complete_sandbox_image_update("sandbox-id", &update) + .await + .unwrap(); + assert_eq!( + store + .list_with_selector("sandbox", "default", "team=new", 10, 0) + .await + .unwrap() + .len(), + 1 + ); + } + + #[tokio::test] + async fn sqlite_accept_complete_and_legacy_rewrite_preserve_independent_state() { + let (store, original) = fixture().await; + let version = resource_version(&original); + let accepted = store + .begin_sandbox_image_update("sandbox-id", version, 0, &intent()) + .await + .unwrap(); + assert_eq!(resource_version(&accepted), version + 1); + assert_eq!(image(&accepted), "new-image"); + let pending = store + .get_sandbox_image_state("sandbox-id") + .await + .unwrap() + .unwrap(); + assert_eq!(pending.pending, Some(intent())); + // Generic Sandbox decode/reencode cannot remove the separate intent. + let decoded = Sandbox::decode(accepted.encode_to_vec().as_slice()).unwrap(); + assert!( + decoded + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap() + .driver_operation_pending + ); + assert_eq!( + decoded + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap() + .driver_operation_id, + intent().operation_id + ); + store.put_message(&decoded).await.unwrap(); + assert_eq!( + store.get_sandbox_image_state("sandbox-id").await.unwrap(), + Some(pending) + ); + let before_complete = store + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(); + let completed = store + .complete_sandbox_image_update("sandbox-id", &intent()) + .await + .unwrap(); + assert_eq!( + resource_version(&completed), + resource_version(&before_complete) + 1 + ); + let retained = store + .get_sandbox_image_state("sandbox-id") + .await + .unwrap() + .unwrap(); + assert_eq!(retained.revision, 1); + assert!(retained.pending.is_none()); + assert!(retained.previous_provisioning.is_none()); + assert_eq!(completed.status, original.status); + // A Start based on any pre-completion snapshot must lose its CAS. + let mut stale_start = before_complete.clone(); + stale_start.status.as_mut().unwrap().phase = SandboxPhase::Starting as i32; + assert!(matches!( + store + .put_if( + "sandbox", + "sandbox-id", + "sandbox-name", + "default", + &stale_start.encode_to_vec(), + None, + super::super::WriteCondition::MatchResourceVersion(resource_version( + &before_complete + )), + ) + .await, + Err(PersistenceError::Conflict { .. }) + )); + // Concurrent completion of the same intent returns the current object + // without a second version bump. A newer generation invalidates it. + assert_eq!( + store + .complete_sandbox_image_update("sandbox-id", &intent()) + .await + .unwrap(), + completed + ); + let next = StoredSandboxImageUpdate { + operation_id: "next-operation".into(), + previous_image: "new-image".into(), + image: "newer-image".into(), + revision: 2, + ..Default::default() + }; + store + .begin_sandbox_image_update("sandbox-id", resource_version(&completed), 1, &next) + .await + .unwrap(); + assert!( + store + .complete_sandbox_image_update("sandbox-id", &intent()) + .await + .is_err() + ); + assert_eq!( + store + .get_sandbox_image_state("sandbox-id") + .await + .unwrap() + .unwrap() + .pending, + Some(next) + ); + } + + #[tokio::test] + async fn sqlite_restores_prior_provisioning_and_wrong_completion_keeps_fence() { + let (store, mut original) = fixture().await; + let previous = SandboxProvisioning { + attempt_id: "timed-out-attempt".into(), + driver_operation_id: "settled-driver-operation".into(), + timeout_time: Some(prost_types::Timestamp { + seconds: 123, + nanos: 0, + }), + cleanup_completed_time: Some(prost_types::Timestamp { + seconds: 124, + nanos: 0, + }), + ..Default::default() + }; + original.status.as_mut().unwrap().provisioning = Some(previous.clone()); + store.put_message(&original).await.unwrap(); + let mut original = store + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(); + let accepted = store + .begin_sandbox_image_update("sandbox-id", resource_version(&original), 0, &intent()) + .await + .unwrap(); + let mut wrong = intent(); + wrong.operation_id = "stale-operation".into(); + assert!( + store + .complete_sandbox_image_update("sandbox-id", &wrong) + .await + .is_err() + ); + assert_eq!( + store + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(), + accepted + ); + let state = store + .get_sandbox_image_state("sandbox-id") + .await + .unwrap() + .unwrap(); + assert_eq!(state.pending, Some(intent())); + assert_eq!(state.previous_provisioning, Some(previous.clone())); + let completed = store + .complete_sandbox_image_update("sandbox-id", &intent()) + .await + .unwrap(); + let mut expected = previous; + expected.driver_operation_id = intent().operation_id; + assert_eq!( + completed.status.as_ref().unwrap().provisioning, + Some(expected.clone()) + ); + original.status.as_mut().unwrap().provisioning = Some(expected); + assert_eq!(completed.status, original.status); + assert!( + store + .get_sandbox_image_state("sandbox-id") + .await + .unwrap() + .unwrap() + .previous_provisioning + .is_none() + ); + } + + #[tokio::test] + async fn sqlite_failed_cas_has_no_private_write() { + let (store, original) = fixture().await; + for version in [0, resource_version(&original) + 1] { + assert!(matches!( + store + .begin_sandbox_image_update("sandbox-id", version, 0, &intent()) + .await, + Err(PersistenceError::Conflict { .. }) + )); + } + assert!( + store + .get_sandbox_image_state("sandbox-id") + .await + .unwrap() + .is_none() + ); + assert_eq!( + store + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(), + original + ); + } + + #[tokio::test] + async fn sqlite_private_write_failure_rolls_back_sandbox_and_completion() { + let (store, original) = fixture().await; + let Store::Sqlite(sqlite) = &store else { + unreachable!() + }; + sqlx::query( + "CREATE TRIGGER fail_image_insert BEFORE INSERT ON objects +WHEN NEW.object_type = 'sandbox_image_state' +BEGIN SELECT RAISE(ABORT, 'injected image state write failure'); END", + ) + .execute(&sqlite.pool) + .await + .unwrap(); + assert!( + store + .begin_sandbox_image_update("sandbox-id", resource_version(&original), 0, &intent()) + .await + .is_err() + ); + assert_eq!( + store + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(), + original + ); + assert!( + store + .get_sandbox_image_state("sandbox-id") + .await + .unwrap() + .is_none() + ); + assert!( + store + .list_pending_sandbox_update_ids(None, 10) + .await + .unwrap() + .is_empty() + ); + sqlx::query("DROP TRIGGER fail_image_insert") + .execute(&sqlite.pool) + .await + .unwrap(); + let accepted = store + .begin_sandbox_image_update("sandbox-id", resource_version(&original), 0, &intent()) + .await + .unwrap(); + let pending = store.get_sandbox_image_state("sandbox-id").await.unwrap(); + sqlx::query( + "CREATE TRIGGER fail_image_update BEFORE UPDATE ON objects +WHEN NEW.object_type = 'sandbox_image_state' +BEGIN SELECT RAISE(ABORT, 'injected image state write failure'); END", + ) + .execute(&sqlite.pool) + .await + .unwrap(); + assert!( + store + .complete_sandbox_image_update("sandbox-id", &intent()) + .await + .is_err() + ); + assert_eq!( + store + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(), + accepted + ); + assert_eq!( + store.get_sandbox_image_state("sandbox-id").await.unwrap(), + pending + ); + assert_eq!( + store + .list_pending_sandbox_update_ids(None, 10) + .await + .unwrap(), + ["sandbox-id"] + ); + } + + #[tokio::test] + #[ignore = "requires a disposable PostgreSQL database in OPENSHELL_TEST_POSTGRES_URL; run mise run test:rust:postgres"] + async fn postgres_sandbox_image_state_survives_legacy_writes_and_fences_concurrent_updates() { + use super::super::{WriteCondition, test_postgres::TestSchema}; + + let schema = TestSchema::create("sandbox_image").await; + let first = schema.connect_store().await; + let second = schema.connect_store().await; + crate::ensure_default_workspace(&first).await.unwrap(); + first.put_message(&sandbox()).await.unwrap(); + let original = second + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(); + let version = resource_version(&original); + let left = intent(); + let right = StoredSandboxImageUpdate { + operation_id: "competing-operation".into(), + image: "competing-image".into(), + ..intent() + }; + + // Independent pools contend on the same sandbox row; exactly one + // transaction may publish revision 1 and its matching operation fence. + let (left_result, right_result) = tokio::join!( + first.begin_sandbox_image_update("sandbox-id", version, 0, &left), + second.begin_sandbox_image_update("sandbox-id", version, 0, &right), + ); + let (accepted, winning_intent) = match (left_result, right_result) { + (Ok(accepted), Err(PersistenceError::Conflict { .. })) => (accepted, left), + (Err(PersistenceError::Conflict { .. }), Ok(accepted)) => (accepted, right), + results => panic!("expected one accepted image update and one conflict: {results:?}"), + }; + let visible = second + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(); + assert_eq!(visible, accepted); + assert_eq!(resource_version(&visible), version + 1); + assert_eq!(image(&visible), winning_intent.image); + let pending = second + .get_sandbox_image_state("sandbox-id") + .await + .unwrap() + .unwrap(); + assert_eq!(pending.revision, 1); + assert_eq!(pending.pending, Some(winning_intent.clone())); + let operation = visible + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap(); + assert!(operation.driver_operation_pending); + assert_eq!(operation.driver_operation_id, winning_intent.operation_id); + + let mut stale_start = original; + stale_start.status.as_mut().unwrap().phase = SandboxPhase::Starting as i32; + assert!(matches!( + second + .put_if( + "sandbox", + "sandbox-id", + "sandbox-name", + "default", + &stale_start.encode_to_vec(), + None, + WriteCondition::MatchResourceVersion(version), + ) + .await, + Err(PersistenceError::Conflict { .. }) + )); + + // Model an old gateway's ordinary unrelated Sandbox-only SQL rewrite. + // All provisioning fields used here are baseline-known. The private + // state has no public Sandbox field for this roundtrip to overwrite. + let mut legacy_rewrite = Sandbox::decode(visible.encode_to_vec().as_slice()).unwrap(); + legacy_rewrite + .metadata + .as_mut() + .unwrap() + .annotations + .insert( + "test.openshell.dev/legacy-write".into(), + "unrelated-edit".into(), + ); + let Store::Postgres(legacy_store) = &second else { + unreachable!() + }; + let rewritten = sqlx::query( + "UPDATE objects SET payload = $2, resource_version = resource_version + 1, +updated_at_ms = $3 WHERE object_type = 'sandbox' AND id = $1 AND resource_version = $4", + ) + .bind("sandbox-id") + .bind(legacy_rewrite.encode_to_vec()) + .bind(current_time_ms()) + .bind(i64::try_from(resource_version(&visible)).unwrap()) + .execute(&legacy_store.pool) + .await + .unwrap(); + assert_eq!(rewritten.rows_affected(), 1); + assert_eq!( + first.get_sandbox_image_state("sandbox-id").await.unwrap(), + Some(pending) + ); + let rewritten = first + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(); + assert_eq!(resource_version(&rewritten), resource_version(&visible) + 1); + let operation = rewritten + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap(); + assert!(operation.driver_operation_pending); + assert_eq!(operation.driver_operation_id, winning_intent.operation_id); + let private_identity = sqlx::query_as::<_, (Option, String)>( + "SELECT name, workspace FROM objects WHERE object_type = $1 AND id = $2", + ) + .bind(IMAGE_STATE_OBJECT_TYPE) + .bind(state_id("sandbox-id")) + .fetch_one(&legacy_store.pool) + .await + .unwrap(); + assert_eq!(private_identity, (None, String::new())); + + let completed = first + .complete_sandbox_image_update("sandbox-id", &winning_intent) + .await + .unwrap(); + assert_eq!( + resource_version(&completed), + resource_version(&rewritten) + 1 + ); + assert!(completed.status.as_ref().unwrap().provisioning.is_none()); + // A second replica finishing the same reconciliation gets the current + // object without a conflict or a second resource-version increment. + assert_eq!( + second + .complete_sandbox_image_update("sandbox-id", &winning_intent) + .await + .unwrap(), + completed + ); + let retained = second + .get_sandbox_image_state("sandbox-id") + .await + .unwrap() + .unwrap(); + assert_eq!(retained.revision, 1); + assert!(retained.pending.is_none()); + assert!(retained.previous_provisioning.is_none()); + + let next = StoredSandboxImageUpdate { + operation_id: "next-generation-operation".into(), + previous_image: winning_intent.image.clone(), + image: "next-generation-image".into(), + revision: 2, + ..Default::default() + }; + let accepted_next = second + .begin_sandbox_image_update("sandbox-id", resource_version(&completed), 1, &next) + .await + .unwrap(); + assert_eq!(image(&accepted_next), next.image); + assert!(matches!( + first + .complete_sandbox_image_update("sandbox-id", &winning_intent) + .await, + Err(PersistenceError::Conflict { .. }) + )); + let still_pending = first + .get_sandbox_image_state("sandbox-id") + .await + .unwrap() + .unwrap(); + assert_eq!(still_pending.revision, 2); + assert_eq!(still_pending.pending, Some(next.clone())); + assert_eq!( + first + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(), + accepted_next + ); + + let final_sandbox = first + .complete_sandbox_image_update("sandbox-id", &next) + .await + .unwrap(); + assert_eq!( + resource_version(&final_sandbox), + resource_version(&accepted_next) + 1 + ); + assert!( + final_sandbox + .status + .as_ref() + .unwrap() + .provisioning + .is_none() + ); + assert_eq!( + second + .get_message::("sandbox-id") + .await + .unwrap() + .unwrap(), + final_sandbox + ); + let retained = second + .get_sandbox_image_state("sandbox-id") + .await + .unwrap() + .unwrap(); + assert_eq!(retained.revision, 2); + assert!(retained.pending.is_none()); + assert!(retained.previous_provisioning.is_none()); + // Completion also invalidates a Start CAS based on the pending snapshot. + assert!(matches!( + second + .put_if( + "sandbox", + "sandbox-id", + "sandbox-name", + "default", + &accepted_next.encode_to_vec(), + None, + WriteCondition::MatchResourceVersion(resource_version(&accepted_next)), + ) + .await, + Err(PersistenceError::Conflict { .. }) + )); + + first.close().await; + second.close().await; + schema.drop_schema().await; + } +} diff --git a/crates/openshell-server/src/persistence/sqlite.rs b/crates/openshell-server/src/persistence/sqlite.rs index 94d52a67f2..8c434dcaa4 100644 --- a/crates/openshell-server/src/persistence/sqlite.rs +++ b/crates/openshell-server/src/persistence/sqlite.rs @@ -48,7 +48,7 @@ pub const DEFAULT_MAX_CONNECTIONS: u32 = 5; #[derive(Debug, Clone)] pub struct SqliteStore { - pool: SqlitePool, + pub(super) pool: SqlitePool, /// Pool for writes whose loss after a crash is harmless; see /// [`SqliteStore::create_relaxed`]. On-disk stores open it with /// `synchronous=NORMAL`; in-memory stores share `pool`. diff --git a/crates/openshell-server/src/reflection.rs b/crates/openshell-server/src/reflection.rs index 140b53659e..d8eb12482e 100644 --- a/crates/openshell-server/src/reflection.rs +++ b/crates/openshell-server/src/reflection.rs @@ -28,7 +28,7 @@ use openshell_core::Config; use crate::multiplex::GrpcRateLimiter; const REFLECTED_PROTO_ROOTS: &[&str] = &["openshell.proto"]; -const ADVERTISED_SERVICES: &[&str] = &["openshell.v1.OpenShell"]; +const ADVERTISED_SERVICES: &[&str] = &["openshell.v1.OpenShell", "openshell.v1.SandboxUpdates"]; pub type GatewayReflectionServer = ServerReflectionServer; diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index 49d158637a..08a2141884 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -15,6 +15,11 @@ include!(concat!(env!("OUT_DIR"), "/openshell.storage.v1.rs")); +mod sandbox_image { + include!(concat!(env!("OUT_DIR"), "/openshell.storage.image.v1.rs")); +} +pub(crate) use sandbox_image::{StoredSandboxImageState, StoredSandboxImageUpdate}; + pub(crate) const STORAGE_FILE_DESCRIPTOR_SET: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/storage_descriptor.bin")); @@ -163,12 +168,21 @@ mod tests { // and SUSPENDED values to the shared // SandboxPhase enum. Stored rows never carried those values, so prior // payloads decode unchanged. + + // Image updates add a separate public service/request and a new private + // image-state format. Existing public status and frozen storage.v1 stay + // unchanged; only the private durable closure grows. + // Configuration updates add presence-aware argv/map wrappers shared with + // private durable intent. Existing image-only payloads decode with all four + // selections absent; storage.v1 remains frozen. Public field 3 retains its + // image wire identity, now with explicit presence. Compute configuration + // updates use a separate capability/service and add one classified RPC. const PUBLIC_RPC_SCHEMA_SHA256: &str = - "f24c9b77a85f59ae9ab60f783d45654d78bf566fcf260f9131f1aba39fa4b59b"; + "be99bb3fcc7894eb85d774f54b642173e70dd37bc8e5fcaf3fdff2454ae689e1"; const DURABLE_SCHEMA_SHA256: &str = - "ffb20595e36032a9476d1c6956e2dcdc814f85286fc4735751e5ec6f499ea9ed"; + "cb15f8f79a855064a43fe8c6cd908bf07eca6604e2bf8ab7d2de0ef33e97da4e"; const PUBLIC_DURABLE_OVERLAP_SHA256: &str = - "ac1ffc4b5b81467de27270a4e33f6db1e31eeb2c52f79750dbaaf19515eadbb2"; + "c0c3ea17f105aaa6d843c466fd4240451c2105dce7675384935d4aaecdac8abf"; // Encoded with the schema that omitted stable_placeholder. Keep these // bytes fixed so the current encoder cannot hide a compatibility change. const LEGACY_STATIC_PROFILE: &str = "0a240a116c65676163792d70726f66696c652d6964120d6c65676163792d737461746963280712510a0d6c65676163792d73746174696312184c6567616379207374617469632063726564656e7469616c2a260a076170695f6b65791a1153594e5448455449435f4150495f4b455920012a06626561726572"; @@ -229,6 +243,7 @@ mod tests { ".openshell.storage.v1.StoredConfigUpdateOperation", ".openshell.storage.v1.StoredProviderCredentialRefreshStateV2", ".openshell.storage.v1.StoredProviderProfile", + ".openshell.storage.image.v1.StoredSandboxImageState", ".openshell.v1.Sandbox", ".openshell.v1.SandboxWorkloadTemplate", ".openshell.v1.ServiceEndpoint", @@ -471,7 +486,12 @@ mod tests { let public = FileDescriptorSet::decode(openshell_core::FILE_DESCRIPTOR_SET) .expect("public descriptor set must decode"); for file in public.file { - assert_ne!(file.package.as_deref(), Some("openshell.storage.v1")); + assert!( + !file + .package + .as_deref() + .is_some_and(|package| package.starts_with("openshell.storage.")) + ); for message in file.message_type { let name = message.name.expect("message name"); assert!( @@ -576,7 +596,8 @@ mod tests { } if !matches!( (package, service_name), - ("openshell.v1", "OpenShell") | ("openshell.inference.v1", "Inference") + ("openshell.v1", "OpenShell" | "SandboxUpdates") + | ("openshell.inference.v1", "Inference") ) { continue; } @@ -631,7 +652,7 @@ mod tests { } assert_eq!( compiled_method_count, - 106 + PROVIDER_READINESS_RPC_SIGNATURES.len() + 108 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len() + PEER_CONFIG_RPC_SIGNATURES.len() + isolation_methods.len() @@ -641,7 +662,7 @@ mod tests { ); assert_eq!( methods.len(), - 80 + PROVIDER_READINESS_RPC_SIGNATURES.len() + 81 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len() + PEER_CONFIG_RPC_SIGNATURES.len(), "inventory every public gateway RPC" @@ -702,9 +723,9 @@ mod tests { overlap_hash.as_str(), ), ( - (330, 31), - (95, 22), - (83, 22), + (334, 31), + (100, 22), + (86, 22), PUBLIC_RPC_SCHEMA_SHA256, DURABLE_SCHEMA_SHA256, PUBLIC_DURABLE_OVERLAP_SHA256 diff --git a/crates/openshell-server/src/supervisor_pool.rs b/crates/openshell-server/src/supervisor_pool.rs index 459deb0d08..cd2dc478bc 100644 --- a/crates/openshell-server/src/supervisor_pool.rs +++ b/crates/openshell-server/src/supervisor_pool.rs @@ -4810,7 +4810,11 @@ mod tests { let launching = fixture.replica("gw-b"); let (pass, launched) = tokio::join!( pool.reconcile_once(), - launching.place_launch(launch("rt-route-new0", "sandbox-new", "generation-1")), + Box::pin(launching.place_launch(launch( + "rt-route-new0", + "sandbox-new", + "generation-1" + ))), ); pass.unwrap(); // Every route is re-adopted by the new process at the next term, diff --git a/crates/openshell-server/src/test_support.rs b/crates/openshell-server/src/test_support.rs index 454af49d8c..e12bd617a9 100644 --- a/crates/openshell-server/src/test_support.rs +++ b/crates/openshell-server/src/test_support.rs @@ -105,6 +105,12 @@ pub enum FakeComputeDriverCall { CreateSandbox { sandbox: Option, }, + UpdateSandboxConfiguration { + request: openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest, + }, + UpdateSandboxImage { + request: openshell_core::proto::compute::v1::SandboxUpdatePreconditions, + }, StopSandbox { sandbox_id: String, sandbox_name: String, @@ -140,6 +146,9 @@ struct FakeComputeDriverState { sandboxes: HashMap, calls: Vec, traceparents: Vec, + image_update_error: Option, + image_validation_error: Option, + image_update_pause: Option<(Arc, Arc)>, } impl Default for FakeComputeDriver { @@ -183,6 +192,9 @@ impl FakeComputeDriver { sandboxes: HashMap::new(), calls: Vec::new(), traceparents: Vec::new(), + image_update_error: None, + image_validation_error: None, + image_update_pause: None, })), } } @@ -225,6 +237,22 @@ impl FakeComputeDriver { self } + #[must_use] + pub fn with_sandbox_image_updates(self) -> Self { + self.with_state(|state| { + state + .capabilities + .extension + .as_mut() + .unwrap() + .supported_capabilities + .push( + openshell_core::extension_protocol::COMPUTE_UPDATE_SANDBOX_CONFIGURATION.into(), + ); + }); + self + } + #[must_use] pub fn calls(&self) -> Vec { self.with_state(|state| state.calls.clone()) @@ -253,13 +281,31 @@ impl FakeComputeDriver { let driver = self.clone(); let task = tokio::spawn(async move { tonic::transport::Server::builder() - .add_service(ComputeDriverServer::new(driver)) + .add_service(ComputeDriverServer::new(driver.clone())) + .add_service(openshell_core::proto::compute::v1::compute_driver_configuration_updates_server::ComputeDriverConfigurationUpdatesServer::new(driver.clone())) .serve_with_incoming(UnixIncoming { listener }) .await }); Ok(FakeComputeDriverServerHandle { socket_path, task }) } + pub fn pause_image_updates(&self) -> (Arc, Arc) { + let entered = Arc::new(tokio::sync::Notify::new()); + let release = Arc::new(tokio::sync::Semaphore::new(0)); + self.with_state(|state| { + state.image_update_pause = Some((entered.clone(), release.clone())); + }); + (entered, release) + } + + pub fn set_image_update_error(&self, code: Option) { + self.with_state(|state| state.image_update_error = code); + } + + pub fn set_image_validation_error(&self, code: Option) { + self.with_state(|state| state.image_validation_error = code); + } + fn with_state(&self, f: impl FnOnce(&mut FakeComputeDriverState) -> R) -> R { let mut state = self .state @@ -310,6 +356,50 @@ impl Stream for UnixIncoming { } } +#[tonic::async_trait] +impl openshell_core::compute::ComputeDriver for FakeComputeDriver { + async fn update_sandbox_configuration( + &self, + request: Request, + ) -> Result< + Response, + Status, + > { + let pause = self.with_state(|state| state.image_update_pause.clone()); + if let Some((entered, release)) = pause { + entered.notify_one(); + release.acquire().await.unwrap().forget(); + } + self.record_traceparent(request.metadata()); + self.with_state(|state| { + let request = request.into_inner(); + let validate_only = request.preconditions.as_ref().unwrap().validate_only; + state.calls.push(FakeComputeDriverCall::UpdateSandboxImage { + request: request.preconditions.clone().unwrap(), + }); + state + .calls + .push(FakeComputeDriverCall::UpdateSandboxConfiguration { request }); + if let Some(code) = state.image_validation_error.filter(|_| validate_only) { + return Err(Status::new(code, "injected configuration validation error")); + } + if let Some(code) = state.image_update_error.filter(|_| !validate_only) { + return Err(Status::new(code, "injected configuration update error")); + } + Ok(Response::new( + openshell_core::proto::compute::v1::UpdateSandboxConfigurationResponse {}, + )) + }) + } +} + +#[tonic::async_trait] +impl openshell_core::proto::compute::v1::compute_driver_configuration_updates_server::ComputeDriverConfigurationUpdates for FakeComputeDriver { + async fn update_sandbox_configuration(&self, request: Request) -> Result, Status> { + openshell_core::compute::ComputeDriver::update_sandbox_configuration(self, request).await + } +} + #[tonic::async_trait] impl ComputeDriver for FakeComputeDriver { async fn authenticate_sandbox( diff --git a/deploy/helm/openshell-workspace/templates/role.yaml b/deploy/helm/openshell-workspace/templates/role.yaml index 25d7b4f72e..0d80ecccde 100644 --- a/deploy/helm/openshell-workspace/templates/role.yaml +++ b/deploy/helm/openshell-workspace/templates/role.yaml @@ -42,7 +42,20 @@ rules: resources: - pods verbs: + - create + - delete - get + # Updates must rule out every remaining workload Pod, including Pods + # identified only by owner UID or the controller's recorded name. + - list + - patch + - watch + - apiGroups: [""] + resources: ["services"] + verbs: ["create", "get"] + - apiGroups: ["networking.k8s.io"] + resources: ["networkpolicies"] + verbs: ["create", "get"] # Generation-scoped sandbox bootstrap Secrets. - apiGroups: - "" diff --git a/deploy/helm/openshell-workspace/tests/workspace_test.yaml b/deploy/helm/openshell-workspace/tests/workspace_test.yaml index d4d3489f01..d3270cea27 100644 --- a/deploy/helm/openshell-workspace/tests/workspace_test.yaml +++ b/deploy/helm/openshell-workspace/tests/workspace_test.yaml @@ -12,6 +12,28 @@ release: namespace: app-a tests: + - it: grants supervisor lifecycle and complete stopped-update pod inspection + template: templates/role.yaml + asserts: + - contains: + path: rules + content: + apiGroups: [""] + resources: ["pods"] + verbs: ["create", "delete", "get", "list", "patch", "watch"] + - contains: + path: rules + content: + apiGroups: [""] + resources: ["services"] + verbs: ["create", "get"] + - contains: + path: rules + content: + apiGroups: ["networking.k8s.io"] + resources: ["networkpolicies"] + verbs: ["create", "get"] + - it: creates the workspace role in the release namespace template: templates/role.yaml asserts: diff --git a/docs/how-it-works/gateways/configuration.mdx b/docs/how-it-works/gateways/configuration.mdx index ef49f8ec86..a58f5552da 100644 --- a/docs/how-it-works/gateways/configuration.mdx +++ b/docs/how-it-works/gateways/configuration.mdx @@ -255,6 +255,9 @@ metrics_bind_address = "0.0.0.0:9090" log_level = "info" +# Activate only after retiring incompatible gateways and outstanding driver work. +enable_sandbox_updates = false + # Connection ceiling for the gateway's database pool. Omit to use the backend # default: 10 for Postgres, 5 for on-disk SQLite. database_max_connections = 10 @@ -499,6 +502,46 @@ Changing the installation name, the hostname format, or both changes every hash URL issued before the change; previously issued URLs stop resolving until callers request a fresh URL. +## Sandbox Updates + +`[openshell.gateway] enable_sandbox_updates` defaults to `false`. It gates +the separate `openshell.v1.SandboxUpdates/UpdateSandbox` RPC on the existing +gateway listener. It gates image, command, environment, label, and annotation +updates for stopped sandboxes. Upgrade every gateway before activation. +Disabling the flag stops new updates; accepted updates still recover automatically. +The equivalent gateway process flag and environment variable +are `--enable-sandbox-updates true` and +`OPENSHELL_ENABLE_SANDBOX_UPDATES=true`. + +Enable it only after completing the +[activation checklist](/kubernetes/high-availability#activate-sandbox-updates). +Setting it to `true` attests that every gateway that can write the database is +compatible and incompatible gateways cannot return. Keep it false throughout a +rolling upgrade, including while outstanding operations from retired gateways +settle. Gateway membership checks fail closed but do not prove retirement. PostgreSQL +administrative and monitoring sessions do not block updates; connection +`application_name` settings are preserved. + +For a schema-v2 file, set: + +```toml +[openshell.gateway] +enable_sandbox_updates = true +``` + +The canonical Helm values form uses a dotted table key: + +```yaml +gatewayConfig: + "openshell.gateway": + enable_sandbox_updates: true +``` + +This is not an automatic mixed-version migration switch. Do not downgrade while +an image update is pending or retained sandboxes require the private image +revision protocol. SQLite supports one gateway process only; activation attests +that the previous process has stopped, not that sharing a SQLite file enables HA. + ## Database Pool `[openshell.gateway] database_max_connections` sets the connection ceiling for the gateway's database pool. One pool is shared by every database-backed RPC, so this value is also the gateway's ceiling on concurrent database work. When the pool is saturated, callers queue on connection acquisition and sqlx logs that the time to acquire a connection exceeded its slow threshold. The background readiness monitor draws from the same pool, so sustained saturation can push its database ping past the check timeout and flip `/readyz` to not-ready while the gateway is otherwise serving. diff --git a/docs/how-it-works/sandboxes/overview.mdx b/docs/how-it-works/sandboxes/overview.mdx index aadfdbe281..a3b01280e1 100644 --- a/docs/how-it-works/sandboxes/overview.mdx +++ b/docs/how-it-works/sandboxes/overview.mdx @@ -1006,10 +1006,130 @@ command. Starting a fresh instance invalidates SSH sessions issued for the previous runtime generation. Delete an inactive sandbox normally when you no longer need its retained state. +To change the configuration of a retained `Completed` or `Error/MainProcessFailed` +sandbox, run `sandbox stop` first. Stop cleans up compute and moves the sandbox +to `Stopped`, preserving its exit code until the next start. This stop behavior +also applies when sandbox updates are disabled. + +If the terminal sandbox's compute resource is already gone, stop succeeds and +preserves its terminal phase and exit code. Configuration updates require a +retained compute resource. + If an infrastructure failure leaves a sandbox in `Error`, fix the reported cause, then run `sandbox stop` followed by `sandbox start`. Wait for stop to succeed before starting. This retains the sandbox and its persistent workspace on backends that support restarting stopped sandboxes; it cannot recover deleted resources or storage. For a provisioning timeout, wait for cleanup to finish, then run `sandbox start` directly. +### Update the Image of a Stopped Sandbox + +Use `openshell.v1.SandboxUpdates/UpdateSandbox` to change a stopped sandbox's +workload image, command, environment overrides, labels, and annotations together. This separate service uses the same gateway listener +and authentication as `OpenShell`; it requires `sandbox:write` and workspace +user access. This operation initially supports the Kubernetes compute driver +for workload changes; metadata-only updates work with every compute driver. There is no +`sandbox update` CLI command or curated SDK update method. + +Image updates default to disabled. Ask the operator to complete the +[compatible deployment rollout](/kubernetes/high-availability#activate-sandbox-updates) +before enabling `enable_sandbox_updates` in the +[gateway configuration](/how-it-works/gateways/configuration#sandbox-updates). + +The update preserves the sandbox ID, Kubernetes custom resource, and persistent +volume claim. It does not delete and recreate the sandbox, reseed its existing +workspace from the new image, or change the trusted supervisor and sandbox +runtime images. Omitted fields retain their current values. A supplied command +replaces the argv list without shell expansion; an empty list restores the default +login shell resolved from the workload image. The Kubernetes driver rejects an +update before acceptance if the encoded command and other custom-resource +annotations exceed Kubernetes' combined 256 KiB annotation limit. +A supplied environment map replaces +sandbox-level overrides; clearing it restores any template-provided defaults. +Labels and annotations replace user metadata on the gateway resource, preserve +system-owned keys, and do not set Kubernetes pod metadata. Attempts to supply +system-owned keys are rejected. Metadata-only changes commit directly without +contacting the compute driver. The retention annotation is preserved when omitted. +Annotation changes accepted through `UpdateConfig` after the sandbox update is +accepted remain in place when the update completes, including after a retry. + +Without a pending update, a request whose target values already match is a no-op. +Use a different image tag or an immutable digest to select new image content. +Workload changes require `openshell.compute.update-sandbox-configuration`. +Upgrade every gateway before enabling updates. + +Follow this sequence through the CLI and a generated/raw RPC client. + +1. Run `openshell sandbox stop my-sandbox` and wait for `Stopped`. +2. Call `GetSandbox` with the named `workspace_scope` and sandbox `name`. Read + `sandbox.metadata.resource_version` after stop completes. +3. Call `UpdateSandbox` with the same scope and name, the selected update fields, + and `expected_resource_version` set to that current, nonzero + version. The response is a `SandboxResponse`; the sandbox remains stopped. +4. After the update succeeds, run `openshell sandbox start my-sandbox` to launch + the new image. + +With an authenticated Rust SDK `OpenShellClient`, use the separate raw client +and the `current_resource_version` read in step 2: + +```rust +use openshell_sdk::raw::{UpdateSandboxRequest, proto}; + +let mut updates = client.raw_sandbox_updates_fresh().await?; +let response = updates.update_sandbox(UpdateSandboxRequest { + workspace_scope: Some(proto::workspace_selector("default")), + name: "my-sandbox".into(), + image: Some("registry.example.com/agents/my-agent:2.0".into()), + command: Some(proto::SandboxCommandUpdate { + argv: vec!["/usr/local/bin/start-agent".into(), "--new-option".into()], + }), + environment: Some(proto::SandboxMapUpdate { + values: [("AGENT_MODE".into(), "updated".into())].into_iter().collect(), + }), + expected_resource_version: current_resource_version, + request_id: String::new(), + ..Default::default() +}).await?; +``` + +The helper shares the main client's transport and live authentication slot. +Generated clients use the `SandboxUpdates` service, not `OpenShell`. + +If the new image fails to start and provisioning times out, wait until +`provisioning.cleanup_completed_time` is present and +`provisioning.driver_operation_pending` is false. Run `sandbox stop` to move the +reclaimed sandbox from `Error` to `Stopped`, then repeat the update sequence with +a working image. Stop retains the timeout record and storage; it does not stop +compute again. This Stop behavior also applies when updates are disabled. +Cleanup that is still pending cannot be bypassed. + +An `ABORTED` version conflict requires another read before you construct a new +conditional request. The gateway limits driver preflight and each accepted update +attempt to one minute each. A timed-out preflight does not accept an update. +An accepted update continues after client cancellation until it completes or +times out. A timeout releases the sandbox for recovery or deletion; it does not +roll back an update the driver may have applied. +An update failure or disconnected response can leave a +pending operation, even when `sandbox.spec.template.image` already names the +target image. Start, stop, and provisioning retry remain blocked until reconciliation +succeeds. The gateway retries accepted updates at startup and periodically, even +if new updates are disabled. Failed automatic attempts wait 1, 2, 4, 8, then +15 minutes before another attempt becomes eligible. Recovery continues at the +15-minute limit; periodic scans can add up to a minute to the wait. Each gateway +retries independently, and restarting a gateway resets its retry delay. +If the driver is unavailable or no longer supports +updates, restore a compatible driver. Explicit deletion can reclaim a sandbox +whose update cannot complete; deletion also removes its stored workspace. + +To resume immediately, read the current resource version and call `UpdateSandbox` +with the same scope and name, that version, and no selected fields. The gateway +uses the stored operation and target values. With no pending update, this returns +`FAILED_PRECONDITION`. The operation ID is visible in +`status.provisioning.driver_operation_id` while pending; the full intent stays private. + +The optional `request_id` uses [durable request admission](/sdk/api-errors#durable-request-admission). +Reusing an unresolved ID returns `REQUEST_OUTCOME_UNCERTAIN`. For explicit resume, +use a new ID or omit it, and use the fresh resource version. Do not change the +original payload under its original ID. A confirmed successful request still +replays without applying another update. + ## Delete Sandboxes Deleting a sandbox stops all processes, releases resources, and purges injected credentials. diff --git a/docs/kubernetes/high-availability.mdx b/docs/kubernetes/high-availability.mdx index 8ab09ceca3..73f40cc6f4 100644 --- a/docs/kubernetes/high-availability.mdx +++ b/docs/kubernetes/high-availability.mdx @@ -504,6 +504,45 @@ during the upgrade, the older replicas serialize every mutation across the fleet. Mutations can be slower and can briefly fail under load until the rollout finishes. +### Activate Sandbox Updates + +Sandbox configuration updates default to disabled. Setting +`[openshell.gateway] enable_sandbox_updates = true` is an operator +attestation that the deployment uniformly supports the image-update protocol. +Use this sequence before activating the +[separate update service](/how-it-works/sandboxes/overview#update-the-image-of-a-stopped-sandbox). + +1. Keep the setting false throughout the rolling upgrade. Drain and retire + every incompatible gateway, including unregistered processes and Jobs, and + settle all outstanding driver operations they started. A completed + Kubernetes rollout alone does not establish this. +2. Prevent incompatible gateways from returning. Pin compatible images and + retire old rollback targets, Jobs, and other launch configurations that could + reconnect to the database. +3. Confirm live gateway membership records advertise sandbox-update protocol 1. + Missing or incompatible capabilities, malformed records, and truncated + membership reads reject updates. These observations are not durable proof + of fleet compatibility. Expiry or absence of a membership record does not + prove that its process and driver work have stopped. +4. Activate the flag only after completing retirement and preventing old + gateways from returning. See the exact + [TOML, environment, process flag, and Helm forms](/how-it-works/gateways/configuration#sandbox-updates). + +A pending image update atomically sets the existing +`provisioning.driver_operation_pending` and `provisioning.driver_operation_id` +fields alongside private image intent. Start, stop, and provisioning-retry paths honor this per-sandbox barrier, including after a +legacy public record round-trip. Successful reconciliation preserves current +provisioning diagnostics and clocks, clears the pending flag, and retains the +new operation ID to fence delayed callbacks. Previously absent provisioning +remains absent unless concurrent writes added diagnostics or clocks. This +barrier does not replace the deployment activation checklist. +Image intent and revision are not new public `SandboxStatus` fields. + +Do not downgrade while updates are pending or retained sandboxes require the +private revision protocol. SQLite supports one gateway process only. For a +SQLite gateway, activation attests that the old process has stopped; a shared +SQLite file is not an HA deployment. + ## (Experimental) Multi-tenant supervisor By default, the Kubernetes driver runs a supervisor Pod for each sandbox. With diff --git a/docs/kubernetes/setup.mdx b/docs/kubernetes/setup.mdx index 565a8f101d..bbc223e32f 100644 --- a/docs/kubernetes/setup.mdx +++ b/docs/kubernetes/setup.mdx @@ -409,13 +409,19 @@ managed mode, the configured image-pull Secrets. The gateway stages their contents into each sandbox runtime generation's Secrets in the workspace namespace. -The namespaced Role covers sandbox lifecycle and identity: +The namespaced Role covers sandbox lifecycle, supervisor resources, and identity. +Both the gateway chart and separate workspace chart grant these permissions. +Stopped-sandbox updates use direct Pod `get` requests to check known workload and +supervisor Pod names for remaining Pods. | API Group | Resource | Verbs | |---|---|---| | `agents.x-k8s.io` | `sandboxes`, `sandboxes/status` | create, delete, get, list, patch, update, watch | | `""` | `events` | get, list, watch | -| `""` | `pods` | get | +| `""` | `pods` | create, delete, get, list, patch, watch | +| `""` | `services` | create, get | +| `""` | `secrets` | create, delete | +| `networking.k8s.io` | `networkpolicies` | create, get | The ClusterRole grants node inspection and token validation: diff --git a/docs/sdk/api-errors.mdx b/docs/sdk/api-errors.mdx index 23a34c3b96..5d45369e68 100644 --- a/docs/sdk/api-errors.mdx +++ b/docs/sdk/api-errors.mdx @@ -56,7 +56,7 @@ guarantee. | `ABORTED` | Read fresh state before retrying a conditional operation. | | `UNAVAILABLE`, `RESOURCE_EXHAUSTED` | A transient condition may recover. Apply backoff and any minimum retry delay only to a retry-safe operation. | | `DEADLINE_EXCEEDED`, `CANCELLED` | A mutation's outcome can be unknown. Cancellation does not imply rollback. | -| `UNIMPLEMENTED` | Check gateway and SDK version compatibility. | +| `UNIMPLEMENTED` | Check gateway and SDK version compatibility and whether the compute driver supports the operation. | | `INTERNAL`, `UNKNOWN`, `DATA_LOSS` | Preserve the status and correlation metadata for diagnosis. Do not blindly retry mutations. | ## SDK access @@ -92,19 +92,23 @@ No SDK automatically retries a mutation as a result of decoding these details. ## Durable request admission -Thirty user-callable unary RPCs accept an optional `request_id` in their protobuf requests. +Thirty-one user-callable unary RPCs accept an optional `request_id` in their protobuf requests. | Resource | RPCs | |---|---| | Workspace | `CreateWorkspace`, `DeleteWorkspace` | | Workspace member | `AddWorkspaceMember`, `RemoveWorkspaceMember` | | Sandbox template | `CreateSandboxTemplate`, `DeleteSandboxTemplate` | -| Sandbox | `CreateSandbox`, `DeleteSandbox`, `StartSandbox`, `StopSandbox`, `AttachSandboxProvider`, `DetachSandboxProvider` | +| Sandbox | `CreateSandbox`, `UpdateSandbox`, `DeleteSandbox`, `StartSandbox`, `StopSandbox`, `AttachSandboxProvider`, `DetachSandboxProvider` | | Service | `ExposeService`, `DeleteService` | | Provider | `CreateProvider`, `UpdateProvider`, `DeleteProvider`, `ConfigureProviderRefresh`, `DeleteProviderRefresh`, `RotateProviderCredential` | | Provider profile | `ImportProviderProfiles`, `UpdateProviderProfiles`, `DeleteProviderProfile` | | Policy and config | `UpdateConfig`, `ApproveDraftChunk`, `RejectDraftChunk`, `EditDraftChunk`, `UndoDraftChunk`, `ApproveAllDraftChunks`, `ClearDraftChunks` | +`UpdateSandbox` belongs to the separate `openshell.v1.SandboxUpdates` service +on the same authenticated gateway listener. The other RPCs above remain on +`openshell.v1.OpenShell`. + Use a nonzero, hyphenated UUID of exactly 36 characters. Uppercase and lowercase forms identify the same request. An empty ID preserves the existing behavior without deduplication. This ID is separate from transport correlation metadata. @@ -192,6 +196,35 @@ from this feature. Sandbox principals cannot opt into `UpdateConfig` admission. Check gateway compatibility before relying on IDs; older protobuf servers can silently ignore unknown fields. +### Sandbox image update recovery + +`SandboxUpdates.UpdateSandbox` conditionally updates image, command, environment overrides, labels, and annotations for stopped +Kubernetes sandboxes. It defaults to disabled and returns `FAILED_PRECONDITION` +until the operator [activates a compatible deployment](/kubernetes/high-availability#activate-sandbox-updates). +After activation, unsupported workload changes return `UNIMPLEMENTED`; metadata-only +updates do not require driver support. For a new update, supply a named +`workspace_scope`, sandbox `name`, at least one selected update field, and a required nonzero +`expected_resource_version` read from `sandbox.metadata.resource_version`. +A stale version returns `ABORTED`; read fresh state before constructing another +conditional request. The operation does not start compute. + +A failed or interrupted update can leave a pending operation. The desired spec +alone does not prove driver reconciliation completed. Start, stop, and provisioning +retry return `FAILED_PRECONDITION` until completion. Recovery runs automatically, +including when new updates are disabled. To resume immediately, read the current +resource version and send `UpdateSandbox` with no selected fields. The gateway +loads the stored intent; no pending intent returns `FAILED_PRECONDITION`. +Explicit delete can reclaim a sandbox whose update cannot complete. + +If the original request supplied `request_id`, an error leaves its admission +unresolved. Reusing that ID returns `REQUEST_OUTCOME_UNCERTAIN`; changing its +payload returns `REQUEST_ID_PAYLOAD_MISMATCH`. For explicit resume, use the fresh +version with a new ID or no ID. A confirmed success replays the current sandbox +without applying another update. + +See [Update the Image of a Stopped Sandbox](/how-it-works/sandboxes/overview#update-the-image-of-a-stopped-sandbox) +for the stop, update, and start workflow and storage guarantees. + ## Exec launch admission `ExecSandbox` and `ExecSandboxInteractive` accept an optional `request_id` in diff --git a/e2e/rust/e2e-kubernetes.sh b/e2e/rust/e2e-kubernetes.sh index 1ffa79cea5..9bdcf4e6a2 100755 --- a/e2e/rust/e2e-kubernetes.sh +++ b/e2e/rust/e2e-kubernetes.sh @@ -98,12 +98,22 @@ run_suite() { } run_e2e() { + if [ "${OPENSHELL_E2E_KUBE_TEST:-}" = "kubernetes_image_update" ]; then + cargo test --locked --manifest-path "${ROOT}/tests/suites/features/Cargo.toml" \ + -p openshell-test-feature-kubernetes-updates -- --nocapture + return + fi run_conformance if [ "${E2E_FEATURES}" = "e2e-conformance-only" ]; then echo "::notice::Skipping Rust e2e tests (e2e-conformance-only)" return 0 fi + if [ -z "${OPENSHELL_E2E_KUBE_TEST:-}" ]; then + cargo test --locked --manifest-path "${ROOT}/tests/suites/features/Cargo.toml" \ + -p openshell-test-feature-kubernetes-updates -- --nocapture + fi + # Pin `--target-dir` so the profile `junit.path` resolves regardless of `CARGO_TARGET_DIR`. cargo nextest run --profile e2e-kubernetes \ --config-file "${ROOT}/.config/nextest.toml" \ diff --git a/e2e/with-kube-gateway.sh b/e2e/with-kube-gateway.sh index 9ee8eec244..31c0771dc8 100755 --- a/e2e/with-kube-gateway.sh +++ b/e2e/with-kube-gateway.sh @@ -1319,6 +1319,12 @@ helm_extra_args+=(--set supervisor.sandboxRuntime.networkPolicyEnforced=true) if [ "${ADDITIONAL_CA_MODE}" = "1" ]; then helm_extra_args+=(--set "supervisor.network.additionalCaConfigMapName=${ADDITIONAL_CA_SOURCE_CONFIG_MAP}") fi +# This wrapper installs an isolated, uniformly built test gateway, not a +# production rolling upgrade. Explicitly activate the image-update scenario; +# callers can keep activation disabled for negative coverage. +if [ "${OPENSHELL_E2E_KUBE_IMAGE_UPDATES:-1}" = "1" ]; then + helm_extra_args+=(--set 'gatewayConfig.openshell\.gateway.enable_sandbox_updates=true') +fi if [ "${OPENSHELL_E2E_EXTERNAL_COMPUTE_DRIVER:-0}" = "1" ]; then if [ "${OPENSHELL_E2E_KUBE_BUILD_IMAGES}" != "1" ]; then echo "ERROR: external Kubernetes driver e2e requires OPENSHELL_E2E_KUBE_BUILD_IMAGES=1." >&2 diff --git a/proto/compute_driver.proto b/proto/compute_driver.proto index 2eba7ef2af..e50f41d0ee 100644 --- a/proto/compute_driver.proto +++ b/proto/compute_driver.proto @@ -74,6 +74,35 @@ service ComputeDriver { rpc DeleteWorkspace(DeleteWorkspaceRequest) returns (DeleteWorkspaceResponse); } +// Optional stopped-sandbox configuration update extension on the same endpoint. +// Baseline generated driver traits remain unchanged. Negotiate +// openshell.compute.update-sandbox-configuration before calling this service. +service ComputeDriverConfigurationUpdates { + rpc UpdateSandboxConfiguration(UpdateSandboxConfigurationRequest) + returns (UpdateSandboxConfigurationResponse); +} + +message UpdateSandboxConfigurationRequest { + // Shared lifecycle, image, revision and runtime identity preconditions. + SandboxUpdatePreconditions preconditions = 1; + // Absent leaves the command unchanged; empty restores the default shell. + SandboxMainProcessUpdate main_process = 2; + // Effective user environment after template inheritance. Presence replaces it. + SandboxEnvironmentUpdate environment = 3; +} + +// Only workload argv and its existing TTY setting are carried to the driver. +message SandboxMainProcessUpdate { + repeated string command = 1; + bool tty = 2; +} + +message SandboxEnvironmentUpdate { + map values = 1; +} + +message UpdateSandboxConfigurationResponse {} + message GetCapabilitiesRequest { // Gateway protocol metadata. Drivers must reject unmet requirements. openshell.extension.v1.PeerMetadata gateway = 1; @@ -441,6 +470,26 @@ message CreateSandboxResponse { string runtime_identity = 1; } +message SandboxUpdatePreconditions { + string sandbox_id = 1; + string name = 2; + string image = 3; + // Pre-update image. A different current image conflicts unless durable driver + // evidence identifies this exact operation as already applied. + string expected_image = 4; + // Never adopt a same-name replacement resource. + string expected_runtime_identity = 5; + // Stable UUID across retries, recorded atomically with the image change. + string operation_id = 6; + // Perform all admission and state checks without writing any resource. The + // gateway uses this before recording durable intent; apply rechecks them. + bool validate_only = 7; + // Monotonic sandbox image revision. The driver atomically records it with + // the image and rejects earlier revisions even if expected_image matches. + uint64 revision = 8; +} + + message StopSandboxRequest { // Stable sandbox ID stored by the gateway. string sandbox_id = 1; diff --git a/proto/openshell.proto b/proto/openshell.proto index 1be9dc7da8..e498a84af9 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -858,6 +858,22 @@ service OpenShell { } } +// Optional sandbox mutations are separate from OpenShell so existing generated +// OpenShell server implementations remain source-compatible. +service SandboxUpdates { + // Update the workload image, command, environment, labels, or annotations of a + // stopped sandbox while preserving its identity and retained storage. Requires + // compute-driver support for the selected changes and explicit gateway activation + // of sandbox updates. + rpc UpdateSandbox(UpdateSandboxRequest) returns (SandboxResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } +} + // IssueSandboxToken request. Empty body; identity is established by the // authentication credentials carried in the request headers (a projected // Kubernetes ServiceAccount JWT in the K8s driver path). @@ -1330,6 +1346,40 @@ message CreateSandboxRequest { repeated SandboxServiceExposure service_exposures = 9; } +// Update selected configuration of a stopped sandbox. Omitted fields are unchanged. +// Start explicitly after a successful update. With no fields selected, resume +// the pending operation stored by the gateway using the current resource version. +message UpdateSandboxRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 1; + // Canonical name of the existing sandbox; this is not a rename or upsert. + string name = 2; + // Nonempty OCI workload image reference. This does not change trusted runtime + // images or reseed an existing workspace. + optional string image = 3; + // Replace workload argv; an empty list restores image-resolved default shell behavior. + SandboxCommandUpdate command = 6; + // Replace sandbox-level overrides. Empty clears overrides; template defaults remain. + SandboxMapUpdate environment = 7; + // Replace user metadata, preserving gateway-owned keys. These do not set pod metadata. + SandboxMapUpdate labels = 8; + SandboxMapUpdate annotations = 9; + // Required nonzero resource version read by the caller. + uint64 expected_resource_version = 4; + // Optional nonzero UUID for durable at-most-once admission. + string request_id = 5; +} + +// Presence selects replacement, including an explicitly empty list. +message SandboxCommandUpdate { + repeated string argv = 1; +} + +// Presence selects replacement, including an explicitly empty map. +message SandboxMapUpdate { + map values = 1; +} + message CreateSandboxTemplateRequest { // Workspace scope. Only a named workspace selection is accepted. openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; diff --git a/sdk/go/openshell/v1/internal/converter/coverage_test.go b/sdk/go/openshell/v1/internal/converter/coverage_test.go index 785e12c9c6..8a457d0014 100644 --- a/sdk/go/openshell/v1/internal/converter/coverage_test.go +++ b/sdk/go/openshell/v1/internal/converter/coverage_test.go @@ -135,7 +135,9 @@ func TestConverterCoversAllProtoFields_SandboxStatus(t *testing.T) { // Provisioning carries gateway-owned attempt, deadline, and cleanup state; // the curated API exposes its outcome through phase and conditions. Detailed // lifecycle bookkeeping remains available through the raw protobuf API. - skipped := fieldSet{"main_process_instance_id": true, "configuration_activated": true, "provisioning": true} + // Image updates are raw-only; their pending reconciliation intent and + // monotonic image revision bookkeeping are not part of the curated status model. + skipped := fieldSet{"main_process_instance_id": true, "configuration_activated": true, "provisioning": true, "image_update": true, "image_revision": true} assertAllFieldsCovered(t, (&pb.SandboxStatus{}).ProtoReflect().Descriptor(), handled, skipped) } diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index d60dd50a68..1ae1800784 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -3695,6 +3695,216 @@ func (x *CreateSandboxRequest) GetServiceExposures() []*SandboxServiceExposure { return nil } +// Update selected configuration of a stopped sandbox. Omitted fields are unchanged. +// Start explicitly after a successful update. With no fields selected, resume +// the pending operation stored by the gateway using the current resource version. +type UpdateSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,1,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Canonical name of the existing sandbox; this is not a rename or upsert. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // Nonempty OCI workload image reference. This does not change trusted runtime + // images or reseed an existing workspace. + Image *string `protobuf:"bytes,3,opt,name=image,proto3,oneof" json:"image,omitempty"` + // Replace workload argv; an empty list restores image-resolved default shell behavior. + Command *SandboxCommandUpdate `protobuf:"bytes,6,opt,name=command,proto3" json:"command,omitempty"` + // Replace sandbox-level overrides. Empty clears overrides; template defaults remain. + Environment *SandboxMapUpdate `protobuf:"bytes,7,opt,name=environment,proto3" json:"environment,omitempty"` + // Replace user metadata, preserving gateway-owned keys. These do not set pod metadata. + Labels *SandboxMapUpdate `protobuf:"bytes,8,opt,name=labels,proto3" json:"labels,omitempty"` + Annotations *SandboxMapUpdate `protobuf:"bytes,9,opt,name=annotations,proto3" json:"annotations,omitempty"` + // Required nonzero resource version read by the caller. + ExpectedResourceVersion uint64 `protobuf:"varint,4,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. + RequestId string `protobuf:"bytes,5,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateSandboxRequest) Reset() { + *x = UpdateSandboxRequest{} + mi := &file_openshell_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateSandboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateSandboxRequest) ProtoMessage() {} + +func (x *UpdateSandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[33] + 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 UpdateSandboxRequest.ProtoReflect.Descriptor instead. +func (*UpdateSandboxRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{33} +} + +func (x *UpdateSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *UpdateSandboxRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UpdateSandboxRequest) GetImage() string { + if x != nil && x.Image != nil { + return *x.Image + } + return "" +} + +func (x *UpdateSandboxRequest) GetCommand() *SandboxCommandUpdate { + if x != nil { + return x.Command + } + return nil +} + +func (x *UpdateSandboxRequest) GetEnvironment() *SandboxMapUpdate { + if x != nil { + return x.Environment + } + return nil +} + +func (x *UpdateSandboxRequest) GetLabels() *SandboxMapUpdate { + if x != nil { + return x.Labels + } + return nil +} + +func (x *UpdateSandboxRequest) GetAnnotations() *SandboxMapUpdate { + if x != nil { + return x.Annotations + } + return nil +} + +func (x *UpdateSandboxRequest) GetExpectedResourceVersion() uint64 { + if x != nil { + return x.ExpectedResourceVersion + } + return 0 +} + +func (x *UpdateSandboxRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Presence selects replacement, including an explicitly empty list. +type SandboxCommandUpdate struct { + state protoimpl.MessageState `protogen:"open.v1"` + Argv []string `protobuf:"bytes,1,rep,name=argv,proto3" json:"argv,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxCommandUpdate) Reset() { + *x = SandboxCommandUpdate{} + mi := &file_openshell_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxCommandUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxCommandUpdate) ProtoMessage() {} + +func (x *SandboxCommandUpdate) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[34] + 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 SandboxCommandUpdate.ProtoReflect.Descriptor instead. +func (*SandboxCommandUpdate) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{34} +} + +func (x *SandboxCommandUpdate) GetArgv() []string { + if x != nil { + return x.Argv + } + return nil +} + +// Presence selects replacement, including an explicitly empty map. +type SandboxMapUpdate struct { + state protoimpl.MessageState `protogen:"open.v1"` + Values map[string]string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxMapUpdate) Reset() { + *x = SandboxMapUpdate{} + mi := &file_openshell_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxMapUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxMapUpdate) ProtoMessage() {} + +func (x *SandboxMapUpdate) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[35] + 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 SandboxMapUpdate.ProtoReflect.Descriptor instead. +func (*SandboxMapUpdate) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{35} +} + +func (x *SandboxMapUpdate) GetValues() map[string]string { + if x != nil { + return x.Values + } + return nil +} + type CreateSandboxTemplateRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Workspace scope. Only a named workspace selection is accepted. @@ -3709,7 +3919,7 @@ type CreateSandboxTemplateRequest struct { func (x *CreateSandboxTemplateRequest) Reset() { *x = CreateSandboxTemplateRequest{} - mi := &file_openshell_proto_msgTypes[33] + mi := &file_openshell_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3721,7 +3931,7 @@ func (x *CreateSandboxTemplateRequest) String() string { func (*CreateSandboxTemplateRequest) ProtoMessage() {} func (x *CreateSandboxTemplateRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[33] + mi := &file_openshell_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3734,7 +3944,7 @@ func (x *CreateSandboxTemplateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateSandboxTemplateRequest.ProtoReflect.Descriptor instead. func (*CreateSandboxTemplateRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{33} + return file_openshell_proto_rawDescGZIP(), []int{36} } func (x *CreateSandboxTemplateRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -3769,7 +3979,7 @@ type GetSandboxTemplateRequest struct { func (x *GetSandboxTemplateRequest) Reset() { *x = GetSandboxTemplateRequest{} - mi := &file_openshell_proto_msgTypes[34] + mi := &file_openshell_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3781,7 +3991,7 @@ func (x *GetSandboxTemplateRequest) String() string { func (*GetSandboxTemplateRequest) ProtoMessage() {} func (x *GetSandboxTemplateRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[34] + mi := &file_openshell_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3794,7 +4004,7 @@ func (x *GetSandboxTemplateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxTemplateRequest.ProtoReflect.Descriptor instead. func (*GetSandboxTemplateRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{34} + return file_openshell_proto_rawDescGZIP(), []int{37} } func (x *GetSandboxTemplateRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -3829,7 +4039,7 @@ type ListSandboxTemplatesRequest struct { func (x *ListSandboxTemplatesRequest) Reset() { *x = ListSandboxTemplatesRequest{} - mi := &file_openshell_proto_msgTypes[35] + mi := &file_openshell_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3841,7 +4051,7 @@ func (x *ListSandboxTemplatesRequest) String() string { func (*ListSandboxTemplatesRequest) ProtoMessage() {} func (x *ListSandboxTemplatesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[35] + mi := &file_openshell_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3854,7 +4064,7 @@ func (x *ListSandboxTemplatesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxTemplatesRequest.ProtoReflect.Descriptor instead. func (*ListSandboxTemplatesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{35} + return file_openshell_proto_rawDescGZIP(), []int{38} } func (x *ListSandboxTemplatesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -3901,7 +4111,7 @@ type DeleteSandboxTemplateRequest struct { func (x *DeleteSandboxTemplateRequest) Reset() { *x = DeleteSandboxTemplateRequest{} - mi := &file_openshell_proto_msgTypes[36] + mi := &file_openshell_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3913,7 +4123,7 @@ func (x *DeleteSandboxTemplateRequest) String() string { func (*DeleteSandboxTemplateRequest) ProtoMessage() {} func (x *DeleteSandboxTemplateRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[36] + mi := &file_openshell_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3926,7 +4136,7 @@ func (x *DeleteSandboxTemplateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteSandboxTemplateRequest.ProtoReflect.Descriptor instead. func (*DeleteSandboxTemplateRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{36} + return file_openshell_proto_rawDescGZIP(), []int{39} } func (x *DeleteSandboxTemplateRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -3966,7 +4176,7 @@ type SandboxTemplateResponse struct { func (x *SandboxTemplateResponse) Reset() { *x = SandboxTemplateResponse{} - mi := &file_openshell_proto_msgTypes[37] + mi := &file_openshell_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3978,7 +4188,7 @@ func (x *SandboxTemplateResponse) String() string { func (*SandboxTemplateResponse) ProtoMessage() {} func (x *SandboxTemplateResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[37] + mi := &file_openshell_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3991,7 +4201,7 @@ func (x *SandboxTemplateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxTemplateResponse.ProtoReflect.Descriptor instead. func (*SandboxTemplateResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{37} + return file_openshell_proto_rawDescGZIP(), []int{40} } func (x *SandboxTemplateResponse) GetTemplate() *SandboxWorkloadTemplate { @@ -4012,7 +4222,7 @@ type ListSandboxTemplatesResponse struct { func (x *ListSandboxTemplatesResponse) Reset() { *x = ListSandboxTemplatesResponse{} - mi := &file_openshell_proto_msgTypes[38] + mi := &file_openshell_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4024,7 +4234,7 @@ func (x *ListSandboxTemplatesResponse) String() string { func (*ListSandboxTemplatesResponse) ProtoMessage() {} func (x *ListSandboxTemplatesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[38] + mi := &file_openshell_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4037,7 +4247,7 @@ func (x *ListSandboxTemplatesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxTemplatesResponse.ProtoReflect.Descriptor instead. func (*ListSandboxTemplatesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{38} + return file_openshell_proto_rawDescGZIP(), []int{41} } func (x *ListSandboxTemplatesResponse) GetTemplates() []*SandboxWorkloadTemplate { @@ -4063,7 +4273,7 @@ type DeleteSandboxTemplateResponse struct { func (x *DeleteSandboxTemplateResponse) Reset() { *x = DeleteSandboxTemplateResponse{} - mi := &file_openshell_proto_msgTypes[39] + mi := &file_openshell_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4075,7 +4285,7 @@ func (x *DeleteSandboxTemplateResponse) String() string { func (*DeleteSandboxTemplateResponse) ProtoMessage() {} func (x *DeleteSandboxTemplateResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[39] + mi := &file_openshell_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4088,7 +4298,7 @@ func (x *DeleteSandboxTemplateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteSandboxTemplateResponse.ProtoReflect.Descriptor instead. func (*DeleteSandboxTemplateResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{39} + return file_openshell_proto_rawDescGZIP(), []int{42} } func (x *DeleteSandboxTemplateResponse) GetOutcome() DeletionOutcome { @@ -4115,7 +4325,7 @@ type BeginRootfsTarStagingRequest struct { func (x *BeginRootfsTarStagingRequest) Reset() { *x = BeginRootfsTarStagingRequest{} - mi := &file_openshell_proto_msgTypes[40] + mi := &file_openshell_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4127,7 +4337,7 @@ func (x *BeginRootfsTarStagingRequest) String() string { func (*BeginRootfsTarStagingRequest) ProtoMessage() {} func (x *BeginRootfsTarStagingRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[40] + mi := &file_openshell_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4140,7 +4350,7 @@ func (x *BeginRootfsTarStagingRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BeginRootfsTarStagingRequest.ProtoReflect.Descriptor instead. func (*BeginRootfsTarStagingRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{40} + return file_openshell_proto_rawDescGZIP(), []int{43} } func (x *BeginRootfsTarStagingRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -4183,7 +4393,7 @@ type BeginRootfsTarStagingResponse struct { func (x *BeginRootfsTarStagingResponse) Reset() { *x = BeginRootfsTarStagingResponse{} - mi := &file_openshell_proto_msgTypes[41] + mi := &file_openshell_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4195,7 +4405,7 @@ func (x *BeginRootfsTarStagingResponse) String() string { func (*BeginRootfsTarStagingResponse) ProtoMessage() {} func (x *BeginRootfsTarStagingResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[41] + mi := &file_openshell_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4208,7 +4418,7 @@ func (x *BeginRootfsTarStagingResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BeginRootfsTarStagingResponse.ProtoReflect.Descriptor instead. func (*BeginRootfsTarStagingResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{41} + return file_openshell_proto_rawDescGZIP(), []int{44} } func (x *BeginRootfsTarStagingResponse) GetStagingToken() string { @@ -4251,7 +4461,7 @@ type GetSandboxRequest struct { func (x *GetSandboxRequest) Reset() { *x = GetSandboxRequest{} - mi := &file_openshell_proto_msgTypes[42] + mi := &file_openshell_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4263,7 +4473,7 @@ func (x *GetSandboxRequest) String() string { func (*GetSandboxRequest) ProtoMessage() {} func (x *GetSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[42] + mi := &file_openshell_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4276,7 +4486,7 @@ func (x *GetSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxRequest.ProtoReflect.Descriptor instead. func (*GetSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{42} + return file_openshell_proto_rawDescGZIP(), []int{45} } func (x *GetSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -4312,7 +4522,7 @@ type ListSandboxesRequest struct { func (x *ListSandboxesRequest) Reset() { *x = ListSandboxesRequest{} - mi := &file_openshell_proto_msgTypes[43] + mi := &file_openshell_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4324,7 +4534,7 @@ func (x *ListSandboxesRequest) String() string { func (*ListSandboxesRequest) ProtoMessage() {} func (x *ListSandboxesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[43] + mi := &file_openshell_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4337,7 +4547,7 @@ func (x *ListSandboxesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxesRequest.ProtoReflect.Descriptor instead. func (*ListSandboxesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{43} + return file_openshell_proto_rawDescGZIP(), []int{46} } func (x *ListSandboxesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -4386,7 +4596,7 @@ type ListSandboxProvidersRequest struct { func (x *ListSandboxProvidersRequest) Reset() { *x = ListSandboxProvidersRequest{} - mi := &file_openshell_proto_msgTypes[44] + mi := &file_openshell_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4398,7 +4608,7 @@ func (x *ListSandboxProvidersRequest) String() string { func (*ListSandboxProvidersRequest) ProtoMessage() {} func (x *ListSandboxProvidersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[44] + mi := &file_openshell_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4411,7 +4621,7 @@ func (x *ListSandboxProvidersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxProvidersRequest.ProtoReflect.Descriptor instead. func (*ListSandboxProvidersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{44} + return file_openshell_proto_rawDescGZIP(), []int{47} } func (x *ListSandboxProvidersRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -4464,7 +4674,7 @@ type AttachSandboxProviderRequest struct { func (x *AttachSandboxProviderRequest) Reset() { *x = AttachSandboxProviderRequest{} - mi := &file_openshell_proto_msgTypes[45] + mi := &file_openshell_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4476,7 +4686,7 @@ func (x *AttachSandboxProviderRequest) String() string { func (*AttachSandboxProviderRequest) ProtoMessage() {} func (x *AttachSandboxProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[45] + mi := &file_openshell_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4489,7 +4699,7 @@ func (x *AttachSandboxProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AttachSandboxProviderRequest.ProtoReflect.Descriptor instead. func (*AttachSandboxProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{45} + return file_openshell_proto_rawDescGZIP(), []int{48} } func (x *AttachSandboxProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -4549,7 +4759,7 @@ type DetachSandboxProviderRequest struct { func (x *DetachSandboxProviderRequest) Reset() { *x = DetachSandboxProviderRequest{} - mi := &file_openshell_proto_msgTypes[46] + mi := &file_openshell_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4561,7 +4771,7 @@ func (x *DetachSandboxProviderRequest) String() string { func (*DetachSandboxProviderRequest) ProtoMessage() {} func (x *DetachSandboxProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[46] + mi := &file_openshell_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4574,7 +4784,7 @@ func (x *DetachSandboxProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DetachSandboxProviderRequest.ProtoReflect.Descriptor instead. func (*DetachSandboxProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{46} + return file_openshell_proto_rawDescGZIP(), []int{49} } func (x *DetachSandboxProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -4631,7 +4841,7 @@ type DeleteSandboxRequest struct { func (x *DeleteSandboxRequest) Reset() { *x = DeleteSandboxRequest{} - mi := &file_openshell_proto_msgTypes[47] + mi := &file_openshell_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4643,7 +4853,7 @@ func (x *DeleteSandboxRequest) String() string { func (*DeleteSandboxRequest) ProtoMessage() {} func (x *DeleteSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[47] + mi := &file_openshell_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4656,7 +4866,7 @@ func (x *DeleteSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteSandboxRequest.ProtoReflect.Descriptor instead. func (*DeleteSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{47} + return file_openshell_proto_rawDescGZIP(), []int{50} } func (x *DeleteSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -4702,7 +4912,7 @@ type StopSandboxRequest struct { func (x *StopSandboxRequest) Reset() { *x = StopSandboxRequest{} - mi := &file_openshell_proto_msgTypes[48] + mi := &file_openshell_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4714,7 +4924,7 @@ func (x *StopSandboxRequest) String() string { func (*StopSandboxRequest) ProtoMessage() {} func (x *StopSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[48] + mi := &file_openshell_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4727,7 +4937,7 @@ func (x *StopSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StopSandboxRequest.ProtoReflect.Descriptor instead. func (*StopSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{48} + return file_openshell_proto_rawDescGZIP(), []int{51} } func (x *StopSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -4766,7 +4976,7 @@ type StartSandboxRequest struct { func (x *StartSandboxRequest) Reset() { *x = StartSandboxRequest{} - mi := &file_openshell_proto_msgTypes[49] + mi := &file_openshell_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4778,7 +4988,7 @@ func (x *StartSandboxRequest) String() string { func (*StartSandboxRequest) ProtoMessage() {} func (x *StartSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[49] + mi := &file_openshell_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4791,7 +5001,7 @@ func (x *StartSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartSandboxRequest.ProtoReflect.Descriptor instead. func (*StartSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{49} + return file_openshell_proto_rawDescGZIP(), []int{52} } func (x *StartSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -4827,7 +5037,7 @@ type SuspendSandboxRequest struct { func (x *SuspendSandboxRequest) Reset() { *x = SuspendSandboxRequest{} - mi := &file_openshell_proto_msgTypes[50] + mi := &file_openshell_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4839,7 +5049,7 @@ func (x *SuspendSandboxRequest) String() string { func (*SuspendSandboxRequest) ProtoMessage() {} func (x *SuspendSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[50] + mi := &file_openshell_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4852,7 +5062,7 @@ func (x *SuspendSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SuspendSandboxRequest.ProtoReflect.Descriptor instead. func (*SuspendSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{50} + return file_openshell_proto_rawDescGZIP(), []int{53} } func (x *SuspendSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -4884,7 +5094,7 @@ type ResumeSandboxRequest struct { func (x *ResumeSandboxRequest) Reset() { *x = ResumeSandboxRequest{} - mi := &file_openshell_proto_msgTypes[51] + mi := &file_openshell_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4896,7 +5106,7 @@ func (x *ResumeSandboxRequest) String() string { func (*ResumeSandboxRequest) ProtoMessage() {} func (x *ResumeSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[51] + mi := &file_openshell_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4909,7 +5119,7 @@ func (x *ResumeSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResumeSandboxRequest.ProtoReflect.Descriptor instead. func (*ResumeSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{51} + return file_openshell_proto_rawDescGZIP(), []int{54} } func (x *ResumeSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -4946,7 +5156,7 @@ type SandboxResponse struct { func (x *SandboxResponse) Reset() { *x = SandboxResponse{} - mi := &file_openshell_proto_msgTypes[52] + mi := &file_openshell_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4958,7 +5168,7 @@ func (x *SandboxResponse) String() string { func (*SandboxResponse) ProtoMessage() {} func (x *SandboxResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[52] + mi := &file_openshell_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4971,7 +5181,7 @@ func (x *SandboxResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxResponse.ProtoReflect.Descriptor instead. func (*SandboxResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{52} + return file_openshell_proto_rawDescGZIP(), []int{55} } func (x *SandboxResponse) GetSandbox() *Sandbox { @@ -5000,7 +5210,7 @@ type ListSandboxesResponse struct { func (x *ListSandboxesResponse) Reset() { *x = ListSandboxesResponse{} - mi := &file_openshell_proto_msgTypes[53] + mi := &file_openshell_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5012,7 +5222,7 @@ func (x *ListSandboxesResponse) String() string { func (*ListSandboxesResponse) ProtoMessage() {} func (x *ListSandboxesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[53] + mi := &file_openshell_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5025,7 +5235,7 @@ func (x *ListSandboxesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxesResponse.ProtoReflect.Descriptor instead. func (*ListSandboxesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{53} + return file_openshell_proto_rawDescGZIP(), []int{56} } func (x *ListSandboxesResponse) GetSandboxes() []*Sandbox { @@ -5054,7 +5264,7 @@ type ListSandboxProvidersResponse struct { func (x *ListSandboxProvidersResponse) Reset() { *x = ListSandboxProvidersResponse{} - mi := &file_openshell_proto_msgTypes[54] + mi := &file_openshell_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5066,7 +5276,7 @@ func (x *ListSandboxProvidersResponse) String() string { func (*ListSandboxProvidersResponse) ProtoMessage() {} func (x *ListSandboxProvidersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[54] + mi := &file_openshell_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5079,7 +5289,7 @@ func (x *ListSandboxProvidersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxProvidersResponse.ProtoReflect.Descriptor instead. func (*ListSandboxProvidersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{54} + return file_openshell_proto_rawDescGZIP(), []int{57} } func (x *ListSandboxProvidersResponse) GetProviders() []*datamodelv1.Provider { @@ -5110,7 +5320,7 @@ type AttachSandboxProviderResponse struct { func (x *AttachSandboxProviderResponse) Reset() { *x = AttachSandboxProviderResponse{} - mi := &file_openshell_proto_msgTypes[55] + mi := &file_openshell_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5122,7 +5332,7 @@ func (x *AttachSandboxProviderResponse) String() string { func (*AttachSandboxProviderResponse) ProtoMessage() {} func (x *AttachSandboxProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[55] + mi := &file_openshell_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5135,7 +5345,7 @@ func (x *AttachSandboxProviderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AttachSandboxProviderResponse.ProtoReflect.Descriptor instead. func (*AttachSandboxProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{55} + return file_openshell_proto_rawDescGZIP(), []int{58} } func (x *AttachSandboxProviderResponse) GetSandbox() *Sandbox { @@ -5173,7 +5383,7 @@ type DetachSandboxProviderResponse struct { func (x *DetachSandboxProviderResponse) Reset() { *x = DetachSandboxProviderResponse{} - mi := &file_openshell_proto_msgTypes[56] + mi := &file_openshell_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5185,7 +5395,7 @@ func (x *DetachSandboxProviderResponse) String() string { func (*DetachSandboxProviderResponse) ProtoMessage() {} func (x *DetachSandboxProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[56] + mi := &file_openshell_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5198,7 +5408,7 @@ func (x *DetachSandboxProviderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DetachSandboxProviderResponse.ProtoReflect.Descriptor instead. func (*DetachSandboxProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{56} + return file_openshell_proto_rawDescGZIP(), []int{59} } func (x *DetachSandboxProviderResponse) GetSandbox() *Sandbox { @@ -5240,7 +5450,7 @@ type ProviderDesiredIdentity struct { func (x *ProviderDesiredIdentity) Reset() { *x = ProviderDesiredIdentity{} - mi := &file_openshell_proto_msgTypes[57] + mi := &file_openshell_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5252,7 +5462,7 @@ func (x *ProviderDesiredIdentity) String() string { func (*ProviderDesiredIdentity) ProtoMessage() {} func (x *ProviderDesiredIdentity) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[57] + mi := &file_openshell_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5265,7 +5475,7 @@ func (x *ProviderDesiredIdentity) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderDesiredIdentity.ProtoReflect.Descriptor instead. func (*ProviderDesiredIdentity) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{57} + return file_openshell_proto_rawDescGZIP(), []int{60} } func (x *ProviderDesiredIdentity) GetSandboxId() string { @@ -5340,7 +5550,7 @@ type ConfigSnapshotRevision struct { func (x *ConfigSnapshotRevision) Reset() { *x = ConfigSnapshotRevision{} - mi := &file_openshell_proto_msgTypes[58] + mi := &file_openshell_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5352,7 +5562,7 @@ func (x *ConfigSnapshotRevision) String() string { func (*ConfigSnapshotRevision) ProtoMessage() {} func (x *ConfigSnapshotRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[58] + mi := &file_openshell_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5365,7 +5575,7 @@ func (x *ConfigSnapshotRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigSnapshotRevision.ProtoReflect.Descriptor instead. func (*ConfigSnapshotRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{58} + return file_openshell_proto_rawDescGZIP(), []int{61} } func (x *ConfigSnapshotRevision) GetComponent() isConfigSnapshotRevision_Component { @@ -5442,7 +5652,7 @@ type SandboxConfigRevision struct { func (x *SandboxConfigRevision) Reset() { *x = SandboxConfigRevision{} - mi := &file_openshell_proto_msgTypes[59] + mi := &file_openshell_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5454,7 +5664,7 @@ func (x *SandboxConfigRevision) String() string { func (*SandboxConfigRevision) ProtoMessage() {} func (x *SandboxConfigRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[59] + mi := &file_openshell_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5467,7 +5677,7 @@ func (x *SandboxConfigRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxConfigRevision.ProtoReflect.Descriptor instead. func (*SandboxConfigRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{59} + return file_openshell_proto_rawDescGZIP(), []int{62} } func (x *SandboxConfigRevision) GetConfigRevision() uint64 { @@ -5526,7 +5736,7 @@ type ConfigUpdateOperation struct { func (x *ConfigUpdateOperation) Reset() { *x = ConfigUpdateOperation{} - mi := &file_openshell_proto_msgTypes[60] + mi := &file_openshell_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5538,7 +5748,7 @@ func (x *ConfigUpdateOperation) String() string { func (*ConfigUpdateOperation) ProtoMessage() {} func (x *ConfigUpdateOperation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[60] + mi := &file_openshell_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5551,7 +5761,7 @@ func (x *ConfigUpdateOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigUpdateOperation.ProtoReflect.Descriptor instead. func (*ConfigUpdateOperation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{60} + return file_openshell_proto_rawDescGZIP(), []int{63} } func (x *ConfigUpdateOperation) GetOperationId() string { @@ -5641,7 +5851,7 @@ type ProviderMutationReceipt struct { func (x *ProviderMutationReceipt) Reset() { *x = ProviderMutationReceipt{} - mi := &file_openshell_proto_msgTypes[61] + mi := &file_openshell_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5653,7 +5863,7 @@ func (x *ProviderMutationReceipt) String() string { func (*ProviderMutationReceipt) ProtoMessage() {} func (x *ProviderMutationReceipt) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[61] + mi := &file_openshell_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5666,7 +5876,7 @@ func (x *ProviderMutationReceipt) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderMutationReceipt.ProtoReflect.Descriptor instead. func (*ProviderMutationReceipt) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{61} + return file_openshell_proto_rawDescGZIP(), []int{64} } func (x *ProviderMutationReceipt) GetReceiptId() string { @@ -5742,7 +5952,7 @@ type ProviderReadinessObservation struct { func (x *ProviderReadinessObservation) Reset() { *x = ProviderReadinessObservation{} - mi := &file_openshell_proto_msgTypes[62] + mi := &file_openshell_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5754,7 +5964,7 @@ func (x *ProviderReadinessObservation) String() string { func (*ProviderReadinessObservation) ProtoMessage() {} func (x *ProviderReadinessObservation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[62] + mi := &file_openshell_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5767,7 +5977,7 @@ func (x *ProviderReadinessObservation) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderReadinessObservation.ProtoReflect.Descriptor instead. func (*ProviderReadinessObservation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{62} + return file_openshell_proto_rawDescGZIP(), []int{65} } func (x *ProviderReadinessObservation) GetSessionId() string { @@ -5866,7 +6076,7 @@ type ProviderReadinessStatus struct { func (x *ProviderReadinessStatus) Reset() { *x = ProviderReadinessStatus{} - mi := &file_openshell_proto_msgTypes[63] + mi := &file_openshell_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5878,7 +6088,7 @@ func (x *ProviderReadinessStatus) String() string { func (*ProviderReadinessStatus) ProtoMessage() {} func (x *ProviderReadinessStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[63] + mi := &file_openshell_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5891,7 +6101,7 @@ func (x *ProviderReadinessStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderReadinessStatus.ProtoReflect.Descriptor instead. func (*ProviderReadinessStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{63} + return file_openshell_proto_rawDescGZIP(), []int{66} } func (x *ProviderReadinessStatus) GetReceipt() *ProviderMutationReceipt { @@ -5965,7 +6175,7 @@ type GetSandboxProviderStatusRequest struct { func (x *GetSandboxProviderStatusRequest) Reset() { *x = GetSandboxProviderStatusRequest{} - mi := &file_openshell_proto_msgTypes[64] + mi := &file_openshell_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5977,7 +6187,7 @@ func (x *GetSandboxProviderStatusRequest) String() string { func (*GetSandboxProviderStatusRequest) ProtoMessage() {} func (x *GetSandboxProviderStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[64] + mi := &file_openshell_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5990,7 +6200,7 @@ func (x *GetSandboxProviderStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxProviderStatusRequest.ProtoReflect.Descriptor instead. func (*GetSandboxProviderStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{64} + return file_openshell_proto_rawDescGZIP(), []int{67} } func (x *GetSandboxProviderStatusRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -6030,7 +6240,7 @@ type GetSandboxProviderStatusResponse struct { func (x *GetSandboxProviderStatusResponse) Reset() { *x = GetSandboxProviderStatusResponse{} - mi := &file_openshell_proto_msgTypes[65] + mi := &file_openshell_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6042,7 +6252,7 @@ func (x *GetSandboxProviderStatusResponse) String() string { func (*GetSandboxProviderStatusResponse) ProtoMessage() {} func (x *GetSandboxProviderStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[65] + mi := &file_openshell_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6055,7 +6265,7 @@ func (x *GetSandboxProviderStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxProviderStatusResponse.ProtoReflect.Descriptor instead. func (*GetSandboxProviderStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{65} + return file_openshell_proto_rawDescGZIP(), []int{68} } func (x *GetSandboxProviderStatusResponse) GetStatus() *ProviderReadinessStatus { @@ -6077,7 +6287,7 @@ type ReportProviderReadinessRequest struct { func (x *ReportProviderReadinessRequest) Reset() { *x = ReportProviderReadinessRequest{} - mi := &file_openshell_proto_msgTypes[66] + mi := &file_openshell_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6089,7 +6299,7 @@ func (x *ReportProviderReadinessRequest) String() string { func (*ReportProviderReadinessRequest) ProtoMessage() {} func (x *ReportProviderReadinessRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[66] + mi := &file_openshell_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6102,7 +6312,7 @@ func (x *ReportProviderReadinessRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportProviderReadinessRequest.ProtoReflect.Descriptor instead. func (*ReportProviderReadinessRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{66} + return file_openshell_proto_rawDescGZIP(), []int{69} } func (x *ReportProviderReadinessRequest) GetSandboxId() string { @@ -6132,7 +6342,7 @@ type ReportProviderReadinessResponse struct { func (x *ReportProviderReadinessResponse) Reset() { *x = ReportProviderReadinessResponse{} - mi := &file_openshell_proto_msgTypes[67] + mi := &file_openshell_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6144,7 +6354,7 @@ func (x *ReportProviderReadinessResponse) String() string { func (*ReportProviderReadinessResponse) ProtoMessage() {} func (x *ReportProviderReadinessResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[67] + mi := &file_openshell_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6157,7 +6367,7 @@ func (x *ReportProviderReadinessResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportProviderReadinessResponse.ProtoReflect.Descriptor instead. func (*ReportProviderReadinessResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{67} + return file_openshell_proto_rawDescGZIP(), []int{70} } func (x *ReportProviderReadinessResponse) GetAcceptedSequence() uint64 { @@ -6194,7 +6404,7 @@ type DeleteSandboxResponse struct { func (x *DeleteSandboxResponse) Reset() { *x = DeleteSandboxResponse{} - mi := &file_openshell_proto_msgTypes[68] + mi := &file_openshell_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6206,7 +6416,7 @@ func (x *DeleteSandboxResponse) String() string { func (*DeleteSandboxResponse) ProtoMessage() {} func (x *DeleteSandboxResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[68] + mi := &file_openshell_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6219,7 +6429,7 @@ func (x *DeleteSandboxResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteSandboxResponse.ProtoReflect.Descriptor instead. func (*DeleteSandboxResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{68} + return file_openshell_proto_rawDescGZIP(), []int{71} } func (x *DeleteSandboxResponse) GetOutcome() DeletionOutcome { @@ -6248,7 +6458,7 @@ type CreateSshSessionRequest struct { func (x *CreateSshSessionRequest) Reset() { *x = CreateSshSessionRequest{} - mi := &file_openshell_proto_msgTypes[69] + mi := &file_openshell_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6260,7 +6470,7 @@ func (x *CreateSshSessionRequest) String() string { func (*CreateSshSessionRequest) ProtoMessage() {} func (x *CreateSshSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[69] + mi := &file_openshell_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6273,7 +6483,7 @@ func (x *CreateSshSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateSshSessionRequest.ProtoReflect.Descriptor instead. func (*CreateSshSessionRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{69} + return file_openshell_proto_rawDescGZIP(), []int{72} } func (x *CreateSshSessionRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -6324,7 +6534,7 @@ type CreateSshSessionResponse struct { func (x *CreateSshSessionResponse) Reset() { *x = CreateSshSessionResponse{} - mi := &file_openshell_proto_msgTypes[70] + mi := &file_openshell_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6336,7 +6546,7 @@ func (x *CreateSshSessionResponse) String() string { func (*CreateSshSessionResponse) ProtoMessage() {} func (x *CreateSshSessionResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[70] + mi := &file_openshell_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6349,7 +6559,7 @@ func (x *CreateSshSessionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateSshSessionResponse.ProtoReflect.Descriptor instead. func (*CreateSshSessionResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{70} + return file_openshell_proto_rawDescGZIP(), []int{73} } func (x *CreateSshSessionResponse) GetSandboxId() string { @@ -6424,7 +6634,7 @@ type ExposeServiceRequest struct { func (x *ExposeServiceRequest) Reset() { *x = ExposeServiceRequest{} - mi := &file_openshell_proto_msgTypes[71] + mi := &file_openshell_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6436,7 +6646,7 @@ func (x *ExposeServiceRequest) String() string { func (*ExposeServiceRequest) ProtoMessage() {} func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[71] + mi := &file_openshell_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6449,7 +6659,7 @@ func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead. func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{71} + return file_openshell_proto_rawDescGZIP(), []int{74} } func (x *ExposeServiceRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -6515,7 +6725,7 @@ type GetServiceRequest struct { func (x *GetServiceRequest) Reset() { *x = GetServiceRequest{} - mi := &file_openshell_proto_msgTypes[72] + mi := &file_openshell_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6527,7 +6737,7 @@ func (x *GetServiceRequest) String() string { func (*GetServiceRequest) ProtoMessage() {} func (x *GetServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[72] + mi := &file_openshell_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6540,7 +6750,7 @@ func (x *GetServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetServiceRequest.ProtoReflect.Descriptor instead. func (*GetServiceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{72} + return file_openshell_proto_rawDescGZIP(), []int{75} } func (x *GetServiceRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -6583,7 +6793,7 @@ type ListServicesRequest struct { func (x *ListServicesRequest) Reset() { *x = ListServicesRequest{} - mi := &file_openshell_proto_msgTypes[73] + mi := &file_openshell_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6595,7 +6805,7 @@ func (x *ListServicesRequest) String() string { func (*ListServicesRequest) ProtoMessage() {} func (x *ListServicesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[73] + mi := &file_openshell_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6608,7 +6818,7 @@ func (x *ListServicesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListServicesRequest.ProtoReflect.Descriptor instead. func (*ListServicesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{73} + return file_openshell_proto_rawDescGZIP(), []int{76} } func (x *ListServicesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -6651,7 +6861,7 @@ type ListServicesResponse struct { func (x *ListServicesResponse) Reset() { *x = ListServicesResponse{} - mi := &file_openshell_proto_msgTypes[74] + mi := &file_openshell_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6663,7 +6873,7 @@ func (x *ListServicesResponse) String() string { func (*ListServicesResponse) ProtoMessage() {} func (x *ListServicesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[74] + mi := &file_openshell_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6676,7 +6886,7 @@ func (x *ListServicesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListServicesResponse.ProtoReflect.Descriptor instead. func (*ListServicesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{74} + return file_openshell_proto_rawDescGZIP(), []int{77} } func (x *ListServicesResponse) GetServices() []*ServiceEndpointResponse { @@ -6711,7 +6921,7 @@ type DeleteServiceRequest struct { func (x *DeleteServiceRequest) Reset() { *x = DeleteServiceRequest{} - mi := &file_openshell_proto_msgTypes[75] + mi := &file_openshell_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6723,7 +6933,7 @@ func (x *DeleteServiceRequest) String() string { func (*DeleteServiceRequest) ProtoMessage() {} func (x *DeleteServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[75] + mi := &file_openshell_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6736,7 +6946,7 @@ func (x *DeleteServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteServiceRequest.ProtoReflect.Descriptor instead. func (*DeleteServiceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{75} + return file_openshell_proto_rawDescGZIP(), []int{78} } func (x *DeleteServiceRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -6784,7 +6994,7 @@ type DeleteServiceResponse struct { func (x *DeleteServiceResponse) Reset() { *x = DeleteServiceResponse{} - mi := &file_openshell_proto_msgTypes[76] + mi := &file_openshell_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6796,7 +7006,7 @@ func (x *DeleteServiceResponse) String() string { func (*DeleteServiceResponse) ProtoMessage() {} func (x *DeleteServiceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[76] + mi := &file_openshell_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6809,7 +7019,7 @@ func (x *DeleteServiceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteServiceResponse.ProtoReflect.Descriptor instead. func (*DeleteServiceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{76} + return file_openshell_proto_rawDescGZIP(), []int{79} } func (x *DeleteServiceResponse) GetOutcome() DeletionOutcome { @@ -6842,7 +7052,7 @@ type ServiceEndpoint struct { func (x *ServiceEndpoint) Reset() { *x = ServiceEndpoint{} - mi := &file_openshell_proto_msgTypes[77] + mi := &file_openshell_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6854,7 +7064,7 @@ func (x *ServiceEndpoint) String() string { func (*ServiceEndpoint) ProtoMessage() {} func (x *ServiceEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[77] + mi := &file_openshell_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6867,7 +7077,7 @@ func (x *ServiceEndpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceEndpoint.ProtoReflect.Descriptor instead. func (*ServiceEndpoint) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{77} + return file_openshell_proto_rawDescGZIP(), []int{80} } func (x *ServiceEndpoint) GetMetadata() *datamodelv1.ObjectMeta { @@ -6930,7 +7140,7 @@ type ServiceEndpointResponse struct { func (x *ServiceEndpointResponse) Reset() { *x = ServiceEndpointResponse{} - mi := &file_openshell_proto_msgTypes[78] + mi := &file_openshell_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6942,7 +7152,7 @@ func (x *ServiceEndpointResponse) String() string { func (*ServiceEndpointResponse) ProtoMessage() {} func (x *ServiceEndpointResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[78] + mi := &file_openshell_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6955,7 +7165,7 @@ func (x *ServiceEndpointResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceEndpointResponse.ProtoReflect.Descriptor instead. func (*ServiceEndpointResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{78} + return file_openshell_proto_rawDescGZIP(), []int{81} } func (x *ServiceEndpointResponse) GetEndpoint() *ServiceEndpoint { @@ -6986,7 +7196,7 @@ type RevokeSshSessionRequest struct { func (x *RevokeSshSessionRequest) Reset() { *x = RevokeSshSessionRequest{} - mi := &file_openshell_proto_msgTypes[79] + mi := &file_openshell_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6998,7 +7208,7 @@ func (x *RevokeSshSessionRequest) String() string { func (*RevokeSshSessionRequest) ProtoMessage() {} func (x *RevokeSshSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[79] + mi := &file_openshell_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7011,7 +7221,7 @@ func (x *RevokeSshSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeSshSessionRequest.ProtoReflect.Descriptor instead. func (*RevokeSshSessionRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{79} + return file_openshell_proto_rawDescGZIP(), []int{82} } func (x *RevokeSshSessionRequest) GetToken() string { @@ -7038,7 +7248,7 @@ type RevokeSshSessionResponse struct { func (x *RevokeSshSessionResponse) Reset() { *x = RevokeSshSessionResponse{} - mi := &file_openshell_proto_msgTypes[80] + mi := &file_openshell_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7050,7 +7260,7 @@ func (x *RevokeSshSessionResponse) String() string { func (*RevokeSshSessionResponse) ProtoMessage() {} func (x *RevokeSshSessionResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[80] + mi := &file_openshell_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7063,7 +7273,7 @@ func (x *RevokeSshSessionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeSshSessionResponse.ProtoReflect.Descriptor instead. func (*RevokeSshSessionResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{80} + return file_openshell_proto_rawDescGZIP(), []int{83} } func (x *RevokeSshSessionResponse) GetOutcome() DeletionOutcome { @@ -7113,7 +7323,7 @@ type ExecSandboxRequest struct { func (x *ExecSandboxRequest) Reset() { *x = ExecSandboxRequest{} - mi := &file_openshell_proto_msgTypes[81] + mi := &file_openshell_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7125,7 +7335,7 @@ func (x *ExecSandboxRequest) String() string { func (*ExecSandboxRequest) ProtoMessage() {} func (x *ExecSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[81] + mi := &file_openshell_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7138,7 +7348,7 @@ func (x *ExecSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxRequest.ProtoReflect.Descriptor instead. func (*ExecSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{81} + return file_openshell_proto_rawDescGZIP(), []int{84} } func (x *ExecSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -7235,7 +7445,7 @@ type ExecSandboxStdout struct { func (x *ExecSandboxStdout) Reset() { *x = ExecSandboxStdout{} - mi := &file_openshell_proto_msgTypes[82] + mi := &file_openshell_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7247,7 +7457,7 @@ func (x *ExecSandboxStdout) String() string { func (*ExecSandboxStdout) ProtoMessage() {} func (x *ExecSandboxStdout) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[82] + mi := &file_openshell_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7260,7 +7470,7 @@ func (x *ExecSandboxStdout) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxStdout.ProtoReflect.Descriptor instead. func (*ExecSandboxStdout) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{82} + return file_openshell_proto_rawDescGZIP(), []int{85} } func (x *ExecSandboxStdout) GetData() []byte { @@ -7280,7 +7490,7 @@ type ExecSandboxStderr struct { func (x *ExecSandboxStderr) Reset() { *x = ExecSandboxStderr{} - mi := &file_openshell_proto_msgTypes[83] + mi := &file_openshell_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7292,7 +7502,7 @@ func (x *ExecSandboxStderr) String() string { func (*ExecSandboxStderr) ProtoMessage() {} func (x *ExecSandboxStderr) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[83] + mi := &file_openshell_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7305,7 +7515,7 @@ func (x *ExecSandboxStderr) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxStderr.ProtoReflect.Descriptor instead. func (*ExecSandboxStderr) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{83} + return file_openshell_proto_rawDescGZIP(), []int{86} } func (x *ExecSandboxStderr) GetData() []byte { @@ -7325,7 +7535,7 @@ type ExecSandboxExit struct { func (x *ExecSandboxExit) Reset() { *x = ExecSandboxExit{} - mi := &file_openshell_proto_msgTypes[84] + mi := &file_openshell_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7337,7 +7547,7 @@ func (x *ExecSandboxExit) String() string { func (*ExecSandboxExit) ProtoMessage() {} func (x *ExecSandboxExit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[84] + mi := &file_openshell_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7350,7 +7560,7 @@ func (x *ExecSandboxExit) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxExit.ProtoReflect.Descriptor instead. func (*ExecSandboxExit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{84} + return file_openshell_proto_rawDescGZIP(), []int{87} } func (x *ExecSandboxExit) GetExitCode() int32 { @@ -7375,7 +7585,7 @@ type ExecSandboxEvent struct { func (x *ExecSandboxEvent) Reset() { *x = ExecSandboxEvent{} - mi := &file_openshell_proto_msgTypes[85] + mi := &file_openshell_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7387,7 +7597,7 @@ func (x *ExecSandboxEvent) String() string { func (*ExecSandboxEvent) ProtoMessage() {} func (x *ExecSandboxEvent) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[85] + mi := &file_openshell_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7400,7 +7610,7 @@ func (x *ExecSandboxEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxEvent.ProtoReflect.Descriptor instead. func (*ExecSandboxEvent) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{85} + return file_openshell_proto_rawDescGZIP(), []int{88} } func (x *ExecSandboxEvent) GetPayload() isExecSandboxEvent_Payload { @@ -7482,7 +7692,7 @@ type TcpForwardInit struct { func (x *TcpForwardInit) Reset() { *x = TcpForwardInit{} - mi := &file_openshell_proto_msgTypes[86] + mi := &file_openshell_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7494,7 +7704,7 @@ func (x *TcpForwardInit) String() string { func (*TcpForwardInit) ProtoMessage() {} func (x *TcpForwardInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[86] + mi := &file_openshell_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7507,7 +7717,7 @@ func (x *TcpForwardInit) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpForwardInit.ProtoReflect.Descriptor instead. func (*TcpForwardInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{86} + return file_openshell_proto_rawDescGZIP(), []int{89} } func (x *TcpForwardInit) GetSandbox() string { @@ -7593,7 +7803,7 @@ type TcpForwardFrame struct { func (x *TcpForwardFrame) Reset() { *x = TcpForwardFrame{} - mi := &file_openshell_proto_msgTypes[87] + mi := &file_openshell_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7605,7 +7815,7 @@ func (x *TcpForwardFrame) String() string { func (*TcpForwardFrame) ProtoMessage() {} func (x *TcpForwardFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[87] + mi := &file_openshell_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7618,7 +7828,7 @@ func (x *TcpForwardFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpForwardFrame.ProtoReflect.Descriptor instead. func (*TcpForwardFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{87} + return file_openshell_proto_rawDescGZIP(), []int{90} } func (x *TcpForwardFrame) GetPayload() isTcpForwardFrame_Payload { @@ -7677,7 +7887,7 @@ type ExecSandboxInput struct { func (x *ExecSandboxInput) Reset() { *x = ExecSandboxInput{} - mi := &file_openshell_proto_msgTypes[88] + mi := &file_openshell_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7689,7 +7899,7 @@ func (x *ExecSandboxInput) String() string { func (*ExecSandboxInput) ProtoMessage() {} func (x *ExecSandboxInput) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[88] + mi := &file_openshell_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7702,7 +7912,7 @@ func (x *ExecSandboxInput) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxInput.ProtoReflect.Descriptor instead. func (*ExecSandboxInput) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{88} + return file_openshell_proto_rawDescGZIP(), []int{91} } func (x *ExecSandboxInput) GetPayload() isExecSandboxInput_Payload { @@ -7775,7 +7985,7 @@ type ExecSandboxWindowResize struct { func (x *ExecSandboxWindowResize) Reset() { *x = ExecSandboxWindowResize{} - mi := &file_openshell_proto_msgTypes[89] + mi := &file_openshell_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7787,7 +7997,7 @@ func (x *ExecSandboxWindowResize) String() string { func (*ExecSandboxWindowResize) ProtoMessage() {} func (x *ExecSandboxWindowResize) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[89] + mi := &file_openshell_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7800,7 +8010,7 @@ func (x *ExecSandboxWindowResize) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxWindowResize.ProtoReflect.Descriptor instead. func (*ExecSandboxWindowResize) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{89} + return file_openshell_proto_rawDescGZIP(), []int{92} } func (x *ExecSandboxWindowResize) GetCols() uint32 { @@ -7836,7 +8046,7 @@ type SshSession struct { func (x *SshSession) Reset() { *x = SshSession{} - mi := &file_openshell_proto_msgTypes[90] + mi := &file_openshell_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7848,7 +8058,7 @@ func (x *SshSession) String() string { func (*SshSession) ProtoMessage() {} func (x *SshSession) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[90] + mi := &file_openshell_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7861,7 +8071,7 @@ func (x *SshSession) ProtoReflect() protoreflect.Message { // Deprecated: Use SshSession.ProtoReflect.Descriptor instead. func (*SshSession) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{90} + return file_openshell_proto_rawDescGZIP(), []int{93} } func (x *SshSession) GetMetadata() *datamodelv1.ObjectMeta { @@ -7950,7 +8160,7 @@ type WatchSandboxRequest struct { func (x *WatchSandboxRequest) Reset() { *x = WatchSandboxRequest{} - mi := &file_openshell_proto_msgTypes[91] + mi := &file_openshell_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7962,7 +8172,7 @@ func (x *WatchSandboxRequest) String() string { func (*WatchSandboxRequest) ProtoMessage() {} func (x *WatchSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[91] + mi := &file_openshell_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7975,7 +8185,7 @@ func (x *WatchSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSandboxRequest.ProtoReflect.Descriptor instead. func (*WatchSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{91} + return file_openshell_proto_rawDescGZIP(), []int{94} } func (x *WatchSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -8091,7 +8301,7 @@ type SandboxStreamEvent struct { func (x *SandboxStreamEvent) Reset() { *x = SandboxStreamEvent{} - mi := &file_openshell_proto_msgTypes[92] + mi := &file_openshell_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8103,7 +8313,7 @@ func (x *SandboxStreamEvent) String() string { func (*SandboxStreamEvent) ProtoMessage() {} func (x *SandboxStreamEvent) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[92] + mi := &file_openshell_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8116,7 +8326,7 @@ func (x *SandboxStreamEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxStreamEvent.ProtoReflect.Descriptor instead. func (*SandboxStreamEvent) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{92} + return file_openshell_proto_rawDescGZIP(), []int{95} } func (x *SandboxStreamEvent) GetPayload() isSandboxStreamEvent_Payload { @@ -8238,7 +8448,7 @@ type SandboxLogLine struct { func (x *SandboxLogLine) Reset() { *x = SandboxLogLine{} - mi := &file_openshell_proto_msgTypes[93] + mi := &file_openshell_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8250,7 +8460,7 @@ func (x *SandboxLogLine) String() string { func (*SandboxLogLine) ProtoMessage() {} func (x *SandboxLogLine) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[93] + mi := &file_openshell_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8263,7 +8473,7 @@ func (x *SandboxLogLine) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxLogLine.ProtoReflect.Descriptor instead. func (*SandboxLogLine) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{93} + return file_openshell_proto_rawDescGZIP(), []int{96} } func (x *SandboxLogLine) GetSandboxId() string { @@ -8329,7 +8539,7 @@ type SandboxStreamWarning struct { func (x *SandboxStreamWarning) Reset() { *x = SandboxStreamWarning{} - mi := &file_openshell_proto_msgTypes[94] + mi := &file_openshell_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8341,7 +8551,7 @@ func (x *SandboxStreamWarning) String() string { func (*SandboxStreamWarning) ProtoMessage() {} func (x *SandboxStreamWarning) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[94] + mi := &file_openshell_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8354,7 +8564,7 @@ func (x *SandboxStreamWarning) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxStreamWarning.ProtoReflect.Descriptor instead. func (*SandboxStreamWarning) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{94} + return file_openshell_proto_rawDescGZIP(), []int{97} } func (x *SandboxStreamWarning) GetMessage() string { @@ -8379,7 +8589,7 @@ type CreateProviderRequest struct { func (x *CreateProviderRequest) Reset() { *x = CreateProviderRequest{} - mi := &file_openshell_proto_msgTypes[95] + mi := &file_openshell_proto_msgTypes[98] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8391,7 +8601,7 @@ func (x *CreateProviderRequest) String() string { func (*CreateProviderRequest) ProtoMessage() {} func (x *CreateProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[95] + mi := &file_openshell_proto_msgTypes[98] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8404,7 +8614,7 @@ func (x *CreateProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateProviderRequest.ProtoReflect.Descriptor instead. func (*CreateProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{95} + return file_openshell_proto_rawDescGZIP(), []int{98} } func (x *CreateProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -8440,7 +8650,7 @@ type GetProviderRequest struct { func (x *GetProviderRequest) Reset() { *x = GetProviderRequest{} - mi := &file_openshell_proto_msgTypes[96] + mi := &file_openshell_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8452,7 +8662,7 @@ func (x *GetProviderRequest) String() string { func (*GetProviderRequest) ProtoMessage() {} func (x *GetProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[96] + mi := &file_openshell_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8465,7 +8675,7 @@ func (x *GetProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRequest.ProtoReflect.Descriptor instead. func (*GetProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{96} + return file_openshell_proto_rawDescGZIP(), []int{99} } func (x *GetProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -8500,7 +8710,7 @@ type GetProviderCredentialsRequest struct { func (x *GetProviderCredentialsRequest) Reset() { *x = GetProviderCredentialsRequest{} - mi := &file_openshell_proto_msgTypes[97] + mi := &file_openshell_proto_msgTypes[100] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8512,7 +8722,7 @@ func (x *GetProviderCredentialsRequest) String() string { func (*GetProviderCredentialsRequest) ProtoMessage() {} func (x *GetProviderCredentialsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[97] + mi := &file_openshell_proto_msgTypes[100] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8525,7 +8735,7 @@ func (x *GetProviderCredentialsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderCredentialsRequest.ProtoReflect.Descriptor instead. func (*GetProviderCredentialsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{97} + return file_openshell_proto_rawDescGZIP(), []int{100} } func (x *GetProviderCredentialsRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -8568,7 +8778,7 @@ type ProviderCredentialValue struct { func (x *ProviderCredentialValue) Reset() { *x = ProviderCredentialValue{} - mi := &file_openshell_proto_msgTypes[98] + mi := &file_openshell_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8580,7 +8790,7 @@ func (x *ProviderCredentialValue) String() string { func (*ProviderCredentialValue) ProtoMessage() {} func (x *ProviderCredentialValue) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[98] + mi := &file_openshell_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8593,7 +8803,7 @@ func (x *ProviderCredentialValue) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialValue.ProtoReflect.Descriptor instead. func (*ProviderCredentialValue) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{98} + return file_openshell_proto_rawDescGZIP(), []int{101} } func (x *ProviderCredentialValue) GetValue() string { @@ -8620,7 +8830,7 @@ type GetProviderCredentialsResponse struct { func (x *GetProviderCredentialsResponse) Reset() { *x = GetProviderCredentialsResponse{} - mi := &file_openshell_proto_msgTypes[99] + mi := &file_openshell_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8632,7 +8842,7 @@ func (x *GetProviderCredentialsResponse) String() string { func (*GetProviderCredentialsResponse) ProtoMessage() {} func (x *GetProviderCredentialsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[99] + mi := &file_openshell_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8645,7 +8855,7 @@ func (x *GetProviderCredentialsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderCredentialsResponse.ProtoReflect.Descriptor instead. func (*GetProviderCredentialsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{99} + return file_openshell_proto_rawDescGZIP(), []int{102} } func (x *GetProviderCredentialsResponse) GetCredentials() map[string]*ProviderCredentialValue { @@ -8674,7 +8884,7 @@ type ListProvidersRequest struct { func (x *ListProvidersRequest) Reset() { *x = ListProvidersRequest{} - mi := &file_openshell_proto_msgTypes[100] + mi := &file_openshell_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8686,7 +8896,7 @@ func (x *ListProvidersRequest) String() string { func (*ListProvidersRequest) ProtoMessage() {} func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[100] + mi := &file_openshell_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8699,7 +8909,7 @@ func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProvidersRequest.ProtoReflect.Descriptor instead. func (*ListProvidersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{100} + return file_openshell_proto_rawDescGZIP(), []int{103} } func (x *ListProvidersRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -8751,7 +8961,7 @@ type UpdateProviderRequest struct { func (x *UpdateProviderRequest) Reset() { *x = UpdateProviderRequest{} - mi := &file_openshell_proto_msgTypes[101] + mi := &file_openshell_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8763,7 +8973,7 @@ func (x *UpdateProviderRequest) String() string { func (*UpdateProviderRequest) ProtoMessage() {} func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[101] + mi := &file_openshell_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8776,7 +8986,7 @@ func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderRequest.ProtoReflect.Descriptor instead. func (*UpdateProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{101} + return file_openshell_proto_rawDescGZIP(), []int{104} } func (x *UpdateProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -8830,7 +9040,7 @@ type DeleteProviderRequest struct { func (x *DeleteProviderRequest) Reset() { *x = DeleteProviderRequest{} - mi := &file_openshell_proto_msgTypes[102] + mi := &file_openshell_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8842,7 +9052,7 @@ func (x *DeleteProviderRequest) String() string { func (*DeleteProviderRequest) ProtoMessage() {} func (x *DeleteProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[102] + mi := &file_openshell_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8855,7 +9065,7 @@ func (x *DeleteProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{102} + return file_openshell_proto_rawDescGZIP(), []int{105} } func (x *DeleteProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -8901,7 +9111,7 @@ type ProviderResponse struct { func (x *ProviderResponse) Reset() { *x = ProviderResponse{} - mi := &file_openshell_proto_msgTypes[103] + mi := &file_openshell_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8913,7 +9123,7 @@ func (x *ProviderResponse) String() string { func (*ProviderResponse) ProtoMessage() {} func (x *ProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[103] + mi := &file_openshell_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8926,7 +9136,7 @@ func (x *ProviderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderResponse.ProtoReflect.Descriptor instead. func (*ProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{103} + return file_openshell_proto_rawDescGZIP(), []int{106} } func (x *ProviderResponse) GetProvider() *datamodelv1.Provider { @@ -8962,7 +9172,7 @@ type ListProvidersResponse struct { func (x *ListProvidersResponse) Reset() { *x = ListProvidersResponse{} - mi := &file_openshell_proto_msgTypes[104] + mi := &file_openshell_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8974,7 +9184,7 @@ func (x *ListProvidersResponse) String() string { func (*ListProvidersResponse) ProtoMessage() {} func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[104] + mi := &file_openshell_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8987,7 +9197,7 @@ func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProvidersResponse.ProtoReflect.Descriptor instead. func (*ListProvidersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{104} + return file_openshell_proto_rawDescGZIP(), []int{107} } func (x *ListProvidersResponse) GetProviders() []*datamodelv1.Provider { @@ -9021,7 +9231,7 @@ type ListProviderProfilesRequest struct { func (x *ListProviderProfilesRequest) Reset() { *x = ListProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[105] + mi := &file_openshell_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9033,7 +9243,7 @@ func (x *ListProviderProfilesRequest) String() string { func (*ListProviderProfilesRequest) ProtoMessage() {} func (x *ListProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[105] + mi := &file_openshell_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9046,7 +9256,7 @@ func (x *ListProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*ListProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{105} + return file_openshell_proto_rawDescGZIP(), []int{108} } func (x *ListProviderProfilesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -9082,7 +9292,7 @@ type GetProviderProfileRequest struct { func (x *GetProviderProfileRequest) Reset() { *x = GetProviderProfileRequest{} - mi := &file_openshell_proto_msgTypes[106] + mi := &file_openshell_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9094,7 +9304,7 @@ func (x *GetProviderProfileRequest) String() string { func (*GetProviderProfileRequest) ProtoMessage() {} func (x *GetProviderProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[106] + mi := &file_openshell_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9107,7 +9317,7 @@ func (x *GetProviderProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderProfileRequest.ProtoReflect.Descriptor instead. func (*GetProviderProfileRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{106} + return file_openshell_proto_rawDescGZIP(), []int{109} } func (x *GetProviderProfileRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -9135,7 +9345,7 @@ type ProviderProfileImportItem struct { func (x *ProviderProfileImportItem) Reset() { *x = ProviderProfileImportItem{} - mi := &file_openshell_proto_msgTypes[107] + mi := &file_openshell_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9147,7 +9357,7 @@ func (x *ProviderProfileImportItem) String() string { func (*ProviderProfileImportItem) ProtoMessage() {} func (x *ProviderProfileImportItem) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[107] + mi := &file_openshell_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9160,7 +9370,7 @@ func (x *ProviderProfileImportItem) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileImportItem.ProtoReflect.Descriptor instead. func (*ProviderProfileImportItem) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{107} + return file_openshell_proto_rawDescGZIP(), []int{110} } func (x *ProviderProfileImportItem) GetProfile() *ProviderProfile { @@ -9191,7 +9401,7 @@ type ProviderProfileDiagnostic struct { func (x *ProviderProfileDiagnostic) Reset() { *x = ProviderProfileDiagnostic{} - mi := &file_openshell_proto_msgTypes[108] + mi := &file_openshell_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9203,7 +9413,7 @@ func (x *ProviderProfileDiagnostic) String() string { func (*ProviderProfileDiagnostic) ProtoMessage() {} func (x *ProviderProfileDiagnostic) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[108] + mi := &file_openshell_proto_msgTypes[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9216,7 +9426,7 @@ func (x *ProviderProfileDiagnostic) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileDiagnostic.ProtoReflect.Descriptor instead. func (*ProviderProfileDiagnostic) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{108} + return file_openshell_proto_rawDescGZIP(), []int{111} } func (x *ProviderProfileDiagnostic) GetSource() string { @@ -9273,7 +9483,7 @@ type ProviderCredentialTokenGrantAudienceOverride struct { func (x *ProviderCredentialTokenGrantAudienceOverride) Reset() { *x = ProviderCredentialTokenGrantAudienceOverride{} - mi := &file_openshell_proto_msgTypes[109] + mi := &file_openshell_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9285,7 +9495,7 @@ func (x *ProviderCredentialTokenGrantAudienceOverride) String() string { func (*ProviderCredentialTokenGrantAudienceOverride) ProtoMessage() {} func (x *ProviderCredentialTokenGrantAudienceOverride) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[109] + mi := &file_openshell_proto_msgTypes[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9298,7 +9508,7 @@ func (x *ProviderCredentialTokenGrantAudienceOverride) ProtoReflect() protorefle // Deprecated: Use ProviderCredentialTokenGrantAudienceOverride.ProtoReflect.Descriptor instead. func (*ProviderCredentialTokenGrantAudienceOverride) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{109} + return file_openshell_proto_rawDescGZIP(), []int{112} } func (x *ProviderCredentialTokenGrantAudienceOverride) GetHost() string { @@ -9352,7 +9562,7 @@ type ProviderCredentialTokenGrantSubjectToken struct { func (x *ProviderCredentialTokenGrantSubjectToken) Reset() { *x = ProviderCredentialTokenGrantSubjectToken{} - mi := &file_openshell_proto_msgTypes[110] + mi := &file_openshell_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9364,7 +9574,7 @@ func (x *ProviderCredentialTokenGrantSubjectToken) String() string { func (*ProviderCredentialTokenGrantSubjectToken) ProtoMessage() {} func (x *ProviderCredentialTokenGrantSubjectToken) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[110] + mi := &file_openshell_proto_msgTypes[113] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9377,7 +9587,7 @@ func (x *ProviderCredentialTokenGrantSubjectToken) ProtoReflect() protoreflect.M // Deprecated: Use ProviderCredentialTokenGrantSubjectToken.ProtoReflect.Descriptor instead. func (*ProviderCredentialTokenGrantSubjectToken) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{110} + return file_openshell_proto_rawDescGZIP(), []int{113} } func (x *ProviderCredentialTokenGrantSubjectToken) GetSource() string { @@ -9433,7 +9643,7 @@ type ProviderCredentialTokenGrant struct { func (x *ProviderCredentialTokenGrant) Reset() { *x = ProviderCredentialTokenGrant{} - mi := &file_openshell_proto_msgTypes[111] + mi := &file_openshell_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9445,7 +9655,7 @@ func (x *ProviderCredentialTokenGrant) String() string { func (*ProviderCredentialTokenGrant) ProtoMessage() {} func (x *ProviderCredentialTokenGrant) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[111] + mi := &file_openshell_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9458,7 +9668,7 @@ func (x *ProviderCredentialTokenGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialTokenGrant.ProtoReflect.Descriptor instead. func (*ProviderCredentialTokenGrant) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{111} + return file_openshell_proto_rawDescGZIP(), []int{114} } func (x *ProviderCredentialTokenGrant) GetTokenEndpoint() string { @@ -9562,7 +9772,7 @@ type ProviderProfileCredential struct { func (x *ProviderProfileCredential) Reset() { *x = ProviderProfileCredential{} - mi := &file_openshell_proto_msgTypes[112] + mi := &file_openshell_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9574,7 +9784,7 @@ func (x *ProviderProfileCredential) String() string { func (*ProviderProfileCredential) ProtoMessage() {} func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[112] + mi := &file_openshell_proto_msgTypes[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9587,7 +9797,7 @@ func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileCredential.ProtoReflect.Descriptor instead. func (*ProviderProfileCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{112} + return file_openshell_proto_rawDescGZIP(), []int{115} } func (x *ProviderProfileCredential) GetName() string { @@ -9686,7 +9896,7 @@ type ProviderCredentialRefreshMaterial struct { func (x *ProviderCredentialRefreshMaterial) Reset() { *x = ProviderCredentialRefreshMaterial{} - mi := &file_openshell_proto_msgTypes[113] + mi := &file_openshell_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9698,7 +9908,7 @@ func (x *ProviderCredentialRefreshMaterial) String() string { func (*ProviderCredentialRefreshMaterial) ProtoMessage() {} func (x *ProviderCredentialRefreshMaterial) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[113] + mi := &file_openshell_proto_msgTypes[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9711,7 +9921,7 @@ func (x *ProviderCredentialRefreshMaterial) ProtoReflect() protoreflect.Message // Deprecated: Use ProviderCredentialRefreshMaterial.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshMaterial) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{113} + return file_openshell_proto_rawDescGZIP(), []int{116} } func (x *ProviderCredentialRefreshMaterial) GetName() string { @@ -9756,7 +9966,7 @@ type ProviderCredentialRefreshOutput struct { func (x *ProviderCredentialRefreshOutput) Reset() { *x = ProviderCredentialRefreshOutput{} - mi := &file_openshell_proto_msgTypes[114] + mi := &file_openshell_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9768,7 +9978,7 @@ func (x *ProviderCredentialRefreshOutput) String() string { func (*ProviderCredentialRefreshOutput) ProtoMessage() {} func (x *ProviderCredentialRefreshOutput) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[114] + mi := &file_openshell_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9781,7 +9991,7 @@ func (x *ProviderCredentialRefreshOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefreshOutput.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshOutput) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{114} + return file_openshell_proto_rawDescGZIP(), []int{117} } func (x *ProviderCredentialRefreshOutput) GetOutput() string { @@ -9813,7 +10023,7 @@ type ProviderCredentialRefresh struct { func (x *ProviderCredentialRefresh) Reset() { *x = ProviderCredentialRefresh{} - mi := &file_openshell_proto_msgTypes[115] + mi := &file_openshell_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9825,7 +10035,7 @@ func (x *ProviderCredentialRefresh) String() string { func (*ProviderCredentialRefresh) ProtoMessage() {} func (x *ProviderCredentialRefresh) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[115] + mi := &file_openshell_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9838,7 +10048,7 @@ func (x *ProviderCredentialRefresh) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefresh.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefresh) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{115} + return file_openshell_proto_rawDescGZIP(), []int{118} } func (x *ProviderCredentialRefresh) GetStrategy() ProviderCredentialRefreshStrategy { @@ -9919,7 +10129,7 @@ type ProviderCredentialRefreshStatus struct { func (x *ProviderCredentialRefreshStatus) Reset() { *x = ProviderCredentialRefreshStatus{} - mi := &file_openshell_proto_msgTypes[116] + mi := &file_openshell_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9931,7 +10141,7 @@ func (x *ProviderCredentialRefreshStatus) String() string { func (*ProviderCredentialRefreshStatus) ProtoMessage() {} func (x *ProviderCredentialRefreshStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[116] + mi := &file_openshell_proto_msgTypes[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9944,7 +10154,7 @@ func (x *ProviderCredentialRefreshStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefreshStatus.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{116} + return file_openshell_proto_rawDescGZIP(), []int{119} } func (x *ProviderCredentialRefreshStatus) GetProvider() string { @@ -10049,7 +10259,7 @@ type ProviderProfileDiscovery struct { func (x *ProviderProfileDiscovery) Reset() { *x = ProviderProfileDiscovery{} - mi := &file_openshell_proto_msgTypes[117] + mi := &file_openshell_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10061,7 +10271,7 @@ func (x *ProviderProfileDiscovery) String() string { func (*ProviderProfileDiscovery) ProtoMessage() {} func (x *ProviderProfileDiscovery) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[117] + mi := &file_openshell_proto_msgTypes[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10074,7 +10284,7 @@ func (x *ProviderProfileDiscovery) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileDiscovery.ProtoReflect.Descriptor instead. func (*ProviderProfileDiscovery) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{117} + return file_openshell_proto_rawDescGZIP(), []int{120} } func (x *ProviderProfileDiscovery) GetCredentials() []string { @@ -10096,7 +10306,7 @@ type GetProviderRefreshStatusRequest struct { func (x *GetProviderRefreshStatusRequest) Reset() { *x = GetProviderRefreshStatusRequest{} - mi := &file_openshell_proto_msgTypes[118] + mi := &file_openshell_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10108,7 +10318,7 @@ func (x *GetProviderRefreshStatusRequest) String() string { func (*GetProviderRefreshStatusRequest) ProtoMessage() {} func (x *GetProviderRefreshStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[118] + mi := &file_openshell_proto_msgTypes[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10121,7 +10331,7 @@ func (x *GetProviderRefreshStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRefreshStatusRequest.ProtoReflect.Descriptor instead. func (*GetProviderRefreshStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{118} + return file_openshell_proto_rawDescGZIP(), []int{121} } func (x *GetProviderRefreshStatusRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -10154,7 +10364,7 @@ type GetProviderRefreshStatusResponse struct { func (x *GetProviderRefreshStatusResponse) Reset() { *x = GetProviderRefreshStatusResponse{} - mi := &file_openshell_proto_msgTypes[119] + mi := &file_openshell_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10166,7 +10376,7 @@ func (x *GetProviderRefreshStatusResponse) String() string { func (*GetProviderRefreshStatusResponse) ProtoMessage() {} func (x *GetProviderRefreshStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[119] + mi := &file_openshell_proto_msgTypes[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10179,7 +10389,7 @@ func (x *GetProviderRefreshStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRefreshStatusResponse.ProtoReflect.Descriptor instead. func (*GetProviderRefreshStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{119} + return file_openshell_proto_rawDescGZIP(), []int{122} } func (x *GetProviderRefreshStatusResponse) GetCredentials() []*ProviderCredentialRefreshStatus { @@ -10211,7 +10421,7 @@ type ConfigureProviderRefreshRequest struct { func (x *ConfigureProviderRefreshRequest) Reset() { *x = ConfigureProviderRefreshRequest{} - mi := &file_openshell_proto_msgTypes[120] + mi := &file_openshell_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10223,7 +10433,7 @@ func (x *ConfigureProviderRefreshRequest) String() string { func (*ConfigureProviderRefreshRequest) ProtoMessage() {} func (x *ConfigureProviderRefreshRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[120] + mi := &file_openshell_proto_msgTypes[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10236,7 +10446,7 @@ func (x *ConfigureProviderRefreshRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProviderRefreshRequest.ProtoReflect.Descriptor instead. func (*ConfigureProviderRefreshRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{120} + return file_openshell_proto_rawDescGZIP(), []int{123} } func (x *ConfigureProviderRefreshRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -10304,7 +10514,7 @@ type ConfigureProviderRefreshResponse struct { func (x *ConfigureProviderRefreshResponse) Reset() { *x = ConfigureProviderRefreshResponse{} - mi := &file_openshell_proto_msgTypes[121] + mi := &file_openshell_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10316,7 +10526,7 @@ func (x *ConfigureProviderRefreshResponse) String() string { func (*ConfigureProviderRefreshResponse) ProtoMessage() {} func (x *ConfigureProviderRefreshResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[121] + mi := &file_openshell_proto_msgTypes[124] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10329,7 +10539,7 @@ func (x *ConfigureProviderRefreshResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProviderRefreshResponse.ProtoReflect.Descriptor instead. func (*ConfigureProviderRefreshResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{121} + return file_openshell_proto_rawDescGZIP(), []int{124} } func (x *ConfigureProviderRefreshResponse) GetStatus() *ProviderCredentialRefreshStatus { @@ -10354,7 +10564,7 @@ type RotateProviderCredentialRequest struct { func (x *RotateProviderCredentialRequest) Reset() { *x = RotateProviderCredentialRequest{} - mi := &file_openshell_proto_msgTypes[122] + mi := &file_openshell_proto_msgTypes[125] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10366,7 +10576,7 @@ func (x *RotateProviderCredentialRequest) String() string { func (*RotateProviderCredentialRequest) ProtoMessage() {} func (x *RotateProviderCredentialRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[122] + mi := &file_openshell_proto_msgTypes[125] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10379,7 +10589,7 @@ func (x *RotateProviderCredentialRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateProviderCredentialRequest.ProtoReflect.Descriptor instead. func (*RotateProviderCredentialRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{122} + return file_openshell_proto_rawDescGZIP(), []int{125} } func (x *RotateProviderCredentialRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -10419,7 +10629,7 @@ type RotateProviderCredentialResponse struct { func (x *RotateProviderCredentialResponse) Reset() { *x = RotateProviderCredentialResponse{} - mi := &file_openshell_proto_msgTypes[123] + mi := &file_openshell_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10431,7 +10641,7 @@ func (x *RotateProviderCredentialResponse) String() string { func (*RotateProviderCredentialResponse) ProtoMessage() {} func (x *RotateProviderCredentialResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[123] + mi := &file_openshell_proto_msgTypes[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10444,7 +10654,7 @@ func (x *RotateProviderCredentialResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateProviderCredentialResponse.ProtoReflect.Descriptor instead. func (*RotateProviderCredentialResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{123} + return file_openshell_proto_rawDescGZIP(), []int{126} } func (x *RotateProviderCredentialResponse) GetStatus() *ProviderCredentialRefreshStatus { @@ -10470,7 +10680,7 @@ type DeleteProviderRefreshRequest struct { func (x *DeleteProviderRefreshRequest) Reset() { *x = DeleteProviderRefreshRequest{} - mi := &file_openshell_proto_msgTypes[124] + mi := &file_openshell_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10482,7 +10692,7 @@ func (x *DeleteProviderRefreshRequest) String() string { func (*DeleteProviderRefreshRequest) ProtoMessage() {} func (x *DeleteProviderRefreshRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[124] + mi := &file_openshell_proto_msgTypes[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10495,7 +10705,7 @@ func (x *DeleteProviderRefreshRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRefreshRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderRefreshRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{124} + return file_openshell_proto_rawDescGZIP(), []int{127} } func (x *DeleteProviderRefreshRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -10542,7 +10752,7 @@ type DeleteProviderRefreshResponse struct { func (x *DeleteProviderRefreshResponse) Reset() { *x = DeleteProviderRefreshResponse{} - mi := &file_openshell_proto_msgTypes[125] + mi := &file_openshell_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10554,7 +10764,7 @@ func (x *DeleteProviderRefreshResponse) String() string { func (*DeleteProviderRefreshResponse) ProtoMessage() {} func (x *DeleteProviderRefreshResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[125] + mi := &file_openshell_proto_msgTypes[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10567,7 +10777,7 @@ func (x *DeleteProviderRefreshResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRefreshResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderRefreshResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{125} + return file_openshell_proto_rawDescGZIP(), []int{128} } func (x *DeleteProviderRefreshResponse) GetOutcome() DeletionOutcome { @@ -10610,7 +10820,7 @@ type ProviderProfile struct { func (x *ProviderProfile) Reset() { *x = ProviderProfile{} - mi := &file_openshell_proto_msgTypes[126] + mi := &file_openshell_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10622,7 +10832,7 @@ func (x *ProviderProfile) String() string { func (*ProviderProfile) ProtoMessage() {} func (x *ProviderProfile) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[126] + mi := &file_openshell_proto_msgTypes[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10635,7 +10845,7 @@ func (x *ProviderProfile) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfile.ProtoReflect.Descriptor instead. func (*ProviderProfile) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{126} + return file_openshell_proto_rawDescGZIP(), []int{129} } func (x *ProviderProfile) GetId() string { @@ -10751,7 +10961,7 @@ type ProviderProfileFile struct { func (x *ProviderProfileFile) Reset() { *x = ProviderProfileFile{} - mi := &file_openshell_proto_msgTypes[127] + mi := &file_openshell_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10763,7 +10973,7 @@ func (x *ProviderProfileFile) String() string { func (*ProviderProfileFile) ProtoMessage() {} func (x *ProviderProfileFile) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[127] + mi := &file_openshell_proto_msgTypes[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10776,7 +10986,7 @@ func (x *ProviderProfileFile) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileFile.ProtoReflect.Descriptor instead. func (*ProviderProfileFile) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{127} + return file_openshell_proto_rawDescGZIP(), []int{130} } func (x *ProviderProfileFile) GetPath() string { @@ -10810,7 +11020,7 @@ type ProviderProfileResponse struct { func (x *ProviderProfileResponse) Reset() { *x = ProviderProfileResponse{} - mi := &file_openshell_proto_msgTypes[128] + mi := &file_openshell_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10822,7 +11032,7 @@ func (x *ProviderProfileResponse) String() string { func (*ProviderProfileResponse) ProtoMessage() {} func (x *ProviderProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[128] + mi := &file_openshell_proto_msgTypes[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10835,7 +11045,7 @@ func (x *ProviderProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileResponse.ProtoReflect.Descriptor instead. func (*ProviderProfileResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{128} + return file_openshell_proto_rawDescGZIP(), []int{131} } func (x *ProviderProfileResponse) GetProfile() *ProviderProfile { @@ -10857,7 +11067,7 @@ type ListProviderProfilesResponse struct { func (x *ListProviderProfilesResponse) Reset() { *x = ListProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[129] + mi := &file_openshell_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10869,7 +11079,7 @@ func (x *ListProviderProfilesResponse) String() string { func (*ListProviderProfilesResponse) ProtoMessage() {} func (x *ListProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[129] + mi := &file_openshell_proto_msgTypes[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10882,7 +11092,7 @@ func (x *ListProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*ListProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{129} + return file_openshell_proto_rawDescGZIP(), []int{132} } func (x *ListProviderProfilesResponse) GetProfiles() []*ProviderProfile { @@ -10914,7 +11124,7 @@ type ImportProviderProfilesRequest struct { func (x *ImportProviderProfilesRequest) Reset() { *x = ImportProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[130] + mi := &file_openshell_proto_msgTypes[133] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10926,7 +11136,7 @@ func (x *ImportProviderProfilesRequest) String() string { func (*ImportProviderProfilesRequest) ProtoMessage() {} func (x *ImportProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[130] + mi := &file_openshell_proto_msgTypes[133] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10939,7 +11149,7 @@ func (x *ImportProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*ImportProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{130} + return file_openshell_proto_rawDescGZIP(), []int{133} } func (x *ImportProviderProfilesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -10975,7 +11185,7 @@ type ImportProviderProfilesResponse struct { func (x *ImportProviderProfilesResponse) Reset() { *x = ImportProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[131] + mi := &file_openshell_proto_msgTypes[134] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10987,7 +11197,7 @@ func (x *ImportProviderProfilesResponse) String() string { func (*ImportProviderProfilesResponse) ProtoMessage() {} func (x *ImportProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[131] + mi := &file_openshell_proto_msgTypes[134] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11000,7 +11210,7 @@ func (x *ImportProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*ImportProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{131} + return file_openshell_proto_rawDescGZIP(), []int{134} } func (x *ImportProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -11046,7 +11256,7 @@ type UpdateProviderProfilesRequest struct { func (x *UpdateProviderProfilesRequest) Reset() { *x = UpdateProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[132] + mi := &file_openshell_proto_msgTypes[135] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11058,7 +11268,7 @@ func (x *UpdateProviderProfilesRequest) String() string { func (*UpdateProviderProfilesRequest) ProtoMessage() {} func (x *UpdateProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[132] + mi := &file_openshell_proto_msgTypes[135] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11071,7 +11281,7 @@ func (x *UpdateProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*UpdateProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{132} + return file_openshell_proto_rawDescGZIP(), []int{135} } func (x *UpdateProviderProfilesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -11121,7 +11331,7 @@ type UpdateProviderProfilesResponse struct { func (x *UpdateProviderProfilesResponse) Reset() { *x = UpdateProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[133] + mi := &file_openshell_proto_msgTypes[136] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11133,7 +11343,7 @@ func (x *UpdateProviderProfilesResponse) String() string { func (*UpdateProviderProfilesResponse) ProtoMessage() {} func (x *UpdateProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[133] + mi := &file_openshell_proto_msgTypes[136] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11146,7 +11356,7 @@ func (x *UpdateProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*UpdateProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{133} + return file_openshell_proto_rawDescGZIP(), []int{136} } func (x *UpdateProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -11182,7 +11392,7 @@ type LintProviderProfilesRequest struct { func (x *LintProviderProfilesRequest) Reset() { *x = LintProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[134] + mi := &file_openshell_proto_msgTypes[137] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11194,7 +11404,7 @@ func (x *LintProviderProfilesRequest) String() string { func (*LintProviderProfilesRequest) ProtoMessage() {} func (x *LintProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[134] + mi := &file_openshell_proto_msgTypes[137] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11207,7 +11417,7 @@ func (x *LintProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LintProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*LintProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{134} + return file_openshell_proto_rawDescGZIP(), []int{137} } func (x *LintProviderProfilesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -11238,7 +11448,7 @@ type LintProviderProfilesResponse struct { func (x *LintProviderProfilesResponse) Reset() { *x = LintProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[135] + mi := &file_openshell_proto_msgTypes[138] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11250,7 +11460,7 @@ func (x *LintProviderProfilesResponse) String() string { func (*LintProviderProfilesResponse) ProtoMessage() {} func (x *LintProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[135] + mi := &file_openshell_proto_msgTypes[138] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11263,7 +11473,7 @@ func (x *LintProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LintProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*LintProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{135} + return file_openshell_proto_rawDescGZIP(), []int{138} } func (x *LintProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -11297,7 +11507,7 @@ type DeleteProviderResponse struct { func (x *DeleteProviderResponse) Reset() { *x = DeleteProviderResponse{} - mi := &file_openshell_proto_msgTypes[136] + mi := &file_openshell_proto_msgTypes[139] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11309,7 +11519,7 @@ func (x *DeleteProviderResponse) String() string { func (*DeleteProviderResponse) ProtoMessage() {} func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[136] + mi := &file_openshell_proto_msgTypes[139] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11322,7 +11532,7 @@ func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{136} + return file_openshell_proto_rawDescGZIP(), []int{139} } func (x *DeleteProviderResponse) GetOutcome() DeletionOutcome { @@ -11348,7 +11558,7 @@ type DeleteProviderProfileRequest struct { func (x *DeleteProviderProfileRequest) Reset() { *x = DeleteProviderProfileRequest{} - mi := &file_openshell_proto_msgTypes[137] + mi := &file_openshell_proto_msgTypes[140] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11360,7 +11570,7 @@ func (x *DeleteProviderProfileRequest) String() string { func (*DeleteProviderProfileRequest) ProtoMessage() {} func (x *DeleteProviderProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[137] + mi := &file_openshell_proto_msgTypes[140] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11373,7 +11583,7 @@ func (x *DeleteProviderProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderProfileRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderProfileRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{137} + return file_openshell_proto_rawDescGZIP(), []int{140} } func (x *DeleteProviderProfileRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -11414,7 +11624,7 @@ type DeleteProviderProfileResponse struct { func (x *DeleteProviderProfileResponse) Reset() { *x = DeleteProviderProfileResponse{} - mi := &file_openshell_proto_msgTypes[138] + mi := &file_openshell_proto_msgTypes[141] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11426,7 +11636,7 @@ func (x *DeleteProviderProfileResponse) String() string { func (*DeleteProviderProfileResponse) ProtoMessage() {} func (x *DeleteProviderProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[138] + mi := &file_openshell_proto_msgTypes[141] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11439,7 +11649,7 @@ func (x *DeleteProviderProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderProfileResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderProfileResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{138} + return file_openshell_proto_rawDescGZIP(), []int{141} } func (x *DeleteProviderProfileResponse) GetOutcome() DeletionOutcome { @@ -11468,7 +11678,7 @@ type GetSandboxProviderEnvironmentRequest struct { func (x *GetSandboxProviderEnvironmentRequest) Reset() { *x = GetSandboxProviderEnvironmentRequest{} - mi := &file_openshell_proto_msgTypes[139] + mi := &file_openshell_proto_msgTypes[142] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11480,7 +11690,7 @@ func (x *GetSandboxProviderEnvironmentRequest) String() string { func (*GetSandboxProviderEnvironmentRequest) ProtoMessage() {} func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[139] + mi := &file_openshell_proto_msgTypes[142] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11493,7 +11703,7 @@ func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use GetSandboxProviderEnvironmentRequest.ProtoReflect.Descriptor instead. func (*GetSandboxProviderEnvironmentRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{139} + return file_openshell_proto_rawDescGZIP(), []int{142} } func (x *GetSandboxProviderEnvironmentRequest) GetSandboxId() string { @@ -11529,7 +11739,7 @@ type StaticCredentialEndpointBinding struct { func (x *StaticCredentialEndpointBinding) Reset() { *x = StaticCredentialEndpointBinding{} - mi := &file_openshell_proto_msgTypes[140] + mi := &file_openshell_proto_msgTypes[143] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11541,7 +11751,7 @@ func (x *StaticCredentialEndpointBinding) String() string { func (*StaticCredentialEndpointBinding) ProtoMessage() {} func (x *StaticCredentialEndpointBinding) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[140] + mi := &file_openshell_proto_msgTypes[143] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11554,7 +11764,7 @@ func (x *StaticCredentialEndpointBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticCredentialEndpointBinding.ProtoReflect.Descriptor instead. func (*StaticCredentialEndpointBinding) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{140} + return file_openshell_proto_rawDescGZIP(), []int{143} } func (x *StaticCredentialEndpointBinding) GetHost() string { @@ -11598,7 +11808,7 @@ type StaticCredentialBinding struct { func (x *StaticCredentialBinding) Reset() { *x = StaticCredentialBinding{} - mi := &file_openshell_proto_msgTypes[141] + mi := &file_openshell_proto_msgTypes[144] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11610,7 +11820,7 @@ func (x *StaticCredentialBinding) String() string { func (*StaticCredentialBinding) ProtoMessage() {} func (x *StaticCredentialBinding) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[141] + mi := &file_openshell_proto_msgTypes[144] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11623,7 +11833,7 @@ func (x *StaticCredentialBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticCredentialBinding.ProtoReflect.Descriptor instead. func (*StaticCredentialBinding) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{141} + return file_openshell_proto_rawDescGZIP(), []int{144} } func (x *StaticCredentialBinding) GetEndpoints() []*StaticCredentialEndpointBinding { @@ -11693,7 +11903,7 @@ type GetSandboxProviderEnvironmentResponse struct { func (x *GetSandboxProviderEnvironmentResponse) Reset() { *x = GetSandboxProviderEnvironmentResponse{} - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[145] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11705,7 +11915,7 @@ func (x *GetSandboxProviderEnvironmentResponse) String() string { func (*GetSandboxProviderEnvironmentResponse) ProtoMessage() {} func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[145] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11718,7 +11928,7 @@ func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use GetSandboxProviderEnvironmentResponse.ProtoReflect.Descriptor instead. func (*GetSandboxProviderEnvironmentResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{142} + return file_openshell_proto_rawDescGZIP(), []int{145} } func (x *GetSandboxProviderEnvironmentResponse) GetEnvironment() map[string]string { @@ -11827,7 +12037,7 @@ type ProviderEnvironmentValue struct { func (x *ProviderEnvironmentValue) Reset() { *x = ProviderEnvironmentValue{} - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[146] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11839,7 +12049,7 @@ func (x *ProviderEnvironmentValue) String() string { func (*ProviderEnvironmentValue) ProtoMessage() {} func (x *ProviderEnvironmentValue) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[146] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11852,7 +12062,7 @@ func (x *ProviderEnvironmentValue) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderEnvironmentValue.ProtoReflect.Descriptor instead. func (*ProviderEnvironmentValue) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{143} + return file_openshell_proto_rawDescGZIP(), []int{146} } func (x *ProviderEnvironmentValue) GetName() string { @@ -11918,7 +12128,7 @@ type ProviderEnvironmentSnapshot struct { func (x *ProviderEnvironmentSnapshot) Reset() { *x = ProviderEnvironmentSnapshot{} - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[147] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11930,7 +12140,7 @@ func (x *ProviderEnvironmentSnapshot) String() string { func (*ProviderEnvironmentSnapshot) ProtoMessage() {} func (x *ProviderEnvironmentSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[147] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11943,7 +12153,7 @@ func (x *ProviderEnvironmentSnapshot) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderEnvironmentSnapshot.ProtoReflect.Descriptor instead. func (*ProviderEnvironmentSnapshot) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{144} + return file_openshell_proto_rawDescGZIP(), []int{147} } func (x *ProviderEnvironmentSnapshot) GetProviderEnvRevision() uint64 { @@ -12012,7 +12222,7 @@ type ExchangeProviderSubjectTokenRequest struct { func (x *ExchangeProviderSubjectTokenRequest) Reset() { *x = ExchangeProviderSubjectTokenRequest{} - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[148] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12024,7 +12234,7 @@ func (x *ExchangeProviderSubjectTokenRequest) String() string { func (*ExchangeProviderSubjectTokenRequest) ProtoMessage() {} func (x *ExchangeProviderSubjectTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[148] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12037,7 +12247,7 @@ func (x *ExchangeProviderSubjectTokenRequest) ProtoReflect() protoreflect.Messag // Deprecated: Use ExchangeProviderSubjectTokenRequest.ProtoReflect.Descriptor instead. func (*ExchangeProviderSubjectTokenRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{145} + return file_openshell_proto_rawDescGZIP(), []int{148} } func (x *ExchangeProviderSubjectTokenRequest) GetSandboxId() string { @@ -12079,7 +12289,7 @@ type ExchangeProviderSubjectTokenResponse struct { func (x *ExchangeProviderSubjectTokenResponse) Reset() { *x = ExchangeProviderSubjectTokenResponse{} - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[149] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12091,7 +12301,7 @@ func (x *ExchangeProviderSubjectTokenResponse) String() string { func (*ExchangeProviderSubjectTokenResponse) ProtoMessage() {} func (x *ExchangeProviderSubjectTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[149] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12104,7 +12314,7 @@ func (x *ExchangeProviderSubjectTokenResponse) ProtoReflect() protoreflect.Messa // Deprecated: Use ExchangeProviderSubjectTokenResponse.ProtoReflect.Descriptor instead. func (*ExchangeProviderSubjectTokenResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{146} + return file_openshell_proto_rawDescGZIP(), []int{149} } func (x *ExchangeProviderSubjectTokenResponse) GetAccessToken() string { @@ -12178,7 +12388,7 @@ type UpdateConfigRequest struct { func (x *UpdateConfigRequest) Reset() { *x = UpdateConfigRequest{} - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[150] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12190,7 +12400,7 @@ func (x *UpdateConfigRequest) String() string { func (*UpdateConfigRequest) ProtoMessage() {} func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[150] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12203,7 +12413,7 @@ func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigRequest.ProtoReflect.Descriptor instead. func (*UpdateConfigRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{147} + return file_openshell_proto_rawDescGZIP(), []int{150} } func (x *UpdateConfigRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -12300,7 +12510,7 @@ type PolicyMergeOperation struct { func (x *PolicyMergeOperation) Reset() { *x = PolicyMergeOperation{} - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[151] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12312,7 +12522,7 @@ func (x *PolicyMergeOperation) String() string { func (*PolicyMergeOperation) ProtoMessage() {} func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[151] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12325,7 +12535,7 @@ func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyMergeOperation.ProtoReflect.Descriptor instead. func (*PolicyMergeOperation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{148} + return file_openshell_proto_rawDescGZIP(), []int{151} } func (x *PolicyMergeOperation) GetOperation() isPolicyMergeOperation_Operation { @@ -12439,7 +12649,7 @@ type AddNetworkRule struct { func (x *AddNetworkRule) Reset() { *x = AddNetworkRule{} - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[152] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12451,7 +12661,7 @@ func (x *AddNetworkRule) String() string { func (*AddNetworkRule) ProtoMessage() {} func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[152] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12464,7 +12674,7 @@ func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use AddNetworkRule.ProtoReflect.Descriptor instead. func (*AddNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{149} + return file_openshell_proto_rawDescGZIP(), []int{152} } func (x *AddNetworkRule) GetRuleName() string { @@ -12492,7 +12702,7 @@ type RemoveNetworkEndpoint struct { func (x *RemoveNetworkEndpoint) Reset() { *x = RemoveNetworkEndpoint{} - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[153] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12504,7 +12714,7 @@ func (x *RemoveNetworkEndpoint) String() string { func (*RemoveNetworkEndpoint) ProtoMessage() {} func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[153] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12517,7 +12727,7 @@ func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkEndpoint.ProtoReflect.Descriptor instead. func (*RemoveNetworkEndpoint) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{150} + return file_openshell_proto_rawDescGZIP(), []int{153} } func (x *RemoveNetworkEndpoint) GetRuleName() string { @@ -12550,7 +12760,7 @@ type RemoveNetworkRule struct { func (x *RemoveNetworkRule) Reset() { *x = RemoveNetworkRule{} - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[154] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12562,7 +12772,7 @@ func (x *RemoveNetworkRule) String() string { func (*RemoveNetworkRule) ProtoMessage() {} func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[154] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12575,7 +12785,7 @@ func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkRule.ProtoReflect.Descriptor instead. func (*RemoveNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{151} + return file_openshell_proto_rawDescGZIP(), []int{154} } func (x *RemoveNetworkRule) GetRuleName() string { @@ -12604,7 +12814,7 @@ type L7RuleTarget struct { func (x *L7RuleTarget) Reset() { *x = L7RuleTarget{} - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[155] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12616,7 +12826,7 @@ func (x *L7RuleTarget) String() string { func (*L7RuleTarget) ProtoMessage() {} func (x *L7RuleTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[155] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12629,7 +12839,7 @@ func (x *L7RuleTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use L7RuleTarget.ProtoReflect.Descriptor instead. func (*L7RuleTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{152} + return file_openshell_proto_rawDescGZIP(), []int{155} } func (x *L7RuleTarget) GetRuleName() string { @@ -12684,7 +12894,7 @@ type AddDenyRules struct { func (x *AddDenyRules) Reset() { *x = AddDenyRules{} - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[156] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12696,7 +12906,7 @@ func (x *AddDenyRules) String() string { func (*AddDenyRules) ProtoMessage() {} func (x *AddDenyRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[156] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12709,7 +12919,7 @@ func (x *AddDenyRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddDenyRules.ProtoReflect.Descriptor instead. func (*AddDenyRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{153} + return file_openshell_proto_rawDescGZIP(), []int{156} } func (x *AddDenyRules) GetDenyRules() []*sandboxv1.L7DenyRule { @@ -12736,7 +12946,7 @@ type AddAllowRules struct { func (x *AddAllowRules) Reset() { *x = AddAllowRules{} - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[157] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12748,7 +12958,7 @@ func (x *AddAllowRules) String() string { func (*AddAllowRules) ProtoMessage() {} func (x *AddAllowRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[157] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12761,7 +12971,7 @@ func (x *AddAllowRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddAllowRules.ProtoReflect.Descriptor instead. func (*AddAllowRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{154} + return file_openshell_proto_rawDescGZIP(), []int{157} } func (x *AddAllowRules) GetRules() []*sandboxv1.L7Rule { @@ -12788,7 +12998,7 @@ type RemoveNetworkBinary struct { func (x *RemoveNetworkBinary) Reset() { *x = RemoveNetworkBinary{} - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[158] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12800,7 +13010,7 @@ func (x *RemoveNetworkBinary) String() string { func (*RemoveNetworkBinary) ProtoMessage() {} func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[158] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12813,7 +13023,7 @@ func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkBinary.ProtoReflect.Descriptor instead. func (*RemoveNetworkBinary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{155} + return file_openshell_proto_rawDescGZIP(), []int{158} } func (x *RemoveNetworkBinary) GetRuleName() string { @@ -12849,7 +13059,7 @@ type UpdateConfigResponse struct { func (x *UpdateConfigResponse) Reset() { *x = UpdateConfigResponse{} - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[159] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12861,7 +13071,7 @@ func (x *UpdateConfigResponse) String() string { func (*UpdateConfigResponse) ProtoMessage() {} func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[159] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12874,7 +13084,7 @@ func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigResponse.ProtoReflect.Descriptor instead. func (*UpdateConfigResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{156} + return file_openshell_proto_rawDescGZIP(), []int{159} } func (x *UpdateConfigResponse) GetVersion() uint32 { @@ -12928,7 +13138,7 @@ type GetSandboxPolicyStatusRequest struct { func (x *GetSandboxPolicyStatusRequest) Reset() { *x = GetSandboxPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[160] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12940,7 +13150,7 @@ func (x *GetSandboxPolicyStatusRequest) String() string { func (*GetSandboxPolicyStatusRequest) ProtoMessage() {} func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[160] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12953,7 +13163,7 @@ func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{157} + return file_openshell_proto_rawDescGZIP(), []int{160} } func (x *GetSandboxPolicyStatusRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -12997,7 +13207,7 @@ type GetSandboxPolicyStatusResponse struct { func (x *GetSandboxPolicyStatusResponse) Reset() { *x = GetSandboxPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[161] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13009,7 +13219,7 @@ func (x *GetSandboxPolicyStatusResponse) String() string { func (*GetSandboxPolicyStatusResponse) ProtoMessage() {} func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[161] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13022,7 +13232,7 @@ func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{158} + return file_openshell_proto_rawDescGZIP(), []int{161} } func (x *GetSandboxPolicyStatusResponse) GetRevision() *SandboxPolicyRevision { @@ -13059,7 +13269,7 @@ type ListSandboxPoliciesRequest struct { func (x *ListSandboxPoliciesRequest) Reset() { *x = ListSandboxPoliciesRequest{} - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[162] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13071,7 +13281,7 @@ func (x *ListSandboxPoliciesRequest) String() string { func (*ListSandboxPoliciesRequest) ProtoMessage() {} func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[162] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13084,7 +13294,7 @@ func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesRequest.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{159} + return file_openshell_proto_rawDescGZIP(), []int{162} } func (x *ListSandboxPoliciesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -13136,7 +13346,7 @@ type ListSandboxPoliciesResponse struct { func (x *ListSandboxPoliciesResponse) Reset() { *x = ListSandboxPoliciesResponse{} - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[163] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13148,7 +13358,7 @@ func (x *ListSandboxPoliciesResponse) String() string { func (*ListSandboxPoliciesResponse) ProtoMessage() {} func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[163] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13161,7 +13371,7 @@ func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesResponse.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{160} + return file_openshell_proto_rawDescGZIP(), []int{163} } func (x *ListSandboxPoliciesResponse) GetRevisions() []*SandboxPolicyRevision { @@ -13195,7 +13405,7 @@ type ReportPolicyStatusRequest struct { func (x *ReportPolicyStatusRequest) Reset() { *x = ReportPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[164] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13207,7 +13417,7 @@ func (x *ReportPolicyStatusRequest) String() string { func (*ReportPolicyStatusRequest) ProtoMessage() {} func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[164] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13220,7 +13430,7 @@ func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{161} + return file_openshell_proto_rawDescGZIP(), []int{164} } func (x *ReportPolicyStatusRequest) GetSandboxId() string { @@ -13260,7 +13470,7 @@ type ReportPolicyStatusResponse struct { func (x *ReportPolicyStatusResponse) Reset() { *x = ReportPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[165] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13272,7 +13482,7 @@ func (x *ReportPolicyStatusResponse) String() string { func (*ReportPolicyStatusResponse) ProtoMessage() {} func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[165] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13285,7 +13495,7 @@ func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{162} + return file_openshell_proto_rawDescGZIP(), []int{165} } type SandboxConfigurationAdmission struct { @@ -13303,7 +13513,7 @@ type SandboxConfigurationAdmission struct { func (x *SandboxConfigurationAdmission) Reset() { *x = SandboxConfigurationAdmission{} - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[166] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13315,7 +13525,7 @@ func (x *SandboxConfigurationAdmission) String() string { func (*SandboxConfigurationAdmission) ProtoMessage() {} func (x *SandboxConfigurationAdmission) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[166] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13328,7 +13538,7 @@ func (x *SandboxConfigurationAdmission) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxConfigurationAdmission.ProtoReflect.Descriptor instead. func (*SandboxConfigurationAdmission) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{163} + return file_openshell_proto_rawDescGZIP(), []int{166} } func (x *SandboxConfigurationAdmission) GetInstanceId() string { @@ -13392,7 +13602,7 @@ type ReportSandboxConfigurationRequest struct { func (x *ReportSandboxConfigurationRequest) Reset() { *x = ReportSandboxConfigurationRequest{} - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[167] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13404,7 +13614,7 @@ func (x *ReportSandboxConfigurationRequest) String() string { func (*ReportSandboxConfigurationRequest) ProtoMessage() {} func (x *ReportSandboxConfigurationRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[167] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13417,7 +13627,7 @@ func (x *ReportSandboxConfigurationRequest) ProtoReflect() protoreflect.Message // Deprecated: Use ReportSandboxConfigurationRequest.ProtoReflect.Descriptor instead. func (*ReportSandboxConfigurationRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{164} + return file_openshell_proto_rawDescGZIP(), []int{167} } func (x *ReportSandboxConfigurationRequest) GetSandboxId() string { @@ -13449,7 +13659,7 @@ type ReportSandboxConfigurationResponse struct { func (x *ReportSandboxConfigurationResponse) Reset() { *x = ReportSandboxConfigurationResponse{} - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[168] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13461,7 +13671,7 @@ func (x *ReportSandboxConfigurationResponse) String() string { func (*ReportSandboxConfigurationResponse) ProtoMessage() {} func (x *ReportSandboxConfigurationResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[168] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13474,7 +13684,7 @@ func (x *ReportSandboxConfigurationResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ReportSandboxConfigurationResponse.ProtoReflect.Descriptor instead. func (*ReportSandboxConfigurationResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{165} + return file_openshell_proto_rawDescGZIP(), []int{168} } // A versioned policy revision with metadata. @@ -13507,7 +13717,7 @@ type SandboxPolicyRevision struct { func (x *SandboxPolicyRevision) Reset() { *x = SandboxPolicyRevision{} - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[169] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13519,7 +13729,7 @@ func (x *SandboxPolicyRevision) String() string { func (*SandboxPolicyRevision) ProtoMessage() {} func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[169] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13532,7 +13742,7 @@ func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxPolicyRevision.ProtoReflect.Descriptor instead. func (*SandboxPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{166} + return file_openshell_proto_rawDescGZIP(), []int{169} } func (x *SandboxPolicyRevision) GetVersion() uint32 { @@ -13612,7 +13822,7 @@ type GetSandboxLogsRequest struct { func (x *GetSandboxLogsRequest) Reset() { *x = GetSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[170] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13624,7 +13834,7 @@ func (x *GetSandboxLogsRequest) String() string { func (*GetSandboxLogsRequest) ProtoMessage() {} func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[170] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13637,7 +13847,7 @@ func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*GetSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{167} + return file_openshell_proto_rawDescGZIP(), []int{170} } func (x *GetSandboxLogsRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -13695,7 +13905,7 @@ type PushSandboxLogsRequest struct { func (x *PushSandboxLogsRequest) Reset() { *x = PushSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[171] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13707,7 +13917,7 @@ func (x *PushSandboxLogsRequest) String() string { func (*PushSandboxLogsRequest) ProtoMessage() {} func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[171] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13720,7 +13930,7 @@ func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*PushSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{168} + return file_openshell_proto_rawDescGZIP(), []int{171} } func (x *PushSandboxLogsRequest) GetSandboxId() string { @@ -13746,7 +13956,7 @@ type PushSandboxLogsResponse struct { func (x *PushSandboxLogsResponse) Reset() { *x = PushSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[172] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13758,7 +13968,7 @@ func (x *PushSandboxLogsResponse) String() string { func (*PushSandboxLogsResponse) ProtoMessage() {} func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[172] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13771,7 +13981,7 @@ func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*PushSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{169} + return file_openshell_proto_rawDescGZIP(), []int{172} } // Get sandbox logs response. @@ -13787,7 +13997,7 @@ type GetSandboxLogsResponse struct { func (x *GetSandboxLogsResponse) Reset() { *x = GetSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[173] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13799,7 +14009,7 @@ func (x *GetSandboxLogsResponse) String() string { func (*GetSandboxLogsResponse) ProtoMessage() {} func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[173] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13812,7 +14022,7 @@ func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*GetSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{170} + return file_openshell_proto_rawDescGZIP(), []int{173} } func (x *GetSandboxLogsResponse) GetLogs() []*SandboxLogLine { @@ -13846,7 +14056,7 @@ type SupervisorMessage struct { func (x *SupervisorMessage) Reset() { *x = SupervisorMessage{} - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[174] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13858,7 +14068,7 @@ func (x *SupervisorMessage) String() string { func (*SupervisorMessage) ProtoMessage() {} func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[174] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13871,7 +14081,7 @@ func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorMessage.ProtoReflect.Descriptor instead. func (*SupervisorMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{171} + return file_openshell_proto_rawDescGZIP(), []int{174} } func (x *SupervisorMessage) GetPayload() isSupervisorMessage_Payload { @@ -13979,7 +14189,7 @@ type GatewayMessage struct { func (x *GatewayMessage) Reset() { *x = GatewayMessage{} - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[175] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13991,7 +14201,7 @@ func (x *GatewayMessage) String() string { func (*GatewayMessage) ProtoMessage() {} func (x *GatewayMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[175] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14004,7 +14214,7 @@ func (x *GatewayMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayMessage.ProtoReflect.Descriptor instead. func (*GatewayMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{172} + return file_openshell_proto_rawDescGZIP(), []int{175} } func (x *GatewayMessage) GetPayload() isGatewayMessage_Payload { @@ -14154,7 +14364,7 @@ type SupervisorHello struct { func (x *SupervisorHello) Reset() { *x = SupervisorHello{} - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[176] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14166,7 +14376,7 @@ func (x *SupervisorHello) String() string { func (*SupervisorHello) ProtoMessage() {} func (x *SupervisorHello) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[176] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14179,7 +14389,7 @@ func (x *SupervisorHello) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHello.ProtoReflect.Descriptor instead. func (*SupervisorHello) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{173} + return file_openshell_proto_rawDescGZIP(), []int{176} } func (x *SupervisorHello) GetSandboxId() string { @@ -14252,7 +14462,7 @@ type SessionAccepted struct { func (x *SessionAccepted) Reset() { *x = SessionAccepted{} - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[177] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14264,7 +14474,7 @@ func (x *SessionAccepted) String() string { func (*SessionAccepted) ProtoMessage() {} func (x *SessionAccepted) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[177] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14277,7 +14487,7 @@ func (x *SessionAccepted) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionAccepted.ProtoReflect.Descriptor instead. func (*SessionAccepted) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{174} + return file_openshell_proto_rawDescGZIP(), []int{177} } func (x *SessionAccepted) GetSessionId() string { @@ -14326,7 +14536,7 @@ type ConfigUpdate struct { func (x *ConfigUpdate) Reset() { *x = ConfigUpdate{} - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[178] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14338,7 +14548,7 @@ func (x *ConfigUpdate) String() string { func (*ConfigUpdate) ProtoMessage() {} func (x *ConfigUpdate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[178] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14351,7 +14561,7 @@ func (x *ConfigUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigUpdate.ProtoReflect.Descriptor instead. func (*ConfigUpdate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{175} + return file_openshell_proto_rawDescGZIP(), []int{178} } func (x *ConfigUpdate) GetUpdateId() string { @@ -14388,7 +14598,7 @@ type ConfigApplyFailure struct { func (x *ConfigApplyFailure) Reset() { *x = ConfigApplyFailure{} - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[179] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14400,7 +14610,7 @@ func (x *ConfigApplyFailure) String() string { func (*ConfigApplyFailure) ProtoMessage() {} func (x *ConfigApplyFailure) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[179] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14413,7 +14623,7 @@ func (x *ConfigApplyFailure) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigApplyFailure.ProtoReflect.Descriptor instead. func (*ConfigApplyFailure) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{176} + return file_openshell_proto_rawDescGZIP(), []int{179} } func (x *ConfigApplyFailure) GetCode() string { @@ -14457,7 +14667,7 @@ type ConfigUpdateResult struct { func (x *ConfigUpdateResult) Reset() { *x = ConfigUpdateResult{} - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[180] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14469,7 +14679,7 @@ func (x *ConfigUpdateResult) String() string { func (*ConfigUpdateResult) ProtoMessage() {} func (x *ConfigUpdateResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[180] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14482,7 +14692,7 @@ func (x *ConfigUpdateResult) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigUpdateResult.ProtoReflect.Descriptor instead. func (*ConfigUpdateResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{177} + return file_openshell_proto_rawDescGZIP(), []int{180} } func (x *ConfigUpdateResult) GetUpdateId() string { @@ -14533,7 +14743,7 @@ type PolicyLoadResult struct { func (x *PolicyLoadResult) Reset() { *x = PolicyLoadResult{} - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[181] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14545,7 +14755,7 @@ func (x *PolicyLoadResult) String() string { func (*PolicyLoadResult) ProtoMessage() {} func (x *PolicyLoadResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[181] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14558,7 +14768,7 @@ func (x *PolicyLoadResult) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyLoadResult.ProtoReflect.Descriptor instead. func (*PolicyLoadResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{178} + return file_openshell_proto_rawDescGZIP(), []int{181} } func (x *PolicyLoadResult) GetVersion() uint32 { @@ -14593,7 +14803,7 @@ type SessionRejected struct { func (x *SessionRejected) Reset() { *x = SessionRejected{} - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[182] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14605,7 +14815,7 @@ func (x *SessionRejected) String() string { func (*SessionRejected) ProtoMessage() {} func (x *SessionRejected) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[182] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14618,7 +14828,7 @@ func (x *SessionRejected) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionRejected.ProtoReflect.Descriptor instead. func (*SessionRejected) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{179} + return file_openshell_proto_rawDescGZIP(), []int{182} } func (x *SessionRejected) GetReason() string { @@ -14647,7 +14857,7 @@ type SessionRedirect struct { func (x *SessionRedirect) Reset() { *x = SessionRedirect{} - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[183] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14659,7 +14869,7 @@ func (x *SessionRedirect) String() string { func (*SessionRedirect) ProtoMessage() {} func (x *SessionRedirect) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[183] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14672,7 +14882,7 @@ func (x *SessionRedirect) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionRedirect.ProtoReflect.Descriptor instead. func (*SessionRedirect) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{180} + return file_openshell_proto_rawDescGZIP(), []int{183} } func (x *SessionRedirect) GetPeerEndpoint() string { @@ -14698,7 +14908,7 @@ type SupervisorHeartbeat struct { func (x *SupervisorHeartbeat) Reset() { *x = SupervisorHeartbeat{} - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[184] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14710,7 +14920,7 @@ func (x *SupervisorHeartbeat) String() string { func (*SupervisorHeartbeat) ProtoMessage() {} func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[184] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14723,7 +14933,7 @@ func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHeartbeat.ProtoReflect.Descriptor instead. func (*SupervisorHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{181} + return file_openshell_proto_rawDescGZIP(), []int{184} } // Gateway heartbeat. @@ -14735,7 +14945,7 @@ type GatewayHeartbeat struct { func (x *GatewayHeartbeat) Reset() { *x = GatewayHeartbeat{} - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[185] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14747,7 +14957,7 @@ func (x *GatewayHeartbeat) String() string { func (*GatewayHeartbeat) ProtoMessage() {} func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[185] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14760,7 +14970,7 @@ func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayHeartbeat.ProtoReflect.Descriptor instead. func (*GatewayHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{182} + return file_openshell_proto_rawDescGZIP(), []int{185} } // Terminal result reported before the supervisor shuts down. A successful RPC @@ -14777,7 +14987,7 @@ type ReportMainProcessExitRequest struct { func (x *ReportMainProcessExitRequest) Reset() { *x = ReportMainProcessExitRequest{} - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[186] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14789,7 +14999,7 @@ func (x *ReportMainProcessExitRequest) String() string { func (*ReportMainProcessExitRequest) ProtoMessage() {} func (x *ReportMainProcessExitRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[186] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14802,7 +15012,7 @@ func (x *ReportMainProcessExitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportMainProcessExitRequest.ProtoReflect.Descriptor instead. func (*ReportMainProcessExitRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{183} + return file_openshell_proto_rawDescGZIP(), []int{186} } func (x *ReportMainProcessExitRequest) GetSandboxId() string { @@ -14834,7 +15044,7 @@ type ReportMainProcessExitResponse struct { func (x *ReportMainProcessExitResponse) Reset() { *x = ReportMainProcessExitResponse{} - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[187] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14846,7 +15056,7 @@ func (x *ReportMainProcessExitResponse) String() string { func (*ReportMainProcessExitResponse) ProtoMessage() {} func (x *ReportMainProcessExitResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[187] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14859,7 +15069,7 @@ func (x *ReportMainProcessExitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportMainProcessExitResponse.ProtoReflect.Descriptor instead. func (*ReportMainProcessExitResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{184} + return file_openshell_proto_rawDescGZIP(), []int{187} } // Terminal-delivery completion reported after all expected foreground SSH @@ -14874,7 +15084,7 @@ type FinalizeMainProcessExitRequest struct { func (x *FinalizeMainProcessExitRequest) Reset() { *x = FinalizeMainProcessExitRequest{} - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[188] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14886,7 +15096,7 @@ func (x *FinalizeMainProcessExitRequest) String() string { func (*FinalizeMainProcessExitRequest) ProtoMessage() {} func (x *FinalizeMainProcessExitRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[188] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14899,7 +15109,7 @@ func (x *FinalizeMainProcessExitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeMainProcessExitRequest.ProtoReflect.Descriptor instead. func (*FinalizeMainProcessExitRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{185} + return file_openshell_proto_rawDescGZIP(), []int{188} } func (x *FinalizeMainProcessExitRequest) GetSandboxId() string { @@ -14924,7 +15134,7 @@ type FinalizeMainProcessExitResponse struct { func (x *FinalizeMainProcessExitResponse) Reset() { *x = FinalizeMainProcessExitResponse{} - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[189] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14936,7 +15146,7 @@ func (x *FinalizeMainProcessExitResponse) String() string { func (*FinalizeMainProcessExitResponse) ProtoMessage() {} func (x *FinalizeMainProcessExitResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[189] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14949,7 +15159,7 @@ func (x *FinalizeMainProcessExitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeMainProcessExitResponse.ProtoReflect.Descriptor instead. func (*FinalizeMainProcessExitResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{186} + return file_openshell_proto_rawDescGZIP(), []int{189} } // Gateway requests the supervisor to open a relay channel. @@ -14978,7 +15188,7 @@ type RelayOpen struct { func (x *RelayOpen) Reset() { *x = RelayOpen{} - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[190] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14990,7 +15200,7 @@ func (x *RelayOpen) String() string { func (*RelayOpen) ProtoMessage() {} func (x *RelayOpen) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[190] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15003,7 +15213,7 @@ func (x *RelayOpen) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpen.ProtoReflect.Descriptor instead. func (*RelayOpen) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{187} + return file_openshell_proto_rawDescGZIP(), []int{190} } func (x *RelayOpen) GetChannelId() string { @@ -15070,7 +15280,7 @@ type SshRelayTarget struct { func (x *SshRelayTarget) Reset() { *x = SshRelayTarget{} - mi := &file_openshell_proto_msgTypes[188] + mi := &file_openshell_proto_msgTypes[191] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15082,7 +15292,7 @@ func (x *SshRelayTarget) String() string { func (*SshRelayTarget) ProtoMessage() {} func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[188] + mi := &file_openshell_proto_msgTypes[191] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15095,7 +15305,7 @@ func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use SshRelayTarget.ProtoReflect.Descriptor instead. func (*SshRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{188} + return file_openshell_proto_rawDescGZIP(), []int{191} } // TCP target dialed by the supervisor from inside the sandbox. @@ -15111,7 +15321,7 @@ type TcpRelayTarget struct { func (x *TcpRelayTarget) Reset() { *x = TcpRelayTarget{} - mi := &file_openshell_proto_msgTypes[189] + mi := &file_openshell_proto_msgTypes[192] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15123,7 +15333,7 @@ func (x *TcpRelayTarget) String() string { func (*TcpRelayTarget) ProtoMessage() {} func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[189] + mi := &file_openshell_proto_msgTypes[192] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15136,7 +15346,7 @@ func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpRelayTarget.ProtoReflect.Descriptor instead. func (*TcpRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{189} + return file_openshell_proto_rawDescGZIP(), []int{192} } func (x *TcpRelayTarget) GetHost() string { @@ -15164,7 +15374,7 @@ type RelayInit struct { func (x *RelayInit) Reset() { *x = RelayInit{} - mi := &file_openshell_proto_msgTypes[190] + mi := &file_openshell_proto_msgTypes[193] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15176,7 +15386,7 @@ func (x *RelayInit) String() string { func (*RelayInit) ProtoMessage() {} func (x *RelayInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[190] + mi := &file_openshell_proto_msgTypes[193] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15189,7 +15399,7 @@ func (x *RelayInit) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayInit.ProtoReflect.Descriptor instead. func (*RelayInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{190} + return file_openshell_proto_rawDescGZIP(), []int{193} } func (x *RelayInit) GetChannelId() string { @@ -15216,7 +15426,7 @@ type RelayFrame struct { func (x *RelayFrame) Reset() { *x = RelayFrame{} - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[194] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15228,7 +15438,7 @@ func (x *RelayFrame) String() string { func (*RelayFrame) ProtoMessage() {} func (x *RelayFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[194] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15241,7 +15451,7 @@ func (x *RelayFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayFrame.ProtoReflect.Descriptor instead. func (*RelayFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{191} + return file_openshell_proto_rawDescGZIP(), []int{194} } func (x *RelayFrame) GetPayload() isRelayFrame_Payload { @@ -15301,7 +15511,7 @@ type PeerRelayInit struct { func (x *PeerRelayInit) Reset() { *x = PeerRelayInit{} - mi := &file_openshell_proto_msgTypes[192] + mi := &file_openshell_proto_msgTypes[195] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15313,7 +15523,7 @@ func (x *PeerRelayInit) String() string { func (*PeerRelayInit) ProtoMessage() {} func (x *PeerRelayInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[192] + mi := &file_openshell_proto_msgTypes[195] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15326,7 +15536,7 @@ func (x *PeerRelayInit) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerRelayInit.ProtoReflect.Descriptor instead. func (*PeerRelayInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{192} + return file_openshell_proto_rawDescGZIP(), []int{195} } func (x *PeerRelayInit) GetSandboxId() string { @@ -15364,7 +15574,7 @@ type PeerRelayFrame struct { func (x *PeerRelayFrame) Reset() { *x = PeerRelayFrame{} - mi := &file_openshell_proto_msgTypes[193] + mi := &file_openshell_proto_msgTypes[196] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15376,7 +15586,7 @@ func (x *PeerRelayFrame) String() string { func (*PeerRelayFrame) ProtoMessage() {} func (x *PeerRelayFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[193] + mi := &file_openshell_proto_msgTypes[196] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15389,7 +15599,7 @@ func (x *PeerRelayFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerRelayFrame.ProtoReflect.Descriptor instead. func (*PeerRelayFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{193} + return file_openshell_proto_rawDescGZIP(), []int{196} } func (x *PeerRelayFrame) GetPayload() isPeerRelayFrame_Payload { @@ -15444,7 +15654,7 @@ type PeerConfigSandboxTarget struct { func (x *PeerConfigSandboxTarget) Reset() { *x = PeerConfigSandboxTarget{} - mi := &file_openshell_proto_msgTypes[194] + mi := &file_openshell_proto_msgTypes[197] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15456,7 +15666,7 @@ func (x *PeerConfigSandboxTarget) String() string { func (*PeerConfigSandboxTarget) ProtoMessage() {} func (x *PeerConfigSandboxTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[194] + mi := &file_openshell_proto_msgTypes[197] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15469,7 +15679,7 @@ func (x *PeerConfigSandboxTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerConfigSandboxTarget.ProtoReflect.Descriptor instead. func (*PeerConfigSandboxTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{194} + return file_openshell_proto_rawDescGZIP(), []int{197} } func (x *PeerConfigSandboxTarget) GetSandboxId() string { @@ -15497,7 +15707,7 @@ type PeerConfigProviderTarget struct { func (x *PeerConfigProviderTarget) Reset() { *x = PeerConfigProviderTarget{} - mi := &file_openshell_proto_msgTypes[195] + mi := &file_openshell_proto_msgTypes[198] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15509,7 +15719,7 @@ func (x *PeerConfigProviderTarget) String() string { func (*PeerConfigProviderTarget) ProtoMessage() {} func (x *PeerConfigProviderTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[195] + mi := &file_openshell_proto_msgTypes[198] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15522,7 +15732,7 @@ func (x *PeerConfigProviderTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerConfigProviderTarget.ProtoReflect.Descriptor instead. func (*PeerConfigProviderTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{195} + return file_openshell_proto_rawDescGZIP(), []int{198} } func (x *PeerConfigProviderTarget) GetWorkspace() string { @@ -15554,7 +15764,7 @@ type PeerNotifyConfigUpdateRequest struct { func (x *PeerNotifyConfigUpdateRequest) Reset() { *x = PeerNotifyConfigUpdateRequest{} - mi := &file_openshell_proto_msgTypes[196] + mi := &file_openshell_proto_msgTypes[199] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15566,7 +15776,7 @@ func (x *PeerNotifyConfigUpdateRequest) String() string { func (*PeerNotifyConfigUpdateRequest) ProtoMessage() {} func (x *PeerNotifyConfigUpdateRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[196] + mi := &file_openshell_proto_msgTypes[199] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15579,7 +15789,7 @@ func (x *PeerNotifyConfigUpdateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerNotifyConfigUpdateRequest.ProtoReflect.Descriptor instead. func (*PeerNotifyConfigUpdateRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{196} + return file_openshell_proto_rawDescGZIP(), []int{199} } func (x *PeerNotifyConfigUpdateRequest) GetScope() isPeerNotifyConfigUpdateRequest_Scope { @@ -15663,7 +15873,7 @@ type PeerNotifyConfigUpdateResponse struct { func (x *PeerNotifyConfigUpdateResponse) Reset() { *x = PeerNotifyConfigUpdateResponse{} - mi := &file_openshell_proto_msgTypes[197] + mi := &file_openshell_proto_msgTypes[200] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15675,7 +15885,7 @@ func (x *PeerNotifyConfigUpdateResponse) String() string { func (*PeerNotifyConfigUpdateResponse) ProtoMessage() {} func (x *PeerNotifyConfigUpdateResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[197] + mi := &file_openshell_proto_msgTypes[200] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15688,7 +15898,7 @@ func (x *PeerNotifyConfigUpdateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerNotifyConfigUpdateResponse.ProtoReflect.Descriptor instead. func (*PeerNotifyConfigUpdateResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{197} + return file_openshell_proto_rawDescGZIP(), []int{200} } func (x *PeerNotifyConfigUpdateResponse) GetStaleOwner() bool { @@ -15713,7 +15923,7 @@ type RelayOpenResult struct { func (x *RelayOpenResult) Reset() { *x = RelayOpenResult{} - mi := &file_openshell_proto_msgTypes[198] + mi := &file_openshell_proto_msgTypes[201] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15725,7 +15935,7 @@ func (x *RelayOpenResult) String() string { func (*RelayOpenResult) ProtoMessage() {} func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[198] + mi := &file_openshell_proto_msgTypes[201] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15738,7 +15948,7 @@ func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpenResult.ProtoReflect.Descriptor instead. func (*RelayOpenResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{198} + return file_openshell_proto_rawDescGZIP(), []int{201} } func (x *RelayOpenResult) GetChannelId() string { @@ -15775,7 +15985,7 @@ type RelayClose struct { func (x *RelayClose) Reset() { *x = RelayClose{} - mi := &file_openshell_proto_msgTypes[199] + mi := &file_openshell_proto_msgTypes[202] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15787,7 +15997,7 @@ func (x *RelayClose) String() string { func (*RelayClose) ProtoMessage() {} func (x *RelayClose) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[199] + mi := &file_openshell_proto_msgTypes[202] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15800,7 +16010,7 @@ func (x *RelayClose) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayClose.ProtoReflect.Descriptor instead. func (*RelayClose) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{199} + return file_openshell_proto_rawDescGZIP(), []int{202} } func (x *RelayClose) GetChannelId() string { @@ -15834,7 +16044,7 @@ type L7RequestSample struct { func (x *L7RequestSample) Reset() { *x = L7RequestSample{} - mi := &file_openshell_proto_msgTypes[200] + mi := &file_openshell_proto_msgTypes[203] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15846,7 +16056,7 @@ func (x *L7RequestSample) String() string { func (*L7RequestSample) ProtoMessage() {} func (x *L7RequestSample) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[200] + mi := &file_openshell_proto_msgTypes[203] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15859,7 +16069,7 @@ func (x *L7RequestSample) ProtoReflect() protoreflect.Message { // Deprecated: Use L7RequestSample.ProtoReflect.Descriptor instead. func (*L7RequestSample) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{200} + return file_openshell_proto_rawDescGZIP(), []int{203} } func (x *L7RequestSample) GetMethod() string { @@ -15933,7 +16143,7 @@ type DenialSummary struct { func (x *DenialSummary) Reset() { *x = DenialSummary{} - mi := &file_openshell_proto_msgTypes[201] + mi := &file_openshell_proto_msgTypes[204] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15945,7 +16155,7 @@ func (x *DenialSummary) String() string { func (*DenialSummary) ProtoMessage() {} func (x *DenialSummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[201] + mi := &file_openshell_proto_msgTypes[204] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15958,7 +16168,7 @@ func (x *DenialSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialSummary.ProtoReflect.Descriptor instead. func (*DenialSummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{201} + return file_openshell_proto_rawDescGZIP(), []int{204} } func (x *DenialSummary) GetSandboxId() string { @@ -16093,7 +16303,7 @@ type DenialGroupCount struct { func (x *DenialGroupCount) Reset() { *x = DenialGroupCount{} - mi := &file_openshell_proto_msgTypes[202] + mi := &file_openshell_proto_msgTypes[205] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16105,7 +16315,7 @@ func (x *DenialGroupCount) String() string { func (*DenialGroupCount) ProtoMessage() {} func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[202] + mi := &file_openshell_proto_msgTypes[205] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16118,7 +16328,7 @@ func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialGroupCount.ProtoReflect.Descriptor instead. func (*DenialGroupCount) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{202} + return file_openshell_proto_rawDescGZIP(), []int{205} } func (x *DenialGroupCount) GetDenyGroup() string { @@ -16151,7 +16361,7 @@ type NetworkActivitySummary struct { func (x *NetworkActivitySummary) Reset() { *x = NetworkActivitySummary{} - mi := &file_openshell_proto_msgTypes[203] + mi := &file_openshell_proto_msgTypes[206] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16163,7 +16373,7 @@ func (x *NetworkActivitySummary) String() string { func (*NetworkActivitySummary) ProtoMessage() {} func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[203] + mi := &file_openshell_proto_msgTypes[206] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16176,7 +16386,7 @@ func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkActivitySummary.ProtoReflect.Descriptor instead. func (*NetworkActivitySummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{203} + return file_openshell_proto_rawDescGZIP(), []int{206} } func (x *NetworkActivitySummary) GetNetworkActivityCount() uint32 { @@ -16264,7 +16474,7 @@ type PolicyChunk struct { func (x *PolicyChunk) Reset() { *x = PolicyChunk{} - mi := &file_openshell_proto_msgTypes[204] + mi := &file_openshell_proto_msgTypes[207] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16276,7 +16486,7 @@ func (x *PolicyChunk) String() string { func (*PolicyChunk) ProtoMessage() {} func (x *PolicyChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[204] + mi := &file_openshell_proto_msgTypes[207] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16289,7 +16499,7 @@ func (x *PolicyChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyChunk.ProtoReflect.Descriptor instead. func (*PolicyChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{204} + return file_openshell_proto_rawDescGZIP(), []int{207} } func (x *PolicyChunk) GetId() string { @@ -16477,7 +16687,7 @@ type DraftPolicyUpdate struct { func (x *DraftPolicyUpdate) Reset() { *x = DraftPolicyUpdate{} - mi := &file_openshell_proto_msgTypes[205] + mi := &file_openshell_proto_msgTypes[208] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16489,7 +16699,7 @@ func (x *DraftPolicyUpdate) String() string { func (*DraftPolicyUpdate) ProtoMessage() {} func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[205] + mi := &file_openshell_proto_msgTypes[208] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16502,7 +16712,7 @@ func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftPolicyUpdate.ProtoReflect.Descriptor instead. func (*DraftPolicyUpdate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{205} + return file_openshell_proto_rawDescGZIP(), []int{208} } func (x *DraftPolicyUpdate) GetDraftVersion() uint64 { @@ -16561,7 +16771,7 @@ type SubmitPolicyAnalysisRequest struct { func (x *SubmitPolicyAnalysisRequest) Reset() { *x = SubmitPolicyAnalysisRequest{} - mi := &file_openshell_proto_msgTypes[206] + mi := &file_openshell_proto_msgTypes[209] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16573,7 +16783,7 @@ func (x *SubmitPolicyAnalysisRequest) String() string { func (*SubmitPolicyAnalysisRequest) ProtoMessage() {} func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[206] + mi := &file_openshell_proto_msgTypes[209] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16586,7 +16796,7 @@ func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisRequest.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{206} + return file_openshell_proto_rawDescGZIP(), []int{209} } func (x *SubmitPolicyAnalysisRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16649,7 +16859,7 @@ type SubmitPolicyAnalysisResponse struct { func (x *SubmitPolicyAnalysisResponse) Reset() { *x = SubmitPolicyAnalysisResponse{} - mi := &file_openshell_proto_msgTypes[207] + mi := &file_openshell_proto_msgTypes[210] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16661,7 +16871,7 @@ func (x *SubmitPolicyAnalysisResponse) String() string { func (*SubmitPolicyAnalysisResponse) ProtoMessage() {} func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[207] + mi := &file_openshell_proto_msgTypes[210] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16674,7 +16884,7 @@ func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisResponse.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{207} + return file_openshell_proto_rawDescGZIP(), []int{210} } func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunks() uint32 { @@ -16719,7 +16929,7 @@ type GetDraftPolicyRequest struct { func (x *GetDraftPolicyRequest) Reset() { *x = GetDraftPolicyRequest{} - mi := &file_openshell_proto_msgTypes[208] + mi := &file_openshell_proto_msgTypes[211] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16731,7 +16941,7 @@ func (x *GetDraftPolicyRequest) String() string { func (*GetDraftPolicyRequest) ProtoMessage() {} func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[208] + mi := &file_openshell_proto_msgTypes[211] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16744,7 +16954,7 @@ func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyRequest.ProtoReflect.Descriptor instead. func (*GetDraftPolicyRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{208} + return file_openshell_proto_rawDescGZIP(), []int{211} } func (x *GetDraftPolicyRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16784,7 +16994,7 @@ type GetDraftPolicyResponse struct { func (x *GetDraftPolicyResponse) Reset() { *x = GetDraftPolicyResponse{} - mi := &file_openshell_proto_msgTypes[209] + mi := &file_openshell_proto_msgTypes[212] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16796,7 +17006,7 @@ func (x *GetDraftPolicyResponse) String() string { func (*GetDraftPolicyResponse) ProtoMessage() {} func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[209] + mi := &file_openshell_proto_msgTypes[212] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16809,7 +17019,7 @@ func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyResponse.ProtoReflect.Descriptor instead. func (*GetDraftPolicyResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{209} + return file_openshell_proto_rawDescGZIP(), []int{212} } func (x *GetDraftPolicyResponse) GetChunks() []*PolicyChunk { @@ -16860,7 +17070,7 @@ type ApproveDraftChunkRequest struct { func (x *ApproveDraftChunkRequest) Reset() { *x = ApproveDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[210] + mi := &file_openshell_proto_msgTypes[213] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16872,7 +17082,7 @@ func (x *ApproveDraftChunkRequest) String() string { func (*ApproveDraftChunkRequest) ProtoMessage() {} func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[210] + mi := &file_openshell_proto_msgTypes[213] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16885,7 +17095,7 @@ func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkRequest.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{210} + return file_openshell_proto_rawDescGZIP(), []int{213} } func (x *ApproveDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16935,7 +17145,7 @@ type ApproveDraftChunkResponse struct { func (x *ApproveDraftChunkResponse) Reset() { *x = ApproveDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[211] + mi := &file_openshell_proto_msgTypes[214] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16947,7 +17157,7 @@ func (x *ApproveDraftChunkResponse) String() string { func (*ApproveDraftChunkResponse) ProtoMessage() {} func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[211] + mi := &file_openshell_proto_msgTypes[214] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16960,7 +17170,7 @@ func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkResponse.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{211} + return file_openshell_proto_rawDescGZIP(), []int{214} } func (x *ApproveDraftChunkResponse) GetPolicyVersion() uint32 { @@ -16996,7 +17206,7 @@ type RejectDraftChunkRequest struct { func (x *RejectDraftChunkRequest) Reset() { *x = RejectDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[212] + mi := &file_openshell_proto_msgTypes[215] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17008,7 +17218,7 @@ func (x *RejectDraftChunkRequest) String() string { func (*RejectDraftChunkRequest) ProtoMessage() {} func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[212] + mi := &file_openshell_proto_msgTypes[215] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17021,7 +17231,7 @@ func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkRequest.ProtoReflect.Descriptor instead. func (*RejectDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{212} + return file_openshell_proto_rawDescGZIP(), []int{215} } func (x *RejectDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17067,7 +17277,7 @@ type RejectDraftChunkResponse struct { func (x *RejectDraftChunkResponse) Reset() { *x = RejectDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[213] + mi := &file_openshell_proto_msgTypes[216] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17079,7 +17289,7 @@ func (x *RejectDraftChunkResponse) String() string { func (*RejectDraftChunkResponse) ProtoMessage() {} func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[213] + mi := &file_openshell_proto_msgTypes[216] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17092,7 +17302,7 @@ func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkResponse.ProtoReflect.Descriptor instead. func (*RejectDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{213} + return file_openshell_proto_rawDescGZIP(), []int{216} } // Approve all pending chunks. @@ -17106,7 +17316,7 @@ type DraftChunkApproval struct { func (x *DraftChunkApproval) Reset() { *x = DraftChunkApproval{} - mi := &file_openshell_proto_msgTypes[214] + mi := &file_openshell_proto_msgTypes[217] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17118,7 +17328,7 @@ func (x *DraftChunkApproval) String() string { func (*DraftChunkApproval) ProtoMessage() {} func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[214] + mi := &file_openshell_proto_msgTypes[217] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17131,7 +17341,7 @@ func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftChunkApproval.ProtoReflect.Descriptor instead. func (*DraftChunkApproval) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{214} + return file_openshell_proto_rawDescGZIP(), []int{217} } func (x *DraftChunkApproval) GetChunkId() string { @@ -17167,7 +17377,7 @@ type ApproveAllDraftChunksRequest struct { func (x *ApproveAllDraftChunksRequest) Reset() { *x = ApproveAllDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[215] + mi := &file_openshell_proto_msgTypes[218] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17179,7 +17389,7 @@ func (x *ApproveAllDraftChunksRequest) String() string { func (*ApproveAllDraftChunksRequest) ProtoMessage() {} func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[215] + mi := &file_openshell_proto_msgTypes[218] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17192,7 +17402,7 @@ func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{215} + return file_openshell_proto_rawDescGZIP(), []int{218} } func (x *ApproveAllDraftChunksRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17247,7 +17457,7 @@ type ApproveAllDraftChunksResponse struct { func (x *ApproveAllDraftChunksResponse) Reset() { *x = ApproveAllDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[216] + mi := &file_openshell_proto_msgTypes[219] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17259,7 +17469,7 @@ func (x *ApproveAllDraftChunksResponse) String() string { func (*ApproveAllDraftChunksResponse) ProtoMessage() {} func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[216] + mi := &file_openshell_proto_msgTypes[219] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17272,7 +17482,7 @@ func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{216} + return file_openshell_proto_rawDescGZIP(), []int{219} } func (x *ApproveAllDraftChunksResponse) GetPolicyVersion() uint32 { @@ -17322,7 +17532,7 @@ type EditDraftChunkRequest struct { func (x *EditDraftChunkRequest) Reset() { *x = EditDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[217] + mi := &file_openshell_proto_msgTypes[220] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17334,7 +17544,7 @@ func (x *EditDraftChunkRequest) String() string { func (*EditDraftChunkRequest) ProtoMessage() {} func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[217] + mi := &file_openshell_proto_msgTypes[220] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17347,7 +17557,7 @@ func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkRequest.ProtoReflect.Descriptor instead. func (*EditDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{217} + return file_openshell_proto_rawDescGZIP(), []int{220} } func (x *EditDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17393,7 +17603,7 @@ type EditDraftChunkResponse struct { func (x *EditDraftChunkResponse) Reset() { *x = EditDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[218] + mi := &file_openshell_proto_msgTypes[221] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17405,7 +17615,7 @@ func (x *EditDraftChunkResponse) String() string { func (*EditDraftChunkResponse) ProtoMessage() {} func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[218] + mi := &file_openshell_proto_msgTypes[221] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17418,7 +17628,7 @@ func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkResponse.ProtoReflect.Descriptor instead. func (*EditDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{218} + return file_openshell_proto_rawDescGZIP(), []int{221} } // Reverse an approval (remove merged rule from active policy). @@ -17438,7 +17648,7 @@ type UndoDraftChunkRequest struct { func (x *UndoDraftChunkRequest) Reset() { *x = UndoDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[219] + mi := &file_openshell_proto_msgTypes[222] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17450,7 +17660,7 @@ func (x *UndoDraftChunkRequest) String() string { func (*UndoDraftChunkRequest) ProtoMessage() {} func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[219] + mi := &file_openshell_proto_msgTypes[222] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17463,7 +17673,7 @@ func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkRequest.ProtoReflect.Descriptor instead. func (*UndoDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{219} + return file_openshell_proto_rawDescGZIP(), []int{222} } func (x *UndoDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17506,7 +17716,7 @@ type UndoDraftChunkResponse struct { func (x *UndoDraftChunkResponse) Reset() { *x = UndoDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[220] + mi := &file_openshell_proto_msgTypes[223] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17518,7 +17728,7 @@ func (x *UndoDraftChunkResponse) String() string { func (*UndoDraftChunkResponse) ProtoMessage() {} func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[220] + mi := &file_openshell_proto_msgTypes[223] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17531,7 +17741,7 @@ func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkResponse.ProtoReflect.Descriptor instead. func (*UndoDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{220} + return file_openshell_proto_rawDescGZIP(), []int{223} } func (x *UndoDraftChunkResponse) GetPolicyVersion() uint32 { @@ -17563,7 +17773,7 @@ type ClearDraftChunksRequest struct { func (x *ClearDraftChunksRequest) Reset() { *x = ClearDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[221] + mi := &file_openshell_proto_msgTypes[224] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17575,7 +17785,7 @@ func (x *ClearDraftChunksRequest) String() string { func (*ClearDraftChunksRequest) ProtoMessage() {} func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[221] + mi := &file_openshell_proto_msgTypes[224] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17588,7 +17798,7 @@ func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ClearDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{221} + return file_openshell_proto_rawDescGZIP(), []int{224} } func (x *ClearDraftChunksRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17622,7 +17832,7 @@ type ClearDraftChunksResponse struct { func (x *ClearDraftChunksResponse) Reset() { *x = ClearDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[222] + mi := &file_openshell_proto_msgTypes[225] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17634,7 +17844,7 @@ func (x *ClearDraftChunksResponse) String() string { func (*ClearDraftChunksResponse) ProtoMessage() {} func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[222] + mi := &file_openshell_proto_msgTypes[225] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17647,7 +17857,7 @@ func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ClearDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{222} + return file_openshell_proto_rawDescGZIP(), []int{225} } func (x *ClearDraftChunksResponse) GetChunksCleared() uint32 { @@ -17669,7 +17879,7 @@ type GetDraftHistoryRequest struct { func (x *GetDraftHistoryRequest) Reset() { *x = GetDraftHistoryRequest{} - mi := &file_openshell_proto_msgTypes[223] + mi := &file_openshell_proto_msgTypes[226] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17681,7 +17891,7 @@ func (x *GetDraftHistoryRequest) String() string { func (*GetDraftHistoryRequest) ProtoMessage() {} func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[223] + mi := &file_openshell_proto_msgTypes[226] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17694,7 +17904,7 @@ func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryRequest.ProtoReflect.Descriptor instead. func (*GetDraftHistoryRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{223} + return file_openshell_proto_rawDescGZIP(), []int{226} } func (x *GetDraftHistoryRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17728,7 +17938,7 @@ type DraftHistoryEntry struct { func (x *DraftHistoryEntry) Reset() { *x = DraftHistoryEntry{} - mi := &file_openshell_proto_msgTypes[224] + mi := &file_openshell_proto_msgTypes[227] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17740,7 +17950,7 @@ func (x *DraftHistoryEntry) String() string { func (*DraftHistoryEntry) ProtoMessage() {} func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[224] + mi := &file_openshell_proto_msgTypes[227] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17753,7 +17963,7 @@ func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftHistoryEntry.ProtoReflect.Descriptor instead. func (*DraftHistoryEntry) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{224} + return file_openshell_proto_rawDescGZIP(), []int{227} } func (x *DraftHistoryEntry) GetEventTime() *timestamppb.Timestamp { @@ -17794,7 +18004,7 @@ type GetDraftHistoryResponse struct { func (x *GetDraftHistoryResponse) Reset() { *x = GetDraftHistoryResponse{} - mi := &file_openshell_proto_msgTypes[225] + mi := &file_openshell_proto_msgTypes[228] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17806,7 +18016,7 @@ func (x *GetDraftHistoryResponse) String() string { func (*GetDraftHistoryResponse) ProtoMessage() {} func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[225] + mi := &file_openshell_proto_msgTypes[228] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17819,7 +18029,7 @@ func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryResponse.ProtoReflect.Descriptor instead. func (*GetDraftHistoryResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{225} + return file_openshell_proto_rawDescGZIP(), []int{228} } func (x *GetDraftHistoryResponse) GetEntries() []*DraftHistoryEntry { @@ -17844,7 +18054,7 @@ type CreateWorkspaceRequest struct { func (x *CreateWorkspaceRequest) Reset() { *x = CreateWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[226] + mi := &file_openshell_proto_msgTypes[229] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17856,7 +18066,7 @@ func (x *CreateWorkspaceRequest) String() string { func (*CreateWorkspaceRequest) ProtoMessage() {} func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[226] + mi := &file_openshell_proto_msgTypes[229] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17869,7 +18079,7 @@ func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceRequest.ProtoReflect.Descriptor instead. func (*CreateWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{226} + return file_openshell_proto_rawDescGZIP(), []int{229} } func (x *CreateWorkspaceRequest) GetName() string { @@ -17903,7 +18113,7 @@ type CreateWorkspaceResponse struct { func (x *CreateWorkspaceResponse) Reset() { *x = CreateWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[227] + mi := &file_openshell_proto_msgTypes[230] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17915,7 +18125,7 @@ func (x *CreateWorkspaceResponse) String() string { func (*CreateWorkspaceResponse) ProtoMessage() {} func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[227] + mi := &file_openshell_proto_msgTypes[230] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17928,7 +18138,7 @@ func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceResponse.ProtoReflect.Descriptor instead. func (*CreateWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{227} + return file_openshell_proto_rawDescGZIP(), []int{230} } func (x *CreateWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -17949,7 +18159,7 @@ type GetWorkspaceRequest struct { func (x *GetWorkspaceRequest) Reset() { *x = GetWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[228] + mi := &file_openshell_proto_msgTypes[231] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17961,7 +18171,7 @@ func (x *GetWorkspaceRequest) String() string { func (*GetWorkspaceRequest) ProtoMessage() {} func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[228] + mi := &file_openshell_proto_msgTypes[231] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17974,7 +18184,7 @@ func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceRequest.ProtoReflect.Descriptor instead. func (*GetWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{228} + return file_openshell_proto_rawDescGZIP(), []int{231} } func (x *GetWorkspaceRequest) GetName() string { @@ -17994,7 +18204,7 @@ type GetWorkspaceResponse struct { func (x *GetWorkspaceResponse) Reset() { *x = GetWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[229] + mi := &file_openshell_proto_msgTypes[232] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18006,7 +18216,7 @@ func (x *GetWorkspaceResponse) String() string { func (*GetWorkspaceResponse) ProtoMessage() {} func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[229] + mi := &file_openshell_proto_msgTypes[232] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18019,7 +18229,7 @@ func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceResponse.ProtoReflect.Descriptor instead. func (*GetWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{229} + return file_openshell_proto_rawDescGZIP(), []int{232} } func (x *GetWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -18046,7 +18256,7 @@ type ListWorkspacesRequest struct { func (x *ListWorkspacesRequest) Reset() { *x = ListWorkspacesRequest{} - mi := &file_openshell_proto_msgTypes[230] + mi := &file_openshell_proto_msgTypes[233] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18058,7 +18268,7 @@ func (x *ListWorkspacesRequest) String() string { func (*ListWorkspacesRequest) ProtoMessage() {} func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[230] + mi := &file_openshell_proto_msgTypes[233] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18071,7 +18281,7 @@ func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesRequest.ProtoReflect.Descriptor instead. func (*ListWorkspacesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{230} + return file_openshell_proto_rawDescGZIP(), []int{233} } func (x *ListWorkspacesRequest) GetPageSize() int32 { @@ -18107,7 +18317,7 @@ type ListWorkspacesResponse struct { func (x *ListWorkspacesResponse) Reset() { *x = ListWorkspacesResponse{} - mi := &file_openshell_proto_msgTypes[231] + mi := &file_openshell_proto_msgTypes[234] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18119,7 +18329,7 @@ func (x *ListWorkspacesResponse) String() string { func (*ListWorkspacesResponse) ProtoMessage() {} func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[231] + mi := &file_openshell_proto_msgTypes[234] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18132,7 +18342,7 @@ func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesResponse.ProtoReflect.Descriptor instead. func (*ListWorkspacesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{231} + return file_openshell_proto_rawDescGZIP(), []int{234} } func (x *ListWorkspacesResponse) GetWorkspaces() []*datamodelv1.Workspace { @@ -18163,7 +18373,7 @@ type DeleteWorkspaceRequest struct { func (x *DeleteWorkspaceRequest) Reset() { *x = DeleteWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[232] + mi := &file_openshell_proto_msgTypes[235] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18175,7 +18385,7 @@ func (x *DeleteWorkspaceRequest) String() string { func (*DeleteWorkspaceRequest) ProtoMessage() {} func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[232] + mi := &file_openshell_proto_msgTypes[235] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18188,7 +18398,7 @@ func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceRequest.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{232} + return file_openshell_proto_rawDescGZIP(), []int{235} } func (x *DeleteWorkspaceRequest) GetName() string { @@ -18222,7 +18432,7 @@ type DeleteWorkspaceResponse struct { func (x *DeleteWorkspaceResponse) Reset() { *x = DeleteWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[233] + mi := &file_openshell_proto_msgTypes[236] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18234,7 +18444,7 @@ func (x *DeleteWorkspaceResponse) String() string { func (*DeleteWorkspaceResponse) ProtoMessage() {} func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[233] + mi := &file_openshell_proto_msgTypes[236] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18247,7 +18457,7 @@ func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceResponse.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{233} + return file_openshell_proto_rawDescGZIP(), []int{236} } func (x *DeleteWorkspaceResponse) GetOutcome() DeletionOutcome { @@ -18271,7 +18481,7 @@ type WorkspaceMember struct { func (x *WorkspaceMember) Reset() { *x = WorkspaceMember{} - mi := &file_openshell_proto_msgTypes[234] + mi := &file_openshell_proto_msgTypes[237] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18283,7 +18493,7 @@ func (x *WorkspaceMember) String() string { func (*WorkspaceMember) ProtoMessage() {} func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[234] + mi := &file_openshell_proto_msgTypes[237] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18296,7 +18506,7 @@ func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceMember.ProtoReflect.Descriptor instead. func (*WorkspaceMember) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{234} + return file_openshell_proto_rawDescGZIP(), []int{237} } func (x *WorkspaceMember) GetMetadata() *datamodelv1.ObjectMeta { @@ -18337,7 +18547,7 @@ type AddWorkspaceMemberRequest struct { func (x *AddWorkspaceMemberRequest) Reset() { *x = AddWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[235] + mi := &file_openshell_proto_msgTypes[238] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18349,7 +18559,7 @@ func (x *AddWorkspaceMemberRequest) String() string { func (*AddWorkspaceMemberRequest) ProtoMessage() {} func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[235] + mi := &file_openshell_proto_msgTypes[238] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18362,7 +18572,7 @@ func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{235} + return file_openshell_proto_rawDescGZIP(), []int{238} } func (x *AddWorkspaceMemberRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -18403,7 +18613,7 @@ type AddWorkspaceMemberResponse struct { func (x *AddWorkspaceMemberResponse) Reset() { *x = AddWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[236] + mi := &file_openshell_proto_msgTypes[239] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18415,7 +18625,7 @@ func (x *AddWorkspaceMemberResponse) String() string { func (*AddWorkspaceMemberResponse) ProtoMessage() {} func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[236] + mi := &file_openshell_proto_msgTypes[239] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18428,7 +18638,7 @@ func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{236} + return file_openshell_proto_rawDescGZIP(), []int{239} } func (x *AddWorkspaceMemberResponse) GetMember() *WorkspaceMember { @@ -18454,7 +18664,7 @@ type RemoveWorkspaceMemberRequest struct { func (x *RemoveWorkspaceMemberRequest) Reset() { *x = RemoveWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[237] + mi := &file_openshell_proto_msgTypes[240] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18466,7 +18676,7 @@ func (x *RemoveWorkspaceMemberRequest) String() string { func (*RemoveWorkspaceMemberRequest) ProtoMessage() {} func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[237] + mi := &file_openshell_proto_msgTypes[240] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18479,7 +18689,7 @@ func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{237} + return file_openshell_proto_rawDescGZIP(), []int{240} } func (x *RemoveWorkspaceMemberRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -18520,7 +18730,7 @@ type RemoveWorkspaceMemberResponse struct { func (x *RemoveWorkspaceMemberResponse) Reset() { *x = RemoveWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[238] + mi := &file_openshell_proto_msgTypes[241] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18532,7 +18742,7 @@ func (x *RemoveWorkspaceMemberResponse) String() string { func (*RemoveWorkspaceMemberResponse) ProtoMessage() {} func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[238] + mi := &file_openshell_proto_msgTypes[241] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18545,7 +18755,7 @@ func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{238} + return file_openshell_proto_rawDescGZIP(), []int{241} } func (x *RemoveWorkspaceMemberResponse) GetOutcome() DeletionOutcome { @@ -18572,7 +18782,7 @@ type ListWorkspaceMembersRequest struct { func (x *ListWorkspaceMembersRequest) Reset() { *x = ListWorkspaceMembersRequest{} - mi := &file_openshell_proto_msgTypes[239] + mi := &file_openshell_proto_msgTypes[242] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18584,7 +18794,7 @@ func (x *ListWorkspaceMembersRequest) String() string { func (*ListWorkspaceMembersRequest) ProtoMessage() {} func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[239] + mi := &file_openshell_proto_msgTypes[242] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18597,7 +18807,7 @@ func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersRequest.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{239} + return file_openshell_proto_rawDescGZIP(), []int{242} } func (x *ListWorkspaceMembersRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -18633,7 +18843,7 @@ type ListWorkspaceMembersResponse struct { func (x *ListWorkspaceMembersResponse) Reset() { *x = ListWorkspaceMembersResponse{} - mi := &file_openshell_proto_msgTypes[240] + mi := &file_openshell_proto_msgTypes[243] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18645,7 +18855,7 @@ func (x *ListWorkspaceMembersResponse) String() string { func (*ListWorkspaceMembersResponse) ProtoMessage() {} func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[240] + mi := &file_openshell_proto_msgTypes[243] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18658,7 +18868,7 @@ func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersResponse.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{240} + return file_openshell_proto_rawDescGZIP(), []int{243} } func (x *ListWorkspaceMembersResponse) GetMembers() []*WorkspaceMember { @@ -18693,7 +18903,7 @@ type ExtensionServiceCredential struct { func (x *ExtensionServiceCredential) Reset() { *x = ExtensionServiceCredential{} - mi := &file_openshell_proto_msgTypes[241] + mi := &file_openshell_proto_msgTypes[244] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18705,7 +18915,7 @@ func (x *ExtensionServiceCredential) String() string { func (*ExtensionServiceCredential) ProtoMessage() {} func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[241] + mi := &file_openshell_proto_msgTypes[244] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18718,7 +18928,7 @@ func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ExtensionServiceCredential.ProtoReflect.Descriptor instead. func (*ExtensionServiceCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{241} + return file_openshell_proto_rawDescGZIP(), []int{244} } func (x *ExtensionServiceCredential) GetServiceName() string { @@ -18758,7 +18968,7 @@ type EndpointObservation struct { func (x *EndpointObservation) Reset() { *x = EndpointObservation{} - mi := &file_openshell_proto_msgTypes[242] + mi := &file_openshell_proto_msgTypes[245] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18770,7 +18980,7 @@ func (x *EndpointObservation) String() string { func (*EndpointObservation) ProtoMessage() {} func (x *EndpointObservation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[242] + mi := &file_openshell_proto_msgTypes[245] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18783,7 +18993,7 @@ func (x *EndpointObservation) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointObservation.ProtoReflect.Descriptor instead. func (*EndpointObservation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{242} + return file_openshell_proto_rawDescGZIP(), []int{245} } func (x *EndpointObservation) GetEndpointId() string { @@ -18836,7 +19046,7 @@ type ReportEndpointStatusRequest struct { func (x *ReportEndpointStatusRequest) Reset() { *x = ReportEndpointStatusRequest{} - mi := &file_openshell_proto_msgTypes[243] + mi := &file_openshell_proto_msgTypes[246] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18848,7 +19058,7 @@ func (x *ReportEndpointStatusRequest) String() string { func (*ReportEndpointStatusRequest) ProtoMessage() {} func (x *ReportEndpointStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[243] + mi := &file_openshell_proto_msgTypes[246] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18861,7 +19071,7 @@ func (x *ReportEndpointStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportEndpointStatusRequest.ProtoReflect.Descriptor instead. func (*ReportEndpointStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{243} + return file_openshell_proto_rawDescGZIP(), []int{246} } func (x *ReportEndpointStatusRequest) GetSandboxId() string { @@ -18930,7 +19140,7 @@ type TokenGrantStatus struct { func (x *TokenGrantStatus) Reset() { *x = TokenGrantStatus{} - mi := &file_openshell_proto_msgTypes[244] + mi := &file_openshell_proto_msgTypes[247] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18942,7 +19152,7 @@ func (x *TokenGrantStatus) String() string { func (*TokenGrantStatus) ProtoMessage() {} func (x *TokenGrantStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[244] + mi := &file_openshell_proto_msgTypes[247] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18955,7 +19165,7 @@ func (x *TokenGrantStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use TokenGrantStatus.ProtoReflect.Descriptor instead. func (*TokenGrantStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{244} + return file_openshell_proto_rawDescGZIP(), []int{247} } func (x *TokenGrantStatus) GetLastFailure() *TokenGrantFailureObservation { @@ -18982,7 +19192,7 @@ type TokenGrantFailureObservation struct { func (x *TokenGrantFailureObservation) Reset() { *x = TokenGrantFailureObservation{} - mi := &file_openshell_proto_msgTypes[245] + mi := &file_openshell_proto_msgTypes[248] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18994,7 +19204,7 @@ func (x *TokenGrantFailureObservation) String() string { func (*TokenGrantFailureObservation) ProtoMessage() {} func (x *TokenGrantFailureObservation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[245] + mi := &file_openshell_proto_msgTypes[248] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -19007,7 +19217,7 @@ func (x *TokenGrantFailureObservation) ProtoReflect() protoreflect.Message { // Deprecated: Use TokenGrantFailureObservation.ProtoReflect.Descriptor instead. func (*TokenGrantFailureObservation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{245} + return file_openshell_proto_rawDescGZIP(), []int{248} } func (x *TokenGrantFailureObservation) GetDestinationHost() string { @@ -19040,7 +19250,7 @@ type ReportEndpointStatusResponse struct { func (x *ReportEndpointStatusResponse) Reset() { *x = ReportEndpointStatusResponse{} - mi := &file_openshell_proto_msgTypes[246] + mi := &file_openshell_proto_msgTypes[249] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -19052,7 +19262,7 @@ func (x *ReportEndpointStatusResponse) String() string { func (*ReportEndpointStatusResponse) ProtoMessage() {} func (x *ReportEndpointStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[246] + mi := &file_openshell_proto_msgTypes[249] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -19065,7 +19275,7 @@ func (x *ReportEndpointStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportEndpointStatusResponse.ProtoReflect.Descriptor instead. func (*ReportEndpointStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{246} + return file_openshell_proto_rawDescGZIP(), []int{249} } // A configured endpoint and its last accepted network result in one record. @@ -19099,7 +19309,7 @@ type EndpointStatus struct { func (x *EndpointStatus) Reset() { *x = EndpointStatus{} - mi := &file_openshell_proto_msgTypes[247] + mi := &file_openshell_proto_msgTypes[250] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -19111,7 +19321,7 @@ func (x *EndpointStatus) String() string { func (*EndpointStatus) ProtoMessage() {} func (x *EndpointStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[247] + mi := &file_openshell_proto_msgTypes[250] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -19124,7 +19334,7 @@ func (x *EndpointStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointStatus.ProtoReflect.Descriptor instead. func (*EndpointStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{247} + return file_openshell_proto_rawDescGZIP(), []int{250} } func (x *EndpointStatus) GetEndpointId() string { @@ -19219,7 +19429,7 @@ type SandboxProvisioning struct { func (x *SandboxProvisioning) Reset() { *x = SandboxProvisioning{} - mi := &file_openshell_proto_msgTypes[248] + mi := &file_openshell_proto_msgTypes[251] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -19231,7 +19441,7 @@ func (x *SandboxProvisioning) String() string { func (*SandboxProvisioning) ProtoMessage() {} func (x *SandboxProvisioning) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[248] + mi := &file_openshell_proto_msgTypes[251] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -19244,7 +19454,7 @@ func (x *SandboxProvisioning) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxProvisioning.ProtoReflect.Descriptor instead. func (*SandboxProvisioning) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{248} + return file_openshell_proto_rawDescGZIP(), []int{251} } func (x *SandboxProvisioning) GetAttemptId() string { @@ -19367,7 +19577,7 @@ type SandboxServiceExposure struct { func (x *SandboxServiceExposure) Reset() { *x = SandboxServiceExposure{} - mi := &file_openshell_proto_msgTypes[249] + mi := &file_openshell_proto_msgTypes[252] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -19379,7 +19589,7 @@ func (x *SandboxServiceExposure) String() string { func (*SandboxServiceExposure) ProtoMessage() {} func (x *SandboxServiceExposure) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[249] + mi := &file_openshell_proto_msgTypes[252] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -19392,7 +19602,7 @@ func (x *SandboxServiceExposure) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxServiceExposure.ProtoReflect.Descriptor instead. func (*SandboxServiceExposure) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{249} + return file_openshell_proto_rawDescGZIP(), []int{252} } func (x *SandboxServiceExposure) GetService() string { @@ -19446,7 +19656,7 @@ type EndpointFailure struct { func (x *EndpointFailure) Reset() { *x = EndpointFailure{} - mi := &file_openshell_proto_msgTypes[250] + mi := &file_openshell_proto_msgTypes[253] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -19458,7 +19668,7 @@ func (x *EndpointFailure) String() string { func (*EndpointFailure) ProtoMessage() {} func (x *EndpointFailure) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[250] + mi := &file_openshell_proto_msgTypes[253] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -19471,7 +19681,7 @@ func (x *EndpointFailure) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointFailure.ProtoReflect.Descriptor instead. func (*EndpointFailure) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{250} + return file_openshell_proto_rawDescGZIP(), []int{253} } func (x *EndpointFailure) GetDomain() EndpointFailureDomain { @@ -19729,6 +19939,25 @@ const file_openshell_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xf8\x03\n" + + "\x14UpdateSandboxRequest\x12R\n" + + "\x0fworkspace_scope\x18\x01 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x19\n" + + "\x05image\x18\x03 \x01(\tH\x00R\x05image\x88\x01\x01\x12<\n" + + "\acommand\x18\x06 \x01(\v2\".openshell.v1.SandboxCommandUpdateR\acommand\x12@\n" + + "\venvironment\x18\a \x01(\v2\x1e.openshell.v1.SandboxMapUpdateR\venvironment\x126\n" + + "\x06labels\x18\b \x01(\v2\x1e.openshell.v1.SandboxMapUpdateR\x06labels\x12@\n" + + "\vannotations\x18\t \x01(\v2\x1e.openshell.v1.SandboxMapUpdateR\vannotations\x12:\n" + + "\x19expected_resource_version\x18\x04 \x01(\x04R\x17expectedResourceVersion\x12\x1d\n" + + "\n" + + "request_id\x18\x05 \x01(\tR\trequestIdB\b\n" + + "\x06_image\"*\n" + + "\x14SandboxCommandUpdate\x12\x12\n" + + "\x04argv\x18\x01 \x03(\tR\x04argv\"\x91\x01\n" + + "\x10SandboxMapUpdate\x12B\n" + + "\x06values\x18\x01 \x03(\v2*.openshell.v1.SandboxMapUpdate.ValuesEntryR\x06values\x1a9\n" + + "\vValuesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd4\x01\n" + "\x1cCreateSandboxTemplateRequest\x12R\n" + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12A\n" + @@ -21326,7 +21555,10 @@ const file_openshell_proto_rawDesc = "" + "\x15RemoveWorkspaceMember\x12*.openshell.v1.RemoveWorkspaceMemberRequest\x1a+.openshell.v1.RemoveWorkspaceMemberResponse\"$\x82\xb5\x18 \n" + "\x06bearer\x12\x05admin\"\x0fworkspace:write\x12\x91\x01\n" + "\x14ListWorkspaceMembers\x12).openshell.v1.ListWorkspaceMembersRequest\x1a*.openshell.v1.ListWorkspaceMembersResponse\"\"\x82\xb5\x18\x1e\n" + - "\x06bearer\x12\x04user\"\x0eworkspace:readb\x06proto3" + "\x06bearer\x12\x04user\"\x0eworkspace:read2\x87\x01\n" + + "\x0eSandboxUpdates\x12u\n" + + "\rUpdateSandbox\x12\".openshell.v1.UpdateSandboxRequest\x1a\x1d.openshell.v1.SandboxResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:writeb\x06proto3" var ( file_openshell_proto_rawDescOnce sync.Once @@ -21341,7 +21573,7 @@ func file_openshell_proto_rawDescGZIP() []byte { } var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 22) -var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 277) +var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 281) var file_openshell_proto_goTypes = []any{ (ExtensionKind)(0), // 0: openshell.v1.ExtensionKind (SandboxPhase)(0), // 1: openshell.v1.SandboxPhase @@ -21398,276 +21630,280 @@ var file_openshell_proto_goTypes = []any{ (*SandboxCondition)(nil), // 52: openshell.v1.SandboxCondition (*PlatformEvent)(nil), // 53: openshell.v1.PlatformEvent (*CreateSandboxRequest)(nil), // 54: openshell.v1.CreateSandboxRequest - (*CreateSandboxTemplateRequest)(nil), // 55: openshell.v1.CreateSandboxTemplateRequest - (*GetSandboxTemplateRequest)(nil), // 56: openshell.v1.GetSandboxTemplateRequest - (*ListSandboxTemplatesRequest)(nil), // 57: openshell.v1.ListSandboxTemplatesRequest - (*DeleteSandboxTemplateRequest)(nil), // 58: openshell.v1.DeleteSandboxTemplateRequest - (*SandboxTemplateResponse)(nil), // 59: openshell.v1.SandboxTemplateResponse - (*ListSandboxTemplatesResponse)(nil), // 60: openshell.v1.ListSandboxTemplatesResponse - (*DeleteSandboxTemplateResponse)(nil), // 61: openshell.v1.DeleteSandboxTemplateResponse - (*BeginRootfsTarStagingRequest)(nil), // 62: openshell.v1.BeginRootfsTarStagingRequest - (*BeginRootfsTarStagingResponse)(nil), // 63: openshell.v1.BeginRootfsTarStagingResponse - (*GetSandboxRequest)(nil), // 64: openshell.v1.GetSandboxRequest - (*ListSandboxesRequest)(nil), // 65: openshell.v1.ListSandboxesRequest - (*ListSandboxProvidersRequest)(nil), // 66: openshell.v1.ListSandboxProvidersRequest - (*AttachSandboxProviderRequest)(nil), // 67: openshell.v1.AttachSandboxProviderRequest - (*DetachSandboxProviderRequest)(nil), // 68: openshell.v1.DetachSandboxProviderRequest - (*DeleteSandboxRequest)(nil), // 69: openshell.v1.DeleteSandboxRequest - (*StopSandboxRequest)(nil), // 70: openshell.v1.StopSandboxRequest - (*StartSandboxRequest)(nil), // 71: openshell.v1.StartSandboxRequest - (*SuspendSandboxRequest)(nil), // 72: openshell.v1.SuspendSandboxRequest - (*ResumeSandboxRequest)(nil), // 73: openshell.v1.ResumeSandboxRequest - (*SandboxResponse)(nil), // 74: openshell.v1.SandboxResponse - (*ListSandboxesResponse)(nil), // 75: openshell.v1.ListSandboxesResponse - (*ListSandboxProvidersResponse)(nil), // 76: openshell.v1.ListSandboxProvidersResponse - (*AttachSandboxProviderResponse)(nil), // 77: openshell.v1.AttachSandboxProviderResponse - (*DetachSandboxProviderResponse)(nil), // 78: openshell.v1.DetachSandboxProviderResponse - (*ProviderDesiredIdentity)(nil), // 79: openshell.v1.ProviderDesiredIdentity - (*ConfigSnapshotRevision)(nil), // 80: openshell.v1.ConfigSnapshotRevision - (*SandboxConfigRevision)(nil), // 81: openshell.v1.SandboxConfigRevision - (*ConfigUpdateOperation)(nil), // 82: openshell.v1.ConfigUpdateOperation - (*ProviderMutationReceipt)(nil), // 83: openshell.v1.ProviderMutationReceipt - (*ProviderReadinessObservation)(nil), // 84: openshell.v1.ProviderReadinessObservation - (*ProviderReadinessStatus)(nil), // 85: openshell.v1.ProviderReadinessStatus - (*GetSandboxProviderStatusRequest)(nil), // 86: openshell.v1.GetSandboxProviderStatusRequest - (*GetSandboxProviderStatusResponse)(nil), // 87: openshell.v1.GetSandboxProviderStatusResponse - (*ReportProviderReadinessRequest)(nil), // 88: openshell.v1.ReportProviderReadinessRequest - (*ReportProviderReadinessResponse)(nil), // 89: openshell.v1.ReportProviderReadinessResponse - (*DeleteSandboxResponse)(nil), // 90: openshell.v1.DeleteSandboxResponse - (*CreateSshSessionRequest)(nil), // 91: openshell.v1.CreateSshSessionRequest - (*CreateSshSessionResponse)(nil), // 92: openshell.v1.CreateSshSessionResponse - (*ExposeServiceRequest)(nil), // 93: openshell.v1.ExposeServiceRequest - (*GetServiceRequest)(nil), // 94: openshell.v1.GetServiceRequest - (*ListServicesRequest)(nil), // 95: openshell.v1.ListServicesRequest - (*ListServicesResponse)(nil), // 96: openshell.v1.ListServicesResponse - (*DeleteServiceRequest)(nil), // 97: openshell.v1.DeleteServiceRequest - (*DeleteServiceResponse)(nil), // 98: openshell.v1.DeleteServiceResponse - (*ServiceEndpoint)(nil), // 99: openshell.v1.ServiceEndpoint - (*ServiceEndpointResponse)(nil), // 100: openshell.v1.ServiceEndpointResponse - (*RevokeSshSessionRequest)(nil), // 101: openshell.v1.RevokeSshSessionRequest - (*RevokeSshSessionResponse)(nil), // 102: openshell.v1.RevokeSshSessionResponse - (*ExecSandboxRequest)(nil), // 103: openshell.v1.ExecSandboxRequest - (*ExecSandboxStdout)(nil), // 104: openshell.v1.ExecSandboxStdout - (*ExecSandboxStderr)(nil), // 105: openshell.v1.ExecSandboxStderr - (*ExecSandboxExit)(nil), // 106: openshell.v1.ExecSandboxExit - (*ExecSandboxEvent)(nil), // 107: openshell.v1.ExecSandboxEvent - (*TcpForwardInit)(nil), // 108: openshell.v1.TcpForwardInit - (*TcpForwardFrame)(nil), // 109: openshell.v1.TcpForwardFrame - (*ExecSandboxInput)(nil), // 110: openshell.v1.ExecSandboxInput - (*ExecSandboxWindowResize)(nil), // 111: openshell.v1.ExecSandboxWindowResize - (*SshSession)(nil), // 112: openshell.v1.SshSession - (*WatchSandboxRequest)(nil), // 113: openshell.v1.WatchSandboxRequest - (*SandboxStreamEvent)(nil), // 114: openshell.v1.SandboxStreamEvent - (*SandboxLogLine)(nil), // 115: openshell.v1.SandboxLogLine - (*SandboxStreamWarning)(nil), // 116: openshell.v1.SandboxStreamWarning - (*CreateProviderRequest)(nil), // 117: openshell.v1.CreateProviderRequest - (*GetProviderRequest)(nil), // 118: openshell.v1.GetProviderRequest - (*GetProviderCredentialsRequest)(nil), // 119: openshell.v1.GetProviderCredentialsRequest - (*ProviderCredentialValue)(nil), // 120: openshell.v1.ProviderCredentialValue - (*GetProviderCredentialsResponse)(nil), // 121: openshell.v1.GetProviderCredentialsResponse - (*ListProvidersRequest)(nil), // 122: openshell.v1.ListProvidersRequest - (*UpdateProviderRequest)(nil), // 123: openshell.v1.UpdateProviderRequest - (*DeleteProviderRequest)(nil), // 124: openshell.v1.DeleteProviderRequest - (*ProviderResponse)(nil), // 125: openshell.v1.ProviderResponse - (*ListProvidersResponse)(nil), // 126: openshell.v1.ListProvidersResponse - (*ListProviderProfilesRequest)(nil), // 127: openshell.v1.ListProviderProfilesRequest - (*GetProviderProfileRequest)(nil), // 128: openshell.v1.GetProviderProfileRequest - (*ProviderProfileImportItem)(nil), // 129: openshell.v1.ProviderProfileImportItem - (*ProviderProfileDiagnostic)(nil), // 130: openshell.v1.ProviderProfileDiagnostic - (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 131: openshell.v1.ProviderCredentialTokenGrantAudienceOverride - (*ProviderCredentialTokenGrantSubjectToken)(nil), // 132: openshell.v1.ProviderCredentialTokenGrantSubjectToken - (*ProviderCredentialTokenGrant)(nil), // 133: openshell.v1.ProviderCredentialTokenGrant - (*ProviderProfileCredential)(nil), // 134: openshell.v1.ProviderProfileCredential - (*ProviderCredentialRefreshMaterial)(nil), // 135: openshell.v1.ProviderCredentialRefreshMaterial - (*ProviderCredentialRefreshOutput)(nil), // 136: openshell.v1.ProviderCredentialRefreshOutput - (*ProviderCredentialRefresh)(nil), // 137: openshell.v1.ProviderCredentialRefresh - (*ProviderCredentialRefreshStatus)(nil), // 138: openshell.v1.ProviderCredentialRefreshStatus - (*ProviderProfileDiscovery)(nil), // 139: openshell.v1.ProviderProfileDiscovery - (*GetProviderRefreshStatusRequest)(nil), // 140: openshell.v1.GetProviderRefreshStatusRequest - (*GetProviderRefreshStatusResponse)(nil), // 141: openshell.v1.GetProviderRefreshStatusResponse - (*ConfigureProviderRefreshRequest)(nil), // 142: openshell.v1.ConfigureProviderRefreshRequest - (*ConfigureProviderRefreshResponse)(nil), // 143: openshell.v1.ConfigureProviderRefreshResponse - (*RotateProviderCredentialRequest)(nil), // 144: openshell.v1.RotateProviderCredentialRequest - (*RotateProviderCredentialResponse)(nil), // 145: openshell.v1.RotateProviderCredentialResponse - (*DeleteProviderRefreshRequest)(nil), // 146: openshell.v1.DeleteProviderRefreshRequest - (*DeleteProviderRefreshResponse)(nil), // 147: openshell.v1.DeleteProviderRefreshResponse - (*ProviderProfile)(nil), // 148: openshell.v1.ProviderProfile - (*ProviderProfileFile)(nil), // 149: openshell.v1.ProviderProfileFile - (*ProviderProfileResponse)(nil), // 150: openshell.v1.ProviderProfileResponse - (*ListProviderProfilesResponse)(nil), // 151: openshell.v1.ListProviderProfilesResponse - (*ImportProviderProfilesRequest)(nil), // 152: openshell.v1.ImportProviderProfilesRequest - (*ImportProviderProfilesResponse)(nil), // 153: openshell.v1.ImportProviderProfilesResponse - (*UpdateProviderProfilesRequest)(nil), // 154: openshell.v1.UpdateProviderProfilesRequest - (*UpdateProviderProfilesResponse)(nil), // 155: openshell.v1.UpdateProviderProfilesResponse - (*LintProviderProfilesRequest)(nil), // 156: openshell.v1.LintProviderProfilesRequest - (*LintProviderProfilesResponse)(nil), // 157: openshell.v1.LintProviderProfilesResponse - (*DeleteProviderResponse)(nil), // 158: openshell.v1.DeleteProviderResponse - (*DeleteProviderProfileRequest)(nil), // 159: openshell.v1.DeleteProviderProfileRequest - (*DeleteProviderProfileResponse)(nil), // 160: openshell.v1.DeleteProviderProfileResponse - (*GetSandboxProviderEnvironmentRequest)(nil), // 161: openshell.v1.GetSandboxProviderEnvironmentRequest - (*StaticCredentialEndpointBinding)(nil), // 162: openshell.v1.StaticCredentialEndpointBinding - (*StaticCredentialBinding)(nil), // 163: openshell.v1.StaticCredentialBinding - (*GetSandboxProviderEnvironmentResponse)(nil), // 164: openshell.v1.GetSandboxProviderEnvironmentResponse - (*ProviderEnvironmentValue)(nil), // 165: openshell.v1.ProviderEnvironmentValue - (*ProviderEnvironmentSnapshot)(nil), // 166: openshell.v1.ProviderEnvironmentSnapshot - (*ExchangeProviderSubjectTokenRequest)(nil), // 167: openshell.v1.ExchangeProviderSubjectTokenRequest - (*ExchangeProviderSubjectTokenResponse)(nil), // 168: openshell.v1.ExchangeProviderSubjectTokenResponse - (*UpdateConfigRequest)(nil), // 169: openshell.v1.UpdateConfigRequest - (*PolicyMergeOperation)(nil), // 170: openshell.v1.PolicyMergeOperation - (*AddNetworkRule)(nil), // 171: openshell.v1.AddNetworkRule - (*RemoveNetworkEndpoint)(nil), // 172: openshell.v1.RemoveNetworkEndpoint - (*RemoveNetworkRule)(nil), // 173: openshell.v1.RemoveNetworkRule - (*L7RuleTarget)(nil), // 174: openshell.v1.L7RuleTarget - (*AddDenyRules)(nil), // 175: openshell.v1.AddDenyRules - (*AddAllowRules)(nil), // 176: openshell.v1.AddAllowRules - (*RemoveNetworkBinary)(nil), // 177: openshell.v1.RemoveNetworkBinary - (*UpdateConfigResponse)(nil), // 178: openshell.v1.UpdateConfigResponse - (*GetSandboxPolicyStatusRequest)(nil), // 179: openshell.v1.GetSandboxPolicyStatusRequest - (*GetSandboxPolicyStatusResponse)(nil), // 180: openshell.v1.GetSandboxPolicyStatusResponse - (*ListSandboxPoliciesRequest)(nil), // 181: openshell.v1.ListSandboxPoliciesRequest - (*ListSandboxPoliciesResponse)(nil), // 182: openshell.v1.ListSandboxPoliciesResponse - (*ReportPolicyStatusRequest)(nil), // 183: openshell.v1.ReportPolicyStatusRequest - (*ReportPolicyStatusResponse)(nil), // 184: openshell.v1.ReportPolicyStatusResponse - (*SandboxConfigurationAdmission)(nil), // 185: openshell.v1.SandboxConfigurationAdmission - (*ReportSandboxConfigurationRequest)(nil), // 186: openshell.v1.ReportSandboxConfigurationRequest - (*ReportSandboxConfigurationResponse)(nil), // 187: openshell.v1.ReportSandboxConfigurationResponse - (*SandboxPolicyRevision)(nil), // 188: openshell.v1.SandboxPolicyRevision - (*GetSandboxLogsRequest)(nil), // 189: openshell.v1.GetSandboxLogsRequest - (*PushSandboxLogsRequest)(nil), // 190: openshell.v1.PushSandboxLogsRequest - (*PushSandboxLogsResponse)(nil), // 191: openshell.v1.PushSandboxLogsResponse - (*GetSandboxLogsResponse)(nil), // 192: openshell.v1.GetSandboxLogsResponse - (*SupervisorMessage)(nil), // 193: openshell.v1.SupervisorMessage - (*GatewayMessage)(nil), // 194: openshell.v1.GatewayMessage - (*SupervisorHello)(nil), // 195: openshell.v1.SupervisorHello - (*SessionAccepted)(nil), // 196: openshell.v1.SessionAccepted - (*ConfigUpdate)(nil), // 197: openshell.v1.ConfigUpdate - (*ConfigApplyFailure)(nil), // 198: openshell.v1.ConfigApplyFailure - (*ConfigUpdateResult)(nil), // 199: openshell.v1.ConfigUpdateResult - (*PolicyLoadResult)(nil), // 200: openshell.v1.PolicyLoadResult - (*SessionRejected)(nil), // 201: openshell.v1.SessionRejected - (*SessionRedirect)(nil), // 202: openshell.v1.SessionRedirect - (*SupervisorHeartbeat)(nil), // 203: openshell.v1.SupervisorHeartbeat - (*GatewayHeartbeat)(nil), // 204: openshell.v1.GatewayHeartbeat - (*ReportMainProcessExitRequest)(nil), // 205: openshell.v1.ReportMainProcessExitRequest - (*ReportMainProcessExitResponse)(nil), // 206: openshell.v1.ReportMainProcessExitResponse - (*FinalizeMainProcessExitRequest)(nil), // 207: openshell.v1.FinalizeMainProcessExitRequest - (*FinalizeMainProcessExitResponse)(nil), // 208: openshell.v1.FinalizeMainProcessExitResponse - (*RelayOpen)(nil), // 209: openshell.v1.RelayOpen - (*SshRelayTarget)(nil), // 210: openshell.v1.SshRelayTarget - (*TcpRelayTarget)(nil), // 211: openshell.v1.TcpRelayTarget - (*RelayInit)(nil), // 212: openshell.v1.RelayInit - (*RelayFrame)(nil), // 213: openshell.v1.RelayFrame - (*PeerRelayInit)(nil), // 214: openshell.v1.PeerRelayInit - (*PeerRelayFrame)(nil), // 215: openshell.v1.PeerRelayFrame - (*PeerConfigSandboxTarget)(nil), // 216: openshell.v1.PeerConfigSandboxTarget - (*PeerConfigProviderTarget)(nil), // 217: openshell.v1.PeerConfigProviderTarget - (*PeerNotifyConfigUpdateRequest)(nil), // 218: openshell.v1.PeerNotifyConfigUpdateRequest - (*PeerNotifyConfigUpdateResponse)(nil), // 219: openshell.v1.PeerNotifyConfigUpdateResponse - (*RelayOpenResult)(nil), // 220: openshell.v1.RelayOpenResult - (*RelayClose)(nil), // 221: openshell.v1.RelayClose - (*L7RequestSample)(nil), // 222: openshell.v1.L7RequestSample - (*DenialSummary)(nil), // 223: openshell.v1.DenialSummary - (*DenialGroupCount)(nil), // 224: openshell.v1.DenialGroupCount - (*NetworkActivitySummary)(nil), // 225: openshell.v1.NetworkActivitySummary - (*PolicyChunk)(nil), // 226: openshell.v1.PolicyChunk - (*DraftPolicyUpdate)(nil), // 227: openshell.v1.DraftPolicyUpdate - (*SubmitPolicyAnalysisRequest)(nil), // 228: openshell.v1.SubmitPolicyAnalysisRequest - (*SubmitPolicyAnalysisResponse)(nil), // 229: openshell.v1.SubmitPolicyAnalysisResponse - (*GetDraftPolicyRequest)(nil), // 230: openshell.v1.GetDraftPolicyRequest - (*GetDraftPolicyResponse)(nil), // 231: openshell.v1.GetDraftPolicyResponse - (*ApproveDraftChunkRequest)(nil), // 232: openshell.v1.ApproveDraftChunkRequest - (*ApproveDraftChunkResponse)(nil), // 233: openshell.v1.ApproveDraftChunkResponse - (*RejectDraftChunkRequest)(nil), // 234: openshell.v1.RejectDraftChunkRequest - (*RejectDraftChunkResponse)(nil), // 235: openshell.v1.RejectDraftChunkResponse - (*DraftChunkApproval)(nil), // 236: openshell.v1.DraftChunkApproval - (*ApproveAllDraftChunksRequest)(nil), // 237: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 238: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 239: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 240: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 241: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 242: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 243: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 244: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 245: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 246: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 247: openshell.v1.GetDraftHistoryResponse - (*CreateWorkspaceRequest)(nil), // 248: openshell.v1.CreateWorkspaceRequest - (*CreateWorkspaceResponse)(nil), // 249: openshell.v1.CreateWorkspaceResponse - (*GetWorkspaceRequest)(nil), // 250: openshell.v1.GetWorkspaceRequest - (*GetWorkspaceResponse)(nil), // 251: openshell.v1.GetWorkspaceResponse - (*ListWorkspacesRequest)(nil), // 252: openshell.v1.ListWorkspacesRequest - (*ListWorkspacesResponse)(nil), // 253: openshell.v1.ListWorkspacesResponse - (*DeleteWorkspaceRequest)(nil), // 254: openshell.v1.DeleteWorkspaceRequest - (*DeleteWorkspaceResponse)(nil), // 255: openshell.v1.DeleteWorkspaceResponse - (*WorkspaceMember)(nil), // 256: openshell.v1.WorkspaceMember - (*AddWorkspaceMemberRequest)(nil), // 257: openshell.v1.AddWorkspaceMemberRequest - (*AddWorkspaceMemberResponse)(nil), // 258: openshell.v1.AddWorkspaceMemberResponse - (*RemoveWorkspaceMemberRequest)(nil), // 259: openshell.v1.RemoveWorkspaceMemberRequest - (*RemoveWorkspaceMemberResponse)(nil), // 260: openshell.v1.RemoveWorkspaceMemberResponse - (*ListWorkspaceMembersRequest)(nil), // 261: openshell.v1.ListWorkspaceMembersRequest - (*ListWorkspaceMembersResponse)(nil), // 262: openshell.v1.ListWorkspaceMembersResponse - (*ExtensionServiceCredential)(nil), // 263: openshell.v1.ExtensionServiceCredential - (*EndpointObservation)(nil), // 264: openshell.v1.EndpointObservation - (*ReportEndpointStatusRequest)(nil), // 265: openshell.v1.ReportEndpointStatusRequest - (*TokenGrantStatus)(nil), // 266: openshell.v1.TokenGrantStatus - (*TokenGrantFailureObservation)(nil), // 267: openshell.v1.TokenGrantFailureObservation - (*ReportEndpointStatusResponse)(nil), // 268: openshell.v1.ReportEndpointStatusResponse - (*EndpointStatus)(nil), // 269: openshell.v1.EndpointStatus - (*SandboxProvisioning)(nil), // 270: openshell.v1.SandboxProvisioning - (*SandboxServiceExposure)(nil), // 271: openshell.v1.SandboxServiceExposure - (*EndpointFailure)(nil), // 272: openshell.v1.EndpointFailure - nil, // 273: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 274: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 275: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 276: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 277: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry - nil, // 278: openshell.v1.PlatformEvent.MetadataEntry - nil, // 279: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 280: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 281: openshell.v1.SandboxResponse.ServiceUrlsEntry - nil, // 282: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 283: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 284: openshell.v1.GetProviderCredentialsResponse.CredentialsEntry - nil, // 285: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry - nil, // 286: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 287: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 288: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 289: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry - nil, // 290: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 291: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 292: openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntry - nil, // 293: openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry - nil, // 294: openshell.v1.ProviderEnvironmentSnapshot.FilesEntry - nil, // 295: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 296: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 297: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 298: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*timestamppb.Timestamp)(nil), // 299: google.protobuf.Timestamp - (*datamodelv1.ObjectMeta)(nil), // 300: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 301: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 302: google.protobuf.Struct - (*durationpb.Duration)(nil), // 303: google.protobuf.Duration - (*datamodelv1.WorkspaceSelector)(nil), // 304: openshell.datamodel.v1.WorkspaceSelector - (*datamodelv1.Provider)(nil), // 305: openshell.datamodel.v1.Provider - (sandboxv1.PolicySource)(0), // 306: openshell.sandbox.v1.PolicySource - (*sandboxv1.NetworkEndpoint)(nil), // 307: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 308: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 309: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 310: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 311: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 312: openshell.sandbox.v1.L7Rule - (*sandboxv1.SandboxConfigSnapshot)(nil), // 313: openshell.sandbox.v1.SandboxConfigSnapshot - (*datamodelv1.Workspace)(nil), // 314: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 315: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 316: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 317: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 318: openshell.sandbox.v1.GetGatewayConfigResponse + (*UpdateSandboxRequest)(nil), // 55: openshell.v1.UpdateSandboxRequest + (*SandboxCommandUpdate)(nil), // 56: openshell.v1.SandboxCommandUpdate + (*SandboxMapUpdate)(nil), // 57: openshell.v1.SandboxMapUpdate + (*CreateSandboxTemplateRequest)(nil), // 58: openshell.v1.CreateSandboxTemplateRequest + (*GetSandboxTemplateRequest)(nil), // 59: openshell.v1.GetSandboxTemplateRequest + (*ListSandboxTemplatesRequest)(nil), // 60: openshell.v1.ListSandboxTemplatesRequest + (*DeleteSandboxTemplateRequest)(nil), // 61: openshell.v1.DeleteSandboxTemplateRequest + (*SandboxTemplateResponse)(nil), // 62: openshell.v1.SandboxTemplateResponse + (*ListSandboxTemplatesResponse)(nil), // 63: openshell.v1.ListSandboxTemplatesResponse + (*DeleteSandboxTemplateResponse)(nil), // 64: openshell.v1.DeleteSandboxTemplateResponse + (*BeginRootfsTarStagingRequest)(nil), // 65: openshell.v1.BeginRootfsTarStagingRequest + (*BeginRootfsTarStagingResponse)(nil), // 66: openshell.v1.BeginRootfsTarStagingResponse + (*GetSandboxRequest)(nil), // 67: openshell.v1.GetSandboxRequest + (*ListSandboxesRequest)(nil), // 68: openshell.v1.ListSandboxesRequest + (*ListSandboxProvidersRequest)(nil), // 69: openshell.v1.ListSandboxProvidersRequest + (*AttachSandboxProviderRequest)(nil), // 70: openshell.v1.AttachSandboxProviderRequest + (*DetachSandboxProviderRequest)(nil), // 71: openshell.v1.DetachSandboxProviderRequest + (*DeleteSandboxRequest)(nil), // 72: openshell.v1.DeleteSandboxRequest + (*StopSandboxRequest)(nil), // 73: openshell.v1.StopSandboxRequest + (*StartSandboxRequest)(nil), // 74: openshell.v1.StartSandboxRequest + (*SuspendSandboxRequest)(nil), // 75: openshell.v1.SuspendSandboxRequest + (*ResumeSandboxRequest)(nil), // 76: openshell.v1.ResumeSandboxRequest + (*SandboxResponse)(nil), // 77: openshell.v1.SandboxResponse + (*ListSandboxesResponse)(nil), // 78: openshell.v1.ListSandboxesResponse + (*ListSandboxProvidersResponse)(nil), // 79: openshell.v1.ListSandboxProvidersResponse + (*AttachSandboxProviderResponse)(nil), // 80: openshell.v1.AttachSandboxProviderResponse + (*DetachSandboxProviderResponse)(nil), // 81: openshell.v1.DetachSandboxProviderResponse + (*ProviderDesiredIdentity)(nil), // 82: openshell.v1.ProviderDesiredIdentity + (*ConfigSnapshotRevision)(nil), // 83: openshell.v1.ConfigSnapshotRevision + (*SandboxConfigRevision)(nil), // 84: openshell.v1.SandboxConfigRevision + (*ConfigUpdateOperation)(nil), // 85: openshell.v1.ConfigUpdateOperation + (*ProviderMutationReceipt)(nil), // 86: openshell.v1.ProviderMutationReceipt + (*ProviderReadinessObservation)(nil), // 87: openshell.v1.ProviderReadinessObservation + (*ProviderReadinessStatus)(nil), // 88: openshell.v1.ProviderReadinessStatus + (*GetSandboxProviderStatusRequest)(nil), // 89: openshell.v1.GetSandboxProviderStatusRequest + (*GetSandboxProviderStatusResponse)(nil), // 90: openshell.v1.GetSandboxProviderStatusResponse + (*ReportProviderReadinessRequest)(nil), // 91: openshell.v1.ReportProviderReadinessRequest + (*ReportProviderReadinessResponse)(nil), // 92: openshell.v1.ReportProviderReadinessResponse + (*DeleteSandboxResponse)(nil), // 93: openshell.v1.DeleteSandboxResponse + (*CreateSshSessionRequest)(nil), // 94: openshell.v1.CreateSshSessionRequest + (*CreateSshSessionResponse)(nil), // 95: openshell.v1.CreateSshSessionResponse + (*ExposeServiceRequest)(nil), // 96: openshell.v1.ExposeServiceRequest + (*GetServiceRequest)(nil), // 97: openshell.v1.GetServiceRequest + (*ListServicesRequest)(nil), // 98: openshell.v1.ListServicesRequest + (*ListServicesResponse)(nil), // 99: openshell.v1.ListServicesResponse + (*DeleteServiceRequest)(nil), // 100: openshell.v1.DeleteServiceRequest + (*DeleteServiceResponse)(nil), // 101: openshell.v1.DeleteServiceResponse + (*ServiceEndpoint)(nil), // 102: openshell.v1.ServiceEndpoint + (*ServiceEndpointResponse)(nil), // 103: openshell.v1.ServiceEndpointResponse + (*RevokeSshSessionRequest)(nil), // 104: openshell.v1.RevokeSshSessionRequest + (*RevokeSshSessionResponse)(nil), // 105: openshell.v1.RevokeSshSessionResponse + (*ExecSandboxRequest)(nil), // 106: openshell.v1.ExecSandboxRequest + (*ExecSandboxStdout)(nil), // 107: openshell.v1.ExecSandboxStdout + (*ExecSandboxStderr)(nil), // 108: openshell.v1.ExecSandboxStderr + (*ExecSandboxExit)(nil), // 109: openshell.v1.ExecSandboxExit + (*ExecSandboxEvent)(nil), // 110: openshell.v1.ExecSandboxEvent + (*TcpForwardInit)(nil), // 111: openshell.v1.TcpForwardInit + (*TcpForwardFrame)(nil), // 112: openshell.v1.TcpForwardFrame + (*ExecSandboxInput)(nil), // 113: openshell.v1.ExecSandboxInput + (*ExecSandboxWindowResize)(nil), // 114: openshell.v1.ExecSandboxWindowResize + (*SshSession)(nil), // 115: openshell.v1.SshSession + (*WatchSandboxRequest)(nil), // 116: openshell.v1.WatchSandboxRequest + (*SandboxStreamEvent)(nil), // 117: openshell.v1.SandboxStreamEvent + (*SandboxLogLine)(nil), // 118: openshell.v1.SandboxLogLine + (*SandboxStreamWarning)(nil), // 119: openshell.v1.SandboxStreamWarning + (*CreateProviderRequest)(nil), // 120: openshell.v1.CreateProviderRequest + (*GetProviderRequest)(nil), // 121: openshell.v1.GetProviderRequest + (*GetProviderCredentialsRequest)(nil), // 122: openshell.v1.GetProviderCredentialsRequest + (*ProviderCredentialValue)(nil), // 123: openshell.v1.ProviderCredentialValue + (*GetProviderCredentialsResponse)(nil), // 124: openshell.v1.GetProviderCredentialsResponse + (*ListProvidersRequest)(nil), // 125: openshell.v1.ListProvidersRequest + (*UpdateProviderRequest)(nil), // 126: openshell.v1.UpdateProviderRequest + (*DeleteProviderRequest)(nil), // 127: openshell.v1.DeleteProviderRequest + (*ProviderResponse)(nil), // 128: openshell.v1.ProviderResponse + (*ListProvidersResponse)(nil), // 129: openshell.v1.ListProvidersResponse + (*ListProviderProfilesRequest)(nil), // 130: openshell.v1.ListProviderProfilesRequest + (*GetProviderProfileRequest)(nil), // 131: openshell.v1.GetProviderProfileRequest + (*ProviderProfileImportItem)(nil), // 132: openshell.v1.ProviderProfileImportItem + (*ProviderProfileDiagnostic)(nil), // 133: openshell.v1.ProviderProfileDiagnostic + (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 134: openshell.v1.ProviderCredentialTokenGrantAudienceOverride + (*ProviderCredentialTokenGrantSubjectToken)(nil), // 135: openshell.v1.ProviderCredentialTokenGrantSubjectToken + (*ProviderCredentialTokenGrant)(nil), // 136: openshell.v1.ProviderCredentialTokenGrant + (*ProviderProfileCredential)(nil), // 137: openshell.v1.ProviderProfileCredential + (*ProviderCredentialRefreshMaterial)(nil), // 138: openshell.v1.ProviderCredentialRefreshMaterial + (*ProviderCredentialRefreshOutput)(nil), // 139: openshell.v1.ProviderCredentialRefreshOutput + (*ProviderCredentialRefresh)(nil), // 140: openshell.v1.ProviderCredentialRefresh + (*ProviderCredentialRefreshStatus)(nil), // 141: openshell.v1.ProviderCredentialRefreshStatus + (*ProviderProfileDiscovery)(nil), // 142: openshell.v1.ProviderProfileDiscovery + (*GetProviderRefreshStatusRequest)(nil), // 143: openshell.v1.GetProviderRefreshStatusRequest + (*GetProviderRefreshStatusResponse)(nil), // 144: openshell.v1.GetProviderRefreshStatusResponse + (*ConfigureProviderRefreshRequest)(nil), // 145: openshell.v1.ConfigureProviderRefreshRequest + (*ConfigureProviderRefreshResponse)(nil), // 146: openshell.v1.ConfigureProviderRefreshResponse + (*RotateProviderCredentialRequest)(nil), // 147: openshell.v1.RotateProviderCredentialRequest + (*RotateProviderCredentialResponse)(nil), // 148: openshell.v1.RotateProviderCredentialResponse + (*DeleteProviderRefreshRequest)(nil), // 149: openshell.v1.DeleteProviderRefreshRequest + (*DeleteProviderRefreshResponse)(nil), // 150: openshell.v1.DeleteProviderRefreshResponse + (*ProviderProfile)(nil), // 151: openshell.v1.ProviderProfile + (*ProviderProfileFile)(nil), // 152: openshell.v1.ProviderProfileFile + (*ProviderProfileResponse)(nil), // 153: openshell.v1.ProviderProfileResponse + (*ListProviderProfilesResponse)(nil), // 154: openshell.v1.ListProviderProfilesResponse + (*ImportProviderProfilesRequest)(nil), // 155: openshell.v1.ImportProviderProfilesRequest + (*ImportProviderProfilesResponse)(nil), // 156: openshell.v1.ImportProviderProfilesResponse + (*UpdateProviderProfilesRequest)(nil), // 157: openshell.v1.UpdateProviderProfilesRequest + (*UpdateProviderProfilesResponse)(nil), // 158: openshell.v1.UpdateProviderProfilesResponse + (*LintProviderProfilesRequest)(nil), // 159: openshell.v1.LintProviderProfilesRequest + (*LintProviderProfilesResponse)(nil), // 160: openshell.v1.LintProviderProfilesResponse + (*DeleteProviderResponse)(nil), // 161: openshell.v1.DeleteProviderResponse + (*DeleteProviderProfileRequest)(nil), // 162: openshell.v1.DeleteProviderProfileRequest + (*DeleteProviderProfileResponse)(nil), // 163: openshell.v1.DeleteProviderProfileResponse + (*GetSandboxProviderEnvironmentRequest)(nil), // 164: openshell.v1.GetSandboxProviderEnvironmentRequest + (*StaticCredentialEndpointBinding)(nil), // 165: openshell.v1.StaticCredentialEndpointBinding + (*StaticCredentialBinding)(nil), // 166: openshell.v1.StaticCredentialBinding + (*GetSandboxProviderEnvironmentResponse)(nil), // 167: openshell.v1.GetSandboxProviderEnvironmentResponse + (*ProviderEnvironmentValue)(nil), // 168: openshell.v1.ProviderEnvironmentValue + (*ProviderEnvironmentSnapshot)(nil), // 169: openshell.v1.ProviderEnvironmentSnapshot + (*ExchangeProviderSubjectTokenRequest)(nil), // 170: openshell.v1.ExchangeProviderSubjectTokenRequest + (*ExchangeProviderSubjectTokenResponse)(nil), // 171: openshell.v1.ExchangeProviderSubjectTokenResponse + (*UpdateConfigRequest)(nil), // 172: openshell.v1.UpdateConfigRequest + (*PolicyMergeOperation)(nil), // 173: openshell.v1.PolicyMergeOperation + (*AddNetworkRule)(nil), // 174: openshell.v1.AddNetworkRule + (*RemoveNetworkEndpoint)(nil), // 175: openshell.v1.RemoveNetworkEndpoint + (*RemoveNetworkRule)(nil), // 176: openshell.v1.RemoveNetworkRule + (*L7RuleTarget)(nil), // 177: openshell.v1.L7RuleTarget + (*AddDenyRules)(nil), // 178: openshell.v1.AddDenyRules + (*AddAllowRules)(nil), // 179: openshell.v1.AddAllowRules + (*RemoveNetworkBinary)(nil), // 180: openshell.v1.RemoveNetworkBinary + (*UpdateConfigResponse)(nil), // 181: openshell.v1.UpdateConfigResponse + (*GetSandboxPolicyStatusRequest)(nil), // 182: openshell.v1.GetSandboxPolicyStatusRequest + (*GetSandboxPolicyStatusResponse)(nil), // 183: openshell.v1.GetSandboxPolicyStatusResponse + (*ListSandboxPoliciesRequest)(nil), // 184: openshell.v1.ListSandboxPoliciesRequest + (*ListSandboxPoliciesResponse)(nil), // 185: openshell.v1.ListSandboxPoliciesResponse + (*ReportPolicyStatusRequest)(nil), // 186: openshell.v1.ReportPolicyStatusRequest + (*ReportPolicyStatusResponse)(nil), // 187: openshell.v1.ReportPolicyStatusResponse + (*SandboxConfigurationAdmission)(nil), // 188: openshell.v1.SandboxConfigurationAdmission + (*ReportSandboxConfigurationRequest)(nil), // 189: openshell.v1.ReportSandboxConfigurationRequest + (*ReportSandboxConfigurationResponse)(nil), // 190: openshell.v1.ReportSandboxConfigurationResponse + (*SandboxPolicyRevision)(nil), // 191: openshell.v1.SandboxPolicyRevision + (*GetSandboxLogsRequest)(nil), // 192: openshell.v1.GetSandboxLogsRequest + (*PushSandboxLogsRequest)(nil), // 193: openshell.v1.PushSandboxLogsRequest + (*PushSandboxLogsResponse)(nil), // 194: openshell.v1.PushSandboxLogsResponse + (*GetSandboxLogsResponse)(nil), // 195: openshell.v1.GetSandboxLogsResponse + (*SupervisorMessage)(nil), // 196: openshell.v1.SupervisorMessage + (*GatewayMessage)(nil), // 197: openshell.v1.GatewayMessage + (*SupervisorHello)(nil), // 198: openshell.v1.SupervisorHello + (*SessionAccepted)(nil), // 199: openshell.v1.SessionAccepted + (*ConfigUpdate)(nil), // 200: openshell.v1.ConfigUpdate + (*ConfigApplyFailure)(nil), // 201: openshell.v1.ConfigApplyFailure + (*ConfigUpdateResult)(nil), // 202: openshell.v1.ConfigUpdateResult + (*PolicyLoadResult)(nil), // 203: openshell.v1.PolicyLoadResult + (*SessionRejected)(nil), // 204: openshell.v1.SessionRejected + (*SessionRedirect)(nil), // 205: openshell.v1.SessionRedirect + (*SupervisorHeartbeat)(nil), // 206: openshell.v1.SupervisorHeartbeat + (*GatewayHeartbeat)(nil), // 207: openshell.v1.GatewayHeartbeat + (*ReportMainProcessExitRequest)(nil), // 208: openshell.v1.ReportMainProcessExitRequest + (*ReportMainProcessExitResponse)(nil), // 209: openshell.v1.ReportMainProcessExitResponse + (*FinalizeMainProcessExitRequest)(nil), // 210: openshell.v1.FinalizeMainProcessExitRequest + (*FinalizeMainProcessExitResponse)(nil), // 211: openshell.v1.FinalizeMainProcessExitResponse + (*RelayOpen)(nil), // 212: openshell.v1.RelayOpen + (*SshRelayTarget)(nil), // 213: openshell.v1.SshRelayTarget + (*TcpRelayTarget)(nil), // 214: openshell.v1.TcpRelayTarget + (*RelayInit)(nil), // 215: openshell.v1.RelayInit + (*RelayFrame)(nil), // 216: openshell.v1.RelayFrame + (*PeerRelayInit)(nil), // 217: openshell.v1.PeerRelayInit + (*PeerRelayFrame)(nil), // 218: openshell.v1.PeerRelayFrame + (*PeerConfigSandboxTarget)(nil), // 219: openshell.v1.PeerConfigSandboxTarget + (*PeerConfigProviderTarget)(nil), // 220: openshell.v1.PeerConfigProviderTarget + (*PeerNotifyConfigUpdateRequest)(nil), // 221: openshell.v1.PeerNotifyConfigUpdateRequest + (*PeerNotifyConfigUpdateResponse)(nil), // 222: openshell.v1.PeerNotifyConfigUpdateResponse + (*RelayOpenResult)(nil), // 223: openshell.v1.RelayOpenResult + (*RelayClose)(nil), // 224: openshell.v1.RelayClose + (*L7RequestSample)(nil), // 225: openshell.v1.L7RequestSample + (*DenialSummary)(nil), // 226: openshell.v1.DenialSummary + (*DenialGroupCount)(nil), // 227: openshell.v1.DenialGroupCount + (*NetworkActivitySummary)(nil), // 228: openshell.v1.NetworkActivitySummary + (*PolicyChunk)(nil), // 229: openshell.v1.PolicyChunk + (*DraftPolicyUpdate)(nil), // 230: openshell.v1.DraftPolicyUpdate + (*SubmitPolicyAnalysisRequest)(nil), // 231: openshell.v1.SubmitPolicyAnalysisRequest + (*SubmitPolicyAnalysisResponse)(nil), // 232: openshell.v1.SubmitPolicyAnalysisResponse + (*GetDraftPolicyRequest)(nil), // 233: openshell.v1.GetDraftPolicyRequest + (*GetDraftPolicyResponse)(nil), // 234: openshell.v1.GetDraftPolicyResponse + (*ApproveDraftChunkRequest)(nil), // 235: openshell.v1.ApproveDraftChunkRequest + (*ApproveDraftChunkResponse)(nil), // 236: openshell.v1.ApproveDraftChunkResponse + (*RejectDraftChunkRequest)(nil), // 237: openshell.v1.RejectDraftChunkRequest + (*RejectDraftChunkResponse)(nil), // 238: openshell.v1.RejectDraftChunkResponse + (*DraftChunkApproval)(nil), // 239: openshell.v1.DraftChunkApproval + (*ApproveAllDraftChunksRequest)(nil), // 240: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 241: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 242: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 243: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 244: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 245: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 246: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 247: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 248: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 249: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 250: openshell.v1.GetDraftHistoryResponse + (*CreateWorkspaceRequest)(nil), // 251: openshell.v1.CreateWorkspaceRequest + (*CreateWorkspaceResponse)(nil), // 252: openshell.v1.CreateWorkspaceResponse + (*GetWorkspaceRequest)(nil), // 253: openshell.v1.GetWorkspaceRequest + (*GetWorkspaceResponse)(nil), // 254: openshell.v1.GetWorkspaceResponse + (*ListWorkspacesRequest)(nil), // 255: openshell.v1.ListWorkspacesRequest + (*ListWorkspacesResponse)(nil), // 256: openshell.v1.ListWorkspacesResponse + (*DeleteWorkspaceRequest)(nil), // 257: openshell.v1.DeleteWorkspaceRequest + (*DeleteWorkspaceResponse)(nil), // 258: openshell.v1.DeleteWorkspaceResponse + (*WorkspaceMember)(nil), // 259: openshell.v1.WorkspaceMember + (*AddWorkspaceMemberRequest)(nil), // 260: openshell.v1.AddWorkspaceMemberRequest + (*AddWorkspaceMemberResponse)(nil), // 261: openshell.v1.AddWorkspaceMemberResponse + (*RemoveWorkspaceMemberRequest)(nil), // 262: openshell.v1.RemoveWorkspaceMemberRequest + (*RemoveWorkspaceMemberResponse)(nil), // 263: openshell.v1.RemoveWorkspaceMemberResponse + (*ListWorkspaceMembersRequest)(nil), // 264: openshell.v1.ListWorkspaceMembersRequest + (*ListWorkspaceMembersResponse)(nil), // 265: openshell.v1.ListWorkspaceMembersResponse + (*ExtensionServiceCredential)(nil), // 266: openshell.v1.ExtensionServiceCredential + (*EndpointObservation)(nil), // 267: openshell.v1.EndpointObservation + (*ReportEndpointStatusRequest)(nil), // 268: openshell.v1.ReportEndpointStatusRequest + (*TokenGrantStatus)(nil), // 269: openshell.v1.TokenGrantStatus + (*TokenGrantFailureObservation)(nil), // 270: openshell.v1.TokenGrantFailureObservation + (*ReportEndpointStatusResponse)(nil), // 271: openshell.v1.ReportEndpointStatusResponse + (*EndpointStatus)(nil), // 272: openshell.v1.EndpointStatus + (*SandboxProvisioning)(nil), // 273: openshell.v1.SandboxProvisioning + (*SandboxServiceExposure)(nil), // 274: openshell.v1.SandboxServiceExposure + (*EndpointFailure)(nil), // 275: openshell.v1.EndpointFailure + nil, // 276: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 277: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 278: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 279: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 280: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + nil, // 281: openshell.v1.PlatformEvent.MetadataEntry + nil, // 282: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 283: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 284: openshell.v1.SandboxMapUpdate.ValuesEntry + nil, // 285: openshell.v1.SandboxResponse.ServiceUrlsEntry + nil, // 286: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 287: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 288: openshell.v1.GetProviderCredentialsResponse.CredentialsEntry + nil, // 289: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + nil, // 290: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 291: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 292: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 293: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + nil, // 294: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 295: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 296: openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntry + nil, // 297: openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry + nil, // 298: openshell.v1.ProviderEnvironmentSnapshot.FilesEntry + nil, // 299: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 300: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 301: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 302: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*timestamppb.Timestamp)(nil), // 303: google.protobuf.Timestamp + (*datamodelv1.ObjectMeta)(nil), // 304: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 305: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 306: google.protobuf.Struct + (*durationpb.Duration)(nil), // 307: google.protobuf.Duration + (*datamodelv1.WorkspaceSelector)(nil), // 308: openshell.datamodel.v1.WorkspaceSelector + (*datamodelv1.Provider)(nil), // 309: openshell.datamodel.v1.Provider + (sandboxv1.PolicySource)(0), // 310: openshell.sandbox.v1.PolicySource + (*sandboxv1.NetworkEndpoint)(nil), // 311: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 312: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 313: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 314: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 315: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 316: openshell.sandbox.v1.L7Rule + (*sandboxv1.SandboxConfigSnapshot)(nil), // 317: openshell.sandbox.v1.SandboxConfigSnapshot + (*datamodelv1.Workspace)(nil), // 318: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 319: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 320: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 321: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 322: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 299, // 0: openshell.v1.IssueSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp - 299, // 1: openshell.v1.RefreshSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp - 263, // 2: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential - 299, // 3: openshell.v1.RefreshSandboxTokenResponse.sandbox_expiration_time:type_name -> google.protobuf.Timestamp + 303, // 0: openshell.v1.IssueSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp + 303, // 1: openshell.v1.RefreshSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp + 266, // 2: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential + 303, // 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 33, // 6: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo @@ -21678,530 +21914,538 @@ var file_openshell_proto_depIdxs = []int32{ 36, // 11: openshell.v1.ResourceCapabilities.cpu:type_name -> openshell.v1.CpuResourceCapabilities 37, // 12: openshell.v1.ResourceCapabilities.memory:type_name -> openshell.v1.MemoryResourceCapabilities 38, // 13: openshell.v1.ResourceCapabilities.gpu:type_name -> openshell.v1.GpuResourceCapabilities - 300, // 14: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 304, // 14: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 40, // 15: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec 51, // 16: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus 50, // 17: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance - 273, // 18: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 276, // 18: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry 43, // 19: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 301, // 20: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 305, // 20: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy 41, // 21: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements 17, // 22: openshell.v1.SandboxSpec.restart_policy:type_name -> openshell.v1.SandboxRestartPolicy 42, // 23: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 274, // 24: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 275, // 25: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 276, // 26: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 302, // 27: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 302, // 28: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 300, // 29: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 277, // 24: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 278, // 25: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 279, // 26: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 306, // 27: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 306, // 28: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 304, // 29: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 45, // 30: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec 46, // 31: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig - 302, // 32: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct + 306, // 32: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct 48, // 33: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel - 277, // 34: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + 280, // 34: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry 47, // 35: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources 42, // 36: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements 49, // 37: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup - 303, // 38: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration + 307, // 38: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration 52, // 39: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition 1, // 40: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 269, // 41: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus - 185, // 42: openshell.v1.SandboxStatus.configuration_admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 270, // 43: openshell.v1.SandboxStatus.provisioning:type_name -> openshell.v1.SandboxProvisioning - 299, // 44: openshell.v1.SandboxStatus.next_restart_time:type_name -> google.protobuf.Timestamp - 299, // 45: openshell.v1.SandboxStatus.main_process_started_time:type_name -> google.protobuf.Timestamp - 267, // 46: openshell.v1.SandboxStatus.last_token_grant_failure:type_name -> openshell.v1.TokenGrantFailureObservation - 299, // 47: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp - 299, // 48: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp - 278, // 49: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 304, // 50: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 41: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus + 188, // 42: openshell.v1.SandboxStatus.configuration_admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 273, // 43: openshell.v1.SandboxStatus.provisioning:type_name -> openshell.v1.SandboxProvisioning + 303, // 44: openshell.v1.SandboxStatus.next_restart_time:type_name -> google.protobuf.Timestamp + 303, // 45: openshell.v1.SandboxStatus.main_process_started_time:type_name -> google.protobuf.Timestamp + 270, // 46: openshell.v1.SandboxStatus.last_token_grant_failure:type_name -> openshell.v1.TokenGrantFailureObservation + 303, // 47: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp + 303, // 48: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp + 281, // 49: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 308, // 50: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 40, // 51: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 279, // 52: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 280, // 53: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 271, // 54: openshell.v1.CreateSandboxRequest.service_exposures:type_name -> openshell.v1.SandboxServiceExposure - 304, // 55: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 44, // 56: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 304, // 57: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 58: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 59: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 44, // 60: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 44, // 61: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate - 18, // 62: openshell.v1.DeleteSandboxTemplateResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 304, // 63: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 299, // 64: openshell.v1.BeginRootfsTarStagingResponse.expiration_time:type_name -> google.protobuf.Timestamp - 304, // 65: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 66: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 67: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 68: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 69: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 70: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 71: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 72: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 73: openshell.v1.SuspendSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 74: openshell.v1.ResumeSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 39, // 75: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 281, // 76: openshell.v1.SandboxResponse.service_urls:type_name -> openshell.v1.SandboxResponse.ServiceUrlsEntry - 39, // 77: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 305, // 78: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 39, // 79: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 83, // 80: openshell.v1.AttachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt - 39, // 81: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 83, // 82: openshell.v1.DetachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt - 81, // 83: openshell.v1.ConfigSnapshotRevision.sandbox_config:type_name -> openshell.v1.SandboxConfigRevision - 79, // 84: openshell.v1.ConfigSnapshotRevision.provider_target:type_name -> openshell.v1.ProviderDesiredIdentity - 306, // 85: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource - 5, // 86: openshell.v1.ConfigUpdateOperation.component:type_name -> openshell.v1.ConfigComponent - 80, // 87: openshell.v1.ConfigUpdateOperation.target_revision:type_name -> openshell.v1.ConfigSnapshotRevision - 7, // 88: openshell.v1.ConfigUpdateOperation.state:type_name -> openshell.v1.ConfigUpdateOperationState - 6, // 89: openshell.v1.ConfigUpdateOperation.outcome:type_name -> openshell.v1.ConfigApplyOutcome - 299, // 90: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp - 299, // 91: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp - 299, // 92: openshell.v1.ConfigUpdateOperation.completed_time:type_name -> google.protobuf.Timestamp - 2, // 93: openshell.v1.ProviderMutationReceipt.kind:type_name -> openshell.v1.ProviderMutationKind - 79, // 94: openshell.v1.ProviderMutationReceipt.desired:type_name -> openshell.v1.ProviderDesiredIdentity - 299, // 95: openshell.v1.ProviderMutationReceipt.persisted_time:type_name -> google.protobuf.Timestamp - 4, // 96: openshell.v1.ProviderReadinessObservation.reason:type_name -> openshell.v1.ProviderReadinessReason - 83, // 97: openshell.v1.ProviderReadinessStatus.receipt:type_name -> openshell.v1.ProviderMutationReceipt - 3, // 98: openshell.v1.ProviderReadinessStatus.state:type_name -> openshell.v1.ProviderReadinessState - 4, // 99: openshell.v1.ProviderReadinessStatus.reason:type_name -> openshell.v1.ProviderReadinessReason - 84, // 100: openshell.v1.ProviderReadinessStatus.observed:type_name -> openshell.v1.ProviderReadinessObservation - 299, // 101: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp - 299, // 102: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp - 82, // 103: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation - 304, // 104: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 85, // 105: openshell.v1.GetSandboxProviderStatusResponse.status:type_name -> openshell.v1.ProviderReadinessStatus - 84, // 106: openshell.v1.ReportProviderReadinessRequest.observation:type_name -> openshell.v1.ProviderReadinessObservation - 303, // 107: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration - 303, // 108: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration - 18, // 109: openshell.v1.DeleteSandboxResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 304, // 110: openshell.v1.CreateSshSessionRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 299, // 111: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp - 304, // 112: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 20, // 113: openshell.v1.ExposeServiceRequest.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode - 304, // 114: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 115: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 100, // 116: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 304, // 117: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 18, // 118: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 300, // 119: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 20, // 120: openshell.v1.ServiceEndpoint.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode - 99, // 121: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 18, // 122: openshell.v1.RevokeSshSessionResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 304, // 123: openshell.v1.ExecSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 282, // 124: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 303, // 125: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration - 104, // 126: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 105, // 127: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 106, // 128: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 210, // 129: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 211, // 130: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 108, // 131: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 103, // 132: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 111, // 133: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 300, // 134: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 299, // 135: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp - 304, // 136: openshell.v1.WatchSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 299, // 137: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp - 39, // 138: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 115, // 139: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 53, // 140: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 116, // 141: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 227, // 142: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 299, // 143: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp - 283, // 144: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 304, // 145: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 305, // 146: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 304, // 147: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 148: openshell.v1.GetProviderCredentialsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 303, // 149: openshell.v1.GetProviderCredentialsRequest.minimum_remaining_lifetime:type_name -> google.protobuf.Duration - 299, // 150: openshell.v1.ProviderCredentialValue.expiration_time:type_name -> google.protobuf.Timestamp - 284, // 151: openshell.v1.GetProviderCredentialsResponse.credentials:type_name -> openshell.v1.GetProviderCredentialsResponse.CredentialsEntry - 304, // 152: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 153: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 305, // 154: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 285, // 155: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry - 304, // 156: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 305, // 157: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 83, // 158: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt - 305, // 159: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 304, // 160: openshell.v1.ListProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 161: openshell.v1.GetProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 148, // 162: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 303, // 163: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration - 131, // 164: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride - 8, // 165: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType - 132, // 166: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken - 137, // 167: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 133, // 168: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant - 9, // 169: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 303, // 170: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration - 303, // 171: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration - 135, // 172: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 136, // 173: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput - 9, // 174: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 299, // 175: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp - 299, // 176: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp - 299, // 177: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp - 16, // 178: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 299, // 179: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp - 304, // 180: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 138, // 181: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 304, // 182: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 9, // 183: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 286, // 184: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 299, // 185: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp - 138, // 186: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 304, // 187: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 138, // 188: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 304, // 189: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 18, // 190: openshell.v1.DeleteProviderRefreshResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 10, // 191: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 134, // 192: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 307, // 193: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 308, // 194: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 139, // 195: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 287, // 196: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 149, // 197: openshell.v1.ProviderProfile.files:type_name -> openshell.v1.ProviderProfileFile - 148, // 198: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 148, // 199: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 304, // 200: openshell.v1.ImportProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 129, // 201: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 130, // 202: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 148, // 203: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 304, // 204: openshell.v1.UpdateProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 129, // 205: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 130, // 206: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 148, // 207: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 304, // 208: openshell.v1.LintProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 129, // 209: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 130, // 210: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 18, // 211: openshell.v1.DeleteProviderResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 304, // 212: openshell.v1.DeleteProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 18, // 213: openshell.v1.DeleteProviderProfileResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 162, // 214: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 288, // 215: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 289, // 216: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry - 290, // 217: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 291, // 218: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 4, // 219: openshell.v1.GetSandboxProviderEnvironmentResponse.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason - 292, // 220: openshell.v1.GetSandboxProviderEnvironmentResponse.files:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntry - 11, // 221: openshell.v1.ProviderEnvironmentValue.classification:type_name -> openshell.v1.ProviderEnvironmentValueClassification - 163, // 222: openshell.v1.ProviderEnvironmentValue.static_credential_binding:type_name -> openshell.v1.StaticCredentialBinding - 299, // 223: openshell.v1.ProviderEnvironmentValue.expiration_time:type_name -> google.protobuf.Timestamp - 165, // 224: openshell.v1.ProviderEnvironmentSnapshot.values:type_name -> openshell.v1.ProviderEnvironmentValue - 293, // 225: openshell.v1.ProviderEnvironmentSnapshot.dynamic_credentials:type_name -> openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry - 4, // 226: openshell.v1.ProviderEnvironmentSnapshot.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason - 294, // 227: openshell.v1.ProviderEnvironmentSnapshot.files:type_name -> openshell.v1.ProviderEnvironmentSnapshot.FilesEntry - 303, // 228: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration - 304, // 229: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 301, // 230: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 309, // 231: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 170, // 232: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 295, // 233: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 171, // 234: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 172, // 235: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 173, // 236: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 175, // 237: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 176, // 238: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 177, // 239: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 310, // 240: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 308, // 241: openshell.v1.L7RuleTarget.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 311, // 242: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 174, // 243: openshell.v1.AddDenyRules.target:type_name -> openshell.v1.L7RuleTarget - 312, // 244: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 174, // 245: openshell.v1.AddAllowRules.target:type_name -> openshell.v1.L7RuleTarget - 296, // 246: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 304, // 247: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 188, // 248: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 304, // 249: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 188, // 250: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 13, // 251: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 12, // 252: openshell.v1.SandboxConfigurationAdmission.state:type_name -> openshell.v1.ConfigurationAdmissionState - 185, // 253: openshell.v1.ReportSandboxConfigurationRequest.admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 13, // 254: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 299, // 255: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp - 299, // 256: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp - 301, // 257: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 297, // 258: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 304, // 259: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 299, // 260: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp - 115, // 261: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 115, // 262: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 195, // 263: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 203, // 264: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 220, // 265: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 221, // 266: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 199, // 267: openshell.v1.SupervisorMessage.config_update_result:type_name -> openshell.v1.ConfigUpdateResult - 196, // 268: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 201, // 269: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 204, // 270: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 209, // 271: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 221, // 272: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 202, // 273: openshell.v1.GatewayMessage.session_redirect:type_name -> openshell.v1.SessionRedirect - 197, // 274: openshell.v1.GatewayMessage.config_update:type_name -> openshell.v1.ConfigUpdate - 197, // 275: openshell.v1.SessionAccepted.bootstrap:type_name -> openshell.v1.ConfigUpdate - 303, // 276: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration - 313, // 277: openshell.v1.ConfigUpdate.sandbox_config:type_name -> openshell.sandbox.v1.SandboxConfigSnapshot - 166, // 278: openshell.v1.ConfigUpdate.provider_environment:type_name -> openshell.v1.ProviderEnvironmentSnapshot - 6, // 279: openshell.v1.ConfigUpdateResult.outcome:type_name -> openshell.v1.ConfigApplyOutcome - 198, // 280: openshell.v1.ConfigUpdateResult.failure:type_name -> openshell.v1.ConfigApplyFailure - 185, // 281: openshell.v1.ConfigUpdateResult.admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 200, // 282: openshell.v1.ConfigUpdateResult.policy_load:type_name -> openshell.v1.PolicyLoadResult - 210, // 283: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 211, // 284: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 212, // 285: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 209, // 286: openshell.v1.PeerRelayInit.relay_open:type_name -> openshell.v1.RelayOpen - 214, // 287: openshell.v1.PeerRelayFrame.init:type_name -> openshell.v1.PeerRelayInit - 216, // 288: openshell.v1.PeerNotifyConfigUpdateRequest.sandbox:type_name -> openshell.v1.PeerConfigSandboxTarget - 217, // 289: openshell.v1.PeerNotifyConfigUpdateRequest.provider:type_name -> openshell.v1.PeerConfigProviderTarget - 299, // 290: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp - 299, // 291: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp - 222, // 292: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 224, // 293: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 310, // 294: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 299, // 295: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp - 299, // 296: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp - 299, // 297: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp - 299, // 298: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp - 301, // 299: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 301, // 300: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 304, // 301: openshell.v1.SubmitPolicyAnalysisRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 223, // 302: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 226, // 303: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 225, // 304: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 304, // 305: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 226, // 306: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 299, // 307: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp - 304, // 308: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 309: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 310: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 236, // 311: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 304, // 312: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 310, // 313: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 304, // 314: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 315: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 304, // 316: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 299, // 317: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp - 246, // 318: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 298, // 319: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 314, // 320: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 314, // 321: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 314, // 322: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 18, // 323: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 300, // 324: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 15, // 325: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 304, // 326: openshell.v1.AddWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 15, // 327: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 256, // 328: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 304, // 329: openshell.v1.RemoveWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 18, // 330: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 304, // 331: openshell.v1.ListWorkspaceMembersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 256, // 332: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 299, // 333: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp - 19, // 334: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult - 272, // 335: openshell.v1.EndpointObservation.failure:type_name -> openshell.v1.EndpointFailure - 264, // 336: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation - 266, // 337: openshell.v1.ReportEndpointStatusRequest.token_grant_status:type_name -> openshell.v1.TokenGrantStatus - 267, // 338: openshell.v1.TokenGrantStatus.last_failure:type_name -> openshell.v1.TokenGrantFailureObservation - 272, // 339: openshell.v1.TokenGrantFailureObservation.failure:type_name -> openshell.v1.EndpointFailure - 19, // 340: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult - 299, // 341: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp - 272, // 342: openshell.v1.EndpointStatus.last_failure:type_name -> openshell.v1.EndpointFailure - 299, // 343: openshell.v1.SandboxProvisioning.configuration_change_time:type_name -> google.protobuf.Timestamp - 299, // 344: openshell.v1.SandboxProvisioning.first_rejection_time:type_name -> google.protobuf.Timestamp - 299, // 345: openshell.v1.SandboxProvisioning.deadline:type_name -> google.protobuf.Timestamp - 299, // 346: openshell.v1.SandboxProvisioning.timeout_time:type_name -> google.protobuf.Timestamp - 299, // 347: openshell.v1.SandboxProvisioning.cleanup_completed_time:type_name -> google.protobuf.Timestamp - 299, // 348: openshell.v1.SandboxProvisioning.cleanup_retry_time:type_name -> google.protobuf.Timestamp - 299, // 349: openshell.v1.SandboxProvisioning.attachment_change_time:type_name -> google.protobuf.Timestamp - 299, // 350: openshell.v1.SandboxProvisioning.preparation_deadline:type_name -> google.protobuf.Timestamp - 299, // 351: openshell.v1.SandboxProvisioning.admission_start_time:type_name -> google.protobuf.Timestamp - 20, // 352: openshell.v1.SandboxServiceExposure.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode - 21, // 353: openshell.v1.EndpointFailure.domain:type_name -> openshell.v1.EndpointFailureDomain - 16, // 354: openshell.v1.EndpointFailure.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 120, // 355: openshell.v1.GetProviderCredentialsResponse.CredentialsEntry.value:type_name -> openshell.v1.ProviderCredentialValue - 299, // 356: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 299, // 357: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 134, // 358: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 163, // 359: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 134, // 360: openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 26, // 361: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 28, // 362: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 30, // 363: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 54, // 364: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 62, // 365: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest - 64, // 366: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 65, // 367: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 55, // 368: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest - 56, // 369: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest - 57, // 370: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest - 58, // 371: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest - 66, // 372: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 67, // 373: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 68, // 374: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 86, // 375: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest - 69, // 376: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 70, // 377: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 71, // 378: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 72, // 379: openshell.v1.OpenShell.SuspendSandbox:input_type -> openshell.v1.SuspendSandboxRequest - 73, // 380: openshell.v1.OpenShell.ResumeSandbox:input_type -> openshell.v1.ResumeSandboxRequest - 91, // 381: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 93, // 382: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 94, // 383: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 95, // 384: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 97, // 385: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 101, // 386: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 103, // 387: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 109, // 388: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 110, // 389: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 117, // 390: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 118, // 391: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 119, // 392: openshell.v1.OpenShell.GetProviderCredentials:input_type -> openshell.v1.GetProviderCredentialsRequest - 122, // 393: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 127, // 394: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 128, // 395: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 152, // 396: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 154, // 397: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 156, // 398: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 123, // 399: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 140, // 400: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 142, // 401: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 144, // 402: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 146, // 403: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 124, // 404: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 159, // 405: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 315, // 406: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 316, // 407: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 169, // 408: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 179, // 409: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 181, // 410: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 183, // 411: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 265, // 412: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest - 88, // 413: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest - 186, // 414: openshell.v1.OpenShell.ReportSandboxConfiguration:input_type -> openshell.v1.ReportSandboxConfigurationRequest - 161, // 415: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 167, // 416: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 189, // 417: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 190, // 418: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 193, // 419: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 205, // 420: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 207, // 421: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest - 213, // 422: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 215, // 423: openshell.v1.OpenShell.PeerRelay:input_type -> openshell.v1.PeerRelayFrame - 88, // 424: openshell.v1.OpenShell.PeerReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest - 265, // 425: openshell.v1.OpenShell.PeerReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest - 86, // 426: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest - 218, // 427: openshell.v1.OpenShell.PeerNotifyConfigUpdate:input_type -> openshell.v1.PeerNotifyConfigUpdateRequest - 113, // 428: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 228, // 429: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 230, // 430: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 232, // 431: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 234, // 432: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 237, // 433: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 239, // 434: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 241, // 435: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 243, // 436: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 245, // 437: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 22, // 438: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 24, // 439: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 248, // 440: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 250, // 441: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 252, // 442: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 254, // 443: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 257, // 444: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 259, // 445: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 261, // 446: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 27, // 447: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 29, // 448: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 31, // 449: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 74, // 450: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 63, // 451: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse - 74, // 452: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 75, // 453: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 59, // 454: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 59, // 455: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 60, // 456: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse - 61, // 457: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse - 76, // 458: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 77, // 459: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 78, // 460: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 87, // 461: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse - 90, // 462: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 74, // 463: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 74, // 464: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 74, // 465: openshell.v1.OpenShell.SuspendSandbox:output_type -> openshell.v1.SandboxResponse - 74, // 466: openshell.v1.OpenShell.ResumeSandbox:output_type -> openshell.v1.SandboxResponse - 92, // 467: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 100, // 468: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 100, // 469: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 96, // 470: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 98, // 471: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 102, // 472: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 107, // 473: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 109, // 474: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 107, // 475: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 125, // 476: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 125, // 477: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 121, // 478: openshell.v1.OpenShell.GetProviderCredentials:output_type -> openshell.v1.GetProviderCredentialsResponse - 126, // 479: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 151, // 480: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 150, // 481: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 153, // 482: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 155, // 483: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 157, // 484: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 125, // 485: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 141, // 486: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 143, // 487: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 145, // 488: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 147, // 489: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 158, // 490: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 160, // 491: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 317, // 492: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 318, // 493: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 178, // 494: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 180, // 495: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 182, // 496: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 184, // 497: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 268, // 498: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse - 89, // 499: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse - 187, // 500: openshell.v1.OpenShell.ReportSandboxConfiguration:output_type -> openshell.v1.ReportSandboxConfigurationResponse - 164, // 501: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 168, // 502: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 192, // 503: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 191, // 504: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 194, // 505: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 206, // 506: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 208, // 507: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse - 213, // 508: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 215, // 509: openshell.v1.OpenShell.PeerRelay:output_type -> openshell.v1.PeerRelayFrame - 89, // 510: openshell.v1.OpenShell.PeerReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse - 268, // 511: openshell.v1.OpenShell.PeerReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse - 87, // 512: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse - 219, // 513: openshell.v1.OpenShell.PeerNotifyConfigUpdate:output_type -> openshell.v1.PeerNotifyConfigUpdateResponse - 114, // 514: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 229, // 515: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 231, // 516: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 233, // 517: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 235, // 518: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 238, // 519: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 240, // 520: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 242, // 521: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 244, // 522: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 247, // 523: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 23, // 524: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 25, // 525: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 249, // 526: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 251, // 527: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 253, // 528: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 255, // 529: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 258, // 530: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 260, // 531: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 262, // 532: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 447, // [447:533] is the sub-list for method output_type - 361, // [361:447] is the sub-list for method input_type - 361, // [361:361] is the sub-list for extension type_name - 361, // [361:361] is the sub-list for extension extendee - 0, // [0:361] is the sub-list for field type_name + 282, // 52: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 283, // 53: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 274, // 54: openshell.v1.CreateSandboxRequest.service_exposures:type_name -> openshell.v1.SandboxServiceExposure + 308, // 55: openshell.v1.UpdateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 56, // 56: openshell.v1.UpdateSandboxRequest.command:type_name -> openshell.v1.SandboxCommandUpdate + 57, // 57: openshell.v1.UpdateSandboxRequest.environment:type_name -> openshell.v1.SandboxMapUpdate + 57, // 58: openshell.v1.UpdateSandboxRequest.labels:type_name -> openshell.v1.SandboxMapUpdate + 57, // 59: openshell.v1.UpdateSandboxRequest.annotations:type_name -> openshell.v1.SandboxMapUpdate + 284, // 60: openshell.v1.SandboxMapUpdate.values:type_name -> openshell.v1.SandboxMapUpdate.ValuesEntry + 308, // 61: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 44, // 62: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 308, // 63: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 64: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 65: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 44, // 66: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 44, // 67: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate + 18, // 68: openshell.v1.DeleteSandboxTemplateResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 308, // 69: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 303, // 70: openshell.v1.BeginRootfsTarStagingResponse.expiration_time:type_name -> google.protobuf.Timestamp + 308, // 71: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 72: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 73: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 74: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 75: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 76: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 77: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 78: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 79: openshell.v1.SuspendSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 80: openshell.v1.ResumeSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 39, // 81: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 285, // 82: openshell.v1.SandboxResponse.service_urls:type_name -> openshell.v1.SandboxResponse.ServiceUrlsEntry + 39, // 83: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 309, // 84: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 39, // 85: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 86, // 86: openshell.v1.AttachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 39, // 87: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 86, // 88: openshell.v1.DetachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 84, // 89: openshell.v1.ConfigSnapshotRevision.sandbox_config:type_name -> openshell.v1.SandboxConfigRevision + 82, // 90: openshell.v1.ConfigSnapshotRevision.provider_target:type_name -> openshell.v1.ProviderDesiredIdentity + 310, // 91: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource + 5, // 92: openshell.v1.ConfigUpdateOperation.component:type_name -> openshell.v1.ConfigComponent + 83, // 93: openshell.v1.ConfigUpdateOperation.target_revision:type_name -> openshell.v1.ConfigSnapshotRevision + 7, // 94: openshell.v1.ConfigUpdateOperation.state:type_name -> openshell.v1.ConfigUpdateOperationState + 6, // 95: openshell.v1.ConfigUpdateOperation.outcome:type_name -> openshell.v1.ConfigApplyOutcome + 303, // 96: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp + 303, // 97: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp + 303, // 98: openshell.v1.ConfigUpdateOperation.completed_time:type_name -> google.protobuf.Timestamp + 2, // 99: openshell.v1.ProviderMutationReceipt.kind:type_name -> openshell.v1.ProviderMutationKind + 82, // 100: openshell.v1.ProviderMutationReceipt.desired:type_name -> openshell.v1.ProviderDesiredIdentity + 303, // 101: openshell.v1.ProviderMutationReceipt.persisted_time:type_name -> google.protobuf.Timestamp + 4, // 102: openshell.v1.ProviderReadinessObservation.reason:type_name -> openshell.v1.ProviderReadinessReason + 86, // 103: openshell.v1.ProviderReadinessStatus.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 3, // 104: openshell.v1.ProviderReadinessStatus.state:type_name -> openshell.v1.ProviderReadinessState + 4, // 105: openshell.v1.ProviderReadinessStatus.reason:type_name -> openshell.v1.ProviderReadinessReason + 87, // 106: openshell.v1.ProviderReadinessStatus.observed:type_name -> openshell.v1.ProviderReadinessObservation + 303, // 107: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp + 303, // 108: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp + 85, // 109: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation + 308, // 110: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 88, // 111: openshell.v1.GetSandboxProviderStatusResponse.status:type_name -> openshell.v1.ProviderReadinessStatus + 87, // 112: openshell.v1.ReportProviderReadinessRequest.observation:type_name -> openshell.v1.ProviderReadinessObservation + 307, // 113: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration + 307, // 114: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration + 18, // 115: openshell.v1.DeleteSandboxResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 308, // 116: openshell.v1.CreateSshSessionRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 303, // 117: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp + 308, // 118: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 20, // 119: openshell.v1.ExposeServiceRequest.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode + 308, // 120: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 121: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 103, // 122: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 308, // 123: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 18, // 124: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 304, // 125: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 20, // 126: openshell.v1.ServiceEndpoint.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode + 102, // 127: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 18, // 128: openshell.v1.RevokeSshSessionResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 308, // 129: openshell.v1.ExecSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 286, // 130: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 307, // 131: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration + 107, // 132: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 108, // 133: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 109, // 134: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 213, // 135: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 214, // 136: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 111, // 137: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 106, // 138: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 114, // 139: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 304, // 140: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 303, // 141: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp + 308, // 142: openshell.v1.WatchSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 303, // 143: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp + 39, // 144: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 118, // 145: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 53, // 146: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 119, // 147: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 230, // 148: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 303, // 149: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp + 287, // 150: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 308, // 151: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 309, // 152: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 308, // 153: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 154: openshell.v1.GetProviderCredentialsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 307, // 155: openshell.v1.GetProviderCredentialsRequest.minimum_remaining_lifetime:type_name -> google.protobuf.Duration + 303, // 156: openshell.v1.ProviderCredentialValue.expiration_time:type_name -> google.protobuf.Timestamp + 288, // 157: openshell.v1.GetProviderCredentialsResponse.credentials:type_name -> openshell.v1.GetProviderCredentialsResponse.CredentialsEntry + 308, // 158: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 159: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 309, // 160: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 289, // 161: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + 308, // 162: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 309, // 163: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 86, // 164: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt + 309, // 165: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 308, // 166: openshell.v1.ListProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 167: openshell.v1.GetProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 151, // 168: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 307, // 169: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration + 134, // 170: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 8, // 171: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType + 135, // 172: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 140, // 173: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 136, // 174: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 9, // 175: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 307, // 176: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration + 307, // 177: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration + 138, // 178: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 139, // 179: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 9, // 180: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 303, // 181: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp + 303, // 182: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp + 303, // 183: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp + 16, // 184: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 303, // 185: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp + 308, // 186: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 141, // 187: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 308, // 188: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 9, // 189: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 290, // 190: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 303, // 191: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp + 141, // 192: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 308, // 193: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 141, // 194: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 308, // 195: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 18, // 196: openshell.v1.DeleteProviderRefreshResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 10, // 197: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 137, // 198: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 311, // 199: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 312, // 200: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 142, // 201: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 291, // 202: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 152, // 203: openshell.v1.ProviderProfile.files:type_name -> openshell.v1.ProviderProfileFile + 151, // 204: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 151, // 205: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 308, // 206: openshell.v1.ImportProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 132, // 207: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 133, // 208: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 151, // 209: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 308, // 210: openshell.v1.UpdateProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 132, // 211: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 133, // 212: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 151, // 213: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 308, // 214: openshell.v1.LintProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 132, // 215: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 133, // 216: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 18, // 217: openshell.v1.DeleteProviderResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 308, // 218: openshell.v1.DeleteProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 18, // 219: openshell.v1.DeleteProviderProfileResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 165, // 220: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 292, // 221: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 293, // 222: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + 294, // 223: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 295, // 224: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 4, // 225: openshell.v1.GetSandboxProviderEnvironmentResponse.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason + 296, // 226: openshell.v1.GetSandboxProviderEnvironmentResponse.files:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntry + 11, // 227: openshell.v1.ProviderEnvironmentValue.classification:type_name -> openshell.v1.ProviderEnvironmentValueClassification + 166, // 228: openshell.v1.ProviderEnvironmentValue.static_credential_binding:type_name -> openshell.v1.StaticCredentialBinding + 303, // 229: openshell.v1.ProviderEnvironmentValue.expiration_time:type_name -> google.protobuf.Timestamp + 168, // 230: openshell.v1.ProviderEnvironmentSnapshot.values:type_name -> openshell.v1.ProviderEnvironmentValue + 297, // 231: openshell.v1.ProviderEnvironmentSnapshot.dynamic_credentials:type_name -> openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry + 4, // 232: openshell.v1.ProviderEnvironmentSnapshot.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason + 298, // 233: openshell.v1.ProviderEnvironmentSnapshot.files:type_name -> openshell.v1.ProviderEnvironmentSnapshot.FilesEntry + 307, // 234: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration + 308, // 235: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 305, // 236: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 313, // 237: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 173, // 238: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 299, // 239: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 174, // 240: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 175, // 241: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 176, // 242: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 178, // 243: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 179, // 244: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 180, // 245: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 314, // 246: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 312, // 247: openshell.v1.L7RuleTarget.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 315, // 248: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 177, // 249: openshell.v1.AddDenyRules.target:type_name -> openshell.v1.L7RuleTarget + 316, // 250: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 177, // 251: openshell.v1.AddAllowRules.target:type_name -> openshell.v1.L7RuleTarget + 300, // 252: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 308, // 253: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 191, // 254: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 308, // 255: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 191, // 256: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 13, // 257: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 12, // 258: openshell.v1.SandboxConfigurationAdmission.state:type_name -> openshell.v1.ConfigurationAdmissionState + 188, // 259: openshell.v1.ReportSandboxConfigurationRequest.admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 13, // 260: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 303, // 261: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp + 303, // 262: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp + 305, // 263: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 301, // 264: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 308, // 265: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 303, // 266: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp + 118, // 267: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 118, // 268: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 198, // 269: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 206, // 270: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 223, // 271: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 224, // 272: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 202, // 273: openshell.v1.SupervisorMessage.config_update_result:type_name -> openshell.v1.ConfigUpdateResult + 199, // 274: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 204, // 275: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 207, // 276: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 212, // 277: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 224, // 278: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 205, // 279: openshell.v1.GatewayMessage.session_redirect:type_name -> openshell.v1.SessionRedirect + 200, // 280: openshell.v1.GatewayMessage.config_update:type_name -> openshell.v1.ConfigUpdate + 200, // 281: openshell.v1.SessionAccepted.bootstrap:type_name -> openshell.v1.ConfigUpdate + 307, // 282: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration + 317, // 283: openshell.v1.ConfigUpdate.sandbox_config:type_name -> openshell.sandbox.v1.SandboxConfigSnapshot + 169, // 284: openshell.v1.ConfigUpdate.provider_environment:type_name -> openshell.v1.ProviderEnvironmentSnapshot + 6, // 285: openshell.v1.ConfigUpdateResult.outcome:type_name -> openshell.v1.ConfigApplyOutcome + 201, // 286: openshell.v1.ConfigUpdateResult.failure:type_name -> openshell.v1.ConfigApplyFailure + 188, // 287: openshell.v1.ConfigUpdateResult.admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 203, // 288: openshell.v1.ConfigUpdateResult.policy_load:type_name -> openshell.v1.PolicyLoadResult + 213, // 289: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 214, // 290: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 215, // 291: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 212, // 292: openshell.v1.PeerRelayInit.relay_open:type_name -> openshell.v1.RelayOpen + 217, // 293: openshell.v1.PeerRelayFrame.init:type_name -> openshell.v1.PeerRelayInit + 219, // 294: openshell.v1.PeerNotifyConfigUpdateRequest.sandbox:type_name -> openshell.v1.PeerConfigSandboxTarget + 220, // 295: openshell.v1.PeerNotifyConfigUpdateRequest.provider:type_name -> openshell.v1.PeerConfigProviderTarget + 303, // 296: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp + 303, // 297: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp + 225, // 298: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 227, // 299: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 314, // 300: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 303, // 301: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp + 303, // 302: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp + 303, // 303: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp + 303, // 304: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp + 305, // 305: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 305, // 306: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 308, // 307: openshell.v1.SubmitPolicyAnalysisRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 226, // 308: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 229, // 309: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 228, // 310: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 308, // 311: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 229, // 312: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 303, // 313: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp + 308, // 314: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 315: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 316: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 239, // 317: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 308, // 318: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 314, // 319: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 308, // 320: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 321: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 308, // 322: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 303, // 323: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp + 249, // 324: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 302, // 325: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 318, // 326: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 318, // 327: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 318, // 328: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 18, // 329: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 304, // 330: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 15, // 331: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 308, // 332: openshell.v1.AddWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 15, // 333: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 259, // 334: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 308, // 335: openshell.v1.RemoveWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 18, // 336: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 308, // 337: openshell.v1.ListWorkspaceMembersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 259, // 338: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 303, // 339: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp + 19, // 340: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult + 275, // 341: openshell.v1.EndpointObservation.failure:type_name -> openshell.v1.EndpointFailure + 267, // 342: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation + 269, // 343: openshell.v1.ReportEndpointStatusRequest.token_grant_status:type_name -> openshell.v1.TokenGrantStatus + 270, // 344: openshell.v1.TokenGrantStatus.last_failure:type_name -> openshell.v1.TokenGrantFailureObservation + 275, // 345: openshell.v1.TokenGrantFailureObservation.failure:type_name -> openshell.v1.EndpointFailure + 19, // 346: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult + 303, // 347: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp + 275, // 348: openshell.v1.EndpointStatus.last_failure:type_name -> openshell.v1.EndpointFailure + 303, // 349: openshell.v1.SandboxProvisioning.configuration_change_time:type_name -> google.protobuf.Timestamp + 303, // 350: openshell.v1.SandboxProvisioning.first_rejection_time:type_name -> google.protobuf.Timestamp + 303, // 351: openshell.v1.SandboxProvisioning.deadline:type_name -> google.protobuf.Timestamp + 303, // 352: openshell.v1.SandboxProvisioning.timeout_time:type_name -> google.protobuf.Timestamp + 303, // 353: openshell.v1.SandboxProvisioning.cleanup_completed_time:type_name -> google.protobuf.Timestamp + 303, // 354: openshell.v1.SandboxProvisioning.cleanup_retry_time:type_name -> google.protobuf.Timestamp + 303, // 355: openshell.v1.SandboxProvisioning.attachment_change_time:type_name -> google.protobuf.Timestamp + 303, // 356: openshell.v1.SandboxProvisioning.preparation_deadline:type_name -> google.protobuf.Timestamp + 303, // 357: openshell.v1.SandboxProvisioning.admission_start_time:type_name -> google.protobuf.Timestamp + 20, // 358: openshell.v1.SandboxServiceExposure.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode + 21, // 359: openshell.v1.EndpointFailure.domain:type_name -> openshell.v1.EndpointFailureDomain + 16, // 360: openshell.v1.EndpointFailure.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 123, // 361: openshell.v1.GetProviderCredentialsResponse.CredentialsEntry.value:type_name -> openshell.v1.ProviderCredentialValue + 303, // 362: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 303, // 363: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 137, // 364: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 166, // 365: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 137, // 366: openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 26, // 367: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 28, // 368: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 30, // 369: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 54, // 370: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 65, // 371: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest + 67, // 372: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 68, // 373: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 58, // 374: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest + 59, // 375: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest + 60, // 376: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest + 61, // 377: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest + 69, // 378: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 70, // 379: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 71, // 380: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 89, // 381: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest + 72, // 382: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 73, // 383: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 74, // 384: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 75, // 385: openshell.v1.OpenShell.SuspendSandbox:input_type -> openshell.v1.SuspendSandboxRequest + 76, // 386: openshell.v1.OpenShell.ResumeSandbox:input_type -> openshell.v1.ResumeSandboxRequest + 94, // 387: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 96, // 388: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 97, // 389: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 98, // 390: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 100, // 391: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 104, // 392: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 106, // 393: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 112, // 394: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 113, // 395: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 120, // 396: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 121, // 397: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 122, // 398: openshell.v1.OpenShell.GetProviderCredentials:input_type -> openshell.v1.GetProviderCredentialsRequest + 125, // 399: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 130, // 400: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 131, // 401: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 155, // 402: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 157, // 403: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 159, // 404: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 126, // 405: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 143, // 406: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 145, // 407: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 147, // 408: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 149, // 409: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 127, // 410: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 162, // 411: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 319, // 412: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 320, // 413: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 172, // 414: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 182, // 415: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 184, // 416: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 186, // 417: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 268, // 418: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest + 91, // 419: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest + 189, // 420: openshell.v1.OpenShell.ReportSandboxConfiguration:input_type -> openshell.v1.ReportSandboxConfigurationRequest + 164, // 421: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 170, // 422: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 192, // 423: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 193, // 424: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 196, // 425: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 208, // 426: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 210, // 427: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest + 216, // 428: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 218, // 429: openshell.v1.OpenShell.PeerRelay:input_type -> openshell.v1.PeerRelayFrame + 91, // 430: openshell.v1.OpenShell.PeerReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest + 268, // 431: openshell.v1.OpenShell.PeerReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest + 89, // 432: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest + 221, // 433: openshell.v1.OpenShell.PeerNotifyConfigUpdate:input_type -> openshell.v1.PeerNotifyConfigUpdateRequest + 116, // 434: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 231, // 435: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 233, // 436: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 235, // 437: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 237, // 438: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 240, // 439: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 242, // 440: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 244, // 441: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 246, // 442: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 248, // 443: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 22, // 444: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 24, // 445: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 251, // 446: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 253, // 447: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 255, // 448: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 257, // 449: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 260, // 450: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 262, // 451: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 264, // 452: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 55, // 453: openshell.v1.SandboxUpdates.UpdateSandbox:input_type -> openshell.v1.UpdateSandboxRequest + 27, // 454: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 29, // 455: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 31, // 456: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 77, // 457: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 66, // 458: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse + 77, // 459: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 78, // 460: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 62, // 461: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 62, // 462: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 63, // 463: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse + 64, // 464: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse + 79, // 465: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 80, // 466: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 81, // 467: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 90, // 468: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse + 93, // 469: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 77, // 470: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 77, // 471: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 77, // 472: openshell.v1.OpenShell.SuspendSandbox:output_type -> openshell.v1.SandboxResponse + 77, // 473: openshell.v1.OpenShell.ResumeSandbox:output_type -> openshell.v1.SandboxResponse + 95, // 474: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 103, // 475: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 103, // 476: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 99, // 477: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 101, // 478: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 105, // 479: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 110, // 480: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 112, // 481: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 110, // 482: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 128, // 483: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 128, // 484: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 124, // 485: openshell.v1.OpenShell.GetProviderCredentials:output_type -> openshell.v1.GetProviderCredentialsResponse + 129, // 486: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 154, // 487: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 153, // 488: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 156, // 489: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 158, // 490: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 160, // 491: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 128, // 492: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 144, // 493: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 146, // 494: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 148, // 495: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 150, // 496: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 161, // 497: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 163, // 498: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 321, // 499: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 322, // 500: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 181, // 501: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 183, // 502: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 185, // 503: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 187, // 504: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 271, // 505: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse + 92, // 506: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse + 190, // 507: openshell.v1.OpenShell.ReportSandboxConfiguration:output_type -> openshell.v1.ReportSandboxConfigurationResponse + 167, // 508: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 171, // 509: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 195, // 510: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 194, // 511: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 197, // 512: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 209, // 513: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 211, // 514: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse + 216, // 515: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 218, // 516: openshell.v1.OpenShell.PeerRelay:output_type -> openshell.v1.PeerRelayFrame + 92, // 517: openshell.v1.OpenShell.PeerReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse + 271, // 518: openshell.v1.OpenShell.PeerReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse + 90, // 519: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse + 222, // 520: openshell.v1.OpenShell.PeerNotifyConfigUpdate:output_type -> openshell.v1.PeerNotifyConfigUpdateResponse + 117, // 521: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 232, // 522: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 234, // 523: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 236, // 524: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 238, // 525: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 241, // 526: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 243, // 527: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 245, // 528: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 247, // 529: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 250, // 530: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 23, // 531: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 25, // 532: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 252, // 533: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 254, // 534: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 256, // 535: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 258, // 536: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 261, // 537: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 263, // 538: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 265, // 539: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 77, // 540: openshell.v1.SandboxUpdates.UpdateSandbox:output_type -> openshell.v1.SandboxResponse + 454, // [454:541] is the sub-list for method output_type + 367, // [367:454] is the sub-list for method input_type + 367, // [367:367] is the sub-list for extension type_name + 367, // [367:367] is the sub-list for extension extendee + 0, // [0:367] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -22212,37 +22456,38 @@ func file_openshell_proto_init() { file_openshell_proto_msgTypes[20].OneofWrappers = []any{} file_openshell_proto_msgTypes[21].OneofWrappers = []any{} file_openshell_proto_msgTypes[29].OneofWrappers = []any{} - file_openshell_proto_msgTypes[58].OneofWrappers = []any{ + file_openshell_proto_msgTypes[33].OneofWrappers = []any{} + file_openshell_proto_msgTypes[61].OneofWrappers = []any{ (*ConfigSnapshotRevision_SandboxConfig)(nil), (*ConfigSnapshotRevision_ProviderEnvironment)(nil), (*ConfigSnapshotRevision_ProviderTarget)(nil), } - file_openshell_proto_msgTypes[85].OneofWrappers = []any{ + file_openshell_proto_msgTypes[88].OneofWrappers = []any{ (*ExecSandboxEvent_Stdout)(nil), (*ExecSandboxEvent_Stderr)(nil), (*ExecSandboxEvent_Exit)(nil), } - file_openshell_proto_msgTypes[86].OneofWrappers = []any{ + file_openshell_proto_msgTypes[89].OneofWrappers = []any{ (*TcpForwardInit_Ssh)(nil), (*TcpForwardInit_Tcp)(nil), } - file_openshell_proto_msgTypes[87].OneofWrappers = []any{ + file_openshell_proto_msgTypes[90].OneofWrappers = []any{ (*TcpForwardFrame_Init)(nil), (*TcpForwardFrame_Data)(nil), } - file_openshell_proto_msgTypes[88].OneofWrappers = []any{ + file_openshell_proto_msgTypes[91].OneofWrappers = []any{ (*ExecSandboxInput_Start)(nil), (*ExecSandboxInput_Stdin)(nil), (*ExecSandboxInput_Resize)(nil), } - file_openshell_proto_msgTypes[92].OneofWrappers = []any{ + file_openshell_proto_msgTypes[95].OneofWrappers = []any{ (*SandboxStreamEvent_Sandbox)(nil), (*SandboxStreamEvent_Log)(nil), (*SandboxStreamEvent_Event)(nil), (*SandboxStreamEvent_Warning)(nil), (*SandboxStreamEvent_DraftPolicyUpdate)(nil), } - file_openshell_proto_msgTypes[148].OneofWrappers = []any{ + file_openshell_proto_msgTypes[151].OneofWrappers = []any{ (*PolicyMergeOperation_AddRule)(nil), (*PolicyMergeOperation_RemoveEndpoint)(nil), (*PolicyMergeOperation_RemoveRule)(nil), @@ -22250,15 +22495,15 @@ func file_openshell_proto_init() { (*PolicyMergeOperation_AddAllowRules)(nil), (*PolicyMergeOperation_RemoveBinary)(nil), } - file_openshell_proto_msgTypes[152].OneofWrappers = []any{} - file_openshell_proto_msgTypes[171].OneofWrappers = []any{ + file_openshell_proto_msgTypes[155].OneofWrappers = []any{} + file_openshell_proto_msgTypes[174].OneofWrappers = []any{ (*SupervisorMessage_Hello)(nil), (*SupervisorMessage_Heartbeat)(nil), (*SupervisorMessage_RelayOpenResult)(nil), (*SupervisorMessage_RelayClose)(nil), (*SupervisorMessage_ConfigUpdateResult)(nil), } - file_openshell_proto_msgTypes[172].OneofWrappers = []any{ + file_openshell_proto_msgTypes[175].OneofWrappers = []any{ (*GatewayMessage_SessionAccepted)(nil), (*GatewayMessage_SessionRejected)(nil), (*GatewayMessage_Heartbeat)(nil), @@ -22267,34 +22512,34 @@ func file_openshell_proto_init() { (*GatewayMessage_SessionRedirect)(nil), (*GatewayMessage_ConfigUpdate)(nil), } - file_openshell_proto_msgTypes[187].OneofWrappers = []any{ + file_openshell_proto_msgTypes[190].OneofWrappers = []any{ (*RelayOpen_Ssh)(nil), (*RelayOpen_Tcp)(nil), } - file_openshell_proto_msgTypes[191].OneofWrappers = []any{ + file_openshell_proto_msgTypes[194].OneofWrappers = []any{ (*RelayFrame_Init)(nil), (*RelayFrame_Data)(nil), } - file_openshell_proto_msgTypes[193].OneofWrappers = []any{ + file_openshell_proto_msgTypes[196].OneofWrappers = []any{ (*PeerRelayFrame_Init)(nil), (*PeerRelayFrame_Data)(nil), } - file_openshell_proto_msgTypes[196].OneofWrappers = []any{ + file_openshell_proto_msgTypes[199].OneofWrappers = []any{ (*PeerNotifyConfigUpdateRequest_Sandbox)(nil), (*PeerNotifyConfigUpdateRequest_Workspace)(nil), (*PeerNotifyConfigUpdateRequest_AllConnected)(nil), (*PeerNotifyConfigUpdateRequest_Provider)(nil), } - file_openshell_proto_msgTypes[250].OneofWrappers = []any{} + file_openshell_proto_msgTypes[253].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_openshell_proto_rawDesc), len(file_openshell_proto_rawDesc)), NumEnums: 22, - NumMessages: 277, + NumMessages: 281, NumExtensions: 0, - NumServices: 1, + NumServices: 2, }, GoTypes: file_openshell_proto_goTypes, DependencyIndexes: file_openshell_proto_depIdxs, diff --git a/sdk/go/proto/openshellv1/openshell_grpc.pb.go b/sdk/go/proto/openshellv1/openshell_grpc.pb.go index 2f13a80383..fbba50b84c 100644 --- a/sdk/go/proto/openshellv1/openshell_grpc.pb.go +++ b/sdk/go/proto/openshellv1/openshell_grpc.pb.go @@ -3635,3 +3635,119 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ }, Metadata: "openshell.proto", } + +const ( + SandboxUpdates_UpdateSandbox_FullMethodName = "/openshell.v1.SandboxUpdates/UpdateSandbox" +) + +// SandboxUpdatesClient is the client API for SandboxUpdates service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Optional sandbox mutations are separate from OpenShell so existing generated +// OpenShell server implementations remain source-compatible. +type SandboxUpdatesClient interface { + // Update the workload image, command, environment, labels, or annotations of a + // stopped sandbox while preserving its identity and retained storage. Requires + // compute-driver support for the selected changes and explicit gateway activation + // of sandbox updates. + UpdateSandbox(ctx context.Context, in *UpdateSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) +} + +type sandboxUpdatesClient struct { + cc grpc.ClientConnInterface +} + +func NewSandboxUpdatesClient(cc grpc.ClientConnInterface) SandboxUpdatesClient { + return &sandboxUpdatesClient{cc} +} + +func (c *sandboxUpdatesClient) UpdateSandbox(ctx context.Context, in *UpdateSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SandboxResponse) + err := c.cc.Invoke(ctx, SandboxUpdates_UpdateSandbox_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SandboxUpdatesServer is the server API for SandboxUpdates service. +// All implementations must embed UnimplementedSandboxUpdatesServer +// for forward compatibility. +// +// Optional sandbox mutations are separate from OpenShell so existing generated +// OpenShell server implementations remain source-compatible. +type SandboxUpdatesServer interface { + // Update the workload image, command, environment, labels, or annotations of a + // stopped sandbox while preserving its identity and retained storage. Requires + // compute-driver support for the selected changes and explicit gateway activation + // of sandbox updates. + UpdateSandbox(context.Context, *UpdateSandboxRequest) (*SandboxResponse, error) + mustEmbedUnimplementedSandboxUpdatesServer() +} + +// UnimplementedSandboxUpdatesServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedSandboxUpdatesServer struct{} + +func (UnimplementedSandboxUpdatesServer) UpdateSandbox(context.Context, *UpdateSandboxRequest) (*SandboxResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateSandbox not implemented") +} +func (UnimplementedSandboxUpdatesServer) mustEmbedUnimplementedSandboxUpdatesServer() {} +func (UnimplementedSandboxUpdatesServer) testEmbeddedByValue() {} + +// UnsafeSandboxUpdatesServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SandboxUpdatesServer will +// result in compilation errors. +type UnsafeSandboxUpdatesServer interface { + mustEmbedUnimplementedSandboxUpdatesServer() +} + +func RegisterSandboxUpdatesServer(s grpc.ServiceRegistrar, srv SandboxUpdatesServer) { + // If the following call panics, it indicates UnimplementedSandboxUpdatesServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&SandboxUpdates_ServiceDesc, srv) +} + +func _SandboxUpdates_UpdateSandbox_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateSandboxRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SandboxUpdatesServer).UpdateSandbox(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SandboxUpdates_UpdateSandbox_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SandboxUpdatesServer).UpdateSandbox(ctx, req.(*UpdateSandboxRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// SandboxUpdates_ServiceDesc is the grpc.ServiceDesc for SandboxUpdates service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var SandboxUpdates_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "openshell.v1.SandboxUpdates", + HandlerType: (*SandboxUpdatesServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "UpdateSandbox", + Handler: _SandboxUpdates_UpdateSandbox_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "openshell.proto", +} diff --git a/sdk/go/tests/compatibility/sandbox_updates_test.go b/sdk/go/tests/compatibility/sandbox_updates_test.go new file mode 100644 index 0000000000..44bc7b023f --- /dev/null +++ b/sdk/go/tests/compatibility/sandbox_updates_test.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package compatibility_test + +import ( + "context" + "net" + "testing" + + dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1" + pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" +) + +func TestSandboxStatusRetainsBaselinePublicShape(t *testing.T) { + // Freeze the pre-image-update fields and tags: private operation state must + // not enlarge the public Rust struct or leak into generated SDK messages. + fields := []string{ + "agent_pod", "agent_fd", "sandbox_fd", "conditions", "phase", + "current_policy_version", "main_process_instance_id", "exit_code", + "endpoint_statuses", "configuration_admission", "configuration_activated", + "provisioning", "restart_count", "next_restart_time", + "main_process_started_time", "last_token_grant_failure", + } + descriptor := (&pb.SandboxStatus{}).ProtoReflect().Descriptor() + require.Equal(t, len(fields), descriptor.Fields().Len()) + for i, name := range fields { + field := descriptor.Fields().ByName(protoreflect.Name(name)) + require.NotNil(t, field, name) + require.Equal(t, protoreflect.FieldNumber(i+2), field.Number(), name) + } + require.Nil(t, pb.File_openshell_proto.Messages().ByName("SandboxImageUpdate")) + legacy := pb.File_openshell_proto.Services().ByName("OpenShell") + require.Nil(t, legacy.Methods().ByName("UpdateSandbox")) + updates := pb.File_openshell_proto.Services().ByName("SandboxUpdates") + require.NotNil(t, updates) + require.Equal(t, 1, updates.Methods().Len()) + method := updates.Methods().Get(0) + require.Equal(t, protoreflect.FullName("openshell.v1.UpdateSandboxRequest"), method.Input().FullName()) + require.Equal(t, protoreflect.FullName("openshell.v1.SandboxResponse"), method.Output().FullName()) + require.Equal(t, "/openshell.v1.SandboxUpdates/UpdateSandbox", pb.SandboxUpdates_UpdateSandbox_FullMethodName) +} + +type legacyHealthServer struct { + pb.UnimplementedOpenShellServer +} + +func (legacyHealthServer) Health(context.Context, *pb.HealthRequest) (*pb.HealthResponse, error) { + return &pb.HealthResponse{Version: "legacy-compatible"}, nil +} + +type imageUpdateServer struct { + pb.UnimplementedSandboxUpdatesServer + request *pb.UpdateSandboxRequest +} + +func (s *imageUpdateServer) UpdateSandbox(_ context.Context, request *pb.UpdateSandboxRequest) (*pb.SandboxResponse, error) { + s.request = proto.Clone(request).(*pb.UpdateSandboxRequest) + return &pb.SandboxResponse{Sandbox: &pb.Sandbox{ + Metadata: &dm.ObjectMeta{Id: "stable-id", Name: request.Name, Workspace: request.WorkspaceScope.GetWorkspace()}, + Spec: &pb.SandboxSpec{Template: &pb.SandboxTemplate{Image: request.GetImage()}}, + }}, nil +} + +func TestSeparateSandboxUpdatesTransportCoexistsWithLegacyOpenShell(t *testing.T) { + listener := bufconn.Listen(1024 * 1024) + server := grpc.NewServer(grpc.UnaryInterceptor(func(ctx context.Context, request any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + if info.FullMethod == pb.SandboxUpdates_UpdateSandbox_FullMethodName { + md, _ := metadata.FromIncomingContext(ctx) + if len(md.Get("authorization")) == 0 { + return nil, status.Error(codes.Unauthenticated, "bearer required") + } + } + return handler(ctx, request) + })) + updatesServer := &imageUpdateServer{} + pb.RegisterOpenShellServer(server, legacyHealthServer{}) + pb.RegisterSandboxUpdatesServer(server, updatesServer) + go func() { _ = server.Serve(listener) }() + t.Cleanup(server.Stop) + t.Cleanup(func() { _ = listener.Close() }) + conn, err := grpc.NewClient("passthrough:///sandbox-updates", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return listener.Dial() + })) + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + legacy := pb.NewOpenShellClient(conn) + health, err := legacy.Health(context.Background(), &pb.HealthRequest{}) + require.NoError(t, err) + require.Equal(t, "legacy-compatible", health.Version) + updates := pb.NewSandboxUpdatesClient(conn) + request := &pb.UpdateSandboxRequest{ + WorkspaceScope: &dm.WorkspaceSelector{Selection: &dm.WorkspaceSelector_Workspace{Workspace: "team"}}, + Name: "sandbox", + Image: proto.String("example/workload:new"), + ExpectedResourceVersion: 42, + RequestId: "550e8400-e29b-41d4-a716-446655440000", + } + _, err = updates.UpdateSandbox(context.Background(), request) + require.Equal(t, codes.Unauthenticated, status.Code(err)) + require.Nil(t, updatesServer.request) + ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("authorization", "Bearer test-token")) + response, err := updates.UpdateSandbox(ctx, request) + require.NoError(t, err) + require.True(t, proto.Equal(request, updatesServer.request)) + require.Equal(t, "stable-id", response.Sandbox.Metadata.Id) + require.Equal(t, "team", response.Sandbox.Metadata.Workspace) + require.Equal(t, request.GetImage(), response.Sandbox.Spec.Template.Image) + // The old service has no accidental update endpoint. + err = conn.Invoke(ctx, "/openshell.v1.OpenShell/UpdateSandbox", request, &pb.SandboxResponse{}) + require.Equal(t, codes.Unimplemented, status.Code(err)) +} diff --git a/sdk/typescript/src/raw.test.ts b/sdk/typescript/src/raw.test.ts index 9d2e02ac6e..798d23b536 100644 --- a/sdk/typescript/src/raw.test.ts +++ b/sdk/typescript/src/raw.test.ts @@ -3,10 +3,55 @@ import type { MessageInitShape } from '@bufbuild/protobuf'; import { timestampFromDate } from '@bufbuild/protobuf/wkt'; -import { createRouterTransport } from '@connectrpc/connect'; +import { createClient, createRouterTransport } from '@connectrpc/connect'; import { describe, expect, it } from 'vitest'; import { SandboxClient } from './index.js'; -import { EndpointResult, type EndpointStatusSchema, OpenShell, SandboxPhase } from './raw.js'; +import { EndpointResult, type EndpointStatusSchema, OpenShell, SandboxPhase, SandboxUpdates } from './raw.js'; + +describe('raw sandbox image updates', () => { + it.each([true, false])('uses a separate service on the existing transport (binary: %s)', async (useBinaryFormat) => { + const transport = createRouterTransport( + (router) => { + router.service(OpenShell, { + getSandbox: () => ({ sandbox: { metadata: { id: 'stable-id' } } }), + }); + router.service(SandboxUpdates, { + updateSandbox: (request) => { + expect(request.workspaceScope?.selection).toEqual({ case: 'workspace', value: 'team' }); + expect(request.name).toBe('sandbox'); + expect(request.expectedResourceVersion).toBe(42n); + expect(request.requestId).toBe('550e8400-e29b-41d4-a716-446655440000'); + return { + sandbox: { + metadata: { id: 'stable-id', name: request.name, workspace: 'team' }, + spec: { template: { image: request.image } }, + status: { phase: SandboxPhase.STOPPED }, + }, + }; + }, + }); + }, + { transport: { useBinaryFormat } }, + ); + const legacy = new SandboxClient(transport); + const updates = createClient(SandboxUpdates, legacy.transport); + expect(Object.hasOwn(OpenShell.method, 'updateSandbox')).toBe(false); + expect(SandboxUpdates.typeName).toBe('openshell.v1.SandboxUpdates'); + const before = await legacy.raw.getSandbox({ name: 'sandbox' }); + const response = await updates.updateSandbox({ + workspaceScope: { selection: { case: 'workspace', value: 'team' } }, + name: 'sandbox', + image: 'example/workload:new', + expectedResourceVersion: 42n, + requestId: '550e8400-e29b-41d4-a716-446655440000', + }); + expect(response.sandbox?.metadata?.id).toBe(before.sandbox?.metadata?.id); + expect(response.sandbox?.spec?.template?.image).toBe('example/workload:new'); + expect(response.sandbox?.status?.phase).toBe(SandboxPhase.STOPPED); + expect(response.sandbox?.status).not.toHaveProperty('imageUpdate'); + expect(response.sandbox?.status).not.toHaveProperty('imageRevision'); + }); +}); describe('raw sandbox endpoint status', () => { it.each([true, false])( diff --git a/sdk/typescript/src/raw.ts b/sdk/typescript/src/raw.ts index 1571e8e23e..067a6558b9 100644 --- a/sdk/typescript/src/raw.ts +++ b/sdk/typescript/src/raw.ts @@ -4,7 +4,7 @@ export * from './gen/datamodel_pb.js'; export * from './gen/google/rpc/error_details_pb.js'; // Advanced surface: the full generated protobuf types (messages, enums, and the -// OpenShell service descriptor) for callers using the raw escape hatch on +// OpenShell and SandboxUpdates service descriptors) for callers using the raw escape hatch on // OpenShellClient / SandboxClient (`.raw` and `.transport`). These are the // uncurated wire types; import them from '@nvidia/openshell-sdk/raw'. The // curated entry point stays free of generated types so its surface does not diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index 29b4407563..17cafd957a 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -1435,6 +1435,52 @@ network supervisor logs for CONNECT and upstream-proxy decisions: kubectl -n logs pod/ --tail=200 | grep -Ei 'upstream|connect|proxy' ``` +#### Sandbox image-update activation and recovery + +`openshell.v1.SandboxUpdates/UpdateSandbox` uses the existing authenticated +gateway listener and defaults to disabled. Check the effective +`[openshell.gateway] enable_sandbox_updates` value and its overrides, +`--enable-sandbox-updates true` or +`OPENSHELL_ENABLE_SANDBOX_UPDATES=true`. Canonical Helm values use: + +```yaml +gatewayConfig: + "openshell.gateway": + enable_sandbox_updates: true +``` + +Do not enable the flag to work around an error. It attests that all incompatible +gateways, including unregistered processes and Jobs, have been drained and +retired, their outstanding driver operations have settled, and old binaries +cannot return through rollout or rollback configurations. Keep it false during +a rolling upgrade. SQLite supports one gateway process, not shared-file HA. + +On PostgreSQL, fresh gateway membership must advertise sandbox-update protocol 1; +malformed or truncated membership reads fail closed. Administrative, monitoring, +and backup connections do not block updates, and custom `application_name` values +are preserved. Membership expiry does not prove a gateway has stopped. Follow the +[activation checklist](https://docs.nvidia.com/openshell/latest/kubernetes/high-availability#activate-sandbox-updates). + +The gateway retries accepted updates automatically, even with new updates disabled. +Repeated automatic failures back off up to 15 minutes per gateway. Replicas retry +independently, and restarting a gateway resets its delay. Recovery warnings include +the attempt count and retry delay. +To resume immediately, read the sandbox resource version and call `UpdateSandbox` +with that version and no selected fields. Restore a compatible driver if recovery +reports missing support. Start, stop, and provisioning retry retain the barrier; +explicit deletion can reclaim an unrecoverable sandbox and its storage. Preserve +request admission semantics: use a new ID or no ID for the explicit resume request, +and never change the original payload under its original ID. See the +[recovery contract](https://docs.nvidia.com/openshell/latest/sdk/api-errors#sandbox-image-update-recovery). + +Split Helm deployments need the current `openshell-workspace` Role. Update +preflight requires `get` on Pods: it checks the controller-recorded workload +name, the Sandbox resource name, and `os-supervisor-` directly. +Any remaining Pod at those names, including a terminating Pod, blocks the update. +The checks make at most three Pod reads regardless of namespace size and do not +discover unexpectedly named Pods. Upgrade the workspace chart alongside the +gateway chart. + ### Step 7: Check VM-Backed Gateways Use the VM driver logs and host diagnostics available in the user's environment. Verify: diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index f87e06fdff..f0c32f2d89 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -527,6 +527,12 @@ stopped or completed. Starting a retained `Completed` or invalidates SSH sessions from the previous runtime generation. Delete remains the operation that removes retained state. +Before updating a retained `Completed` or `Error/MainProcessFailed` sandbox, +run `sandbox stop`. It cleans up compute and transitions to `Stopped`, retaining +the exit code until the next start. If the terminal sandbox's compute resource +is already gone, stop succeeds and preserves its terminal phase and exit code; +configuration updates require retained compute. + The sandbox's SSH host identity survives stop/start and runtime restarts. API clients can read its public fingerprint from the sandbox resource or SSH-session response and pin it to the sandbox ID. Recreating a deleted @@ -554,6 +560,53 @@ state cannot be restored; use `sandbox start` for an explicit clean boot. `sandb `sandbox stop`, and `sandbox delete` on a suspended sandbox discard the retained memory state. +### Change a stopped sandbox's workload image + +Do not invent a `sandbox update` command or use delete/create to change an +existing sandbox's configuration. The separate +`openshell.v1.SandboxUpdates/UpdateSandbox` RPC uses the same gateway listener +and authentication as `OpenShell`. It defaults to disabled; the operator must +complete the published +[activation checklist](https://docs.nvidia.com/openshell/latest/kubernetes/high-availability#activate-sandbox-updates) +before enabling `enable_sandbox_updates`. Keep it false during a rolling +upgrade and never enable it merely to bypass a failed compatibility check. +After activation, image, command, and environment updates initially support only +stopped Kubernetes sandboxes; other drivers return `UNIMPLEMENTED` for those +fields. Metadata-only updates also support stopped sandboxes on other drivers. Stop and +wait for `Stopped`, read the sandbox's current resource version through +`GetSandbox`, call raw `UpdateSandbox` with the named workspace, sandbox name, +selected image, command, environment overrides, labels or annotations, and required nonzero `expected_resource_version`, then start after +success. Rust SDK callers use `client.raw_sandbox_updates_fresh()`; generated +clients use `SandboxUpdates`, not the unchanged `OpenShell` service. There is no +curated SDK update method. The sandbox identity, Kubernetes +custom resource, and persistent volume claim remain; the existing workspace is +not reseeded and trusted runtime images do not change. Omitted update fields are +unchanged; supplied lists and maps replace their user-owned values, including +explicitly empty values. Template environment defaults still apply. System metadata +is protected; top-level labels and annotations do not update pod metadata. +Combined configuration changes require the configuration-update driver capability +and a uniformly upgraded gateway deployment. Metadata-only updates need no +driver capability and preserve the retention annotation. + +For an image that fails to start and reaches a provisioning timeout, wait for +cleanup completion and settlement of the original driver operation, then run +`sandbox stop` to obtain `Stopped` before correcting the image. This retains the +timeout diagnostic and storage and does not bypass pending cleanup. + +An operation-pending error blocks start, stop, and provisioning retry. The gateway +limits preflight and each accepted update attempt to one minute each. A timeout +after acceptance preserves pending intent for recovery; it does not imply rollback. +The gateway retries automatically even when new updates are disabled. Repeated automatic +failures back off up to 15 minutes per gateway; restarting a gateway resets the delay. +To resume immediately, +read a fresh resource version and call `UpdateSandbox` with no selected fields; +the gateway loads the stored targets. Use a new request ID or no ID for this +explicit resume. An unresolved original ID returns uncertainty, and its payload +must not change. Explicit delete can reclaim a sandbox whose update cannot +complete, including its storage. See the published +[image update workflow](https://docs.nvidia.com/openshell/latest/how-it-works/sandboxes/overview#update-the-image-of-a-stopped-sandbox) +and [API errors](https://docs.nvidia.com/openshell/latest/sdk/api-errors). + --- ## Workflow 4: Policy Iteration Loop diff --git a/tasks/test.toml b/tasks/test.toml index 7714d14dd4..6bafa95e9e 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -254,6 +254,14 @@ run = [ "AGENT_SANDBOX_VERSION=v0.4.6 e2e/rust/e2e-kubernetes.sh", ] +["e2e:kubernetes:updates"] +description = "Validate sandbox updates with the combined Helm chart on both Agent Sandbox APIs" +env = { OPENSHELL_E2E_KUBE_TEST = "kubernetes_image_update" } +run = [ + "e2e/rust/e2e-kubernetes.sh", + "AGENT_SANDBOX_VERSION=v0.4.6 e2e/rust/e2e-kubernetes.sh", +] + ["e2e:kubernetes:isolation"] description = "Run Kubernetes e2e with the workload network fence and separate supervisor" run = "e2e/rust/e2e-kubernetes.sh" diff --git a/tests/suites/features/Cargo.lock b/tests/suites/features/Cargo.lock index 7e6509f278..bceb5b94d5 100644 --- a/tests/suites/features/Cargo.lock +++ b/tests/suites/features/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e567177890eb1617b1f774005b66b26b2377afd138a2ca37aae7d8f0c81429d4" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -63,7 +72,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 2.0.20", "time", ] @@ -234,6 +243,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "backtrace" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f4220217f3288537b83684130f42fad43320b1c3b805f1910d683d52ded24f" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + [[package]] name = "base64" version = "0.22.1" @@ -306,7 +339,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_urlencoded", - "thiserror", + "thiserror 2.0.20", "tokio", "tokio-util", "tower-service", @@ -718,6 +751,12 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.10" @@ -889,8 +928,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -917,6 +958,18 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "gimli" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1033caf0b349c518623b5396bfb2cf0bddf44f0306d543a250e5743297aafd10" + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "gloo-timers" version = "0.3.0" @@ -1204,13 +1257,16 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -1259,9 +1315,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" dependencies = [ "displaydoc", "potential_utf", @@ -1273,22 +1329,43 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", ] +[[package]] +name = "icu_locale_fallback" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406e9ae5b95d8aa37db6293405931b766a6172d1848daee22d70eb44aa78a43c" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3632ebbd0c5c3637ba1a40bdf8288f2cb9d2268e73441a69d5fc73d7cfbaa87c" + [[package]] name = "icu_normalizer" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1300,15 +1377,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" [[package]] name = "icu_properties" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" dependencies = [ "displaydoc", "icu_collections", @@ -1321,18 +1398,20 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" [[package]] name = "icu_provider" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" dependencies = [ "displaydoc", "icu_locale_core", + "serde", + "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -1340,6 +1419,29 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_segmenter" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b195dc605f81aa181bec9b3bd14e26cc71b903e4644f6af578ca9772c7a710" +dependencies = [ + "icu_collections", + "icu_locale_fallback", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "smallvec", + "utf8_iter", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913a7d294ad056474749152107309fdee3ec17d2482d1ab70fdc61a81c64b9a3" + [[package]] name = "ident_case" version = "1.0.1" @@ -1379,6 +1481,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "itertools" version = "0.14.0" @@ -1424,7 +1538,7 @@ dependencies = [ "jsonptr", "serde", "serde_json", - "thiserror", + "thiserror 2.0.20", ] [[package]] @@ -1437,7 +1551,7 @@ dependencies = [ "pest_derive", "regex", "serde_json", - "thiserror", + "thiserror 2.0.20", ] [[package]] @@ -1523,7 +1637,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "thiserror", + "thiserror 2.0.20", "tokio", "tokio-util", "tower", @@ -1546,7 +1660,7 @@ dependencies = [ "serde", "serde-value", "serde_json", - "thiserror", + "thiserror 2.0.20", ] [[package]] @@ -1585,7 +1699,7 @@ dependencies = [ "pin-project", "serde", "serde_json", - "thiserror", + "thiserror 2.0.20", "tokio", "tokio-util", "tracing", @@ -1688,6 +1802,36 @@ dependencies = [ "rapidhash", ] +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "mime" version = "0.3.17" @@ -1721,6 +1865,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "nix" version = "0.29.0" @@ -1801,6 +1951,35 @@ dependencies = [ "autocfg", ] +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.17", + "http", + "rand 0.8.8", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2 0.10.9", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" +dependencies = [ + "memchr", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -1816,6 +1995,22 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openshell-bootstrap" +version = "0.0.0" +dependencies = [ + "bytes", + "miette", + "openshell-core", + "openshell-crypto", + "rcgen", + "serde", + "serde_json", + "tempfile", + "tokio", + "tracing", +] + [[package]] name = "openshell-conformance" version = "0.0.0" @@ -1827,6 +2022,43 @@ dependencies = [ "tokio", ] +[[package]] +name = "openshell-core" +version = "0.0.0" +dependencies = [ + "async-trait", + "base64 0.22.1", + "chrono", + "glob", + "ipnet", + "libc", + "miette", + "nix", + "openshell-crypto", + "openshell-extension-core", + "openshell-policy-schema", + "prost", + "prost-types", + "protoc-bin-vendored", + "reqwest", + "rustix", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 2.0.20", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tonic-prost-build", + "tonic-types", + "tracing", + "url", + "uuid", +] + [[package]] name = "openshell-crypto" version = "0.0.0" @@ -1839,7 +2071,7 @@ dependencies = [ "rustls", "serde", "sqlx", - "thiserror", + "thiserror 2.0.20", "tonic", "x509-parser", ] @@ -1879,6 +2111,18 @@ dependencies = [ "url", ] +[[package]] +name = "openshell-extension-core" +version = "0.0.0" +dependencies = [ + "hyper-util", + "serde", + "thiserror 2.0.20", + "tokio", + "tonic", + "tower", +] + [[package]] name = "openshell-ocsf" version = "0.0.0" @@ -1904,7 +2148,7 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry_sdk", "serde_json", - "thiserror", + "thiserror 2.0.20", "tokio", "tonic", "tower-http", @@ -1927,6 +2171,55 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "openshell-policy-schema" +version = "0.0.0" +dependencies = [ + "miette", + "noyalib", + "serde", + "serde_json", + "serde_path_to_error", + "thiserror 2.0.20", +] + +[[package]] +name = "openshell-sdk" +version = "0.0.0" +dependencies = [ + "async-stream", + "async-trait", + "futures", + "hyper", + "hyper-util", + "miette", + "oauth2", + "openshell-core", + "openshell-crypto", + "reqwest", + "rustls", + "serde", + "thiserror 2.0.20", + "tokio", + "tokio-rustls", + "tokio-tungstenite", + "tonic", + "tower", + "tracing", +] + +[[package]] +name = "openshell-test-feature-kubernetes-updates" +version = "0.0.0" +dependencies = [ + "openshell-testkit", + "serde_json", + "tempfile", + "tokio", + "tonic", + "uuid", +] + [[package]] name = "openshell-test-feature-oci-image" version = "0.0.0" @@ -1948,6 +2241,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "openshell-testkit" +version = "0.0.0" +dependencies = [ + "futures", + "openshell-bootstrap", + "openshell-core", + "openshell-sdk", + "openshell-testkit-macros", + "rand 0.9.5", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "openshell-testkit-macros" +version = "0.0.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "openssl-probe" version = "0.2.1" @@ -1964,7 +2281,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror", + "thiserror 2.0.20", "tracing", ] @@ -1979,7 +2296,7 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost", - "thiserror", + "thiserror 2.0.20", "tokio", "tonic", "tonic-types", @@ -2011,7 +2328,7 @@ dependencies = [ "percent-encoding", "portable-atomic", "rand 0.9.5", - "thiserror", + "thiserror 2.0.20", "tokio", "tokio-stream", ] @@ -2025,6 +2342,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "owo-colors" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" + [[package]] name = "parking" version = "2.2.1" @@ -2122,6 +2445,17 @@ dependencies = [ "pest", ] +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + [[package]] name = "pin-project" version = "1.1.13" @@ -2166,6 +2500,8 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ + "serde_core", + "writeable", "zerovec", ] @@ -2184,6 +2520,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -2203,6 +2549,27 @@ dependencies = [ "prost-derive", ] +[[package]] +name = "prost-build" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn 2.0.119", + "tempfile", +] + [[package]] name = "prost-derive" version = "0.14.4" @@ -2225,6 +2592,90 @@ dependencies = [ "prost", ] +[[package]] +name = "protoc-bin-vendored" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8760a25b6ff9c620324822737e468478fa092234190d2e449760344354896ed9" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fa2624782ca04cd44f51554566717377acd240e4c0016d757dd74fccc9324f" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2417e9817fa237dab803ad4dda7357a111656e242959cc6b8f9a1a583367d42" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d189c34636356a46a7ed3188233dc8a88c431278cc54d4a19b096a2d270e985" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171e39f1e846e5f322ced1ac3b8d4cd3a3833ca24b6e5d58b3632574fe6204fa" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873cdcc097593432086661aa432b8078f1cd87bfb02847c332e98ae2c119e966" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb72df001783b8297847fe8f5f874ee400fd742c843d60583e8c23d96977c7f" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b04652167eca899dda05f32f5481adeaf25c623a98ce2fc146a001cc59a2add7" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "263a3f48f01e7309e857138bd47f785585b4a005e8e56c6d2824ce91195999c3" + +[[package]] +name = "pulldown-cmark" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "278e9268c93b9e498a2da46c34d90ba93a3255eb99009646398f34bc73ab36eb" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "22.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bbb29c624230c4bd1047bbdb2aa47e41c860e9665ce62ba9504eebe91bf867" +dependencies = [ + "pulldown-cmark", +] + [[package]] name = "quote" version = "1.0.47" @@ -2246,13 +2697,24 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.5", ] @@ -2267,6 +2729,16 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -2362,6 +2834,45 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -2376,6 +2887,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + [[package]] name = "rustc-hash" version = "2.1.3" @@ -2599,6 +3116,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.21" @@ -2727,7 +3255,7 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 2.0.20", "time", ] @@ -2806,7 +3334,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "smallvec", - "thiserror", + "thiserror 2.0.20", "tokio", "tokio-stream", "tracing", @@ -2847,7 +3375,7 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn 2.0.119", - "thiserror", + "thiserror 2.0.20", "tokio", "url", ] @@ -2874,7 +3402,7 @@ dependencies = [ "sha1 0.11.0", "sha2 0.11.0", "sqlx-core", - "thiserror", + "thiserror 2.0.20", "tracing", ] @@ -2908,7 +3436,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.20", "tracing", "whoami", ] @@ -2932,7 +3460,7 @@ dependencies = [ "percent-encoding", "serde", "sqlx-core", - "thiserror", + "thiserror 2.0.20", "tracing", "url", ] @@ -2966,6 +3494,27 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + [[package]] name = "syn" version = "2.0.119" @@ -2993,6 +3542,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3023,19 +3575,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", ] +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "textwrap" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ecfad6c3abc80a577f2b91c1e412ee57e7a060d430b553c1b0c940974ebcd49" +dependencies = [ + "icu_segmenter", + "unicode-width 0.2.3", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -3090,11 +3682,12 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -3153,6 +3746,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.19" @@ -3200,6 +3809,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic-build" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c07bd4e9f99e2a3ed8bebad596ae02a94e53745b68acc6900e22c3475c8bc44" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "tonic-prost" version = "0.14.6" @@ -3211,6 +3832,22 @@ dependencies = [ "tonic", ] +[[package]] +name = "tonic-prost-build" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05d8957fac6b3542999796cb4449d45da790751962f974a457e5e00c43d78c" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.119", + "tempfile", + "tonic-build", +] + [[package]] name = "tonic-types" version = "0.14.6" @@ -3250,13 +3887,16 @@ dependencies = [ "base64 0.22.1", "bitflags", "bytes", + "futures-util", "http", "http-body", "mime", "pin-project-lite", + "tower", "tower-layer", "tower-service", "tracing", + "url", "uuid", ] @@ -3368,6 +4008,25 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.5", + "rustls", + "rustls-pki-types", + "sha1 0.10.7", + "thiserror 2.0.20", + "utf-8", +] + [[package]] name = "typenum" version = "1.20.1" @@ -3380,6 +4039,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -3407,6 +4072,18 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca718471876992059f251e33aafc334346ada08042b7661dd1fe313a9bced557" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -3435,8 +4112,15 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3509,6 +4193,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.128" @@ -3541,6 +4235,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -3755,7 +4459,7 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror", + "thiserror 2.0.20", "time", ] @@ -3771,9 +4475,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3782,9 +4486,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", @@ -3862,14 +4566,16 @@ dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] name = "zerovec" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -3877,9 +4583,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" dependencies = [ "proc-macro2", "quote", diff --git a/tests/suites/features/Cargo.toml b/tests/suites/features/Cargo.toml index 9259450a16..9b4760c2a9 100644 --- a/tests/suites/features/Cargo.toml +++ b/tests/suites/features/Cargo.toml @@ -3,4 +3,4 @@ [workspace] resolver = "2" -members = ["oci-image", "provider-refresh/keycloak"] +members = ["oci-image", "kubernetes-updates", "provider-refresh/keycloak"] diff --git a/tests/suites/features/kubernetes-updates/Cargo.toml b/tests/suites/features/kubernetes-updates/Cargo.toml new file mode 100644 index 0000000000..3348510e53 --- /dev/null +++ b/tests/suites/features/kubernetes-updates/Cargo.toml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +[package] +name = "openshell-test-feature-kubernetes-updates" +version = "0.0.0" +edition = "2024" + +[dependencies] +openshell-testkit = { path = "../../../testkit/testkit" } +serde_json = "1" +tempfile = "3" +tokio = { version = "1.43", features = ["process", "time"] } +tonic = "0.14" +uuid = { version = "1.10", features = ["v4"] } diff --git a/tests/suites/features/kubernetes-updates/tests/update.rs b/tests/suites/features/kubernetes-updates/tests/update.rs new file mode 100644 index 0000000000..61ac57e9fa --- /dev/null +++ b/tests/suites/features/kubernetes-updates/tests/update.rs @@ -0,0 +1,695 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Exercise the public image-update RPC in both Agent Sandbox API-version lanes. +//! Kubernetes checks are scoped to test-owned resources. + +use std::collections::BTreeMap; +use std::time::Duration; + +use serde_json::Value; +use std::sync::LazyLock; +use testkit::sdk::raw::{AuthedGrpcClient, AuthedSandboxUpdatesClient, proto}; +use testkit::{Context, Sandbox}; + +// Multi-platform release manifests; no test-local tags or cluster image loads. +const ORIGINAL_IMAGE_DEFAULT: &str = "mirror.gcr.io/library/alpine:3.21@sha256:ce64758a109eb420d874a118f87920e625e12d3634e03b4a5573fd9f6e5d3507"; +const UPDATED_IMAGE_DEFAULT: &str = "mirror.gcr.io/library/alpine:3.22@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8"; +const SENTINEL_PATH: &str = "/sandbox/.image-update-sentinel"; + +// CI can use a private mirror without changing the tested image versions. +static ORIGINAL_IMAGE: LazyLock = LazyLock::new(|| { + std::env::var("OPENSHELL_E2E_UPDATE_ORIGINAL_IMAGE") + .unwrap_or_else(|_| ORIGINAL_IMAGE_DEFAULT.into()) +}); +static UPDATED_IMAGE: LazyLock = LazyLock::new(|| { + std::env::var("OPENSHELL_E2E_UPDATE_TARGET_IMAGE") + .unwrap_or_else(|_| UPDATED_IMAGE_DEFAULT.into()) +}); + +fn request_id() -> String { + uuid::Uuid::new_v4().to_string() +} + +fn rpc(body: T) -> tonic::Request { + let mut request = tonic::Request::new(body); + request.set_timeout(Duration::from_secs(60)); + request +} + +async fn get_sandbox(client: &mut AuthedGrpcClient, workspace: &str, name: &str) -> proto::Sandbox { + client + .get_sandbox(rpc(proto::GetSandboxRequest { + workspace_scope: Some(proto::workspace_selector(workspace)), + name: name.to_owned(), + })) + .await + .expect("get test sandbox") + .into_inner() + .sandbox + .expect("Sandbox response") +} + +/// Stop can still be followed by driver-watch status writes. Refresh the CAS +/// version on a definite conflict, without retrying ambiguous driver failures. +async fn update_stopped_sandbox( + client: &mut AuthedGrpcClient, + updates: &mut AuthedSandboxUpdatesClient, + workspace: &str, + mut request: proto::UpdateSandboxRequest, +) -> (proto::Sandbox, proto::UpdateSandboxRequest) { + let mut sandbox_id = None; + for attempt in 0..20 { + let current = get_sandbox(client, workspace, &request.name).await; + let metadata = current.metadata.as_ref().expect("sandbox metadata"); + let id = sandbox_id.get_or_insert_with(|| metadata.id.clone()); + assert_eq!( + &metadata.id, id, + "conditional retry must not target a replacement" + ); + let status = current.status.as_ref().expect("sandbox status"); + assert_eq!(status.phase, proto::SandboxPhase::Stopped as i32); + assert!( + status + .provisioning + .as_ref() + .is_none_or(|record| !record.driver_operation_pending), + "do not retry a newly selected update over a pending operation" + ); + request.expected_resource_version = metadata.resource_version; + if attempt > 0 && !request.request_id.is_empty() { + // A changed version is a new request payload, not a replay. + request.request_id = request_id(); + } + match updates.update_sandbox(rpc(request.clone())).await { + Ok(response) => { + return ( + response.into_inner().sandbox.expect("updated Sandbox"), + request, + ); + } + Err(error) => { + let conflict = error.code() == tonic::Code::Aborted + && testkit::sdk::SdkError::from_status(error.clone()) + .error_details() + .is_some_and(|details| { + details.error_info().is_some_and(|info| { + info.domain == "openshell.nvidia.com" + && info.reason == "RESOURCE_VERSION_CONFLICT" + }) + }); + assert!( + conflict && attempt < 19, + "update stopped sandbox: {error:?}" + ); + eprintln!( + "refreshing sandbox {} after version conflict: {error}", + request.name + ); + tokio::time::sleep(Duration::from_millis(100)).await; + } + } + } + unreachable!("final conflict fails the test") +} + +async fn kubectl_mutation(args: &[&str]) -> std::process::Output { + let namespace = std::env::var("OPENSHELL_E2E_SANDBOX_NAMESPACE").expect("test namespace"); + let context = std::env::var("OPENSHELL_E2E_KUBE_CONTEXT_ACTIVE").expect("test context"); + tokio::process::Command::new("kubectl") + .args([ + "--context", + &context, + "--namespace", + &namespace, + "--request-timeout=30s", + ]) + .args(args) + .kill_on_drop(true) + .output() + .await + .expect("kubectl mutation") +} + +/// Exercise real API-server immutable UID validation independently of the +/// resourceVersion condition, after deleting and recreating the named resource. +async fn assert_recreated_resource_rejects_stale_patch(template: &Value, name: &str) { + use std::io::Write as _; + let resource = serde_json::json!({ + "apiVersion": template["apiVersion"], "kind": "Sandbox", + "metadata": {"name": name}, "spec": template["spec"], + }); + let mut file = tempfile::NamedTempFile::new().expect("test manifest"); + file.write_all(serde_json::to_string(&resource).unwrap().as_bytes()) + .unwrap(); + let path = file.path().to_str().unwrap(); + let create = kubectl_mutation(&["create", "-f", path, "-o", "json"]).await; + assert!( + create.status.success(), + "{}", + String::from_utf8_lossy(&create.stderr) + ); + let original: Value = serde_json::from_slice(&create.stdout).unwrap(); + let delete = kubectl_mutation(&[ + "delete", + "sandboxes.agents.x-k8s.io", + name, + "--wait=true", + "--timeout=60s", + ]) + .await; + assert!( + delete.status.success(), + "{}", + String::from_utf8_lossy(&delete.stderr) + ); + let create = kubectl_mutation(&["create", "-f", path, "-o", "json"]).await; + assert!( + create.status.success(), + "{}", + String::from_utf8_lossy(&create.stderr) + ); + let replacement: Value = serde_json::from_slice(&create.stdout).unwrap(); + let patch = serde_json::json!({"metadata": { + "uid": original["metadata"]["uid"], + "annotations": {"example.com/stale-patch": "must-not-apply"}, + }}) + .to_string(); + let rejected = kubectl_mutation(&[ + "patch", + "sandboxes.agents.x-k8s.io", + name, + "--type=merge", + "-p", + &patch, + ]) + .await; + let after = kubectl_get("sandboxes.agents.x-k8s.io", &[name]).await; + let cleanup = kubectl_mutation(&[ + "delete", + "sandboxes.agents.x-k8s.io", + name, + "--wait=true", + "--timeout=60s", + ]) + .await; + assert!( + cleanup.status.success(), + "{}", + String::from_utf8_lossy(&cleanup.stderr) + ); + assert_ne!(original["metadata"]["uid"], replacement["metadata"]["uid"]); + assert!(!rejected.status.success(), "stale UID patch must fail"); + assert!( + String::from_utf8_lossy(&rejected.stderr).contains("metadata.uid"), + "{}", + String::from_utf8_lossy(&rejected.stderr) + ); + assert!(after["metadata"]["annotations"]["example.com/stale-patch"].is_null()); +} + +async fn kubectl_get(resource: &str, selector: &[&str]) -> Value { + let namespace = std::env::var("OPENSHELL_E2E_SANDBOX_NAMESPACE") + .expect("Kubernetes E2E wrapper must provide the sandbox namespace"); + let context = std::env::var("OPENSHELL_E2E_KUBE_CONTEXT_ACTIVE") + .expect("Kubernetes E2E wrapper must provide its active context"); + assert!(!namespace.trim().is_empty() && !context.trim().is_empty()); + let output = tokio::process::Command::new("kubectl") + .args([ + "--context", + &context, + "--namespace", + &namespace, + "--request-timeout=30s", + "get", + resource, + ]) + .args(selector) + .args(["--output=json"]) + .kill_on_drop(true) + .output() + .await + .expect("run scoped kubectl get"); + assert!( + output.status.success(), + "kubectl get {resource} {selector:?}: {}", + String::from_utf8_lossy(&output.stderr) + ); + serde_json::from_slice(&output.stdout).expect("parse kubectl JSON") +} + +async fn sandbox_resource(id: &str) -> Value { + let selector = format!("openshell.ai/sandbox-id={id}"); + let list = kubectl_get("sandboxes.agents.x-k8s.io", &["--selector", &selector]).await; + let [resource] = list["items"].as_array().expect("Sandbox items").as_slice() else { + panic!("expected exactly one Sandbox resource for {id}: {list}"); + }; + assert_eq!( + resource["metadata"]["labels"]["openshell.ai/sandbox-id"], + id + ); + assert!( + resource["metadata"]["uid"] + .as_str() + .is_some_and(|uid| !uid.is_empty()) + ); + resource.clone() +} + +/// Read claims by their exact names from the actual workload Pod. Do not rely +/// on controller-version-dependent PVC names or labels, or inspect other PVCs. +async fn claim_uids(resource: &Value) -> BTreeMap { + let pod_name = resource["metadata"]["annotations"]["agents.x-k8s.io/pod-name"] + .as_str() + .unwrap_or_else(|| resource["metadata"]["name"].as_str().expect("Sandbox name")); + let pod = kubectl_get("pod", &[pod_name]).await; + assert!( + pod["metadata"]["ownerReferences"] + .as_array() + .expect("workload Pod owners") + .iter() + .any(|owner| owner["uid"] == resource["metadata"]["uid"]), + "workload Pod must belong to this Sandbox resource" + ); + let agent = pod["spec"]["containers"] + .as_array() + .expect("Pod containers") + .iter() + .find(|container| container["name"] == "agent") + .expect("agent container"); + let workspace_volume = agent["volumeMounts"] + .as_array() + .expect("agent volume mounts") + .iter() + .find(|mount| mount["mountPath"] == "/sandbox") + .expect("workspace volume mount"); + let volumes = pod["spec"]["volumes"].as_array().expect("Pod volumes"); + assert!( + volumes.iter().any(|volume| { + volume["name"] == workspace_volume["name"] + && volume["persistentVolumeClaim"]["claimName"] + .as_str() + .is_some() + }), + "workspace must be backed by a PVC" + ); + let mut claims = BTreeMap::new(); + for volume in volumes { + let Some(name) = volume["persistentVolumeClaim"]["claimName"].as_str() else { + continue; + }; + let claim = kubectl_get("persistentvolumeclaim", &[name]).await; + let uid = claim["metadata"]["uid"] + .as_str() + .filter(|uid| !uid.is_empty()) + .expect("PVC UID"); + assert_eq!(claim["status"]["phase"], "Bound"); + claims.insert(name.to_owned(), uid.to_owned()); + } + assert!( + !claims.is_empty(), + "test must exercise PVC-backed workspace storage" + ); + claims +} + +fn assert_resource_retained(before: &Value, after: &Value, image: &str) { + assert_eq!(after["metadata"]["name"], before["metadata"]["name"]); + assert_eq!(after["metadata"]["uid"], before["metadata"]["uid"]); + assert_eq!( + after["spec"]["volumeClaimTemplates"], + before["spec"]["volumeClaimTemplates"] + ); + let agent = after["spec"]["podTemplate"]["spec"]["containers"] + .as_array() + .expect("workload containers") + .iter() + .find(|container| container["name"] == "agent") + .expect("agent container"); + assert_eq!(agent["image"], image); +} + +fn assert_identity_retained(before: &proto::Sandbox, after: &proto::Sandbox) { + assert_eq!( + after.metadata.as_ref().expect("metadata").id, + before.metadata.as_ref().expect("metadata").id + ); + assert_eq!(after.host_key_fingerprint, before.host_key_fingerprint); +} + +async fn assert_base_release(sandbox: &Sandbox, release: &str) { + assert!( + sandbox + .exec(["cat", "/etc/alpine-release"]) + .await + .expect("base image release") + .stdout + .trim() + .starts_with(release) + ); +} + +fn assert_update_completed(original: &proto::Sandbox, updated: &proto::Sandbox) { + assert_identity_retained(original, updated); + let status = updated.status.as_ref().expect("updated status"); + assert_eq!(status.phase, proto::SandboxPhase::Stopped as i32); + assert!( + status + .provisioning + .as_ref() + .is_none_or(|record| !record.driver_operation_pending), + "successful image update must settle the old-understood operation interlock" + ); + assert_eq!( + updated + .spec + .as_ref() + .expect("spec") + .template + .as_ref() + .expect("template") + .image, + *UPDATED_IMAGE + ); +} + +async fn assert_replay( + client: &mut AuthedSandboxUpdatesClient, + update: proto::UpdateSandboxRequest, + updated: &proto::Sandbox, +) { + // Replay restores current resource state, not a captured transient status. + // Use retained driver evidence to prove that no new image operation ran. + let id = &updated.metadata.as_ref().expect("updated metadata").id; + let before = sandbox_resource(id).await; + let evidence = before["metadata"]["annotations"]["openshell.ai/sandbox-image-update"].clone(); + let operation: Value = + serde_json::from_str(evidence.as_str().expect("image operation evidence")) + .expect("parse image operation evidence"); + assert!( + operation["revision"] + .as_u64() + .is_some_and(|revision| revision > 0) + ); + assert!( + operation["operation_id"] + .as_str() + .is_some_and(|id| !id.is_empty()) + ); + let replay = client + .update_sandbox(rpc(update)) + .await + .expect("request-ID replay"); + assert_eq!( + replay + .metadata() + .get("openshell-replayed") + .expect("replay metadata"), + "true" + ); + let replayed = replay.into_inner().sandbox.expect("replayed Sandbox"); + assert_update_completed(updated, &replayed); + assert_eq!(replayed.spec, updated.spec); + let after = sandbox_resource(id).await; + assert_eq!(after["metadata"]["uid"], before["metadata"]["uid"]); + assert_eq!( + after["metadata"]["annotations"]["openshell.ai/sandbox-image-update"], evidence, + "replay must retain the exact driver operation identity and revision" + ); + assert!( + replayed + .metadata + .as_ref() + .expect("replayed metadata") + .resource_version + >= updated + .metadata + .as_ref() + .expect("updated metadata") + .resource_version + ); +} + +async fn assert_image_update_supported(client: &mut AuthedGrpcClient) { + // This test is Kubernetes-only: missing support is a regression, not a skip. + let info = client + .get_gateway_info(rpc(proto::GetGatewayInfoRequest {})) + .await + .expect("get gateway capabilities") + .into_inner(); + assert!(info.extensions.iter().any(|extension| { + extension + .supported_capabilities + .iter() + .any(|capability| capability == "openshell.compute.update-sandbox-configuration") + })); +} + +/// Updating a stopped sandbox retains storage and identity and changes the next workload. +#[testkit::test] +async fn image_update_retains_workspace_and_identity(cx: &mut Context) -> testkit::Result { + let selected = testkit::Gateway::from_env()?; + let workspace = selected.workspace.as_str(); + let gateway = selected.connect().await?; + let mut client = gateway.raw_grpc(); + let mut image_client = gateway.raw_sandbox_updates(); + assert_image_update_supported(&mut client).await; + + let sandbox = cx + // The testkit's unique prefix leaves five characters of the name limit. + .sandbox("image") + .spec(|spec| spec.image = Some(ORIGINAL_IMAGE.to_string())) + .create() + .await?; + sandbox.wait_ready().await?; + let original = get_sandbox(&mut client, workspace, sandbox.name()).await; + let id = &original.metadata.as_ref().expect("sandbox metadata").id; + assert!(!id.is_empty(), "sandbox must have a canonical ID"); + assert!( + original.host_key_fingerprint.starts_with("SHA256:"), + "sandbox must have a gateway-managed SSH identity" + ); + assert_base_release(&sandbox, "3.21.").await; + let sentinel = request_id(); + sandbox + .exec([ + "sh", + "-c", + &format!("printf '%s' '{sentinel}' > '{SENTINEL_PATH}'"), + ]) + .await + .expect("write workspace sentinel"); + let before = sandbox_resource(id).await; + let original_claims = claim_uids(&before).await; + + let running_update = proto::UpdateSandboxRequest { + workspace_scope: Some(proto::workspace_selector(workspace)), + name: sandbox.name().to_owned(), + image: Some(UPDATED_IMAGE.to_string()), + expected_resource_version: original + .metadata + .as_ref() + .expect("metadata") + .resource_version, + request_id: String::new(), + ..Default::default() + }; + let error = image_client + .update_sandbox(rpc(running_update.clone())) + .await + .expect_err("running update must be rejected"); + assert_eq!(error.code(), tonic::Code::FailedPrecondition); + assert_resource_retained(&before, &sandbox_resource(id).await, &ORIGINAL_IMAGE); + + sandbox.stop().await?; + let stopped = get_sandbox(&mut client, workspace, sandbox.name()).await; + let update = proto::UpdateSandboxRequest { + expected_resource_version: stopped + .metadata + .as_ref() + .expect("metadata") + .resource_version, + request_id: request_id(), + command: Some(proto::SandboxCommandUpdate { argv: vec![ + "/bin/sh".into(), "-c".into(), + "mkdir -p /sandbox/harness; printf '%s:%s' \"$AGENT_MODE\" \"$1\" > /sandbox/harness/configuration; exec sleep 86400".into(), + "agent-bootstrap".into(), "new-argument".into(), + ] }), + environment: Some(proto::SandboxMapUpdate { values: std::collections::HashMap::from([("AGENT_MODE".into(), "updated".into())]) }), + labels: Some(proto::SandboxMapUpdate { values: std::collections::HashMap::from([("team".into(), "updated-agent".into())]) }), + annotations: Some(proto::SandboxMapUpdate { values: std::collections::HashMap::from([("example.com/revision".into(), "2".into())]) }), + ..running_update + }; + let (updated, update) = + update_stopped_sandbox(&mut client, &mut image_client, workspace, update).await; + assert_update_completed(&original, &updated); + assert_resource_retained(&before, &sandbox_resource(id).await, &UPDATED_IMAGE); + + assert_replay(&mut image_client, update, &updated).await; + assert_recreated_resource_rejects_stale_patch( + &sandbox_resource(id).await, + &cx.name("uid-race"), + ) + .await; + + sandbox.start().await?; + assert_base_release(&sandbox, "3.22.").await; + assert_eq!( + sandbox + .exec(["cat", "/sandbox/harness/configuration"]) + .await + .expect("updated command and environment ran on start") + .stdout + .trim(), + "updated:new-argument" + ); + let observed = get_sandbox(&mut client, workspace, sandbox.name()).await; + assert_eq!( + observed + .metadata + .as_ref() + .unwrap() + .labels + .get("team") + .map(String::as_str), + Some("updated-agent") + ); + assert_eq!( + observed + .metadata + .as_ref() + .unwrap() + .annotations + .get("example.com/revision") + .map(String::as_str), + Some("2") + ); + assert_eq!( + sandbox + .exec(["cat", SENTINEL_PATH]) + .await + .expect("retained workspace sentinel") + .stdout + .trim(), + sentinel + ); + let current = get_sandbox(&mut client, workspace, sandbox.name()).await; + assert_identity_retained(&original, ¤t); + let after = sandbox_resource(id).await; + assert_resource_retained(&before, &after, &UPDATED_IMAGE); + assert_eq!( + claim_uids(&after).await, + original_claims, + "all mounted PVC names and UIDs must survive image replacement" + ); + sandbox.delete().await?; + Ok(()) +} + +/// A terminal command can be stopped and repaired without replacing its storage. +#[testkit::test] +async fn terminal_workload_can_stop_update_and_restart(cx: &mut Context) -> testkit::Result { + let selected = testkit::Gateway::from_env()?; + let workspace = selected.workspace.as_str(); + let gateway = selected.connect().await?; + let mut client = gateway.raw_grpc(); + let mut updates = gateway.raw_sandbox_updates(); + let sandbox = cx + .sandbox("term") + .restart_policy(testkit::sdk::SandboxRestartPolicy::Never) + .spec(|spec| spec.image = Some(ORIGINAL_IMAGE.to_string())) + .create() + .await?; + sandbox.wait_ready().await?; + let original = get_sandbox(&mut client, workspace, sandbox.name()).await; + let before = sandbox_resource(sandbox.id()).await; + let original_claims = claim_uids(&before).await; + let sentinel = request_id(); + sandbox + .exec([ + "sh", + "-c", + &format!("printf '%s' '{sentinel}' > '{SENTINEL_PATH}'"), + ]) + .await?; + + for exit_code in [0, 1] { + sandbox.stop().await?; + update_stopped_sandbox( + &mut client, + &mut updates, + workspace, + proto::UpdateSandboxRequest { + workspace_scope: Some(proto::workspace_selector(workspace)), + name: sandbox.name().to_owned(), + command: Some(proto::SandboxCommandUpdate { + argv: vec!["/bin/sh".into(), "-c".into(), format!("exit {exit_code}")], + }), + ..Default::default() + }, + ) + .await; + // Do not wait for Ready: the command may exit before Ready is observed. + client + .start_sandbox(rpc(proto::StartSandboxRequest { + workspace_scope: Some(proto::workspace_selector(workspace)), + name: sandbox.name().to_owned(), + ..Default::default() + })) + .await + .expect("start terminal command"); + sandbox.wait_finished().await?; + let terminal = get_sandbox(&mut client, workspace, sandbox.name()).await; + let status = terminal.status.as_ref().unwrap(); + assert_eq!( + status.phase, + if exit_code == 0 { + proto::SandboxPhase::Completed + } else { + proto::SandboxPhase::Error + } as i32 + ); + assert_eq!(status.exit_code, Some(exit_code)); + assert!(status.conditions.iter().any(|condition| condition.reason + == if exit_code == 0 { + "MainProcessCompleted" + } else { + "MainProcessFailed" + })); + + sandbox.stop().await?; + let stopped = get_sandbox(&mut client, workspace, sandbox.name()).await; + assert_eq!( + stopped.status.as_ref().unwrap().phase, + proto::SandboxPhase::Stopped as i32 + ); + assert_eq!(stopped.status.as_ref().unwrap().exit_code, Some(exit_code)); + update_stopped_sandbox( + &mut client, + &mut updates, + workspace, + proto::UpdateSandboxRequest { + workspace_scope: Some(proto::workspace_selector(workspace)), + name: sandbox.name().to_owned(), + command: Some(proto::SandboxCommandUpdate { + argv: vec!["sleep".into(), "86400".into()], + }), + ..Default::default() + }, + ) + .await; + sandbox.start().await?; + let repaired = get_sandbox(&mut client, workspace, sandbox.name()).await; + assert_identity_retained(&original, &repaired); + assert_eq!(repaired.status.as_ref().unwrap().exit_code, None); + assert_eq!( + sandbox.exec(["cat", SENTINEL_PATH]).await?.stdout.trim(), + sentinel + ); + let after = sandbox_resource(sandbox.id()).await; + assert_resource_retained(&before, &after, &ORIGINAL_IMAGE); + assert_eq!(claim_uids(&after).await, original_claims); + } + sandbox.delete().await?; + Ok(()) +}