diff --git a/architecture/gateway.md b/architecture/gateway.md index d4cd80bed6..8645104bc5 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -469,7 +469,7 @@ The storage schema is intentionally narrow: ### Protobuf API and storage boundaries Public RPC contracts and durable protobuf formats have separate ownership. The -`openshell.v1.OpenShell` service currently has 75 RPCs. Their request and +`openshell.v1.OpenShell` service currently has 79 RPCs. Their request and response roots, streaming flags, and transitive message closure come from the public descriptor set generated by `openshell-core`; a fingerprint test in `openshell-server` requires this inventory to be reviewed whenever it changes. @@ -623,8 +623,12 @@ record; sandbox metadata receives the same annotations only as a convenience projection and can retain keys from earlier revisions. Policy revision creation, optional first-policy backfill, metadata projection, and superseding older revisions commit in one database transaction. SQLite serializes this operation -with an immediate transaction, while Postgres locks the sandbox row. A failed -resource-version check or revision insert rolls back the entire operation. +with an immediate transaction. Postgres first locks a dedicated configuration +fence keyed by sandbox ID, then locks the sandbox row. Settings mutations take +the same fence before reading the current policy target. This makes concurrent +policy and settings commits select targets in one database-owned serial order +across gateway replicas. A failed resource-version check, desired-state write, +projection, or operation insert rolls back the entire transaction. SQLite is the default local store; Postgres is supported for deployments that need an external database or multi-replica coordination. Both backends expose @@ -834,12 +838,13 @@ interleave a profile mutation with a sandbox provider-set mutation that would leave an ambiguous final dynamic-token state or a deleted custom profile that is still referenced by a sandbox. -Policy and runtime settings are delivered together through the effective sandbox -config path. A gateway-global policy can override sandbox-scoped policy. The -gateway pushes complete snapshots to active supervisor sessions and periodically -rebuilds them to repair missed delivery. Supervisors hot-reload accepted policy -and acknowledge the exact revision. The legacy poller remains as a mixed-version -compatibility path during this stage. +Policy and runtime settings are delivered together through the supervisor +configuration stream. A gateway-global policy can override sandbox-scoped +policy. The gateway pushes complete snapshots to active supervisor sessions and +owner reconciliation rebuilds them to repair missed delivery. Supervisors +hot-reload accepted policy and acknowledge the exact revision. Gateway and +supervisor protocol revisions must match; there is no configuration polling +compatibility path. External supervisor middleware registration is operator-owned configuration under `[[openshell.supervisor.middleware]]`. At startup the gateway connects to @@ -949,13 +954,48 @@ validates and persists any prepared proposal before accepting the session. `SessionAccepted` then carries the only startup state used to initialize the runtime. Reconnects skip preparation and receive the current gateway bootstrap directly. Supervisors apply later snapshots on the accepted stream and persist -only compact component observations from their results. Previous-revision -supervisors retain polling as a rollout fallback, and owner reconciliation -repairs missed or failed delivery from current database state. Snapshot build, +only compact component observations from their results. Gateway and supervisor +require protocol revision 3; peers that require configuration polling are rejected. +Owner reconciliation repairs missed or failed delivery from current database state. Snapshot build, fanout, or enqueue failure cannot fail a mutation that already committed. Provider snapshots may contain credentials and must not be persisted or included in logs. +For sandbox-scoped policy and settings mutations, the gateway atomically stores +the desired state and a durable operation whose target is the exact policy and +settings revision tuple. Server-side `WAIT_FOR_COMPLETION` reads this durable record +until a correlated stream result or authoritative lifecycle transition makes it +terminal. Completion includes failure, supersession, cancellation, or an inactive +sandbox; an applied operation can still have a degraded outcome. Callers inspect +both state and outcome. A wait timeout does not cancel the committed mutation. +Request replay records the operation identity after commit, before waiting, so +retries can change their wait preference without repeating the mutation. +An unchanged request records completion against the existing revision without +allocating another policy or settings revision. Pending operations require a fresh +stream acknowledgement even when normal reconciliation would suppress an +already-acknowledged snapshot. +Pending-operation reconciliation provides crash recovery and can run +on a gateway other than the request handler; local notifications are wake-up +hints only. Operations persist revisions, outcome, timestamps, response +metadata, and bounded sanitized errors, never complete configuration payloads +or credentials. The SQL status column changes atomically with the encoded +operation. Result correlation queries only pending operations scoped to the +reporting sandbox. Recovery claims bounded due batches, commits each retry +deadline before snapshot construction, groups work by sandbox, and publishes +each component at most once per sandbox pass. + +Operation records and their idempotency keys currently have no automatic +expiration. The gateway retains both until an explicit deletion contract is +defined, so an idempotency key cannot be reused merely because time passed. +On startup, the gateway decodes sandbox configuration operations and repairs their SQL +scope, state, and retry-time projection before selective reconciliation starts. +Provider receipt operations share the stored envelope but remain outside sandbox +configuration retry indexes. The repair is restart-safe and does not change operation resource versions. +The storage decoder preserves legacy millisecond timestamps and retry deadlines +when reading operations written before the protobuf time migration. +Gateways that share a database must be upgraded together while this projection +is introduced. An older gateway does not maintain these query columns. + See [sandbox configuration delivery](sandbox.md#supervisor-configuration-delivery) for bootstrap, revision, and supervisor application semantics. diff --git a/architecture/sandbox.md b/architecture/sandbox.md index fd431094f7..56500f573c 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -72,8 +72,7 @@ replacement from granting authority. run untrusted code yet. 3. `openshell-supervisor` opens its gateway session and receives an authoritative policy, settings, middleware, and provider bootstrap before - attaching to the sandbox. Compatibility protocol revisions continue to use - the polling APIs. + attaching to the sandbox. Protocol revision 3 is required on both peers. Older peers are rejected. 4. The sandbox installs its seccomp notification broker and Landlock baseline, validates its mechanism-specific audit evidence, and reports backend-neutral enforcement properties. The supervisor must accept those properties and @@ -664,9 +663,8 @@ quickly. ## Supervisor Configuration Delivery The gateway and supervisor must implement the same internal supervisor protocol -revision. Peers built before the handshake existed report revision zero and are -accepted for one release with a warning and a counter, because sandboxes keep -their supervisor binary until they are recreated. +revision. A gateway upgrade rejects supervisors from the previous release, so +operators must recreate those sandboxes. On the initial `ConnectSupervisor` stream, `SupervisorHello` carries an explicit workload image discovery result: missing, invalid, or a parsed policy. A gateway @@ -731,8 +729,9 @@ snapshot retains its own content revision. Bootstrap components are independent read projections, not one atomic database snapshot. The sandbox configuration carries the provider-environment revision it was built against. The gateway retries bootstrap construction when that -revision does not match the provider snapshot. Later component updates and -polling repair changes committed while the other projections were being built. +revision does not match the provider snapshot. The owner reconciler periodically +rebuilds current state to repair a change missed while another projection was +being built or delivered. Configuration delivery goes through a gateway-owned routing boundary rather than exposing local supervisor channels to mutation handlers. The current @@ -750,7 +749,8 @@ it receives an accepted acknowledgement. Rejection leaves the stream and supervisor alive so a later complete replacement can repair the generation and release the same workload. After launch, the supervisor separately reports runtime readiness once its relay plane is usable; admission alone never promotes -the sandbox to `Ready`. Compatibility protocol revisions continue using polling. +the sandbox to `Ready`. Both peers require protocol revision 3; revisions that +rely on polling are rejected. The gateway serializes construction per sandbox and component, and coalesces repeated mutations into the latest full snapshot. An enqueue result means only @@ -868,7 +868,7 @@ Policy status delivery uses a FIFO background worker. Retryable delivery failures retain the ordered update and retry with capped exponential backoff; terminal errors are logged and discarded. The outbox is nonblocking and does not discard updates because of a fixed queue capacity, so status endpoint -outages cannot block policy polling, enforcement, settings, or provider +outages cannot block streamed configuration, enforcement, settings, or provider refreshes and cannot permanently lose the initial acknowledgement. Only sandbox-scoped revisions (`PolicySource::Sandbox`, version greater than @@ -881,9 +881,8 @@ the gateway cannot admit the runtime policy it would enforce. ## Failure Behavior -- If compatibility config polling fails, the supervisor keeps its - last-known-good policy. Current protocol sessions use complete streamed - snapshots and reconnect with a fresh bootstrap. +- If the configuration stream disconnects, the supervisor keeps its + last-known-good policy and reconnects with a fresh bootstrap. - If a live policy or middleware-registry update is invalid, the supervisor rejects the update and keeps the current runtime pair. - If an operator-run middleware call fails, the selected config's `on_error` diff --git a/architecture/security-policy.md b/architecture/security-policy.md index 5145113e47..be0019fcf0 100644 --- a/architecture/security-policy.md +++ b/architecture/security-policy.md @@ -226,10 +226,9 @@ A candidate rejected during validation does not replace the active engine or adv The gateway stores sandbox-authored policy revisions separately from derived effective sandbox configuration. Effective configuration can include gateway-global policy overrides and provider-profile policy layers. The -gateway streams complete configuration snapshots to current supervisors, which -attempt to load new dynamic policy into the in-process OPA engine and -acknowledge the exact revision. The immediately previous supervisor protocol -continues to poll during the compatibility window. CLI reads of the latest +gateway streams complete configuration snapshots to the supervisor, which +validates and loads dynamic policy into the in-process OPA engine and +acknowledges the exact revision. CLI reads of the latest sandbox policy use the same effective configuration path. The OPA loader checks the object and list shapes of raw policy data before injecting runtime fields, normalizing values, or expanding access presets. It rejects the first malformed container with a fixed structural error that excludes authored keys and values. This check preserves valid versionless OPA data and runtime-only fields. A rejected OPA engine reload leaves that engine's installed policy, generation, and decisions unchanged; the supervisor separately applies its configured runtime rejection mode. diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index 778cc7dfd1..f24aff9177 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -2980,7 +2980,7 @@ async fn run_async() -> Result<()> { .await?; } else { let name = resolve_sandbox_name(name, &ctx.name, &cli.workspace)?; - run::sandbox_policy_set( + let exit_code = run::sandbox_policy_set( &ctx.endpoint, &name, &policy, @@ -2990,6 +2990,9 @@ async fn run_async() -> Result<()> { &tls, ) .await?; + if exit_code != 0 { + std::process::exit(exit_code); + } } } PolicyCommands::Update { @@ -3008,7 +3011,7 @@ async fn run_async() -> Result<()> { timeout, } => { let name = resolve_sandbox_name(name, &ctx.name, &cli.workspace)?; - run::sandbox_policy_update( + let exit_code = run::sandbox_policy_update( &ctx.endpoint, &name, &add_endpoints, @@ -3027,6 +3030,9 @@ async fn run_async() -> Result<()> { &tls, ) .await?; + if exit_code != 0 { + std::process::exit(exit_code); + } } PolicyCommands::Get { name, diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index 489db3136f..e78111cc01 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -46,7 +46,8 @@ use openshell_bootstrap::{ use openshell_core::net::set_tcp_nodelay_best_effort; use openshell_core::proto::{ ApproveAllDraftChunksRequest, ApproveDraftChunkRequest, BeginRootfsTarStagingRequest, - ClearDraftChunksRequest, CreateSandboxRequest, CreateSandboxTemplateRequest, + ClearDraftChunksRequest, ConfigUpdateConsistency, ConfigUpdateOperation, + ConfigUpdateOperationState, CreateSandboxRequest, CreateSandboxTemplateRequest, CreateSshSessionRequest, DeleteSandboxRequest, DeleteSandboxTemplateRequest, DeleteServiceRequest, DeletionOutcome, EndpointResult, EndpointStatus, ExecSandboxRequest, ExposeServiceRequest, GetCurrentUserRequest, GetDraftHistoryRequest, GetDraftPolicyRequest, @@ -73,6 +74,82 @@ use std::time::{Duration, Instant}; use tonic::{Code, Status}; const PROVISIONAL_CONTAINER_EXIT_RECONCILIATION_TIMEOUT: Duration = Duration::from_secs(5); +const POLICY_WAIT_TIMEOUT_EXIT_CODE: i32 = 124; + +fn report_policy_wait_timeout(status: &Status) -> Option { + if status.code() != Code::DeadlineExceeded { + return None; + } + let operation_id = status + .metadata() + .get("operation-id") + .and_then(|value| value.to_str().ok()); + if let Some(operation_id) = operation_id { + eprintln!( + "{} Timeout waiting for policy update operation {}; update remains committed", + "✗".red().bold(), + operation_id + ); + } else { + eprintln!( + "{} Timeout waiting for policy update; update remains committed", + "✗".red().bold() + ); + } + Some(POLICY_WAIT_TIMEOUT_EXIT_CODE) +} + +fn report_config_update_operation( + operation: Option<&ConfigUpdateOperation>, + version: u32, +) -> Result<()> { + let operation = + operation.ok_or_else(|| miette!("gateway omitted the requested completion operation"))?; + match ConfigUpdateOperationState::try_from(operation.state).unwrap_or_default() { + ConfigUpdateOperationState::Applied => { + if operation.outcome == openshell_core::proto::ConfigApplyOutcome::Degraded as i32 { + eprintln!( + "{} Policy version {} applied with degraded outcome (operation {}): {}", + "!".yellow().bold(), + version, + operation.operation_id, + operation.sanitized_error + ); + return Ok(()); + } + eprintln!( + "{} Policy version {} applied (operation {})", + "✓".green().bold(), + version, + operation.operation_id + ); + Ok(()) + } + ConfigUpdateOperationState::Inactive => { + eprintln!( + "{} Policy version {} committed; sandbox is inactive (operation {})", + "✓".green().bold(), + version, + operation.operation_id + ); + Ok(()) + } + ConfigUpdateOperationState::Failed + | ConfigUpdateOperationState::Superseded + | ConfigUpdateOperationState::Cancelled => Err(miette!( + "policy version {} did not apply: {} (operation {})", + version, + operation.sanitized_error, + operation.operation_id + )), + ConfigUpdateOperationState::Pending | ConfigUpdateOperationState::Unspecified => { + Err(miette!( + "gateway returned a non-terminal completion operation {}", + operation.operation_id + )) + } + } +} fn proto_timestamp_ms(timestamp: Option<&prost_types::Timestamp>) -> i64 { timestamp @@ -4969,7 +5046,7 @@ pub async fn sandbox_policy_set( timeout_secs: u64, workspace: &str, tls: &TlsOptions, -) -> Result<()> { +) -> Result { let policy = load_sandbox_policy(Some(policy_path))? .ok_or_else(|| miette::miette!("No policy loaded from {policy_path}"))?; @@ -4990,17 +5067,35 @@ pub async fn sandbox_policy_set( .and_then(|r| r.into_inner().revision) .map_or(0, |r| r.version); - let response = client + let response = match client .update_config(UpdateConfigRequest { sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( workspace.to_string(), )), policy: Some(policy), + consistency: if wait { + ConfigUpdateConsistency::WaitForCompletion.into() + } else { + ConfigUpdateConsistency::CommitOnly.into() + }, + wait_timeout: Some( + openshell_core::time::duration_from_std(Duration::from_secs(timeout_secs)) + .into_diagnostic()?, + ), ..Default::default() }) .await - .into_diagnostic()?; + { + Ok(response) => response, + Err(status) if wait => { + if let Some(exit_code) = report_policy_wait_timeout(&status) { + return Ok(exit_code); + } + return Err(status).into_diagnostic(); + } + Err(status) => return Err(status).into_diagnostic(), + }; let resp = response.into_inner(); @@ -5011,7 +5106,7 @@ pub async fn sandbox_policy_set( resp.version, &resp.policy_hash[..12] ); - return Ok(()); + return Ok(0); } eprintln!( @@ -5022,70 +5117,11 @@ pub async fn sandbox_policy_set( ); if !wait { - return Ok(()); + return Ok(0); } - // Poll for status until loaded, failed, or timeout. - let deadline = Instant::now() + Duration::from_secs(timeout_secs); - loop { - if Instant::now() > deadline { - eprintln!( - "{} Timeout waiting for policy version {} to load", - "✗".red().bold(), - resp.version - ); - std::process::exit(124); - } - - tokio::time::sleep(Duration::from_secs(1)).await; - - let status_resp = client - .get_sandbox_policy_status(GetSandboxPolicyStatusRequest { - sandbox: name.to_string(), - workspace_scope: Some(openshell_core::proto::workspace_selector( - workspace.to_string(), - )), - version: resp.version, - global: false, - }) - .await - .into_diagnostic()?; - - let inner = status_resp.into_inner(); - if let Some(rev) = &inner.revision { - let status = PolicyStatus::try_from(rev.status).unwrap_or(PolicyStatus::Unspecified); - match status { - PolicyStatus::Loaded => { - eprintln!( - "{} Policy version {} loaded (active version: {})", - "✓".green().bold(), - rev.version, - inner.active_version - ); - return Ok(()); - } - PolicyStatus::Failed => { - eprintln!( - "{} Policy version {} failed to load: {}", - "✗".red().bold(), - rev.version, - rev.load_error - ); - std::process::exit(1); - } - PolicyStatus::Superseded => { - eprintln!( - "{} Policy version {} was superseded (active version: {})", - "⚠".yellow().bold(), - rev.version, - inner.active_version - ); - return Ok(()); - } - _ => {} // still pending, keep polling - } - } - } + report_config_update_operation(resp.operation.as_ref(), resp.version)?; + Ok(0) } /// Preview or atomically submit explicitly scoped incremental policy operations. @@ -5107,7 +5143,7 @@ pub async fn sandbox_policy_update( timeout_secs: u64, workspace: &str, tls: &TlsOptions, -) -> Result<()> { +) -> Result { if dry_run && wait { return Err(miette!("--wait cannot be combined with --dry-run")); } @@ -5156,23 +5192,40 @@ pub async fn sandbox_policy_update( ); print_policy_merge_warnings(&merged.warnings); print_sandbox_policy(&merged.policy); - return Ok(()); + return Ok(0); } let current_version = current.version; let current_hash = current.policy_hash.clone(); - let response = client + let response = match client .update_config(UpdateConfigRequest { sandbox: name.to_string(), workspace_scope: Some(openshell_core::proto::workspace_selector( workspace.to_string(), )), merge_operations: plan.merge_operations, + consistency: if wait { + ConfigUpdateConsistency::WaitForCompletion.into() + } else { + ConfigUpdateConsistency::CommitOnly.into() + }, + wait_timeout: Some( + openshell_core::time::duration_from_std(Duration::from_secs(timeout_secs)) + .into_diagnostic()?, + ), ..Default::default() }) .await - .into_diagnostic()? - .into_inner(); + { + Ok(response) => response.into_inner(), + Err(status) if wait => { + if let Some(exit_code) = report_policy_wait_timeout(&status) { + return Ok(exit_code); + } + return Err(status).into_diagnostic(); + } + Err(status) => return Err(status).into_diagnostic(), + }; print_policy_merge_warnings(&merged.warnings); @@ -5183,7 +5236,7 @@ pub async fn sandbox_policy_update( response.version, short_hash(&response.policy_hash) ); - return Ok(()); + return Ok(0); } eprintln!( @@ -5194,69 +5247,11 @@ pub async fn sandbox_policy_update( ); if !wait { - return Ok(()); + return Ok(0); } - let deadline = Instant::now() + Duration::from_secs(timeout_secs); - loop { - if Instant::now() > deadline { - eprintln!( - "{} Timeout waiting for policy version {} to load", - "✗".red().bold(), - response.version - ); - std::process::exit(124); - } - - tokio::time::sleep(Duration::from_secs(1)).await; - - let status_resp = client - .get_sandbox_policy_status(GetSandboxPolicyStatusRequest { - sandbox: name.to_string(), - workspace_scope: Some(openshell_core::proto::workspace_selector( - workspace.to_string(), - )), - version: response.version, - global: false, - }) - .await - .into_diagnostic()?; - - let inner = status_resp.into_inner(); - if let Some(rev) = &inner.revision { - let status = PolicyStatus::try_from(rev.status).unwrap_or(PolicyStatus::Unspecified); - match status { - PolicyStatus::Loaded => { - eprintln!( - "{} Policy version {} loaded (active version: {})", - "✓".green().bold(), - rev.version, - inner.active_version - ); - return Ok(()); - } - PolicyStatus::Failed => { - eprintln!( - "{} Policy version {} failed to load: {}", - "✗".red().bold(), - rev.version, - rev.load_error - ); - std::process::exit(1); - } - PolicyStatus::Superseded => { - eprintln!( - "{} Policy version {} was superseded (active version: {})", - "⚠".yellow().bold(), - rev.version, - inner.active_version - ); - return Ok(()); - } - _ => {} - } - } - } + report_config_update_operation(response.operation.as_ref(), response.version)?; + Ok(0) } pub async fn sandbox_policy_get( diff --git a/crates/openshell-cli/tests/ensure_providers_integration.rs b/crates/openshell-cli/tests/ensure_providers_integration.rs index e7221e0079..ce53d5f7b3 100644 --- a/crates/openshell-cli/tests/ensure_providers_integration.rs +++ b/crates/openshell-cli/tests/ensure_providers_integration.rs @@ -7,6 +7,10 @@ mod helpers; +use openshell_core::proto::{ + GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, +}; + use helpers::{EnvVarGuard, build_ca, build_client_cert, build_server_cert}; use openshell_cli::run; use openshell_cli::tls::TlsOptions; @@ -19,8 +23,7 @@ use openshell_core::proto::{ ExchangeProviderSubjectTokenRequest, ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, GatewayMessage, GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, - GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest, - GetSandboxProviderEnvironmentResponse, GetSandboxRequest, HealthRequest, HealthResponse, + GetSandboxConfigResponse, GetSandboxRequest, HealthRequest, HealthResponse, ListProvidersRequest, ListProvidersResponse, ListSandboxProvidersRequest, ListSandboxProvidersResponse, ListSandboxesRequest, ListSandboxesResponse, Provider, ProviderResponse, RevokeSshSessionRequest, RevokeSshSessionResponse, SandboxResponse, @@ -586,6 +589,17 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not implemented in test")) } + #[allow(unused_qualifications)] + async fn get_config_update_operation( + &self, + _request: tonic::Request, + ) -> Result< + tonic::Response, + tonic::Status, + > { + Err(tonic::Status::unimplemented("unused")) + } + async fn update_config( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/mtls_integration.rs b/crates/openshell-cli/tests/mtls_integration.rs index ca969dde13..5bc63073a5 100644 --- a/crates/openshell-cli/tests/mtls_integration.rs +++ b/crates/openshell-cli/tests/mtls_integration.rs @@ -438,6 +438,17 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not implemented in test")) } + #[allow(unused_qualifications)] + async fn get_config_update_operation( + &self, + _request: tonic::Request, + ) -> Result< + tonic::Response, + tonic::Status, + > { + Err(tonic::Status::unimplemented("unused")) + } + async fn update_config( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/provider_commands_integration.rs b/crates/openshell-cli/tests/provider_commands_integration.rs index ae1e7363f6..b344f5c66d 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -1381,6 +1381,17 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not implemented in test")) } + #[allow(unused_qualifications)] + async fn get_config_update_operation( + &self, + _request: tonic::Request, + ) -> Result< + tonic::Response, + tonic::Status, + > { + Err(tonic::Status::unimplemented("unused")) + } + async fn update_config( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs index f1408493bc..ea07d5f52e 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -890,6 +890,17 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not implemented in test")) } + #[allow(unused_qualifications)] + async fn get_config_update_operation( + &self, + _request: tonic::Request, + ) -> Result< + tonic::Response, + tonic::Status, + > { + Err(tonic::Status::unimplemented("unused")) + } + async fn update_config( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs index 7a5780d7f3..c37325c184 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -3,6 +3,10 @@ mod helpers; +use openshell_core::proto::{ + GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, +}; + use helpers::{EnvVarGuard, build_ca, build_client_cert, build_server_cert}; use openshell_bootstrap::{load_last_sandbox, save_last_sandbox}; use openshell_cli::run; @@ -17,14 +21,14 @@ use openshell_core::proto::{ ExecSandboxInput, ExecSandboxRequest, GatewayMessage, GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxPolicyStatusRequest, GetSandboxPolicyStatusResponse, - GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, GetSandboxRequest, - HealthRequest, HealthResponse, ListProvidersRequest, ListProvidersResponse, + GetSandboxRequest, HealthRequest, HealthResponse, ListProvidersRequest, ListProvidersResponse, ListSandboxProvidersRequest, ListSandboxProvidersResponse, ListSandboxesRequest, ListSandboxesResponse, NetworkEndpoint, NetworkPolicyRule, PolicyStatus, ProviderResponse, Sandbox, SandboxPolicy, SandboxPolicyRevision, SandboxResponse, SandboxStreamEvent, ServiceStatus, SupervisorMessage, UpdateProviderRequest, WatchSandboxRequest, }; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use tempfile::TempDir; use tokio::net::TcpListener; use tokio::sync::{Mutex, mpsc}; @@ -38,6 +42,7 @@ use tonic::{Response, Status}; #[derive(Clone, Default)] struct SandboxState { last_get_name: Arc>>, + timeout_config_updates: Arc, } #[derive(Clone, Default)] @@ -492,10 +497,28 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not implemented in test")) } + #[allow(unused_qualifications)] + async fn get_config_update_operation( + &self, + _request: tonic::Request, + ) -> Result< + tonic::Response, + tonic::Status, + > { + Err(tonic::Status::unimplemented("unused")) + } + async fn update_config( &self, _request: tonic::Request, ) -> Result, Status> { + if self.state.timeout_config_updates.load(Ordering::Relaxed) { + let mut status = Status::deadline_exceeded("update remains pending"); + status + .metadata_mut() + .insert("operation-id", "operation-timeout-123".parse().unwrap()); + return Err(status); + } Err(Status::unimplemented("not implemented in test")) } @@ -505,7 +528,7 @@ impl OpenShell for TestOpenShell { ) -> Result, Status> { let req = request.into_inner(); assert_eq!(req.sandbox, "my-sandbox"); - assert_eq!(req.version, 3); + assert!(matches!(req.version, 0 | 3)); assert!(!req.global); let policy = SandboxPolicy { @@ -748,7 +771,7 @@ struct TestServer { endpoint: String, tls: TlsOptions, openshell: TestOpenShell, - _dir: TempDir, + dir: TempDir, } async fn run_server() -> TestServer { @@ -795,7 +818,7 @@ async fn run_server() -> TestServer { endpoint, tls, openshell, - _dir: dir, + dir, } } @@ -1035,3 +1058,65 @@ async fn explicit_name_takes_precedence_over_persisted() { "explicit name should be used, not the persisted one" ); } + +#[tokio::test(flavor = "multi_thread")] +async fn policy_wait_timeouts_exit_124_for_set_and_update() { + let ts = run_server().await; + ts.openshell + .state + .timeout_config_updates + .store(true, Ordering::Relaxed); + + let config_dir = tempfile::tempdir().unwrap(); + let mtls_dir = config_dir + .path() + .join("openshell/gateways/timeout-test/mtls"); + std::fs::create_dir_all(&mtls_dir).unwrap(); + for name in ["ca.crt", "tls.crt", "tls.key"] { + std::fs::copy(ts.dir.path().join(name), mtls_dir.join(name)).unwrap(); + } + let policy_dir = tempfile::tempdir().unwrap(); + let policy_path = policy_dir.path().join("policy.yaml"); + std::fs::write(&policy_path, "version: 1\n").unwrap(); + + let common = [ + "--gateway", + "timeout-test", + "--gateway-endpoint", + ts.endpoint.as_str(), + "policy", + ]; + let commands = [ + vec![ + "set", + "my-sandbox", + "--policy", + policy_path.to_str().unwrap(), + "--wait", + "--timeout", + "1", + ], + vec![ + "update", + "my-sandbox", + "--add-endpoint", + "api.example.com:443", + "--wait", + "--timeout", + "1", + ], + ]; + + for args in commands { + let output = std::process::Command::new(env!("CARGO_BIN_EXE_openshell")) + .args(common) + .args(args) + .env("XDG_CONFIG_HOME", config_dir.path()) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(124), "{output:?}"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("operation-timeout-123"), "{stderr}"); + assert!(stderr.contains("remains committed"), "{stderr}"); + } +} diff --git a/crates/openshell-core/src/grpc_client.rs b/crates/openshell-core/src/grpc_client.rs index 48df5c6c5c..ac64fa3a60 100644 --- a/crates/openshell-core/src/grpc_client.rs +++ b/crates/openshell-core/src/grpc_client.rs @@ -1253,14 +1253,14 @@ fn provider_subject_token_exchange_status(status: Status) -> miette::Report { /// A reusable gRPC client for the `OpenShell` service. /// -/// Wraps a tonic channel connected once and reused for policy polling -/// and status reporting, avoiding per-request TLS handshake overhead. +/// Wraps a tonic channel connected once and reused for status reporting and +/// extension-credential rotation, avoiding per-request TLS handshake overhead. #[derive(Clone)] pub struct CachedOpenShellClient { client: OpenShellClient, workspace: Arc>, /// Extension credentials for this supervisor. Cloning the client shares - /// the store, so the middleware registry and the polling loop that rotates + /// the store, so the middleware registry and the stream loop that rotates /// it observe the same slots. extension_credentials: ExtensionCredentialStore, } @@ -1541,7 +1541,7 @@ impl CachedOpenShellClient { endpoint: &str, extension_credentials: ExtensionCredentialStore, ) -> Result { - debug!(endpoint = %endpoint, "Connecting openshell gRPC client for policy polling"); + debug!(endpoint = %endpoint, "Connecting reusable openshell gRPC client"); let client = connect(endpoint).await?; Ok(Self { client, @@ -1619,7 +1619,7 @@ impl CachedOpenShellClient { } /// Rotate every credential currently retained by the installed registry. - /// This remains available when configuration polling fails independently. + /// This remains available independently of streamed configuration updates. pub async fn refresh_installed_extension_credentials(&self) -> Result<()> { let names = self.extension_credentials.names(); if names.is_empty() || !self.extension_credentials.needs_refresh(&names, now_ms()) { diff --git a/crates/openshell-core/src/proposals.rs b/crates/openshell-core/src/proposals.rs index 39810f0668..5107b3780f 100644 --- a/crates/openshell-core/src/proposals.rs +++ b/crates/openshell-core/src/proposals.rs @@ -4,7 +4,7 @@ //! Shared state controlling agent-driven policy proposals. //! //! Initialised once during sandbox start from the `agent_policy_proposals_enabled` -//! setting and updated by the policy poll loop or authoritative supervisor +//! setting and updated by the stream configuration loop or authoritative sidecar control //! when the setting changes. Read by the `policy.local` route handler and by //! the skills installer to gate the agent-controlled mutation surface. diff --git a/crates/openshell-core/src/proto/mod.rs b/crates/openshell-core/src/proto/mod.rs index 06de617ded..6cc2971a15 100644 --- a/crates/openshell-core/src/proto/mod.rs +++ b/crates/openshell-core/src/proto/mod.rs @@ -104,19 +104,7 @@ pub fn all_workspaces_selector() -> WorkspaceSelector { /// The supervisor stream is an internal, version-locked deployment contract. /// Bump this when either peer can no longer honor the previous stream /// semantics. -pub const SUPERVISOR_PROTOCOL_REVISION: u32 = 2; - -/// Stage 1 peers understand snapshot envelopes but do not apply them. They -/// remain compatible while polling is retained for the rollout. -pub const PREVIOUS_SUPERVISOR_PROTOCOL_REVISION: u32 = 1; - -/// Revision implied by peers built before the handshake existed. Proto3 leaves -/// the field unset, so such peers report zero. -/// -/// Sandboxes keep their supervisor binary until they are recreated, so a -/// gateway upgrade must keep serving them for one release. Remove this -/// allowance once every supported release sends an explicit revision. -pub const LEGACY_SUPERVISOR_PROTOCOL_REVISION: u32 = 0; +pub const SUPERVISOR_PROTOCOL_REVISION: u32 = 3; #[cfg(test)] mod tests { diff --git a/crates/openshell-sdk/tests/client_mock.rs b/crates/openshell-sdk/tests/client_mock.rs index 4051cd5136..27e53bf47e 100644 --- a/crates/openshell-sdk/tests/client_mock.rs +++ b/crates/openshell-sdk/tests/client_mock.rs @@ -724,6 +724,17 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("unused")) } + #[allow(unused_qualifications)] + async fn get_config_update_operation( + &self, + _request: tonic::Request, + ) -> Result< + tonic::Response, + tonic::Status, + > { + Err(tonic::Status::unimplemented("unused")) + } + async fn update_config( &self, _: tonic::Request, diff --git a/crates/openshell-server/migrations/postgres/009_config_update_operations.sql b/crates/openshell-server/migrations/postgres/009_config_update_operations.sql new file mode 100644 index 0000000000..8c6850ee62 --- /dev/null +++ b/crates/openshell-server/migrations/postgres/009_config_update_operations.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS sandbox_config_fences ( + sandbox_id TEXT PRIMARY KEY REFERENCES objects(id) ON DELETE CASCADE +); + +ALTER TABLE objects + ADD COLUMN IF NOT EXISTS next_attempt_at_ms BIGINT; + +UPDATE objects +SET next_attempt_at_ms = updated_at_ms +WHERE object_type = 'config_update_operation' + AND next_attempt_at_ms IS NULL; + +CREATE INDEX IF NOT EXISTS objects_type_status_due_idx + ON objects (object_type, status, next_attempt_at_ms, id); diff --git a/crates/openshell-server/migrations/sqlite/009_config_update_operations.sql b/crates/openshell-server/migrations/sqlite/009_config_update_operations.sql new file mode 100644 index 0000000000..3d10614058 --- /dev/null +++ b/crates/openshell-server/migrations/sqlite/009_config_update_operations.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS sandbox_config_fences ( + sandbox_id TEXT PRIMARY KEY REFERENCES objects(id) ON DELETE CASCADE +); + +ALTER TABLE objects + ADD COLUMN next_attempt_at_ms INTEGER; + +UPDATE objects +SET next_attempt_at_ms = updated_at_ms +WHERE object_type = 'config_update_operation' + AND next_attempt_at_ms IS NULL; + +CREATE INDEX IF NOT EXISTS objects_type_status_due_idx + ON objects (object_type, status, next_attempt_at_ms, id); diff --git a/crates/openshell-server/src/config_delivery.rs b/crates/openshell-server/src/config_delivery.rs index 3eb33c0ba1..6464511a86 100644 --- a/crates/openshell-server/src/config_delivery.rs +++ b/crates/openshell-server/src/config_delivery.rs @@ -26,11 +26,6 @@ use crate::supervisor_session::SupervisorSessionRegistry; /// future envelope fields. pub const MAX_SUPERVISOR_CONFIG_MESSAGE_BYTES: usize = 3 * 1024 * 1024; const CONFIG_SNAPSHOT_BUILD_TIMEOUT: Duration = Duration::from_secs(45); -// Stage 1 bootstrap is optional. Keep credential backend stalls well below -// the 15-second relay session-wait budget while polling remains authoritative. -pub const OPTIONAL_CONFIG_BOOTSTRAP_BUILD_TIMEOUT: Duration = Duration::from_secs(1); -// Stage 2 supervisors apply the bootstrap directly, so allow the same bounded -// build window as an ordinary complete snapshot before rejecting the session. pub const REQUIRED_CONFIG_BOOTSTRAP_BUILD_TIMEOUT: Duration = CONFIG_SNAPSHOT_BUILD_TIMEOUT; const MAX_ACTIVE_FANOUT_WORKERS: usize = 64; /// Concurrent snapshot builds allowed per pooled database connection. Builds @@ -88,6 +83,7 @@ pub trait SupervisorConfigRouter: fmt::Debug + Send + Sync { &self, sandbox_id: &str, message: SupervisorConfigMessage, + require_acknowledgement: bool, ) -> DeliveryDisposition; async fn routable_sandbox_ids(&self) -> Vec; @@ -111,8 +107,10 @@ impl SupervisorConfigRouter for LocalSupervisorConfigRouter { &self, sandbox_id: &str, message: SupervisorConfigMessage, + require_acknowledgement: bool, ) -> DeliveryDisposition { - self.sessions.deliver_config(sandbox_id, message) + self.sessions + .deliver_config(sandbox_id, message, require_acknowledgement) } async fn routable_sandbox_ids(&self) -> Vec { @@ -569,7 +567,7 @@ async fn enqueue_sandbox_from_fanout( async fn publish_sandbox_component_now(state: &Arc, key: &DeliveryKey) { let component = key.component.name(); - let build = async { + let build = Box::pin(async { let sandbox = state .store .get_message::(&key.sandbox_id) @@ -579,23 +577,39 @@ async fn publish_sandbox_component_now(state: &Arc, key: &DeliveryK return Ok(None); }; match key.component { - ConfigComponentKind::SandboxConfig => build_sandbox_config_snapshot(state, &sandbox) - .await - .map(|snapshot| SupervisorConfigMessage::SandboxConfig(Box::new(snapshot))), + ConfigComponentKind::SandboxConfig => { + let snapshot = build_sandbox_config_snapshot(state, &sandbox).await?; + let requires_acknowledgement = + crate::config_update_operation::associate_pending_with_snapshot( + state, + &key.sandbox_id, + &snapshot, + ) + .await?; + Ok(( + SupervisorConfigMessage::SandboxConfig(Box::new(snapshot)), + requires_acknowledgement, + )) + } ConfigComponentKind::ProviderEnvironment => { build_provider_environment_snapshot(state, &sandbox, true) .await - .map(SupervisorConfigMessage::ProviderEnvironment) + .map(|snapshot| { + ( + SupervisorConfigMessage::ProviderEnvironment(snapshot), + false, + ) + }) } } .map(Some) - }; + }); match state.config_delivery_queue.run_bounded_build(build).await { Ok(Ok(None)) => {} - Ok(Ok(Some(message))) => { + Ok(Ok(Some((message, requires_acknowledgement)))) => { let disposition = state .supervisor_config_router() - .deliver(&key.sandbox_id, message) + .deliver(&key.sandbox_id, message, requires_acknowledgement) .await; record_delivery(component, disposition); } @@ -879,6 +893,7 @@ mod tests { &self, sandbox_id: &str, _message: SupervisorConfigMessage, + _require_acknowledgement: bool, ) -> DeliveryDisposition { self.visits.send(sandbox_id.to_string()).unwrap(); assert!( @@ -1326,85 +1341,4 @@ mod tests { .provider_env_revision = 7; assert!(bootstrap_revisions_match(&bootstrap)); } - - #[tokio::test] - async fn stalled_credentials_do_not_block_session_acceptance() { - use openshell_core::proto::{CredentialHandle, Provider}; - - let state = test_server_state().await; - state - .store - .put_message(&Provider { - metadata: Some(ObjectMeta { - id: "provider".into(), - name: "provider".into(), - workspace: "default".into(), - ..Default::default() - }), - r#type: "github".into(), - credential_handles: HashMap::from([( - "GITHUB_TOKEN".into(), - CredentialHandle { - driver: "test-static".into(), - handle: "blocked".into(), - ..Default::default() - }, - )]), - ..Default::default() - }) - .await - .unwrap(); - state - .store - .put_message(&Sandbox { - metadata: Some(ObjectMeta { - id: "sandbox".into(), - name: "sandbox".into(), - workspace: "default".into(), - ..Default::default() - }), - spec: Some(SandboxSpec { - providers: vec!["provider".into()], - ..Default::default() - }), - ..Default::default() - }) - .await - .unwrap(); - let (resolve_hit, _release_resolve) = state.credentials.gate_next_resolve(); - tokio::time::timeout(Duration::from_secs(10), async { - let connect = connect_supervisor_stream( - &state, - "sandbox", - openshell_core::proto::PREVIOUS_SUPERVISOR_PROTOCOL_REVISION, - ); - let (response, hit) = tokio::join!(connect, resolve_hit); - hit.expect("bootstrap must reach the stalled credential driver"); - let mut harness = response.unwrap(); - let first = harness.inbound.message().await.unwrap().unwrap(); - let Some(gateway_message::Payload::SessionAccepted(accepted)) = first.payload else { - panic!("expected session acceptance"); - }; - assert!(accepted.bootstrap.is_none()); - assert!( - state - .supervisor_sessions - .is_current_session("sandbox", &accepted.session_id) - ); - // Relay control remains usable while credential resolution is stalled. - let (_, relay) = state - .supervisor_sessions - .open_relay("sandbox", Duration::from_secs(1)) - .await - .unwrap(); - let message = harness.inbound.message().await.unwrap().unwrap(); - assert!(matches!( - message.payload, - Some(gateway_message::Payload::RelayOpen(_)) - )); - drop(relay); - }) - .await - .expect("optional bootstrap must not consume the relay reconnect budget"); - } } diff --git a/crates/openshell-server/src/config_update_operation.rs b/crates/openshell-server/src/config_update_operation.rs index 6e4b13a7a0..c6d45337f3 100644 --- a/crates/openshell-server/src/config_update_operation.rs +++ b/crates/openshell-server/src/config_update_operation.rs @@ -1,33 +1,151 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Durable, exact-target configuration operations, independent of delivery transport. -//! -//! Provider receipts project the common operation resource. Live installation -//! evidence remains session-bound; an applied operation records historical -//! completion and never substitutes for a fresh provider readiness evaluation. - -#![allow(clippy::result_large_err)] // Internal operation helpers preserve gRPC error details. +//! Durable completion tracking for sandbox-scoped desired-state updates. use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use futures::{StreamExt as _, stream}; +use metrics::{counter, gauge, histogram}; +use openshell_core::ObjectId; use openshell_core::proto::{ - ConfigApplyOutcome, ConfigComponent, ConfigSnapshotRevision, ConfigUpdateOperation, - ConfigUpdateOperationState, ObjectMeta, ProviderMutationKind, ProviderMutationReceipt, - ProviderReadinessReason, ProviderReadinessState, ProviderReadinessStatus, - config_snapshot_revision, + ConfigApplyOutcome, ConfigComponent, ConfigComponentApplyResult, ConfigSnapshotRevision, + ConfigUpdateOperation, ConfigUpdateOperationState, ObjectMeta, Sandbox, SandboxConfigRevision, + SandboxPhase, UpdateConfigResponse, config_snapshot_revision, }; -use openshell_core::rpc_error::{self, ErrorDetails, StatusExt}; -use prost::Message; -use sha2::{Digest, Sha256}; -use tonic::{Code, Status}; +use tonic::Status; +use tracing::{debug, info, warn}; +use uuid::Uuid; -use crate::persistence::{ObjectRecord, ObjectType, PersistenceError, Store, WriteCondition}; +use crate::ServerState; +use crate::persistence::{KnownVersionUpdate, ObjectType, current_time_ms}; use crate::storage_proto::StoredConfigUpdateOperation; -/// Object-store namespace shared by configuration completion resources. +pub use crate::provider_config_operation::{ + get_provider_operation, observe_provider_status, record_provider_operation, +}; + +fn timestamp(ms: i64) -> prost_types::Timestamp { + openshell_core::time::timestamp_from_millis(ms).expect("system clock fits protobuf timestamp") +} + pub const CONFIG_UPDATE_OPERATION_OBJECT_TYPE: &str = "config_update_operation"; +const OPERATION_SCAN_PAGE_SIZE: u32 = 250; const MAX_TRANSITION_RETRIES: usize = 8; +const DEFAULT_WAIT_TIMEOUT: Duration = Duration::from_mins(1); +const MAX_WAIT_TIMEOUT: Duration = Duration::from_hours(1); +const MAX_SANITIZED_ERROR_BYTES: usize = 1_024; +const OPERATION_DIMENSION_ANNOTATION: &str = "openshell.nvidia.com/config-operation-dimension"; +const REQUEST_FINGERPRINT_ANNOTATION: &str = "openshell.nvidia.com/request-fingerprint"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OperationDimension { + Policy, + Settings, +} + +impl OperationDimension { + const fn as_str(self) -> &'static str { + match self { + Self::Policy => "policy", + Self::Settings => "settings", + } + } +} + +/// Gateway-local wakeups for callers waiting on one durable operation. +#[derive(Debug, Clone)] +pub struct OperationWatchBus { + inner: Arc>>>, +} + +struct OperationSubscription { + bus: OperationWatchBus, + operation_id: String, + sender: tokio::sync::broadcast::Sender<()>, + receiver: Option>, +} + +impl OperationSubscription { + async fn recv(&mut self) -> Result<(), tokio::sync::broadcast::error::RecvError> { + self.receiver + .as_mut() + .expect("operation subscription receiver is available") + .recv() + .await + } +} + +impl Drop for OperationSubscription { + fn drop(&mut self) { + // Receiver fields normally drop after this method returns. Drop ours + // first so concurrent teardown observes the actual remaining waiter + // count while deciding whether to remove the channel. + drop(self.receiver.take()); + let mut inner = self + .bus + .inner + .lock() + .expect("operation watch bus lock poisoned"); + let remove = inner.get(&self.operation_id).is_some_and(|current| { + current.same_channel(&self.sender) && self.sender.receiver_count() == 0 + }); + if remove { + inner.remove(&self.operation_id); + } + } +} + +impl OperationWatchBus { + #[must_use] + pub fn new() -> Self { + Self { + inner: Arc::new(Mutex::new(HashMap::new())), + } + } + + fn sender_for(&self, operation_id: &str) -> tokio::sync::broadcast::Sender<()> { + let mut inner = self + .inner + .lock() + .expect("operation watch bus lock poisoned"); + inner + .entry(operation_id.to_string()) + .or_insert_with(|| tokio::sync::broadcast::channel(16).0) + .clone() + } + + fn subscribe(&self, operation_id: &str) -> OperationSubscription { + let sender = self.sender_for(operation_id); + OperationSubscription { + bus: self.clone(), + operation_id: operation_id.to_string(), + receiver: Some(sender.subscribe()), + sender, + } + } + + pub fn notify(&self, operation_id: &str) { + let sender = self + .inner + .lock() + .expect("operation watch bus lock poisoned") + .remove(operation_id); + if let Some(sender) = sender { + let _ = sender.send(()); + } + } + + #[cfg(test)] + fn len(&self) -> usize { + self.inner + .lock() + .expect("operation watch bus lock poisoned") + .len() + } +} impl ObjectType for StoredConfigUpdateOperation { fn object_type() -> &'static str { @@ -35,256 +153,238 @@ impl ObjectType for StoredConfigUpdateOperation { } } -/// Provider-specific view of one common configuration operation. -#[derive(Clone, Debug)] -pub struct ProviderOperation { - /// Immutable desired authority captured for this operation. - pub(crate) receipt: ProviderMutationReceipt, - /// A failed initial snapshot is never reconstructed into another target. - pub(crate) snapshot_reason: ProviderReadinessReason, - /// Durable historical outcome; callers must separately evaluate live readiness. - pub(crate) operation: ConfigUpdateOperation, -} - -fn storage_unavailable() -> Status { - // A provider mutation can already have committed when operation persistence - // fails. No retry hint is attached: repeating the mutation is not proven safe. - Status::with_error_details( - Code::Unavailable, - "configuration operation storage unavailable; the saved mutation may remain in effect", - ErrorDetails::with_error_info( - "CONFIG_OPERATION_STORAGE_UNCERTAIN", - rpc_error::ERROR_DOMAIN, - HashMap::new(), - ), - ) +#[derive(Debug, Clone, Copy)] +pub struct OperationTarget { + pub policy_version: u32, + pub settings_revision: u64, } -fn invalid_record() -> Status { - Status::with_error_details( - Code::Internal, - "configuration operation identity is inconsistent", - ErrorDetails::with_error_info( - "CONFIG_OPERATION_INVALID", - rpc_error::ERROR_DOMAIN, - HashMap::new(), - ), - ) +#[derive(Debug, Clone, Default)] +pub struct CommittedResponse { + pub policy_version: u32, + pub policy_hash: String, + pub settings_revision: u64, + pub deleted: bool, + pub annotations: HashMap, } -fn persisted_time(receipt: &ProviderMutationReceipt) -> Result { - // Receipt identity includes the full canonical timestamp. Absence is not - // the Unix epoch, and reducing nanos to milliseconds would merge identities. - let timestamp = receipt.persisted_time.ok_or_else(invalid_record)?; - openshell_core::time::validate_timestamp(×tamp).map_err(|_| invalid_record())?; - Ok(timestamp) -} - -fn observation_id( - receipt: &ProviderMutationReceipt, - snapshot_reason: ProviderReadinessReason, -) -> Result { - let desired = receipt.desired.as_ref().ok_or_else(invalid_record)?; - let mut digest = Sha256::new(); - digest.update(b"openshell/provider-observation/v1\0"); - let target_bytes = desired.encode_to_vec(); - // The target contains only public identity and opaque revision fields. Its - // protobuf has no maps, so encoding is canonical. Length framing prevents - // component concatenation ambiguities across workspaces and provider names. - for component in [ - receipt.workspace.as_bytes(), - receipt.provider.as_bytes(), - target_bytes.as_slice(), - ] { - let length = u64::try_from(component.len()).map_err(|_| invalid_record())?; - digest.update(length.to_be_bytes()); - digest.update(component); - } - // A failed capture and its later successful repair are different targets; - // the original failed operation must remain immutable. - digest.update((snapshot_reason as i32).to_be_bytes()); - let mut bytes = [0_u8; 16]; - for (byte, hashed) in bytes.iter_mut().zip(digest.finalize()) { - *byte = hashed; - } - Ok(uuid::Builder::from_custom_bytes(bytes) - .into_uuid() - .to_string()) -} - -/// Record an exact provider target in the shared configuration-operation store. -/// -/// The caller captures the snapshot only after its provider mutation finishes. -/// This write does not roll back a preceding mutation on failure. An incomplete -/// snapshot is persisted as failed, retaining its original non-secret reason. -/// Observation-only requests reuse the original receipt for the same complete -/// target; source mutations retain their distinct caller-created receipt IDs. -pub async fn record_provider_operation( - store: &Store, - mut receipt: ProviderMutationReceipt, - snapshot_reason: ProviderReadinessReason, -) -> Result { - let observation = receipt.kind == ProviderMutationKind::Observe as i32; - if observation { - receipt.receipt_id = observation_id(&receipt, snapshot_reason)?; - } - let desired = receipt.desired.as_ref().ok_or_else(invalid_record)?; - if receipt.receipt_id.is_empty() - || receipt.workspace.is_empty() - || desired.sandbox_id.is_empty() - { - return Err(invalid_record()); - } - let persisted_time = persisted_time(&receipt)?; - let failed = snapshot_reason != ProviderReadinessReason::Unspecified; - let operation = ConfigUpdateOperation { - operation_id: receipt.receipt_id.clone(), - sandbox_id: desired.sandbox_id.clone(), - component: ConfigComponent::ProviderEnvironment.into(), - target_revision: Some(ConfigSnapshotRevision { - component: Some(config_snapshot_revision::Component::ProviderTarget( - desired.clone(), - )), - }), - state: if failed { - ConfigUpdateOperationState::Failed.into() - } else { - ConfigUpdateOperationState::Pending.into() - }, - outcome: if failed { - ConfigApplyOutcome::FailedClosed.into() - } else { - ConfigApplyOutcome::Unspecified.into() - }, - sanitized_error: if failed { - snapshot_reason.as_str_name().to_string() - } else { - String::new() - }, - created_time: Some(persisted_time), - updated_time: Some(persisted_time), - completed_time: failed.then_some(persisted_time), - }; - let stored = StoredConfigUpdateOperation { +pub fn operation_name(sandbox_id: &str, idempotency_key: &str, operation_id: &str) -> String { + if idempotency_key.is_empty() { + operation_id.to_string() + } else { + format!("{sandbox_id}:{idempotency_key}") + } +} + +fn initial_state(phase: SandboxPhase) -> ConfigUpdateOperationState { + match phase { + SandboxPhase::Stopped | SandboxPhase::Completed => ConfigUpdateOperationState::Inactive, + SandboxPhase::Deleting => ConfigUpdateOperationState::Cancelled, + SandboxPhase::Unspecified + | SandboxPhase::Provisioning + | SandboxPhase::Ready + | SandboxPhase::Error + | SandboxPhase::Unknown + | SandboxPhase::Stopping + | SandboxPhase::Starting => ConfigUpdateOperationState::Pending, + } +} + +pub fn sandbox_phase(sandbox: &Sandbox) -> SandboxPhase { + sandbox + .status + .as_ref() + .and_then(|status| SandboxPhase::try_from(status.phase).ok()) + .unwrap_or_default() +} + +pub fn new_record( + sandbox: &Sandbox, + workspace: &str, + idempotency_key: &str, + dimension: OperationDimension, + request_fingerprint: Option<&str>, + target: OperationTarget, + response: CommittedResponse, +) -> StoredConfigUpdateOperation { + let operation_id = Uuid::new_v4().to_string(); + let now = current_time_ms(); + let phase = sandbox_phase(sandbox); + let state = initial_state(phase); + let completed_time = terminal(state).then(|| timestamp(now)); + StoredConfigUpdateOperation { metadata: Some(ObjectMeta { - id: receipt.receipt_id.clone(), - name: receipt.receipt_id.clone(), - workspace: receipt.workspace.clone(), - created_time: Some(persisted_time), + id: operation_id.clone(), + name: operation_name(sandbox.object_id(), idempotency_key, &operation_id), + created_time: Some(timestamp(now)), + workspace: workspace.to_string(), + annotations: std::iter::once(( + OPERATION_DIMENSION_ANNOTATION.to_string(), + dimension.as_str().to_string(), + )) + .chain( + request_fingerprint + .filter(|_| !idempotency_key.is_empty()) + .map(|fingerprint| { + ( + REQUEST_FINGERPRINT_ANNOTATION.to_string(), + fingerprint.to_string(), + ) + }), + ) + .collect(), ..Default::default() }), - operation: Some(operation), - provider_receipt: Some(receipt.clone()), - provider_snapshot_reason: snapshot_reason.into(), + operation: Some(ConfigUpdateOperation { + operation_id, + sandbox_id: sandbox.object_id().to_string(), + component: ConfigComponent::SandboxConfig.into(), + target_revision: None, + state: state.into(), + outcome: ConfigApplyOutcome::Unspecified.into(), + sanitized_error: String::new(), + created_time: Some(timestamp(now)), + updated_time: Some(timestamp(now)), + completed_time, + }), + target_policy_version: target.policy_version, + target_settings_revision: target.settings_revision, + initial_phase: phase.into(), + idempotency_key: idempotency_key.to_string(), + attempt_count: 0, + next_attempt_time: Some(timestamp(now)), + response_policy_version: response.policy_version, + response_policy_hash: response.policy_hash, + response_settings_revision: response.settings_revision, + response_deleted: response.deleted, + response_annotations: response.annotations, ..Default::default() - }; - let result = store - .put_if( - CONFIG_UPDATE_OPERATION_OBJECT_TYPE, - &receipt.receipt_id, - &receipt.receipt_id, - &receipt.workspace, - &stored.encode_to_vec(), - None, - WriteCondition::MustCreate, - ) - .await; - match result { - Ok(_) => Ok(receipt), - Err(PersistenceError::UniqueViolation { .. }) if observation => { - // Concurrent observers race only on the insert. A conflict never - // updates the winner's timestamp, mutation identity, or outcome. - // Exact comparison also fails closed on an ID collision/corruption. - let existing = - get_provider_operation(store, &receipt.receipt_id, &receipt.workspace).await?; - if existing.receipt.kind != receipt.kind - || existing.receipt.provider != receipt.provider - || existing.receipt.desired != receipt.desired - || existing.snapshot_reason != snapshot_reason - { - return Err(invalid_record()); - } - Ok(existing.receipt) - } - Err(_) => Err(storage_unavailable()), } } -// The record ID, receipt ID, and operation ID deliberately name the same -// durable identity; their distinct schema field names must compare equal. -#[allow(clippy::suspicious_operation_groupings)] -fn decode_provider_operation( - record: &ObjectRecord, -) -> Result<(StoredConfigUpdateOperation, ProviderOperation), Status> { - let stored = StoredConfigUpdateOperation::decode(record.payload.as_slice()) - .map_err(|_| invalid_record())?; - let receipt = stored - .provider_receipt - .as_ref() - .ok_or_else(invalid_record)?; - let operation = stored.operation.as_ref().ok_or_else(invalid_record)?; - let metadata = stored.metadata.as_ref().ok_or_else(invalid_record)?; - let desired = receipt.desired.as_ref().ok_or_else(invalid_record)?; - let persisted_time = persisted_time(receipt)?; - let snapshot_reason = ProviderReadinessReason::try_from(stored.provider_snapshot_reason) - .map_err(|_| invalid_record())?; - if record.id != receipt.receipt_id - || record.workspace != receipt.workspace - || metadata.id != record.id - || metadata.workspace != record.workspace - || operation.operation_id != record.id - || operation.sandbox_id != desired.sandbox_id - || operation.created_time != Some(persisted_time) - || metadata.created_time != Some(persisted_time) - || operation.component != ConfigComponent::ProviderEnvironment as i32 - || operation - .target_revision +pub async fn find_idempotent( + state: &ServerState, + workspace: &str, + sandbox_id: &str, + idempotency_key: &str, + request_fingerprint: &str, +) -> Result, Status> { + if idempotency_key.is_empty() { + return Ok(None); + } + let existing = state + .store + .get_message_by_name::( + workspace, + &operation_name(sandbox_id, idempotency_key, ""), + ) + .await + .map_err(|error| Status::internal(format!("fetch update operation failed: {error}")))?; + if let Some(record) = existing.as_ref() { + let stored_fingerprint = record + .metadata .as_ref() - .and_then(|revision| revision.component.as_ref()) - != Some(&config_snapshot_revision::Component::ProviderTarget( - desired.clone(), - )) - || ConfigUpdateOperationState::try_from(operation.state).is_err() - { - return Err(invalid_record()); + .and_then(|metadata| metadata.annotations.get(REQUEST_FINGERPRINT_ANNOTATION)); + if stored_fingerprint.is_none_or(|stored| stored != request_fingerprint) { + return Err(Status::invalid_argument( + "idempotency_key was already used with a different request payload", + )); + } } - let provider = ProviderOperation { - receipt: receipt.clone(), - snapshot_reason, - operation: operation.clone(), - }; - Ok((stored, provider)) + Ok(existing) } -async fn load_provider_operation( - store: &Store, +pub async fn get_record( + state: &ServerState, operation_id: &str, - workspace: &str, -) -> Result<(ObjectRecord, StoredConfigUpdateOperation, ProviderOperation), Status> { - let record = store - .get(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, operation_id) +) -> Result, Status> { + state + .store + .get_message::(operation_id) .await - .map_err(|_| storage_unavailable())? - .filter(|record| record.workspace == workspace) - .ok_or_else(|| Status::not_found("provider operation not found"))?; - let (stored, provider) = decode_provider_operation(&record)?; - Ok((record, stored, provider)) -} - -/// Read a provider projection after the RPC has authorized the workspace. -/// -/// Looking up an operation from a different workspace returns the same result -/// as a missing operation and never reveals its receipt or desired target. -pub async fn get_provider_operation( - store: &Store, - operation_id: &str, - workspace: &str, -) -> Result { - let (_, _, provider) = load_provider_operation(store, operation_id, workspace).await?; - Ok(provider) + .map_err(|error| Status::internal(format!("fetch update operation failed: {error}"))) +} + +/// Rebuild operation query columns from protobuf payloads before selective +/// reconciliation starts. Re-running after an interrupted startup is safe. +pub async fn repair_query_projections(state: &ServerState) -> Result<(), Status> { + let mut offset = 0; + let mut repaired = 0_u64; + loop { + let records = state + .store + .list_all_messages::(OPERATION_SCAN_PAGE_SIZE, offset) + .await + .map_err(|error| { + Status::internal(format!("list update operations for repair failed: {error}")) + })?; + let page_len = records.len(); + for mut record in records { + if record.provider_receipt.is_some() { + continue; + } + for _ in 0..MAX_TRANSITION_RETRIES { + let Some(metadata) = record.metadata.as_ref() else { + return Err(Status::internal("update operation metadata missing")); + }; + let operation_id = metadata.id.clone(); + let resource_version = metadata.resource_version; + if state + .store + .repair_config_operation_projection(&record, resource_version) + .await + .map_err(|error| { + Status::internal(format!( + "repair update operation projection failed: {error}" + )) + })? + { + repaired = repaired.saturating_add(1); + break; + } + let Some(current) = get_record(state, &operation_id).await? else { + break; + }; + record = current; + } + } + if page_len < OPERATION_SCAN_PAGE_SIZE as usize { + break; + } + offset = offset.saturating_add(OPERATION_SCAN_PAGE_SIZE); + } + if repaired > 0 { + info!( + repaired, + "configuration update operation projection repair complete" + ); + } + Ok(()) +} + +pub fn public_operation( + record: &StoredConfigUpdateOperation, +) -> Result { + record + .operation + .clone() + .ok_or_else(|| Status::internal("stored update operation payload missing")) } -fn terminal(state: ConfigUpdateOperationState) -> bool { +pub fn response_from_record( + record: &StoredConfigUpdateOperation, +) -> Result { + Ok(UpdateConfigResponse { + version: record.response_policy_version, + policy_hash: record.response_policy_hash.clone(), + settings_revision: record.response_settings_revision, + deleted: record.response_deleted, + annotations: record.response_annotations.clone(), + operation: Some(public_operation(record)?), + }) +} + +pub fn terminal(state: ConfigUpdateOperationState) -> bool { matches!( state, ConfigUpdateOperationState::Applied @@ -295,469 +395,864 @@ fn terminal(state: ConfigUpdateOperationState) -> bool { ) } -fn completion( - status: &ProviderReadinessStatus, -) -> Result, Status> { - match ProviderReadinessState::try_from(status.state).map_err(|_| invalid_record())? { - ProviderReadinessState::Ready | ProviderReadinessState::Revoked => { - let desired = status - .receipt - .as_ref() - .and_then(|receipt| receipt.desired.as_ref()) - .ok_or_else(invalid_record)?; - let observed = status.observed.as_ref().ok_or_else(invalid_record)?; - // The RPC validates current session ownership and freshness. Check - // the complete target again before converting its result into a - // durable terminal transition; a partial install is never applied. - if status.reason != ProviderReadinessReason::Unspecified as i32 - || observed.reason != ProviderReadinessReason::Unspecified as i32 - || observed.attachment_epoch != desired.attachment_epoch - || observed.provider_env_revision != desired.provider_env_revision - || observed.config_revision != desired.config_revision - || observed.policy_hash != desired.policy_hash - || !observed.credentials_installed - || !observed.policy_active - || !observed.launch_environment_installed - || observed.process_instance_id.is_empty() - || observed.session_id.is_empty() - || status.network_instance_id.is_empty() - || (status.state == ProviderReadinessState::Revoked as i32) - != desired.provider_id.is_empty() - { - return Err(invalid_record()); +fn sanitize_error(value: &str) -> String { + let mut end = value.len().min(MAX_SANITIZED_ERROR_BYTES); + while !value.is_char_boundary(end) { + end = end.saturating_sub(1); + } + value[..end].to_string() +} + +async fn mutate_record( + state: &ServerState, + operation_id: &str, + mut mutate: F, +) -> Result, Status> +where + F: FnMut(&mut StoredConfigUpdateOperation) -> bool, +{ + for _ in 0..MAX_TRANSITION_RETRIES { + let Some(current) = get_record(state, operation_id).await? else { + return Ok(None); + }; + let version = current + .metadata + .as_ref() + .map_or(0, |metadata| metadata.resource_version); + let mut candidate = current.clone(); + let changed = mutate(&mut candidate); + if !changed { + return Ok(Some((current, false))); + } + let updated = state + .store + .update_config_operation_cas(&candidate, version) + .await; + match updated { + Ok(KnownVersionUpdate::Changed(updated)) => return Ok(Some((updated, true))), + Ok(KnownVersionUpdate::Conflict) => {} + Err(error) => { + return Err(Status::internal(format!( + "persist update operation transition failed: {error}" + ))); } - Ok(Some(( - ConfigUpdateOperationState::Applied, - ConfigApplyOutcome::Applied, - ))) } - ProviderReadinessState::Superseded => Ok(Some(( - ConfigUpdateOperationState::Superseded, - ConfigApplyOutcome::IgnoredStale, - ))), - // Live installation failures can recover without changing the desired - // revision. They remain visible in the provider projection but do not - // terminate the operation or authorize a retry of the source mutation. - _ => Ok(None), - } -} - -/// Persist exact completion by CAS and attach its historical resource to a view. -/// -/// Call only after evaluating authenticated current-session evidence. The live -/// status is never changed by this function: expired or superseded evidence -/// cannot become ready because an earlier observation was durably applied. -pub async fn observe_provider_status( - store: &Store, - status: &mut ProviderReadinessStatus, + } + Err(Status::aborted( + "update operation changed concurrently; retry the operation query", + )) +} + +async fn finish( + state: &ServerState, + operation_id: &str, + terminal_state: ConfigUpdateOperationState, + outcome: ConfigApplyOutcome, + error: &str, ) -> Result<(), Status> { - let receipt = status.receipt.as_ref().ok_or_else(invalid_record)?.clone(); - let completion = completion(status)?; - for _ in 0..MAX_TRANSITION_RETRIES { - let (record, mut stored, provider) = - load_provider_operation(store, &receipt.receipt_id, &receipt.workspace).await?; - if provider.receipt != receipt { - return Err(invalid_record()); + let now = current_time_ms(); + let transition = mutate_record(state, operation_id, |record| { + let Some(operation) = record.operation.as_mut() else { + return false; + }; + if ConfigUpdateOperationState::try_from(operation.state) + != Ok(ConfigUpdateOperationState::Pending) + { + return false; } - let state = ConfigUpdateOperationState::try_from(provider.operation.state) - .map_err(|_| invalid_record())?; - let Some((terminal_state, outcome)) = completion.filter(|_| !terminal(state)) else { - status.operation = Some(provider.operation); - return Ok(()); + operation.state = terminal_state.into(); + operation.outcome = outcome.into(); + operation.sanitized_error = sanitize_error(error); + operation.updated_time = Some(timestamp(now)); + operation.completed_time = Some(timestamp(now)); + true + }) + .await?; + record_terminal_transition(state, transition, terminal_state); + Ok(()) +} + +async fn finish_if_target_matches( + state: &ServerState, + operation_id: &str, + requested_revision: &ConfigSnapshotRevision, + terminal_state: ConfigUpdateOperationState, + outcome: ConfigApplyOutcome, + error: &str, +) -> Result<(), Status> { + let now = current_time_ms(); + let transition = mutate_record(state, operation_id, |record| { + let Some(operation) = record.operation.as_mut() else { + return false; }; - // Capturing the desired snapshot failed permanently for this operation. - // A later read must not fabricate a different, successful target. - if provider.snapshot_reason != ProviderReadinessReason::Unspecified { - return Err(invalid_record()); + if ConfigUpdateOperationState::try_from(operation.state) + != Ok(ConfigUpdateOperationState::Pending) + || operation.target_revision.as_ref() != Some(requested_revision) + { + return false; } - let operation = stored.operation.as_mut().ok_or_else(invalid_record)?; operation.state = terminal_state.into(); operation.outcome = outcome.into(); - operation.sanitized_error = if terminal_state == ConfigUpdateOperationState::Superseded { - ProviderReadinessReason::DesiredStateChanged - .as_str_name() - .to_string() - } else { - String::new() + operation.sanitized_error = sanitize_error(error); + operation.updated_time = Some(timestamp(now)); + operation.completed_time = Some(timestamp(now)); + true + }) + .await?; + record_terminal_transition(state, transition, terminal_state); + Ok(()) +} + +fn record_terminal_transition( + state: &ServerState, + transition: Option<(StoredConfigUpdateOperation, bool)>, + terminal_state: ConfigUpdateOperationState, +) { + if let Some((record, true)) = transition { + counter!( + "openshell_config_update_operations_terminal_total", + "state" => terminal_state.as_str_name() + ) + .increment(1); + if let Some(operation) = record.operation.as_ref() { + state + .config_update_operation_watch_bus + .notify(&operation.operation_id); + state.sandbox_watch_bus.notify(&operation.sandbox_id); + } + } +} + +fn snapshot_revision( + snapshot: &openshell_core::proto::SandboxConfigSnapshot, +) -> ConfigSnapshotRevision { + ConfigSnapshotRevision { + component: Some(config_snapshot_revision::Component::SandboxConfig( + SandboxConfigRevision { + config_revision: snapshot.config_revision, + policy_version: snapshot.version, + policy_source: snapshot.policy_source, + global_policy_version: snapshot.global_policy_version, + settings_revision: snapshot.settings_revision, + }, + )), + } +} + +fn target_relation( + record: &StoredConfigUpdateOperation, + snapshot: &openshell_core::proto::SandboxConfigSnapshot, +) -> std::cmp::Ordering { + match operation_dimension(record) { + OperationDimension::Policy => snapshot.version.cmp(&record.target_policy_version), + OperationDimension::Settings => snapshot + .settings_revision + .cmp(&record.target_settings_revision), + } +} + +fn operation_dimension(record: &StoredConfigUpdateOperation) -> OperationDimension { + match record + .metadata + .as_ref() + .and_then(|metadata| metadata.annotations.get(OPERATION_DIMENSION_ANNOTATION)) + .map(String::as_str) + { + Some("policy") => OperationDimension::Policy, + Some("settings") => OperationDimension::Settings, + _ if record.response_policy_version != 0 => OperationDimension::Policy, + _ => OperationDimension::Settings, + } +} + +pub async fn reconcile_one(state: &Arc, operation_id: &str) -> Result<(), Status> { + let Some(record) = get_record(state, operation_id).await? else { + return Ok(()); + }; + reconcile_records_for_sandbox(state, vec![record]).await +} + +async fn reconcile_records_for_sandbox( + state: &Arc, + records: Vec, +) -> Result<(), Status> { + let Some(first_operation) = records.first().and_then(|record| record.operation.as_ref()) else { + return Ok(()); + }; + let sandbox_id = first_operation.sandbox_id.clone(); + + let Some(sandbox) = state + .store + .get_message::(&sandbox_id) + .await + .map_err(|error| Status::internal(format!("fetch operation sandbox failed: {error}")))? + else { + for record in records { + if let Some(operation) = record.operation.as_ref() { + finish( + state, + &operation.operation_id, + ConfigUpdateOperationState::Cancelled, + ConfigApplyOutcome::Unspecified, + "sandbox no longer exists", + ) + .await?; + } + } + return Ok(()); + }; + + match initial_state(sandbox_phase(&sandbox)) { + ConfigUpdateOperationState::Inactive => { + for record in records { + if let Some(operation) = record.operation.as_ref() { + finish( + state, + &operation.operation_id, + ConfigUpdateOperationState::Inactive, + ConfigApplyOutcome::Unspecified, + "", + ) + .await?; + } + } + return Ok(()); + } + ConfigUpdateOperationState::Cancelled => { + for record in records { + if let Some(operation) = record.operation.as_ref() { + finish( + state, + &operation.operation_id, + ConfigUpdateOperationState::Cancelled, + ConfigApplyOutcome::Unspecified, + "sandbox is deleting", + ) + .await?; + } + } + return Ok(()); + } + _ => {} + } + + let now = current_time_ms(); + let mut claimed_records = Vec::new(); + for record in records { + let operation = public_operation(&record)?; + let operation_state = + ConfigUpdateOperationState::try_from(operation.state).unwrap_or_default(); + if terminal(operation_state) { + continue; + } + let claimed = mutate_record(state, &operation.operation_id, |stored| { + let next_attempt_at_ms = stored.next_attempt_at_ms(); + let Some(operation) = stored.operation.as_mut() else { + return false; + }; + if ConfigUpdateOperationState::try_from(operation.state) + != Ok(ConfigUpdateOperationState::Pending) + || next_attempt_at_ms > now + { + return false; + } + operation.updated_time = Some(timestamp(now)); + stored.attempt_count = stored.attempt_count.saturating_add(1); + let exponent = stored.attempt_count.min(8); + let delay_ms = 250_i64.saturating_mul(1_i64 << exponent).min(30_000); + stored.next_attempt_time = Some(timestamp(now.saturating_add(delay_ms))); + true + }) + .await?; + if let Some((claimed, true)) = claimed { + claimed_records.push(claimed); + } + } + if claimed_records.is_empty() { + return Ok(()); + } + + // Claims commit before admission to the bounded delivery queue. The queue + // builds the current snapshot once, records its exact revision on matching + // operations, then sends those same bytes. A failed admission remains + // recoverable when the claim's retry deadline expires. + let publish_provider_environment = claimed_records + .iter() + .any(|record| operation_dimension(record) == OperationDimension::Policy); + let components = if publish_provider_environment { + crate::config_delivery::ConfigComponents::SANDBOX_AND_PROVIDER + } else { + crate::config_delivery::ConfigComponents::SANDBOX_CONFIG + }; + crate::config_delivery::publish_sandbox_components(state, &sandbox_id, components); + Ok(()) +} + +/// Associate pending operations with the exact snapshot that the delivery +/// worker is about to send. The caller must skip delivery if this fails. +pub async fn associate_pending_with_snapshot( + state: &Arc, + sandbox_id: &str, + snapshot: &openshell_core::proto::SandboxConfigSnapshot, +) -> Result { + let records = state + .store + .list_pending_config_operations_for_scope(sandbox_id) + .await + .map_err(|error| Status::internal(format!("list update operations failed: {error}")))?; + let target_revision = snapshot_revision(snapshot); + let now = current_time_ms(); + let mut requires_acknowledgement = false; + for record in records { + let operation = public_operation(&record)?; + match target_relation(&record, snapshot) { + std::cmp::Ordering::Greater => { + finish( + state, + &operation.operation_id, + ConfigUpdateOperationState::Superseded, + ConfigApplyOutcome::IgnoredStale, + "a newer desired revision replaced this update before application", + ) + .await?; + } + std::cmp::Ordering::Less => { + debug!( + operation_id = operation.operation_id, + "desired revision has not reached update operation target" + ); + } + std::cmp::Ordering::Equal => { + requires_acknowledgement = true; + let _ = mutate_record(state, &operation.operation_id, |stored| { + let Some(operation) = stored.operation.as_mut() else { + return false; + }; + if ConfigUpdateOperationState::try_from(operation.state) + != Ok(ConfigUpdateOperationState::Pending) + || operation.target_revision.as_ref() == Some(&target_revision) + { + return false; + } + operation.target_revision = Some(target_revision.clone()); + operation.updated_time = Some(timestamp(now)); + true + }) + .await?; + } + } + } + Ok(requires_acknowledgement) +} + +pub async fn complete_from_apply_results( + state: &Arc, + sandbox_id: &str, + results: &[ConfigComponentApplyResult], +) -> Result<(), Status> { + let relevant: Vec<_> = results + .iter() + .filter(|result| result.component != ConfigComponent::ProviderEnvironment as i32) + .collect(); + if relevant.is_empty() { + return Ok(()); + } + let operations = state + .store + .list_pending_config_operations_for_scope(sandbox_id) + .await + .map_err(|error| Status::internal(format!("list update operations failed: {error}")))?; + for record in operations { + let Some(operation) = record.operation.as_ref() else { + continue; }; - let completed_time = - openshell_core::time::timestamp_from_system_time(std::time::SystemTime::now()) - .map_err(|error| { - Status::internal(format!("create operation completion timestamp: {error}")) - })?; - operation.updated_time = Some(completed_time); - operation.completed_time = Some(completed_time); - let result = store - .put_if( - CONFIG_UPDATE_OPERATION_OBJECT_TYPE, - &record.id, - &record.name, - &record.workspace, - &stored.encode_to_vec(), - record.labels.as_deref(), - WriteCondition::MatchResourceVersion(record.resource_version), - ) - .await; - match result { - Ok(_) => { - status.operation = stored.operation; - return Ok(()); + for result in &relevant { + let requested = result + .requested_revision + .as_ref() + .ok_or_else(|| Status::invalid_argument("configuration result revision missing"))?; + if operation.component != result.component + || operation.target_revision.as_ref() != Some(requested) + { + continue; } - // A competing observer may have completed this operation. Reload - // the authoritative row; a terminal outcome is immutable. - Err(PersistenceError::Conflict { .. }) => {} - Err(_) => return Err(storage_unavailable()), + let outcome = ConfigApplyOutcome::try_from(result.outcome).unwrap_or_default(); + let terminal_state = match outcome { + ConfigApplyOutcome::Applied + | ConfigApplyOutcome::IgnoredDuplicate + | ConfigApplyOutcome::Degraded => ConfigUpdateOperationState::Applied, + ConfigApplyOutcome::IgnoredStale => ConfigUpdateOperationState::Superseded, + ConfigApplyOutcome::RetainedLocalOverride + | ConfigApplyOutcome::FailedRetainedLastKnownGood + | ConfigApplyOutcome::FailedClosed + | ConfigApplyOutcome::Unsupported + | ConfigApplyOutcome::Unspecified => ConfigUpdateOperationState::Failed, + }; + let failure = result + .failure + .as_ref() + .map_or("", |failure| failure.message.as_str()); + finish_if_target_matches( + state, + &operation.operation_id, + requested, + terminal_state, + outcome, + failure, + ) + .await?; + break; } } - Err(rpc_error::resource_version_conflict( - "configuration operation changed concurrently; query its status again", - None, - )) + Ok(()) } -#[cfg(test)] -mod tests { - use super::*; - use openshell_core::proto::{ - ProviderDesiredIdentity, ProviderMutationKind, ProviderReadinessObservation, +pub async fn complete_from_apply_result( + state: &Arc, + sandbox_id: &str, + result: &ConfigComponentApplyResult, +) -> Result<(), Status> { + complete_from_apply_results(state, sandbox_id, std::slice::from_ref(result)).await +} + +pub async fn wait_for_terminal( + state: &Arc, + operation_id: &str, + timeout: Duration, +) -> Result { + let timeout = if timeout.is_zero() { + DEFAULT_WAIT_TIMEOUT + } else { + timeout.min(MAX_WAIT_TIMEOUT) }; - use uuid::Uuid; - - fn receipt() -> ProviderMutationReceipt { - ProviderMutationReceipt { - receipt_id: Uuid::new_v4().to_string(), - mutation_id: Uuid::new_v4().to_string(), - provider: "provider".to_string(), - workspace: "default".to_string(), - kind: ProviderMutationKind::Update.into(), - desired: Some(ProviderDesiredIdentity { - sandbox_id: Uuid::new_v4().to_string(), - sandbox: "sandbox".to_string(), - attachment_epoch: Uuid::new_v4().to_string(), - provider_id: Uuid::new_v4().to_string(), - provider_resource_version: 3, - provider_env_revision: 5, - config_revision: 7, - policy_hash: "policy".to_string(), - }), - persisted_time: Some(prost_types::Timestamp { - seconds: 1_700_000_000, - nanos: 123_456_789, - }), - } + let started = std::time::Instant::now(); + let deadline = tokio::time::Instant::now() + timeout; + let record = get_record(state, operation_id) + .await? + .ok_or_else(|| Status::not_found("update operation not found"))?; + let operation = public_operation(&record)?; + let operation_state = ConfigUpdateOperationState::try_from(operation.state).unwrap_or_default(); + if terminal(operation_state) { + histogram!("openshell_config_update_operation_wait_seconds") + .record(started.elapsed().as_secs_f64()); + return Ok(operation); } - fn ready(receipt: &ProviderMutationReceipt) -> ProviderReadinessStatus { - let desired = receipt.desired.as_ref().unwrap(); - ProviderReadinessStatus { - receipt: Some(receipt.clone()), - state: ProviderReadinessState::Ready.into(), - network_instance_id: Uuid::new_v4().to_string(), - observed: Some(ProviderReadinessObservation { - session_id: Uuid::new_v4().to_string(), - sequence: 1, - attachment_epoch: desired.attachment_epoch.clone(), - provider_env_revision: desired.provider_env_revision, - config_revision: desired.config_revision, - policy_hash: desired.policy_hash.clone(), - credentials_installed: true, - policy_active: true, - launch_environment_installed: true, - process_instance_id: Uuid::new_v4().to_string(), - reason: ProviderReadinessReason::Unspecified.into(), - }), - ..Default::default() + // Subscribe before the second authoritative read so a transition between + // the two reads cannot be missed. A wakeup is only a hint; the fallback + // poll covers other replicas and process restarts. + let mut wake = state + .config_update_operation_watch_bus + .subscribe(operation_id); + loop { + let record = get_record(state, operation_id) + .await? + .ok_or_else(|| Status::not_found("update operation not found"))?; + let operation = public_operation(&record)?; + let operation_state = + ConfigUpdateOperationState::try_from(operation.state).unwrap_or_default(); + if terminal(operation_state) { + histogram!("openshell_config_update_operation_wait_seconds") + .record(started.elapsed().as_secs_f64()); + return Ok(operation); + } + if tokio::time::Instant::now() >= deadline { + let mut status = Status::deadline_exceeded(format!( + "timed out waiting for update operation {operation_id}" + )); + if let Ok(value) = operation_id.parse() { + status.metadata_mut().insert("operation-id", value); + } + return Err(status); + } + tokio::select! { + () = tokio::time::sleep_until((tokio::time::Instant::now() + Duration::from_secs(1)).min(deadline)) => {} + _ = wake.recv() => {} } } +} - #[tokio::test] - async fn provider_receipt_uses_the_common_operation_namespace_and_exact_target() { - let store = crate::persistence::test_store().await; - let receipt = receipt(); - record_provider_operation( - &store, - receipt.clone(), - ProviderReadinessReason::Unspecified, - ) +async fn reconcile_sandbox(state: &Arc, sandbox_id: &str) -> Result<(), Status> { + let operations = state + .store + .list_pending_config_operations_for_scope(sandbox_id) .await - .unwrap(); - let operation = get_provider_operation(&store, &receipt.receipt_id, "default") + .map_err(|error| Status::internal(format!("list sandbox operations failed: {error}")))?; + reconcile_records_for_sandbox(state, operations).await +} + +async fn reconcile_due_batch(state: &Arc) { + let now = current_time_ms(); + match state + .store + .list_due_config_update_operations(now, OPERATION_SCAN_PAGE_SIZE) + .await + { + Ok(operations) => { + let mut sandbox_groups = std::collections::BTreeMap::<_, Vec<_>>::new(); + for record in operations { + let Some(sandbox_id) = record + .operation + .as_ref() + .map(|operation| operation.sandbox_id.clone()) + else { + continue; + }; + sandbox_groups.entry(sandbox_id).or_default().push(record); + } + let concurrency = state.store.max_connections().saturating_sub(1).max(1) as usize; + stream::iter(sandbox_groups) + .for_each_concurrent(concurrency, |(sandbox_id, records)| { + let state = state.clone(); + async move { + if let Err(error) = reconcile_records_for_sandbox(&state, records).await { + warn!(sandbox_id, error = %error, "update operation reconciliation failed"); + } + } + }) + .await; + } + Err(error) => warn!(error = %error, "failed to list due update operations"), + } + match state.store.count_pending_config_update_operations().await { + Ok(pending) => { + gauge!("openshell_config_update_operations_pending") + .set(u32::try_from(pending).unwrap_or(u32::MAX)); + } + Err(error) => warn!(error = %error, "failed to count pending update operations"), + } +} + +pub fn spawn_reconciler(state: Arc, interval: Duration) { + let mut changed_sandboxes = state.sandbox_watch_bus.subscribe_all(); + tokio::spawn(async move { + let mut timer = tokio::time::interval(interval); + timer.tick().await; + loop { + tokio::select! { + _ = timer.tick() => reconcile_due_batch(&state).await, + changed = changed_sandboxes.recv() => { + match changed { + Ok(sandbox_id) => { + if let Err(error) = reconcile_sandbox(&state, &sandbox_id).await { + warn!(sandbox_id, error = %error, "sandbox update operation reconciliation failed"); + } + } + Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { + reconcile_due_batch(&state).await; + } + Err(tokio::sync::broadcast::error::RecvError::Closed) => return, + } + } + } + } + }); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::grpc::test_support::test_server_state; + use openshell_core::proto::{SandboxConfigSnapshot, SandboxSpec}; + + async fn pending_test_operation() -> (Arc, StoredConfigUpdateOperation) { + let state = test_server_state().await; + let sandbox = Sandbox { + metadata: Some(ObjectMeta { + id: "operation-test-sandbox".to_string(), + name: "operation-test-sandbox".to_string(), + workspace: "default".to_string(), + ..Default::default() + }), + spec: Some(SandboxSpec::default()), + ..Default::default() + }; + state.store.put_message(&sandbox).await.unwrap(); + let record = new_record( + &sandbox, + "default", + "operation-test-request", + OperationDimension::Settings, + None, + OperationTarget { + policy_version: 0, + settings_revision: 1, + }, + CommittedResponse::default(), + ); + state + .store + .put_if_with_operation( + crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE, + "operation-test-settings", + "operation-test-sandbox", + "default", + br#"{"revision":1,"settings":{}}"#, + crate::persistence::WriteCondition::MustCreate, + &record, + None, + ) .await .unwrap(); - assert_eq!(operation.receipt, receipt); - assert_eq!(operation.operation.operation_id, receipt.receipt_id); - assert_eq!(operation.operation.created_time, receipt.persisted_time); - assert_eq!(operation.operation.updated_time, receipt.persisted_time); - assert!(operation.operation.completed_time.is_none()); + let operation_id = &record.operation.as_ref().unwrap().operation_id; + ( + state.clone(), + get_record(&state, operation_id).await.unwrap().unwrap(), + ) + } + + #[test] + fn authoritative_phase_classification_is_explicit() { assert_eq!( - operation.operation.state, - ConfigUpdateOperationState::Pending as i32 + initial_state(SandboxPhase::Ready), + ConfigUpdateOperationState::Pending ); - assert!( - store - .get("provider_mutation_receipt", &receipt.receipt_id) - .await - .unwrap() - .is_none() + assert_eq!( + initial_state(SandboxPhase::Provisioning), + ConfigUpdateOperationState::Pending ); assert_eq!( - get_provider_operation(&store, &receipt.receipt_id, "other") - .await - .unwrap_err() - .code(), - Code::NotFound + initial_state(SandboxPhase::Starting), + ConfigUpdateOperationState::Pending + ); + assert_eq!( + initial_state(SandboxPhase::Stopping), + ConfigUpdateOperationState::Pending + ); + assert_eq!( + initial_state(SandboxPhase::Error), + ConfigUpdateOperationState::Pending + ); + assert_eq!( + initial_state(SandboxPhase::Stopped), + ConfigUpdateOperationState::Inactive + ); + assert_eq!( + initial_state(SandboxPhase::Completed), + ConfigUpdateOperationState::Inactive + ); + assert_eq!( + initial_state(SandboxPhase::Deleting), + ConfigUpdateOperationState::Cancelled ); } - #[tokio::test] - async fn incomplete_provider_target_stays_failed_after_later_installation() { - let store = crate::persistence::test_store().await; - let receipt = receipt(); - record_provider_operation( - &store, - receipt.clone(), - ProviderReadinessReason::SnapshotMismatch, - ) - .await - .unwrap(); - let mut status = ready(&receipt); - observe_provider_status(&store, &mut status).await.unwrap(); - let operation = status.operation.unwrap(); - assert_eq!(operation.state, ConfigUpdateOperationState::Failed as i32); - assert_eq!(operation.completed_time, receipt.persisted_time); - } - - #[tokio::test] - async fn receipt_timestamp_requires_presence_and_canonical_nanos() { - let store = crate::persistence::test_store().await; - for invalid_time in [ - None, - Some(prost_types::Timestamp { - seconds: 0, - nanos: -1, + #[test] + fn target_relation_compares_only_the_mutated_dimension() { + let mut record = StoredConfigUpdateOperation { + metadata: Some(ObjectMeta { + annotations: HashMap::from([( + OPERATION_DIMENSION_ANNOTATION.to_string(), + OperationDimension::Policy.as_str().to_string(), + )]), + ..Default::default() }), - Some(prost_types::Timestamp { - seconds: openshell_core::time::MAX_TIMESTAMP_SECONDS + 1, - nanos: 0, - }), - ] { - let mut receipt = receipt(); - receipt.persisted_time = invalid_time; - let error = - record_provider_operation(&store, receipt, ProviderReadinessReason::Unspecified) - .await - .unwrap_err(); - assert_eq!(error.code(), Code::Internal); - } + target_policy_version: 7, + target_settings_revision: 11, + ..Default::default() + }; + let snapshot = |version, settings_revision| SandboxConfigSnapshot { + version, + settings_revision, + ..Default::default() + }; + assert_eq!( - store - .count_in_workspace(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, "default") - .await - .unwrap(), - 0 + target_relation(&record, &snapshot(7, 11)), + std::cmp::Ordering::Equal ); - - // The Unix epoch is a valid explicit timestamp, not missing data. - let mut epoch = receipt(); - epoch.persisted_time = Some(prost_types::Timestamp::default()); - let recorded = - record_provider_operation(&store, epoch.clone(), ProviderReadinessReason::Unspecified) - .await - .unwrap(); - assert_eq!(recorded, epoch); assert_eq!( - get_provider_operation(&store, &epoch.receipt_id, "default") - .await - .unwrap() - .receipt, - epoch + target_relation(&record, &snapshot(8, 11)), + std::cmp::Ordering::Greater + ); + assert_eq!( + target_relation(&record, &snapshot(7, 12)), + std::cmp::Ordering::Equal + ); + assert_eq!( + target_relation(&record, &snapshot(6, 11)), + std::cmp::Ordering::Less ); - } - #[tokio::test] - async fn receipt_timestamp_nanos_are_part_of_exact_completion_identity() { - let store = crate::persistence::test_store().await; - let receipt = receipt(); - record_provider_operation( - &store, - receipt.clone(), - ProviderReadinessReason::Unspecified, - ) - .await - .unwrap(); - let mut changed = ready(&receipt); - changed - .receipt - .as_mut() - .unwrap() - .persisted_time - .as_mut() - .unwrap() - .nanos += 1; + record.metadata.as_mut().unwrap().annotations.insert( + OPERATION_DIMENSION_ANNOTATION.to_string(), + OperationDimension::Settings.as_str().to_string(), + ); assert_eq!( - observe_provider_status(&store, &mut changed) - .await - .unwrap_err() - .code(), - Code::Internal + target_relation(&record, &snapshot(8, 11)), + std::cmp::Ordering::Equal ); - let stored = get_provider_operation(&store, &receipt.receipt_id, "default") - .await - .unwrap(); - assert_eq!(stored.receipt, receipt); assert_eq!( - stored.operation.state, - ConfigUpdateOperationState::Pending as i32 + target_relation(&record, &snapshot(7, 12)), + std::cmp::Ordering::Greater ); - assert!(stored.operation.completed_time.is_none()); - - let mut exact = ready(&receipt); - observe_provider_status(&store, &mut exact).await.unwrap(); - let completed = exact.operation.unwrap(); - assert_eq!(completed.created_time, receipt.persisted_time); - assert!(completed.completed_time.is_some()); - assert_eq!(completed.updated_time, completed.completed_time); - openshell_core::time::validate_timestamp(completed.completed_time.as_ref().unwrap()) - .unwrap(); + } + + #[test] + fn sanitized_errors_are_bounded_on_utf8_boundaries() { + let value = "é".repeat(MAX_SANITIZED_ERROR_BYTES); + let sanitized = sanitize_error(&value); + assert!(sanitized.len() <= MAX_SANITIZED_ERROR_BYTES); + assert!(sanitized.is_char_boundary(sanitized.len())); } #[tokio::test] - async fn stale_or_partial_provider_evidence_cannot_complete_an_operation() { - let store = crate::persistence::test_store().await; - let receipt = receipt(); - record_provider_operation( - &store, - receipt.clone(), - ProviderReadinessReason::Unspecified, - ) - .await - .unwrap(); - let mut stale = ready(&receipt); - stale.observed.as_mut().unwrap().config_revision += 1; - assert!(observe_provider_status(&store, &mut stale).await.is_err()); - let mut partial = ready(&receipt); - partial - .observed - .as_mut() + async fn concurrent_claims_commit_once_and_noop_does_not_churn_version() { + let (state, record) = pending_test_operation().await; + let operation_id = record.operation.as_ref().unwrap().operation_id.clone(); + let now = current_time_ms(); + let claim = || async { + mutate_record(&state, &operation_id, |stored| { + if stored.next_attempt_at_ms() > now { + return false; + } + stored.attempt_count = stored.attempt_count.saturating_add(1); + stored.next_attempt_time = Some(timestamp(now.saturating_add(1_000))); + stored.operation.as_mut().unwrap().updated_time = Some(timestamp(now)); + true + }) + .await + .unwrap() + }; + let (first, second) = tokio::join!(claim(), claim()); + let committed = usize::from(first.as_ref().is_some_and(|(_, changed)| *changed)) + + usize::from(second.as_ref().is_some_and(|(_, changed)| *changed)); + assert_eq!(committed, 1); + + let claimed = get_record(&state, &operation_id).await.unwrap().unwrap(); + assert_eq!(claimed.attempt_count, 1); + let version = claimed.metadata.as_ref().unwrap().resource_version; + let unchanged = mutate_record(&state, &operation_id, |_| false) + .await .unwrap() - .launch_environment_installed = false; - assert!(observe_provider_status(&store, &mut partial).await.is_err()); + .unwrap(); + assert!(!unchanged.1); assert_eq!( - get_provider_operation(&store, &receipt.receipt_id, "default") + get_record(&state, &operation_id) .await .unwrap() - .operation - .state, - ConfigUpdateOperationState::Pending as i32 + .unwrap() + .metadata + .unwrap() + .resource_version, + version ); } #[tokio::test] - async fn applied_history_never_upgrades_an_expired_live_view() { - let store = crate::persistence::test_store().await; - let receipt = receipt(); - record_provider_operation( - &store, - receipt.clone(), - ProviderReadinessReason::Unspecified, + async fn completion_rechecks_exact_target_inside_cas_transition() { + let (state, record) = pending_test_operation().await; + let operation_id = record.operation.as_ref().unwrap().operation_id.clone(); + let expected = ConfigSnapshotRevision { + component: Some(config_snapshot_revision::Component::SandboxConfig( + SandboxConfigRevision { + config_revision: 1, + policy_version: 1, + settings_revision: 1, + ..Default::default() + }, + )), + }; + mutate_record(&state, &operation_id, |stored| { + stored.operation.as_mut().unwrap().target_revision = Some(expected.clone()); + true + }) + .await + .unwrap(); + let before = get_record(&state, &operation_id).await.unwrap().unwrap(); + + finish_if_target_matches( + &state, + &operation_id, + &ConfigSnapshotRevision::default(), + ConfigUpdateOperationState::Applied, + ConfigApplyOutcome::Applied, + "", ) .await .unwrap(); - let mut status = ready(&receipt); - observe_provider_status(&store, &mut status).await.unwrap(); - let before = store - .get(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, &receipt.receipt_id) - .await - .unwrap() - .unwrap(); - status.state = ProviderReadinessState::Pending.into(); - status.reason = ProviderReadinessReason::SupervisorLeaseExpired.into(); - observe_provider_status(&store, &mut status).await.unwrap(); - assert_eq!(status.state, ProviderReadinessState::Pending as i32); + + let after = get_record(&state, &operation_id).await.unwrap().unwrap(); assert_eq!( - status.operation.unwrap().state, - ConfigUpdateOperationState::Applied as i32 + after.metadata.as_ref().unwrap().resource_version, + before.metadata.as_ref().unwrap().resource_version ); - let after = store - .get(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, &receipt.receipt_id) - .await - .unwrap() - .unwrap(); - assert_eq!(before.resource_version, after.resource_version); + assert_eq!( + ConfigUpdateOperationState::try_from(after.operation.unwrap().state).unwrap(), + ConfigUpdateOperationState::Pending + ); + } + + #[test] + fn watch_subscription_drop_removes_only_its_channel() { + let bus = OperationWatchBus::new(); + let old = bus.subscribe("operation"); + assert_eq!(bus.len(), 1); + + bus.notify("operation"); + let replacement = bus.subscribe("operation"); + drop(old); + assert_eq!(bus.len(), 1); + + drop(replacement); + assert_eq!(bus.len(), 0); + } + + #[test] + fn concurrent_watch_subscription_drop_removes_empty_channel() { + let bus = OperationWatchBus::new(); + let first = bus.subscribe("operation"); + let second = bus.subscribe("operation"); + let sender = first.sender.clone(); + assert_eq!(sender.receiver_count(), 2); + + // Keep both destructors outside the map lock until they have dropped + // their receivers. This makes the teardown overlap deterministic. + let guard = bus.inner.lock().expect("operation watch bus lock poisoned"); + let first_drop = std::thread::spawn(move || drop(first)); + let second_drop = std::thread::spawn(move || drop(second)); + let deadline = std::time::Instant::now() + Duration::from_secs(5); + while sender.receiver_count() != 0 && std::time::Instant::now() < deadline { + std::thread::yield_now(); + } + let receivers_dropped_before_lock = sender.receiver_count() == 0; + drop(guard); + first_drop.join().unwrap(); + second_drop.join().unwrap(); + + assert!( + receivers_dropped_before_lock, + "both receivers must drop before either teardown inspects the map" + ); + assert_eq!(bus.len(), 0); } #[tokio::test] - async fn competing_terminal_observers_preserve_the_first_committed_outcome() { - let store = crate::persistence::test_store().await; - let receipt = receipt(); - record_provider_operation( - &store, - receipt.clone(), - ProviderReadinessReason::Unspecified, - ) + async fn terminal_wait_does_not_register_a_watch_channel() { + let (state, record) = pending_test_operation().await; + let operation_id = record.operation.as_ref().unwrap().operation_id.clone(); + mutate_record(&state, &operation_id, |stored| { + stored.operation.as_mut().unwrap().state = ConfigUpdateOperationState::Inactive.into(); + true + }) .await .unwrap(); - let mut applied = ready(&receipt); - let mut superseded = applied.clone(); - superseded.state = ProviderReadinessState::Superseded.into(); - superseded.reason = ProviderReadinessReason::DesiredStateChanged.into(); - let (first, second) = tokio::join!( - observe_provider_status(&store, &mut applied), - observe_provider_status(&store, &mut superseded), - ); - first.unwrap(); - second.unwrap(); - assert_eq!(applied.operation, superseded.operation); - let stored = get_provider_operation(&store, &receipt.receipt_id, "default") + + wait_for_terminal(&state, &operation_id, Duration::from_secs(1)) .await .unwrap(); - assert!(matches!( - ConfigUpdateOperationState::try_from(stored.operation.state).unwrap(), - ConfigUpdateOperationState::Applied | ConfigUpdateOperationState::Superseded - )); + assert_eq!(state.config_update_operation_watch_bus.len(), 0); } #[tokio::test] - async fn failed_observation_and_recovered_snapshot_have_distinct_immutable_receipts() { - let store = crate::persistence::test_store().await; - let mut observed = receipt(); - observed.kind = ProviderMutationKind::Observe.into(); - let failed = record_provider_operation( - &store, - observed.clone(), - ProviderReadinessReason::CredentialsWithheld, - ) - .await - .unwrap(); - observed.mutation_id = Uuid::new_v4().to_string(); - observed.persisted_time.as_mut().unwrap().nanos += 1; - let repeated = record_provider_operation( - &store, - observed.clone(), - ProviderReadinessReason::CredentialsWithheld, - ) - .await - .unwrap(); - assert_eq!(repeated, failed); - let repaired = - record_provider_operation(&store, observed, ProviderReadinessReason::Unspecified) - .await - .unwrap(); - assert_ne!(repaired.receipt_id, failed.receipt_id); - assert_eq!( - get_provider_operation(&store, &failed.receipt_id, "default") - .await - .unwrap() - .operation - .state, - ConfigUpdateOperationState::Failed as i32 - ); - assert_eq!( - store - .count_in_workspace(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, "default") - .await - .unwrap(), - 2 - ); + async fn timed_out_wait_removes_its_watch_channel() { + let (state, record) = pending_test_operation().await; + let operation_id = record.operation.as_ref().unwrap().operation_id.clone(); + + let error = wait_for_terminal(&state, &operation_id, Duration::from_millis(1)) + .await + .unwrap_err(); + assert_eq!(error.code(), tonic::Code::DeadlineExceeded); + assert_eq!(state.config_update_operation_watch_bus.len(), 0); } } diff --git a/crates/openshell-server/src/credentials.rs b/crates/openshell-server/src/credentials.rs index bcc6de9cac..dd9513f2d2 100644 --- a/crates/openshell-server/src/credentials.rs +++ b/crates/openshell-server/src/credentials.rs @@ -96,6 +96,7 @@ pub trait CredentialDriver: std::fmt::Debug + Send + Sync { fn fail_next_delete(&self) {} #[cfg(test)] + #[allow(dead_code)] fn gate_next_resolve( &self, ) -> Option<( @@ -302,6 +303,7 @@ impl CredentialRuntime { } #[cfg(test)] + #[allow(dead_code)] pub(crate) fn gate_next_resolve( &self, ) -> ( diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index 8987bb606d..74e9cd1c7f 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -33,11 +33,12 @@ use openshell_core::proto::{ ExchangeProviderSubjectTokenRequest, ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, ExposeServiceRequest, ExtensionKind, FinalizeMainProcessExitRequest, FinalizeMainProcessExitResponse, GatewayMessage, - GetCurrentUserRequest, GetCurrentUserResponse, GetDraftHistoryRequest, GetDraftHistoryResponse, - GetDraftPolicyRequest, GetDraftPolicyResponse, GetGatewayConfigRequest, - GetGatewayConfigResponse, GetGatewayInfoRequest, GetGatewayInfoResponse, - GetProviderProfileRequest, GetProviderRefreshStatusRequest, GetProviderRefreshStatusResponse, - GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxLogsRequest, + GetConfigUpdateOperationRequest, GetConfigUpdateOperationResponse, GetCurrentUserRequest, + GetCurrentUserResponse, GetDraftHistoryRequest, GetDraftHistoryResponse, GetDraftPolicyRequest, + GetDraftPolicyResponse, GetGatewayConfigRequest, GetGatewayConfigResponse, + GetGatewayInfoRequest, GetGatewayInfoResponse, GetProviderProfileRequest, + GetProviderRefreshStatusRequest, GetProviderRefreshStatusResponse, GetProviderRequest, + GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxLogsRequest, GetSandboxLogsResponse, GetSandboxPolicyStatusRequest, GetSandboxPolicyStatusResponse, GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, GetSandboxProviderStatusRequest, GetSandboxProviderStatusResponse, GetSandboxRequest, @@ -619,13 +620,6 @@ impl OpenShell for OpenShellService { policy::handle_get_gateway_config(&self.state, request).await } - async fn get_sandbox_provider_environment( - &self, - request: Request, - ) -> Result, Status> { - policy::handle_get_sandbox_provider_environment(&self.state, request).await - } - async fn exchange_provider_subject_token( &self, request: Request, @@ -637,7 +631,45 @@ impl OpenShell for OpenShellService { &self, request: Request, ) -> Result, Status> { - mutation_replay::run(&self.state, request).await + let consistency = + openshell_core::proto::ConfigUpdateConsistency::try_from(request.get_ref().consistency) + .map_err(|_| Status::invalid_argument("unknown update consistency"))?; + let wait = consistency == openshell_core::proto::ConfigUpdateConsistency::WaitForCompletion; + if wait && request.get_ref().global { + return Err(Status::invalid_argument( + "WAIT_FOR_COMPLETION is only supported for sandbox-scoped updates", + )); + } + let timeout = policy::config_wait_timeout(request.get_ref().wait_timeout.as_ref())?; + let mut response = mutation_replay::run(&self.state, request).await?; + if wait { + let id = response + .get_ref() + .operation + .as_ref() + .ok_or_else(|| Status::internal("gateway omitted completion operation"))? + .operation_id + .clone(); + response.get_mut().operation = Some( + crate::config_update_operation::wait_for_terminal(&self.state, &id, timeout) + .await?, + ); + } + Ok(response) + } + + async fn get_config_update_operation( + &self, + request: Request, + ) -> Result, Status> { + policy::handle_get_config_update_operation(&self.state, request).await + } + + async fn get_sandbox_provider_environment( + &self, + request: Request, + ) -> Result, Status> { + policy::handle_get_sandbox_provider_environment(&self.state, request).await } async fn get_sandbox_policy_status( diff --git a/crates/openshell-server/src/grpc/mutation_replay.rs b/crates/openshell-server/src/grpc/mutation_replay.rs index a6f8267041..dc476a3fbf 100644 --- a/crates/openshell-server/src/grpc/mutation_replay.rs +++ b/crates/openshell-server/src/grpc/mutation_replay.rs @@ -377,13 +377,18 @@ fn validate_request_id(value: &str) -> Result { Ok(id.hyphenated().to_string()) } -fn fingerprint(request: &M) -> Result { +pub(super) fn fingerprint(request: &M) -> Result { let descriptor = DESCRIPTORS .get_message_by_name(&format!("openshell.v1.{}Request", M::METHOD)) .ok_or_else(|| Status::internal("mutation request descriptor missing"))?; let mut message = DynamicMessage::decode(descriptor, request.encode_to_vec().as_slice()) .map_err(|_| Status::internal("decode mutation request"))?; message.clear_field_by_name("request_id"); + if M::METHOD == "UpdateConfig" { + // Waiting changes response timing, never the admitted mutation identity. + message.clear_field_by_name("consistency"); + message.clear_field_by_name("wait_timeout"); + } let value = serde_json::to_value(message) .map_err(|_| Status::internal("canonicalize mutation request"))?; hash_json(&value) diff --git a/crates/openshell-server/src/grpc/mutation_replay/ordinary.rs b/crates/openshell-server/src/grpc/mutation_replay/ordinary.rs index 48c0146381..9a91f95f16 100644 --- a/crates/openshell-server/src/grpc/mutation_replay/ordinary.rs +++ b/crates/openshell-server/src/grpc/mutation_replay/ordinary.rs @@ -183,6 +183,8 @@ pub(in crate::grpc) enum Outcome { }, Config { sandbox_id: Option, + #[serde(default)] + operation_id: Option, version: u32, policy_hash: String, settings_revision: u64, @@ -882,7 +884,7 @@ mutation!( UpdateConfigRequest, UpdateConfigResponse, "UpdateConfig", - policy::handle_update_config, + policy::handle_update_config_commit, async |req: &UpdateConfigRequest, state: &ServerState, principal: &Principal| { if req.global { if req.workspace_scope.is_some() { @@ -910,6 +912,7 @@ mutation!( } Ok(Outcome::Config { sandbox_id: references.first().map(|r| r.id.clone()), + operation_id: value.operation.as_ref().map(|op| op.operation_id.clone()), version: value.version, policy_hash: value.policy_hash.clone(), settings_revision: value.settings_revision, @@ -920,6 +923,7 @@ mutation!( async |store: &Store, outcome: Outcome| { let Outcome::Config { sandbox_id, + operation_id, version, policy_hash, settings_revision, @@ -932,7 +936,20 @@ mutation!( if let Some(id) = sandbox_id { let _: Sandbox = live(store, &id).await?; } + let operation = if let Some(id) = operation_id { + Some( + store + .get_message::(&id) + .await + .map_err(|_| replay_unavailable())? + .and_then(|record| record.operation) + .ok_or_else(replay_unavailable)?, + ) + } else { + None + }; Ok(UpdateConfigResponse { + operation, version, policy_hash, settings_revision, diff --git a/crates/openshell-server/src/grpc/mutation_replay/ordinary/tests.rs b/crates/openshell-server/src/grpc/mutation_replay/ordinary/tests.rs index 8773249ead..894b21781f 100644 --- a/crates/openshell-server/src/grpc/mutation_replay/ordinary/tests.rs +++ b/crates/openshell-server/src/grpc/mutation_replay/ordinary/tests.rs @@ -1155,3 +1155,101 @@ async fn draft_receipts_replay_after_chunk_state_and_review_tokens_change() { assert_eq!(approved.chunks_approved, 1); assert_eq!(replay(&state, all).await, approved); } + +#[tokio::test] +async fn config_completion_timeout_preserves_replayable_committed_operation() { + use openshell_core::proto::open_shell_server::OpenShell; + use openshell_core::proto::{ + ConfigUpdateConsistency, ConfigUpdateOperationState, Sandbox, SandboxPhase, SandboxSpec, + SandboxStatus, SettingValue, UpdateConfigRequest, setting_value, + }; + let (_directory, state) = protected_state().await; + state + .store + .put_message(&Sandbox { + metadata: Some(ObjectMeta { + id: "completion-sandbox-id".into(), + name: "completion-sandbox".into(), + workspace: "default".into(), + ..Default::default() + }), + spec: Some(SandboxSpec::default()), + status: Some(SandboxStatus { + phase: SandboxPhase::Ready.into(), + ..Default::default() + }), + ..Default::default() + }) + .await + .unwrap(); + let service = crate::grpc::OpenShellService::new(state.clone()); + let mut request = UpdateConfigRequest { + sandbox: "completion-sandbox".into(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + setting_key: "ocsf_json_enabled".into(), + setting_value: Some(SettingValue { + value: Some(setting_value::Value::BoolValue(true)), + }), + request_id: uuid::Uuid::new_v4().to_string(), + consistency: ConfigUpdateConsistency::WaitForCompletion.into(), + wait_timeout: Some(prost_types::Duration { + seconds: 1, + nanos: 0, + }), + ..Default::default() + }; + let error = service + .update_config(authed_request(request.clone())) + .await + .unwrap_err(); + assert_eq!(error.code(), Code::DeadlineExceeded, "{error:?}"); + request.consistency = ConfigUpdateConsistency::CommitOnly.into(); + request.wait_timeout = None; + let response = service + .update_config(authed_request(request.clone())) + .await + .unwrap() + .into_inner(); + assert_eq!(response.settings_revision, 1); + let operation = response.operation.unwrap(); + assert_eq!(operation.state, ConfigUpdateOperationState::Pending as i32); + let replay = service + .update_config(authed_request(request.clone())) + .await + .unwrap() + .into_inner(); + assert_eq!( + replay.operation.unwrap().operation_id, + operation.operation_id + ); + assert_eq!(replay.settings_revision, 1); + + // A new request for the unchanged value still tracks completion of that + // revision, without writing another settings revision. + request.request_id = uuid::Uuid::new_v4().to_string(); + let unchanged = service + .update_config(authed_request(request.clone())) + .await + .unwrap() + .into_inner(); + assert_eq!(unchanged.settings_revision, 1); + let unchanged_operation = unchanged.operation.unwrap(); + assert_ne!(unchanged_operation.operation_id, operation.operation_id); + assert_eq!( + unchanged_operation.state, + ConfigUpdateOperationState::Pending as i32 + ); + let pending = state + .store + .list_pending_config_operations_for_scope("completion-sandbox-id") + .await + .unwrap(); + assert_eq!(pending.len(), 2); + + request.consistency = i32::MAX; + let error = service + .update_config(authed_request(request)) + .await + .unwrap_err(); + assert_eq!(error.code(), Code::InvalidArgument); +} diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 48d2cbe098..102b2bc156 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -24,11 +24,16 @@ pub use endpoint_status::{ use crate::ServerState; use crate::auth::principal::Principal; -use crate::auth::workspace_authz::{MinWorkspaceRole, require_platform_admin}; +use crate::auth::workspace_authz::{ + MinWorkspaceRole, authorize_workspace, require_platform_admin, selected_workspace_name, +}; +use crate::config_update_operation::{ + self, CommittedResponse, OperationDimension, OperationTarget, +}; use crate::pagination::Pagination; +use crate::persistence::ObjectType; use crate::persistence::{ - DraftChunkRecord, ObjectId, ObjectListQuery, ObjectName, ObjectType, ObjectWorkspace, - PolicyRecord, Store, + DraftChunkRecord, ObjectId, ObjectListQuery, ObjectName, ObjectWorkspace, PolicyRecord, Store, }; use crate::policy_store::{AtomicPolicyRevisionWrite, PolicyStoreExt}; use crate::provider_profile_sources::EffectiveProviderProfileCatalog; @@ -49,12 +54,12 @@ use openshell_core::proto::{ AddAllowRules as ProtoAddAllowRules, AddDenyRules as ProtoAddDenyRules, ApproveAllDraftChunksRequest, ApproveAllDraftChunksResponse, ApproveDraftChunkRequest, ApproveDraftChunkResponse, ClearDraftChunksRequest, ClearDraftChunksResponse, - DraftHistoryEntry, EditDraftChunkRequest, EditDraftChunkResponse, EffectiveSetting, + ConfigUpdateConsistency, DraftHistoryEntry, EditDraftChunkRequest, EditDraftChunkResponse, + EffectiveSetting, GetConfigUpdateOperationRequest, GetConfigUpdateOperationResponse, GetDraftHistoryRequest, GetDraftHistoryResponse, GetDraftPolicyRequest, GetDraftPolicyResponse, GetGatewayConfigRequest, GetGatewayConfigResponse, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxLogsRequest, GetSandboxLogsResponse, GetSandboxPolicyStatusRequest, GetSandboxPolicyStatusResponse, - GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, L7RuleTarget as ProtoL7RuleTarget, ListSandboxPoliciesRequest, ListSandboxPoliciesResponse, PolicyChunk, PolicyMergeOperation, PolicySource, PolicyStatus, ProviderEnvironmentSnapshot, ProviderEnvironmentValue, ProviderEnvironmentValueClassification, ProviderReadinessReason, @@ -64,6 +69,9 @@ use openshell_core::proto::{ SubmitPolicyAnalysisRequest, SubmitPolicyAnalysisResponse, UndoDraftChunkRequest, UndoDraftChunkResponse, UpdateConfigRequest, UpdateConfigResponse, }; +use openshell_core::proto::{ + GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, +}; use openshell_core::proto::{ L7DenyRule, L7Rule, NetworkBinary, NetworkEndpoint, NetworkPolicyRule, Provider, Sandbox, SandboxPolicy as ProtoSandboxPolicy, StaticCredentialEndpointBinding, @@ -130,6 +138,81 @@ const STORED_POLICY_SOURCE_SPEC: &str = "sandbox spec policy"; const STORED_POLICY_SOURCE_GLOBAL: &str = "global policy setting"; /// Maximum number of optimistic retry attempts for policy version conflicts. const MERGE_RETRY_LIMIT: usize = 5; +const MAX_IDEMPOTENCY_KEY_BYTES: usize = 256; + +pub(super) fn config_wait_timeout( + value: Option<&prost_types::Duration>, +) -> Result { + value + .map(openshell_core::time::duration_to_std) + .transpose() + .map(Option::unwrap_or_default) + .map_err(|_| Status::invalid_argument("wait_timeout must be a nonnegative duration")) +} + +async fn finish_config_update_operation( + state: &Arc, + operation_id: &str, + consistency: ConfigUpdateConsistency, + timeout: std::time::Duration, +) -> Result, Status> { + config_update_operation::reconcile_one(state, operation_id).await?; + if consistency == ConfigUpdateConsistency::WaitForCompletion { + config_update_operation::wait_for_terminal(state, operation_id, timeout).await?; + } + let record = config_update_operation::get_record(state, operation_id) + .await? + .ok_or_else(|| Status::internal("committed update operation disappeared"))?; + Ok(Response::new( + config_update_operation::response_from_record(&record)?, + )) +} + +#[derive(Clone, Copy)] +struct ConfigOperationIdentity<'a> { + idempotency_key: &'a str, + dimension: OperationDimension, + request_fingerprint: &'a str, +} + +async fn finish_unchanged_config_update( + state: &Arc, + sandbox: &Sandbox, + workspace: &str, + identity: ConfigOperationIdentity<'_>, + response: Response, + consistency: ConfigUpdateConsistency, + timeout: std::time::Duration, +) -> Result, Status> { + let response = response.into_inner(); + let record = config_update_operation::new_record( + sandbox, + workspace, + identity.idempotency_key, + identity.dimension, + Some(identity.request_fingerprint), + OperationTarget { + policy_version: response.version, + settings_revision: response.settings_revision, + }, + CommittedResponse { + policy_version: response.version, + policy_hash: response.policy_hash, + settings_revision: response.settings_revision, + deleted: response.deleted, + annotations: response.annotations, + }, + ); + state + .store + .insert_existing_config_operation(&record, workspace, sandbox.object_name()) + .await + .map_err(|error| { + super::persistence_error_to_status(error, "persist unchanged update operation") + })?; + let operation_id = config_update_operation::public_operation(&record)?.operation_id; + finish_config_update_operation(state, &operation_id, consistency, timeout).await +} // Private wire-only compatibility types for policy history written before // 0.1.0. Public generated bindings intentionally reserve NetworkBinary tag 2, @@ -1793,12 +1876,32 @@ async fn current_effective_policy_from_records( records: &[super::provider::ProviderEnvironmentRecord], ) -> Result { let global_settings = load_global_settings(state.store.as_ref()).await?; - if let Some(global_policy) = decode_policy_from_global_settings(&global_settings)? { + current_effective_policy_for_sandbox_from_inputs( + state, + catalog, + &global_settings, + records, + sandbox, + sandbox_id, + ) + .await +} + +async fn current_effective_policy_for_sandbox_from_inputs( + state: &ServerState, + catalog: &EffectiveProviderProfileCatalog, + global_settings: &StoredSettings, + provider_records: &[super::provider::ProviderEnvironmentRecord], + sandbox: &Sandbox, + sandbox_id: &str, +) -> Result { + if let Some(global_policy) = decode_policy_from_global_settings(global_settings)? { // A global policy is the complete effective policy. Dormant sandbox // history and specs may predate the current schema, but they must not // prevent the valid global policy from being served. - return apply_captured_policy_context( - provider_policy_context_from_records(catalog, records), + return apply_effective_policy_context_from_records( + catalog, + provider_records, global_policy, PolicySource::Global, ); @@ -1820,8 +1923,9 @@ async fn current_effective_policy_from_records( } }; - apply_captured_policy_context( - provider_policy_context_from_records(catalog, records), + apply_effective_policy_context_from_records( + catalog, + provider_records, policy, PolicySource::Sandbox, ) @@ -1862,22 +1966,23 @@ async fn apply_effective_policy_context( policy: ProtoSandboxPolicy, policy_source: PolicySource, ) -> Result { - let provider_context = provider_policy_context_with_catalog( + let provider_records = super::provider::load_provider_environment_records( state.store.as_ref(), - catalog, workspace, provider_names, ) .await?; - apply_captured_policy_context(provider_context, policy, policy_source) + apply_effective_policy_context_from_records(catalog, &provider_records, policy, policy_source) } -fn apply_captured_policy_context( - mut provider_context: ProviderPolicyContext, +fn apply_effective_policy_context_from_records( + catalog: &EffectiveProviderProfileCatalog, + provider_records: &[super::provider::ProviderEnvironmentRecord], mut policy: ProtoSandboxPolicy, policy_source: PolicySource, ) -> Result { clear_provider_credentialed_markers(&mut policy); + let mut provider_context = provider_policy_context_from_records(catalog, provider_records); if !matches!(policy_source, PolicySource::Global) && !provider_context.layers.is_empty() { policy = compose_effective_policy(&policy, &provider_context.layers); } @@ -2438,6 +2543,7 @@ fn update_config_response( settings_revision, deleted, annotations, + operation: None, }) } @@ -2561,9 +2667,7 @@ pub(super) async fn handle_get_sandbox_config( let sandbox_name = request.get_ref().name.clone(); let workspace = match (&principal, request.get_ref().workspace_scope.as_ref()) { (Principal::Sandbox(_), None) => String::new(), - (_, selector) => { - crate::auth::workspace_authz::selected_workspace_name(selector)?.to_string() - } + (_, selector) => selected_workspace_name(selector)?.to_string(), }; let result = handle_get_sandbox_config_inner(state, request).await; match result { @@ -2609,7 +2713,7 @@ async fn handle_get_sandbox_config_inner( let req = request.into_inner(); let workspace = match &principal { Principal::Sandbox(_) if req.workspace_scope.is_none() => "", - _ => crate::auth::workspace_authz::selected_workspace_name(req.workspace_scope.as_ref())?, + _ => selected_workspace_name(req.workspace_scope.as_ref())?, }; let sandbox = super::sandbox::resolve_and_authorize_sandbox_name( state, @@ -2651,6 +2755,12 @@ pub async fn build_sandbox_config_snapshot( .provider_profile_sources .snapshot_catalog(state.store.as_ref(), &workspace) .await?; + let provider_records = super::provider::load_provider_environment_records( + state.store.as_ref(), + &workspace, + &sandbox_provider_names, + ) + .await?; let global_settings = load_global_settings(state.store.as_ref()).await?; let global_policy = decode_policy_from_global_settings(&global_settings)?; @@ -2709,15 +2819,8 @@ pub async fn build_sandbox_config_snapshot( } }; - let global_settings = load_global_settings(state.store.as_ref()).await?; let sandbox_settings = load_sandbox_settings(state.store.as_ref(), &workspace, sandbox.object_name()).await?; - let provider_records = super::provider::load_provider_environment_records( - state.store.as_ref(), - &workspace, - &sandbox_provider_names, - ) - .await?; let mut provider_policy_context = provider_policy_context_from_records(&provider_profile_catalog, &provider_records); @@ -3279,7 +3382,7 @@ fn provider_policy_context_from_records( continue; } - let rule_name = openshell_policy::provider_rule_name(provider.object_name()); + let rule_name = openshell_policy::provider_rule_name(name); let mut rule = profile.network_policy_rule(&rule_name); if rule.endpoints.is_empty() { endpointless_provider_names.insert(name.clone()); @@ -3704,6 +3807,19 @@ fn provider_environment_response( // Update config handler (policy + settings mutations) // --------------------------------------------------------------------------- +/// Commit admission independently of the caller's completion wait. +pub(super) async fn handle_update_config_commit( + state: &Arc, + mut request: Request, +) -> Result, Status> { + if !request.get_ref().global + && request.get_ref().consistency == ConfigUpdateConsistency::WaitForCompletion as i32 + { + request.get_mut().consistency = ConfigUpdateConsistency::CommitOnly as i32; + } + handle_update_config(state, request).await +} + pub(super) async fn handle_update_config( state: &Arc, request: Request, @@ -3756,6 +3872,24 @@ async fn handle_update_config_inner( ) -> Result, Status> { let replay_facts = super::mutation_replay::ordinary::Facts::from_request(&request); let req = request.into_inner(); + let wait_timeout = config_wait_timeout(req.wait_timeout.as_ref())?; + let consistency = ConfigUpdateConsistency::try_from(req.consistency) + .map_err(|_| Status::invalid_argument("unknown update consistency"))?; + let consistency = if consistency == ConfigUpdateConsistency::Unspecified { + ConfigUpdateConsistency::CommitOnly + } else { + consistency + }; + if req.idempotency_key.len() > MAX_IDEMPOTENCY_KEY_BYTES { + return Err(Status::invalid_argument(format!( + "idempotency_key exceeds {MAX_IDEMPOTENCY_KEY_BYTES} bytes" + ))); + } + if !req.idempotency_key.is_empty() && req.idempotency_key.trim() != req.idempotency_key { + return Err(Status::invalid_argument( + "idempotency_key must not have leading or trailing whitespace", + )); + } validate_annotations(&req.annotations, "annotations")?; let sandbox = if req.global { if !req.sandbox.is_empty() || req.workspace_scope.is_some() { @@ -3777,9 +3911,7 @@ async fn handle_update_config_inner( state, principal, &req.sandbox, - crate::auth::workspace_authz::selected_workspace_name( - req.workspace_scope.as_ref(), - )?, + selected_workspace_name(req.workspace_scope.as_ref())?, min_role, ) .await?, @@ -3811,6 +3943,16 @@ async fn handle_update_config_inner( )); } if req.global { + if consistency == ConfigUpdateConsistency::WaitForCompletion { + return Err(Status::invalid_argument( + "WAIT_FOR_COMPLETION is only supported for sandbox-scoped updates", + )); + } + if !req.idempotency_key.is_empty() { + return Err(Status::invalid_argument( + "idempotency_key is only supported for sandbox-scoped updates", + )); + } if !req.annotations.is_empty() { return Err(Status::invalid_argument( "annotations are only supported for sandbox-scoped updates", @@ -3987,7 +4129,7 @@ async fn handle_update_config_inner( save_global_settings(state.store.as_ref(), &global_settings).await?; crate::config_delivery::publish_all_connected( state, - crate::config_delivery::ConfigComponents::SANDBOX_CONFIG, + crate::config_delivery::ConfigComponents::SANDBOX_AND_PROVIDER, ); if req.delete_setting @@ -4015,15 +4157,55 @@ async fn handle_update_config_inner( let sandbox = sandbox.expect("non-global config update resolves a sandbox"); let sandbox_id = sandbox.object_id().to_string(); + let request_fingerprint = super::mutation_replay::fingerprint(&req)?; replay_facts.resource(&sandbox)?; let mut response_annotations = sandbox_metadata_annotations(&sandbox); - if has_setting { - let _settings_guard = state.settings_mutex.lock().await; - let _sandbox_sync_guard = state.compute.sandbox_sync_guard().await.map_err(|error| { - super::persistence_error_to_status(error, "acquire policy mutation lock") - })?; + if let Some(existing) = config_update_operation::find_idempotent( + state, + &workspace, + &sandbox_id, + &req.idempotency_key, + &request_fingerprint, + ) + .await? + { + let operation = config_update_operation::public_operation(&existing)?; + return finish_config_update_operation( + state, + &operation.operation_id, + consistency, + wait_timeout, + ) + .await; + } + let _sandbox_sync_guard = if has_setting { + Some(Box::new( + Box::pin(state.compute.sandbox_sync_guard()) + .await + .map_err(|error| { + super::persistence_error_to_status(error, "acquire policy mutation lock") + })?, + )) + } else { + None + }; + + // Avoid redundant validation and snapshot construction within one gateway. + // The database transaction owns cross-replica serialization and completes + // the unchanged target dimension after locking the sandbox fence. + let config_guard = state.settings_mutex.lock().await; + + let mut projected_annotations = response_annotations.clone(); + projected_annotations.extend(req.annotations.clone()); + let sandbox_projection = crate::persistence::AtomicSandboxProjection { + sandbox_id: &sandbox_id, + annotations: &req.annotations, + expected_resource_version: req.expected_resource_version, + }; + + if has_setting { if key == POLICY_SETTING_KEY { return Err(Status::invalid_argument( "reserved key 'policy' must be set via policy commands", @@ -4044,22 +4226,50 @@ async fn handle_update_config_inner( load_sandbox_settings(state.store.as_ref(), &workspace, sandbox.object_name()) .await?; let removed = sandbox_settings.settings.remove(key).is_some(); + let mut operation_id = None; if removed { sandbox_settings.revision = sandbox_settings.revision.wrapping_add(1); - save_sandbox_settings( + let operation_record = config_update_operation::new_record( + &sandbox, + &workspace, + &req.idempotency_key, + OperationDimension::Settings, + Some(&request_fingerprint), + OperationTarget { + policy_version: 0, + settings_revision: sandbox_settings.revision, + }, + CommittedResponse { + settings_revision: sandbox_settings.revision, + deleted: true, + annotations: projected_annotations.clone(), + ..Default::default() + }, + ); + operation_id = Some( + config_update_operation::public_operation(&operation_record)?.operation_id, + ); + save_sandbox_settings_with_operation( state.store.as_ref(), &workspace, sandbox.object_name(), &sandbox_settings, + &operation_record, + Some(&sandbox_projection), ) .await?; - crate::config_delivery::publish_sandbox_components( - state, - &sandbox_id, - crate::config_delivery::ConfigComponents::SANDBOX_CONFIG, - ); } + drop(config_guard); + if let Some(operation_id) = operation_id { + return finish_config_update_operation( + state, + &operation_id, + consistency, + wait_timeout, + ) + .await; + } response_annotations = persist_update_config_annotations( state, &sandbox_id, @@ -4068,14 +4278,26 @@ async fn handle_update_config_inner( &response_annotations, ) .await?; - - return Ok(update_config_response( - 0, - String::new(), - sandbox_settings.revision, - removed, - response_annotations, - )); + return Box::pin(finish_unchanged_config_update( + state, + &sandbox, + &workspace, + ConfigOperationIdentity { + idempotency_key: &req.idempotency_key, + dimension: OperationDimension::Settings, + request_fingerprint: &request_fingerprint, + }, + update_config_response( + 0, + String::new(), + sandbox_settings.revision, + removed, + response_annotations, + ), + consistency, + wait_timeout, + )) + .await; } if globally_managed { @@ -4093,22 +4315,43 @@ async fn handle_update_config_inner( let mut sandbox_settings = load_sandbox_settings(state.store.as_ref(), &workspace, sandbox.object_name()).await?; let changed = upsert_setting_value(&mut sandbox_settings.settings, key, stored); + let mut operation_id = None; if changed { sandbox_settings.revision = sandbox_settings.revision.wrapping_add(1); - save_sandbox_settings( + let operation_record = config_update_operation::new_record( + &sandbox, + &workspace, + &req.idempotency_key, + OperationDimension::Settings, + Some(&request_fingerprint), + OperationTarget { + policy_version: 0, + settings_revision: sandbox_settings.revision, + }, + CommittedResponse { + settings_revision: sandbox_settings.revision, + annotations: projected_annotations.clone(), + ..Default::default() + }, + ); + operation_id = + Some(config_update_operation::public_operation(&operation_record)?.operation_id); + save_sandbox_settings_with_operation( state.store.as_ref(), &workspace, sandbox.object_name(), &sandbox_settings, + &operation_record, + Some(&sandbox_projection), ) .await?; - crate::config_delivery::publish_sandbox_components( - state, - &sandbox_id, - crate::config_delivery::ConfigComponents::SANDBOX_CONFIG, - ); } + drop(config_guard); + if let Some(operation_id) = operation_id { + return finish_config_update_operation(state, &operation_id, consistency, wait_timeout) + .await; + } response_annotations = persist_update_config_annotations( state, &sandbox_id, @@ -4117,14 +4360,26 @@ async fn handle_update_config_inner( &response_annotations, ) .await?; - - return Ok(update_config_response( - 0, - String::new(), - sandbox_settings.revision, - false, - response_annotations, - )); + return Box::pin(finish_unchanged_config_update( + state, + &sandbox, + &workspace, + ConfigOperationIdentity { + idempotency_key: &req.idempotency_key, + dimension: OperationDimension::Settings, + request_fingerprint: &request_fingerprint, + }, + update_config_response( + 0, + String::new(), + sandbox_settings.revision, + false, + response_annotations, + ), + consistency, + wait_timeout, + )) + .await; } let _sandbox_sync_guard = state.compute.sandbox_sync_guard().await.map_err(|error| { @@ -4152,9 +4407,12 @@ async fn handle_update_config_inner( expected_resource_version: req.expected_resource_version, provenance: &req.annotations, annotations: &req.annotations, + sandbox: &sandbox, + idempotency_key: &req.idempotency_key, + request_fingerprint: &request_fingerprint, }; let baseline_policy = spec.policy.clone(); - let (version, hash, updated_sandbox) = apply_merge_operations_with_retry( + let (version, hash, updated_sandbox, operation_id) = apply_merge_operations_with_retry( state.store.as_ref(), &sandbox_id, &workspace, @@ -4168,11 +4426,7 @@ async fn handle_update_config_inner( Some(&atomic_context), ) .await?; - crate::config_delivery::publish_sandbox_components( - state, - &sandbox_id, - crate::config_delivery::ConfigComponents::SANDBOX_AND_PROVIDER, - ); + drop(config_guard); response_annotations = if let Some(updated_sandbox) = updated_sandbox { sandbox_metadata_annotations(&updated_sandbox) } else { @@ -4221,13 +4475,30 @@ async fn handle_update_config_inner( ); emit_config_update_policy_success(sandbox_caller); - return Ok(update_config_response( - u32::try_from(version).unwrap_or(0), - hash, - 0, - false, - response_annotations, - )); + if let Some(operation_id) = operation_id { + return finish_config_update_operation(state, &operation_id, consistency, wait_timeout) + .await; + } + return Box::pin(finish_unchanged_config_update( + state, + &sandbox, + &workspace, + ConfigOperationIdentity { + idempotency_key: &req.idempotency_key, + dimension: OperationDimension::Policy, + request_fingerprint: &request_fingerprint, + }, + update_config_response( + u32::try_from(version).unwrap_or(0), + hash, + 0, + false, + response_annotations, + ), + consistency, + wait_timeout, + )) + .await; } // Sandbox-scoped policy update. @@ -4310,7 +4581,7 @@ async fn handle_update_config_inner( let payload = new_policy.encode_to_vec(); let hash = deterministic_policy_hash(&new_policy); - let (_next_version, committed_annotations) = { + let (next_version, _committed_annotations, operation_id) = { let mut committed = None; for attempt in 1..=MERGE_RETRY_LIMIT { let latest = state @@ -4338,16 +4609,51 @@ async fn handle_update_config_inner( "UpdateConfig: backfilled spec.policy from sandbox-discovered policy" ); } - return Ok(update_config_response( - u32::try_from(current.version).unwrap_or(0), - hash, - 0, - false, - response_annotations, - )); + drop(config_guard); + return Box::pin(finish_unchanged_config_update( + state, + &sandbox, + &workspace, + ConfigOperationIdentity { + idempotency_key: &req.idempotency_key, + dimension: OperationDimension::Policy, + request_fingerprint: &request_fingerprint, + }, + update_config_response( + u32::try_from(current.version).unwrap_or(0), + hash, + 0, + false, + response_annotations, + ), + consistency, + wait_timeout, + )) + .await; } let next_version = latest.as_ref().map_or(1, |record| record.version + 1); + let mut operation_annotations = response_annotations.clone(); + operation_annotations.extend(req.annotations.clone()); + let operation_record = config_update_operation::new_record( + &sandbox, + &workspace, + &req.idempotency_key, + OperationDimension::Policy, + Some(&request_fingerprint), + OperationTarget { + policy_version: u32::try_from(next_version).unwrap_or(u32::MAX), + settings_revision: 0, + }, + CommittedResponse { + policy_version: u32::try_from(next_version).unwrap_or(u32::MAX), + policy_hash: hash.clone(), + annotations: operation_annotations, + ..Default::default() + }, + ); + let operation_id = + config_update_operation::public_operation(&operation_record)?.operation_id; let write = AtomicPolicyRevisionWrite { id: uuid::Uuid::new_v4().to_string(), sandbox_id: sandbox_id.clone(), @@ -4359,12 +4665,16 @@ async fn handle_update_config_inner( expected_resource_version: req.expected_resource_version, annotations: req.annotations.clone(), backfill_policy: backfill_policy.clone(), + operation: Some(operation_record), }; match state.store.put_policy_revision_atomic(&write).await { Ok(updated_sandbox) => { - committed = - Some((next_version, sandbox_metadata_annotations(&updated_sandbox))); + committed = Some(( + next_version, + sandbox_metadata_annotations(&updated_sandbox), + operation_id, + )); break; } Err(error) if error.is_unique_violation_on("objects_version_uq") => { @@ -4390,12 +4700,7 @@ async fn handle_update_config_inner( )) })? }; - response_annotations = committed_annotations; - crate::config_delivery::publish_sandbox_components( - state, - &sandbox_id, - crate::config_delivery::ConfigComponents::SANDBOX_AND_PROVIDER, - ); + drop(config_guard); state.sandbox_watch_bus.notify(&sandbox_id); if backfill_policy.is_some() { @@ -4405,56 +4710,6 @@ async fn handle_update_config_inner( ); } - let latest = state - .store - .get_latest_policy(&sandbox_id) - .await - .map_err(|e| Status::internal(format!("fetch latest policy failed: {e}")))?; - - let payload = new_policy.encode_to_vec(); - let hash = deterministic_policy_hash(&new_policy); - - if let Some(ref current) = latest - && canonical_policy_record_matches_for_deduplication(current, &hash) - { - return Ok(Response::new(UpdateConfigResponse { - version: u32::try_from(current.version).unwrap_or(0), - policy_hash: hash, - settings_revision: 0, - deleted: false, - annotations: response_annotations, - })); - } - - let next_version = latest.map_or(1, |r| r.version + 1); - let policy_id = uuid::Uuid::new_v4().to_string(); - - state - .store - .put_policy_revision( - &policy_id, - &sandbox_id, - &workspace, - next_version, - &payload, - &hash, - ) - .await - .map_err(|e| Status::internal(format!("persist policy revision failed: {e}")))?; - - crate::config_delivery::publish_sandbox_components( - state, - &sandbox_id, - crate::config_delivery::ConfigComponents::SANDBOX_AND_PROVIDER, - ); - - let _ = state - .store - .supersede_older_policies(&sandbox_id, next_version) - .await; - - state.sandbox_watch_bus.notify(&sandbox_id); - info!( sandbox_id = %sandbox_id, version = next_version, @@ -4463,13 +4718,7 @@ async fn handle_update_config_inner( ); emit_full_policy_update_success(sandbox_caller, next_version); - Ok(update_config_response( - u32::try_from(next_version).unwrap_or(0), - hash, - 0, - false, - response_annotations, - )) + finish_config_update_operation(state, &operation_id, consistency, wait_timeout).await } // --------------------------------------------------------------------------- @@ -4496,9 +4745,7 @@ pub(super) async fn handle_get_sandbox_policy_status( state, &principal, &req.sandbox, - crate::auth::workspace_authz::selected_workspace_name( - req.workspace_scope.as_ref(), - )?, + selected_workspace_name(req.workspace_scope.as_ref())?, MinWorkspaceRole::User, ) .await?, @@ -4542,6 +4789,40 @@ pub(super) async fn handle_get_sandbox_policy_status( })) } +pub(super) async fn handle_get_config_update_operation( + state: &Arc, + request: Request, +) -> Result, Status> { + let principal = super::extract_principal(&request)?; + let req = request.into_inner(); + if req.operation_id.is_empty() { + return Err(Status::invalid_argument("operation_id is required")); + } + let authz = authorize_workspace( + &state.store, + &state.admin_role, + &principal, + selected_workspace_name(req.workspace_scope.as_ref())?, + MinWorkspaceRole::User, + ) + .await?; + let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) + .await? + .name; + let record = config_update_operation::get_record(state, &req.operation_id) + .await? + .filter(|record| { + record + .metadata + .as_ref() + .is_some_and(|metadata| metadata.workspace == workspace) + }) + .ok_or_else(|| Status::not_found("update operation not found"))?; + Ok(Response::new(GetConfigUpdateOperationResponse { + operation: Some(config_update_operation::public_operation(&record)?), + })) +} + pub(super) async fn handle_list_sandbox_policies( state: &Arc, request: Request, @@ -4562,9 +4843,7 @@ pub(super) async fn handle_list_sandbox_policies( state, &principal, &req.sandbox, - crate::auth::workspace_authz::selected_workspace_name( - req.workspace_scope.as_ref(), - )?, + selected_workspace_name(req.workspace_scope.as_ref())?, MinWorkspaceRole::User, ) .await?, @@ -4967,7 +5246,7 @@ pub(super) async fn handle_get_sandbox_logs( state, &principal, &req.sandbox, - crate::auth::workspace_authz::selected_workspace_name(req.workspace_scope.as_ref())?, + selected_workspace_name(req.workspace_scope.as_ref())?, MinWorkspaceRole::User, ) .await?; @@ -5110,7 +5389,7 @@ pub(super) async fn handle_submit_policy_analysis( let req = request.into_inner(); let workspace = super::workspace::resolve_workspace( state.store.as_ref(), - crate::auth::workspace_authz::selected_workspace_name(req.workspace_scope.as_ref())?, + selected_workspace_name(req.workspace_scope.as_ref())?, ) .await? .name; @@ -5558,7 +5837,7 @@ pub(super) async fn handle_get_draft_policy( state, &principal, &req.sandbox, - crate::auth::workspace_authz::selected_workspace_name(req.workspace_scope.as_ref())?, + selected_workspace_name(req.workspace_scope.as_ref())?, MinWorkspaceRole::User, ) .await?; @@ -5629,7 +5908,7 @@ async fn handle_approve_draft_chunk_inner( state, &principal, &req.sandbox, - crate::auth::workspace_authz::selected_workspace_name(req.workspace_scope.as_ref())?, + selected_workspace_name(req.workspace_scope.as_ref())?, MinWorkspaceRole::Admin, ) .await?; @@ -5780,7 +6059,7 @@ async fn handle_reject_draft_chunk_inner( state, &principal, &req.sandbox, - crate::auth::workspace_authz::selected_workspace_name(req.workspace_scope.as_ref())?, + selected_workspace_name(req.workspace_scope.as_ref())?, MinWorkspaceRole::Admin, ) .await?; @@ -5868,7 +6147,7 @@ pub(super) async fn handle_approve_all_draft_chunks( state: &Arc, request: Request, ) -> Result, Status> { - let result = handle_approve_all_draft_chunks_inner(state, request).await; + let result = Box::pin(handle_approve_all_draft_chunks_inner(state, request)).await; if result.is_err() { emit_policy_decision_failure(PolicyDecisionOperation::ApproveAll, 0); } @@ -5886,7 +6165,7 @@ async fn handle_approve_all_draft_chunks_inner( state, &principal, &req.sandbox, - crate::auth::workspace_authz::selected_workspace_name(req.workspace_scope.as_ref())?, + selected_workspace_name(req.workspace_scope.as_ref())?, MinWorkspaceRole::Admin, ) .await?; @@ -6101,7 +6380,7 @@ async fn handle_approve_all_draft_chunks_inner( ) .await { - Ok((version, hash, _)) => (version, hash), + Ok((version, hash, _, _)) => (version, hash), Err(status) => { for (chunk, _, _) in &accepted { persist_pending_application_error(state, &chunk.id, &status).await; @@ -6193,7 +6472,7 @@ pub(super) async fn handle_edit_draft_chunk( state, &principal, &req.sandbox, - crate::auth::workspace_authz::selected_workspace_name(req.workspace_scope.as_ref())?, + selected_workspace_name(req.workspace_scope.as_ref())?, MinWorkspaceRole::Admin, ) .await?; @@ -6265,7 +6544,7 @@ async fn handle_undo_draft_chunk_inner( state, &principal, &req.sandbox, - crate::auth::workspace_authz::selected_workspace_name(req.workspace_scope.as_ref())?, + selected_workspace_name(req.workspace_scope.as_ref())?, MinWorkspaceRole::Admin, ) .await?; @@ -6358,7 +6637,7 @@ pub(super) async fn handle_clear_draft_chunks( state, &principal, &req.sandbox, - crate::auth::workspace_authz::selected_workspace_name(req.workspace_scope.as_ref())?, + selected_workspace_name(req.workspace_scope.as_ref())?, MinWorkspaceRole::Admin, ) .await?; @@ -6394,7 +6673,7 @@ pub(super) async fn handle_get_draft_history( state, &principal, &req.sandbox, - crate::auth::workspace_authz::selected_workspace_name(req.workspace_scope.as_ref())?, + selected_workspace_name(req.workspace_scope.as_ref())?, MinWorkspaceRole::User, ) .await?; @@ -7080,6 +7359,9 @@ struct AtomicPolicyWriteContext<'a> { expected_resource_version: u64, provenance: &'a HashMap, annotations: &'a HashMap, + sandbox: &'a Sandbox, + idempotency_key: &'a str, + request_fingerprint: &'a str, } struct PolicyCredentialBindingValidationContext<'a> { @@ -7234,7 +7516,7 @@ async fn apply_merge_operations_with_retry( validation_context: PolicyMergeValidationContext<'_>, expected_current_effective_hash: Option<&str>, atomic_context: Option<&AtomicPolicyWriteContext<'_>>, -) -> Result<(i64, String, Option), Status> { +) -> Result<(i64, String, Option, Option), Status> { let provider_layers = validation_context.provider_layers; for attempt in 1..=MERGE_RETRY_LIMIT { let latest = store @@ -7293,11 +7575,11 @@ async fn apply_merge_operations_with_retry( && current_hash.as_deref() == Some(hash.as_str()) && atomic_context.is_none_or(|context| current.provenance == *context.provenance) { - return Ok((current.version, hash, None)); + return Ok((current.version, hash, None, None)); } if latest.is_none() && !merged.changed { - return Ok((0, hash, None)); + return Ok((0, hash, None, None)); } let payload = new_policy.encode_to_vec(); @@ -7305,6 +7587,27 @@ async fn apply_merge_operations_with_retry( let policy_id = uuid::Uuid::new_v4().to_string(); let write_result = if let Some(context) = atomic_context { + let mut response_annotations = sandbox_metadata_annotations(context.sandbox); + response_annotations.extend(context.annotations.clone()); + let operation_record = config_update_operation::new_record( + context.sandbox, + workspace, + context.idempotency_key, + OperationDimension::Policy, + Some(context.request_fingerprint), + OperationTarget { + policy_version: u32::try_from(next_version).unwrap_or(u32::MAX), + settings_revision: 0, + }, + CommittedResponse { + policy_version: u32::try_from(next_version).unwrap_or(u32::MAX), + policy_hash: hash.clone(), + annotations: response_annotations, + ..Default::default() + }, + ); + let operation_id = + config_update_operation::public_operation(&operation_record)?.operation_id; store .put_policy_revision_atomic(&AtomicPolicyRevisionWrite { id: policy_id, @@ -7317,9 +7620,10 @@ async fn apply_merge_operations_with_retry( expected_resource_version: context.expected_resource_version, annotations: context.annotations.clone(), backfill_policy: None, + operation: Some(operation_record), }) .await - .map(Some) + .map(|sandbox| (Some(sandbox), Some(operation_id))) } else { store .put_policy_revision( @@ -7331,11 +7635,11 @@ async fn apply_merge_operations_with_retry( &hash, ) .await - .map(|()| None) + .map(|()| (None, None)) }; match write_result { - Ok(updated_sandbox) => { + Ok((updated_sandbox, operation_id)) => { if atomic_context.is_none() { let _ = store .supersede_older_policies(sandbox_id, next_version) @@ -7352,7 +7656,7 @@ async fn apply_merge_operations_with_retry( ); } - return Ok((next_version, hash, updated_sandbox)); + return Ok((next_version, hash, updated_sandbox, operation_id)); } Err(e) => { if e.is_unique_violation_on("objects_version_uq") { @@ -7414,7 +7718,7 @@ async fn merge_chunk_into_policy_with_validation( None, ) .await - .map(|(version, hash, _)| (version, hash)) + .map(|(version, hash, _, _)| (version, hash)) } #[cfg(test)] @@ -7461,7 +7765,7 @@ async fn remove_chunk_from_policy( None, ) .await - .map(|(version, hash, _)| (version, hash)) + .map(|(version, hash, _, _)| (version, hash)) } // --------------------------------------------------------------------------- @@ -7582,6 +7886,7 @@ pub(super) async fn load_sandbox_settings( load_settings_record(store, SANDBOX_SETTINGS_OBJECT_TYPE, workspace, sandbox_name).await } +#[cfg(test)] pub(super) async fn save_sandbox_settings( store: &Store, workspace: &str, @@ -7598,6 +7903,55 @@ pub(super) async fn save_sandbox_settings( .await } +async fn save_sandbox_settings_with_operation( + store: &Store, + workspace: &str, + sandbox_name: &str, + settings: &StoredSettings, + operation: &crate::storage_proto::StoredConfigUpdateOperation, + sandbox_projection: Option<&crate::persistence::AtomicSandboxProjection<'_>>, +) -> Result<(), Status> { + use crate::persistence::WriteCondition; + + let payload = serde_json::to_vec(settings) + .map_err(|error| Status::internal(format!("encode settings payload failed: {error}")))?; + let (id, condition) = if settings.resource_version == 0 { + (uuid::Uuid::new_v4().to_string(), WriteCondition::MustCreate) + } else { + let existing = store + .get_by_name(SANDBOX_SETTINGS_OBJECT_TYPE, workspace, sandbox_name) + .await + .map_err(|error| Status::internal(format!("fetch settings for CAS failed: {error}")))? + .ok_or_else(|| Status::not_found("settings disappeared since load"))?; + ( + existing.id, + WriteCondition::MatchResourceVersion(settings.resource_version), + ) + }; + store + .put_if_with_operation( + SANDBOX_SETTINGS_OBJECT_TYPE, + &id, + sandbox_name, + workspace, + &payload, + condition, + operation, + sandbox_projection, + ) + .await + .map_err(|error| match error { + crate::persistence::PersistenceError::Conflict { .. } => { + Status::aborted("settings were modified concurrently; please retry") + } + crate::persistence::PersistenceError::UniqueViolation { .. } => Status::aborted( + "settings or idempotency key was created concurrently; please retry", + ), + other => super::persistence_error_to_status(other, "persist settings and operation"), + })?; + Ok(()) +} + async fn load_settings_record( store: &Store, object_type: &str, @@ -7799,6 +8153,7 @@ mod tests { Principal, SandboxIdentitySource, SandboxPrincipal, UserPrincipal, }; use crate::grpc::test_support::{authed_request, test_server_state}; + use openshell_core::proto::{ConfigApplyOutcome, ConfigComponent, ConfigComponentApplyResult}; /// An in-memory store with the example profiles imported at platform scope. /// @@ -9330,7 +9685,7 @@ mod tests { .await .expect("store legacy merge base"); - let (version, hash, _) = apply_merge_operations_with_retry( + let (version, hash, _, _) = apply_merge_operations_with_retry( &store, &sandbox_id, "default", @@ -12833,7 +13188,6 @@ mod tests { Vec::new(), ); state.store.put_message(&sandbox).await.unwrap(); - let error = super::super::sandbox::handle_attach_sandbox_provider( &state, authed_request(openshell_core::proto::AttachSandboxProviderRequest { @@ -21642,6 +21996,10 @@ mod tests { merge_operations: vec![], expected_resource_version: current_version, annotations: HashMap::new(), + + consistency: ConfigUpdateConsistency::CommitOnly.into(), + idempotency_key: String::new(), + wait_timeout: None, }), ) .await @@ -21741,6 +22099,10 @@ mod tests { merge_operations: vec![], expected_resource_version: current_version, annotations: annotations.clone(), + + consistency: ConfigUpdateConsistency::CommitOnly.into(), + idempotency_key: String::new(), + wait_timeout: None, }), ) .await @@ -22794,6 +23156,10 @@ mod tests { merge_operations: vec![], expected_resource_version: 99, // stale version annotations: HashMap::new(), + + consistency: ConfigUpdateConsistency::CommitOnly.into(), + idempotency_key: String::new(), + wait_timeout: None, }), ) .await @@ -22896,6 +23262,10 @@ mod tests { merge_operations: vec![], expected_resource_version: initial_version, annotations: HashMap::new(), + + consistency: ConfigUpdateConsistency::CommitOnly.into(), + idempotency_key: String::new(), + wait_timeout: None, }), ) .await @@ -23275,8 +23645,267 @@ mod tests { ); } + #[tokio::test] + async fn stopped_sandbox_setting_update_with_default_version_commits_with_annotations() { + let state = test_server_state().await; + let mut sandbox = test_sandbox( + "sb-inactive-operation", + "inactive-operation", + ProtoSandboxPolicy::default(), + Vec::new(), + ); + sandbox.set_phase(openshell_core::proto::SandboxPhase::Stopped as i32); + state.store.put_message(&sandbox).await.unwrap(); + let request = || { + with_user(Request::new(UpdateConfigRequest { + sandbox: "inactive-operation".to_string(), + setting_key: "ocsf_json_enabled".to_string(), + setting_value: Some(SettingValue { + value: Some(setting_value::Value::BoolValue(true)), + }), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + consistency: ConfigUpdateConsistency::WaitForCompletion.into(), + idempotency_key: "inactive-setting-1".to_string(), + expected_resource_version: 0, + annotations: HashMap::from([("change-ticket".to_string(), "1234".to_string())]), + ..Default::default() + })) + }; + + let first = handle_update_config(&state, request()) + .await + .unwrap() + .into_inner(); + let operation = first.operation.expect("durable operation"); + assert_eq!( + openshell_core::proto::ConfigUpdateOperationState::try_from(operation.state).unwrap(), + openshell_core::proto::ConfigUpdateOperationState::Inactive + ); + assert_eq!(first.settings_revision, 1); + + let retried = handle_update_config(&state, request()) + .await + .unwrap() + .into_inner(); + assert_eq!( + retried.operation.unwrap().operation_id, + operation.operation_id + ); + assert_eq!(retried.settings_revision, first.settings_revision); + + let stored = load_sandbox_settings(&state.store, "default", "inactive-operation") + .await + .unwrap(); + assert_eq!( + stored.settings.get("ocsf_json_enabled"), + Some(&StoredSettingValue::Bool(true)) + ); + let stored_sandbox = state + .store + .get_message_by_name::("default", "inactive-operation") + .await + .unwrap() + .unwrap(); + assert_eq!( + stored_sandbox + .metadata + .as_ref() + .unwrap() + .annotations + .get("change-ticket") + .map(String::as_str), + Some("1234") + ); + } + + #[tokio::test] + async fn idempotency_key_rejects_a_different_update_payload() { + let state = test_server_state().await; + let mut sandbox = test_sandbox( + "sb-idempotency-payload", + "idempotency-payload", + ProtoSandboxPolicy::default(), + Vec::new(), + ); + sandbox.set_phase(openshell_core::proto::SandboxPhase::Stopped as i32); + state.store.put_message(&sandbox).await.unwrap(); + let request = |value| { + with_user(Request::new(UpdateConfigRequest { + sandbox: sandbox.object_name().to_string(), + setting_key: "ocsf_json_enabled".to_string(), + setting_value: Some(SettingValue { + value: Some(setting_value::Value::BoolValue(value)), + }), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + idempotency_key: "payload-bound-key".to_string(), + ..Default::default() + })) + }; + + handle_update_config(&state, request(true)).await.unwrap(); + let error = handle_update_config(&state, request(false)) + .await + .unwrap_err(); + assert_eq!(error.code(), Code::InvalidArgument); + assert!(error.message().contains("different request payload")); + } + + #[tokio::test] + async fn settings_operation_without_policy_history_applies_and_wakes_local_waiter() { + let state = test_server_state().await; + let sandbox = test_sandbox( + "sb-operation-wake", + "operation-wake", + ProtoSandboxPolicy::default(), + Vec::new(), + ); + state.store.put_message(&sandbox).await.unwrap(); + let response = handle_update_config( + &state, + with_user(Request::new(UpdateConfigRequest { + sandbox: sandbox.object_name().to_string(), + setting_key: "ocsf_json_enabled".to_string(), + setting_value: Some(SettingValue { + value: Some(setting_value::Value::BoolValue(true)), + }), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + consistency: ConfigUpdateConsistency::CommitOnly.into(), + idempotency_key: "wake-local-waiter".to_string(), + ..Default::default() + })), + ) + .await + .unwrap() + .into_inner(); + let operation = response.operation.unwrap(); + let operation_id = operation.operation_id.clone(); + let snapshot = build_sandbox_config_snapshot(&state, &sandbox) + .await + .unwrap(); + config_update_operation::associate_pending_with_snapshot( + &state, + sandbox.object_id(), + &snapshot, + ) + .await + .unwrap(); + let requested_revision = config_update_operation::get_record(&state, &operation_id) + .await + .unwrap() + .unwrap() + .operation + .unwrap() + .target_revision + .unwrap(); + let waiter_state = state.clone(); + let waiter = tokio::spawn(async move { + config_update_operation::wait_for_terminal( + &waiter_state, + &operation_id, + std::time::Duration::from_secs(5), + ) + .await + }); + tokio::task::yield_now().await; + config_update_operation::complete_from_apply_result( + &state, + sandbox.object_id(), + &ConfigComponentApplyResult { + component: ConfigComponent::SandboxConfig.into(), + requested_revision: Some(requested_revision), + outcome: ConfigApplyOutcome::Applied.into(), + ..Default::default() + }, + ) + .await + .unwrap(); + let terminal = tokio::time::timeout(std::time::Duration::from_millis(500), waiter) + .await + .expect("terminal transition should notify the local waiter") + .unwrap() + .unwrap(); + assert_eq!( + openshell_core::proto::ConfigUpdateOperationState::try_from(terminal.state).unwrap(), + openshell_core::proto::ConfigUpdateOperationState::Applied + ); + } + + #[tokio::test] + async fn operation_waiter_recovers_when_notification_is_missed() { + let state = test_server_state().await; + let sandbox = test_sandbox( + "sb-operation-poll", + "operation-poll", + ProtoSandboxPolicy::default(), + Vec::new(), + ); + state.store.put_message(&sandbox).await.unwrap(); + state + .store + .put_policy_revision( + "operation-poll-policy", + sandbox.object_id(), + "default", + 1, + &ProtoSandboxPolicy::default().encode_to_vec(), + "operation-poll-policy-hash", + ) + .await + .unwrap(); + let response = handle_update_config( + &state, + with_user(Request::new(UpdateConfigRequest { + sandbox: sandbox.object_name().to_string(), + setting_key: "ocsf_json_enabled".to_string(), + setting_value: Some(SettingValue { + value: Some(setting_value::Value::BoolValue(true)), + }), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + consistency: ConfigUpdateConsistency::CommitOnly.into(), + idempotency_key: "poll-missed-notification".to_string(), + ..Default::default() + })), + ) + .await + .unwrap() + .into_inner(); + let operation_id = response.operation.unwrap().operation_id; + let waiter_state = state.clone(); + let waiter_operation_id = operation_id.clone(); + let waiter = tokio::spawn(async move { + config_update_operation::wait_for_terminal( + &waiter_state, + &waiter_operation_id, + std::time::Duration::from_secs(5), + ) + .await + }); + tokio::task::yield_now().await; + let mut record = config_update_operation::get_record(&state, &operation_id) + .await + .unwrap() + .unwrap(); + record.operation.as_mut().unwrap().state = + openshell_core::proto::ConfigUpdateOperationState::Applied.into(); + let resource_version = record.metadata.as_ref().unwrap().resource_version; + state + .store + .update_config_operation_cas(&record, resource_version) + .await + .unwrap(); + let terminal = tokio::time::timeout(std::time::Duration::from_secs(2), waiter) + .await + .expect("point polling should recover a missed notification") + .unwrap() + .unwrap(); + assert_eq!( + openshell_core::proto::ConfigUpdateOperationState::try_from(terminal.state).unwrap(), + openshell_core::proto::ConfigUpdateOperationState::Applied + ); + } + #[test] - fn provider_stream_values_expand_to_legacy_polling_response() { + fn provider_stream_values_preserve_credential_metadata() { let response = provider_environment_response(ProviderEnvironmentSnapshot { provider_env_revision: 9, values: vec![ diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 5c35010633..6d55cd0f34 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -31,6 +31,7 @@ mod otel_tracing; mod pagination; mod persistence; pub(crate) mod policy_store; +mod provider_config_operation; mod provider_profile_sources; mod provider_refresh; mod readiness; @@ -277,6 +278,9 @@ pub struct ServerState { /// In-memory bus for sandbox update notifications. pub sandbox_watch_bus: SandboxWatchBus, + /// Gateway-local wakeups for durable configuration-operation waiters. + pub(crate) config_update_operation_watch_bus: config_update_operation::OperationWatchBus, + /// In-memory bus for server process logs. pub tracing_log_bus: TracingLogBus, @@ -453,6 +457,7 @@ impl ServerState { credentials, sandbox_index, sandbox_watch_bus, + config_update_operation_watch_bus: config_update_operation::OperationWatchBus::new(), tracing_log_bus, telemetry: telemetry::TelemetryState::new(), ssh_connections_by_token: Mutex::new(HashMap::new()), @@ -883,6 +888,9 @@ pub(crate) async fn run_server( grpc::policy::backfill_legacy_policy_history(&state) .await .map_err(|error| Error::execution(error.to_string()))?; + config_update_operation::repair_query_projections(&state) + .await + .map_err(|error| Error::execution(error.to_string()))?; // Reconcile local-driver running intent before watchers spawn so their // first snapshots observe the post-start backend state. Explicitly stopped @@ -1034,6 +1042,7 @@ pub(crate) async fn run_server( ssh_sessions::spawn_session_reaper(store.clone(), Duration::from_hours(1)); supervisor_session::spawn_relay_reaper(state.clone(), Duration::from_secs(30)); config_delivery::spawn_owner_reconciler(state.clone(), Duration::from_secs(30)); + config_update_operation::spawn_reconciler(state.clone(), Duration::from_secs(5)); provider_refresh::spawn_refresh_worker(state.clone(), Duration::from_mins(1)); shutdown_signal().await; diff --git a/crates/openshell-server/src/persistence/legacy_time_wire.rs b/crates/openshell-server/src/persistence/legacy_time_wire.rs index d5f12bab1b..cbb94e33f7 100644 --- a/crates/openshell-server/src/persistence/legacy_time_wire.rs +++ b/crates/openshell-server/src/persistence/legacy_time_wire.rs @@ -40,6 +40,7 @@ pub(super) fn migrate(object_type: &str, payload: &[u8]) -> PersistenceResult Option<&'static str> { match object_type { + "config_update_operation" => Some("openshell.storage.v1.StoredConfigUpdateOperation"), "sandbox" => Some("openshell.v1.Sandbox"), "provider" => Some("openshell.datamodel.v1.Provider"), "workspace" => Some("openshell.datamodel.v1.Workspace"), @@ -223,8 +224,15 @@ fn conversion(message: &str, field: u32) -> Option { TimestampString as TS, }; match (message, field) { + ( + "openshell.storage.v1.StoredConfigUpdateOperation" + | "openshell.v1.ConfigUpdateOperation" + | "openshell.datamodel.v1.ObjectMeta", + 8, + ) => Some(T { new_tag: 108 }), + ("openshell.v1.ConfigUpdateOperation", 9) => Some(T { new_tag: 109 }), + ("openshell.v1.ConfigUpdateOperation", 10) => Some(T { new_tag: 110 }), ("openshell.datamodel.v1.ObjectMeta", 3) => Some(T { new_tag: 103 }), - ("openshell.datamodel.v1.ObjectMeta", 8) => Some(T { new_tag: 108 }), ("openshell.v1.SshSession", 4) => Some(T { new_tag: 104 }), ("openshell.datamodel.v1.Provider", 5) => Some(M { new_tag: 105 }), ("openshell.v1.SandboxCondition", 5) => Some(TS { new_tag: 105 }), @@ -412,6 +420,41 @@ mod tests { metadata: Option, } + #[test] + fn legacy_configuration_operation_preserves_completion_and_retry_times() { + // Stage 3 before integration with the protobuf time migration: + // Applied, created=1000ms, updated=2000ms, completed=3000ms, retry=4000ms. + let payload = hex::decode("120b280240e80748d00f50b81740a01f").unwrap(); + let migrated = migrate("config_update_operation", &payload).unwrap(); + let record = + crate::storage_proto::StoredConfigUpdateOperation::decode(migrated.as_slice()).unwrap(); + let operation = record.operation.as_ref().unwrap(); + assert_eq!( + operation.state, + openshell_core::proto::ConfigUpdateOperationState::Applied as i32 + ); + assert_eq!( + openshell_core::time::timestamp_to_millis(operation.created_time.as_ref().unwrap()) + .unwrap(), + 1000 + ); + assert_eq!( + openshell_core::time::timestamp_to_millis(operation.updated_time.as_ref().unwrap()) + .unwrap(), + 2000 + ); + assert_eq!( + openshell_core::time::timestamp_to_millis(operation.completed_time.as_ref().unwrap()) + .unwrap(), + 3000 + ); + assert_eq!(record.next_attempt_at_ms(), 4000); + assert_eq!( + migrate("config_update_operation", &migrated).unwrap(), + migrated + ); + } + #[test] fn migrates_nested_metadata_and_timestamp_maps() { let legacy = LegacyProvider { diff --git a/crates/openshell-server/src/persistence/mod.rs b/crates/openshell-server/src/persistence/mod.rs index e1d3f80c98..b2184ec4a9 100644 --- a/crates/openshell-server/src/persistence/mod.rs +++ b/crates/openshell-server/src/persistence/mod.rs @@ -29,6 +29,62 @@ pub const CONFIG_COMPONENT_OBSERVATION_OBJECT_TYPE: &str = "config_component_obs pub type PersistenceResult = Result; +/// Optional sandbox projection committed with a settings mutation and its +/// durable operation. +pub struct AtomicSandboxProjection<'a> { + pub sandbox_id: &'a str, + pub annotations: &'a HashMap, + pub expected_resource_version: u64, +} + +impl AtomicSandboxProjection<'_> { + fn apply_and_sync_operation_response( + &self, + payload: &[u8], + current_resource_version: u64, + operation_record: &mut crate::storage_proto::StoredConfigUpdateOperation, + ) -> PersistenceResult<(openshell_core::proto::Sandbox, bool)> { + use openshell_core::SetResourceVersion as _; + use prost::Message as _; + + if self.expected_resource_version != 0 + && self.expected_resource_version != current_resource_version + { + return Err(PersistenceError::Conflict { + current_resource_version: Some(current_resource_version), + }); + } + + let payload = migrate_legacy_time_fields("sandbox", payload)?; + let mut sandbox = + openshell_core::proto::Sandbox::decode(payload.as_slice()).map_err(|error| { + PersistenceError::Decode(format!("decode sandbox payload failed: {error}")) + })?; + sandbox.set_resource_version(current_resource_version); + let metadata = sandbox.metadata.as_mut().ok_or_else(|| { + PersistenceError::Decode("sandbox payload missing metadata".to_string()) + })?; + let mut changed = false; + for (key, value) in self.annotations { + if metadata.annotations.get(key) != Some(value) { + metadata.annotations.insert(key.clone(), value.clone()); + changed = true; + } + } + operation_record + .response_annotations + .clone_from(&metadata.annotations); + Ok((sandbox, changed)) + } +} + +/// Result of a compare-and-swap update that already has the current payload. +#[derive(Debug)] +pub enum KnownVersionUpdate { + Changed(T), + Conflict, +} + /// Maximum number of object ids sent in one set-based delete statement. /// /// Keep this well below `SQLite`'s bind-variable limit. Backends split larger @@ -394,6 +450,101 @@ impl Store { )) } + /// Write desired state and its durable update operation in one database + /// transaction. Used by sandbox-scoped settings mutations. + #[allow(clippy::too_many_arguments)] + pub async fn put_if_with_operation( + &self, + object_type: &str, + id: &str, + name: &str, + workspace: &str, + payload: &[u8], + condition: WriteCondition, + operation: &crate::storage_proto::StoredConfigUpdateOperation, + sandbox_projection: Option<&AtomicSandboxProjection<'_>>, + ) -> PersistenceResult { + store_dispatch_traced!(self.put_if_with_operation( + object_type, + id, + name, + workspace, + payload, + condition, + operation, + sandbox_projection + )) + } + + /// Persist completion tracking for a request whose desired value is unchanged. + pub async fn insert_existing_config_operation( + &self, + operation: &crate::storage_proto::StoredConfigUpdateOperation, + workspace: &str, + sandbox_name: &str, + ) -> PersistenceResult<()> { + store_dispatch!(self.insert_existing_config_operation(operation, workspace, sandbox_name)) + } + + /// Update an operation payload and its query columns with one CAS write. + pub async fn update_config_operation_cas( + &self, + operation: &crate::storage_proto::StoredConfigUpdateOperation, + expected_resource_version: u64, + ) -> PersistenceResult> + { + let resource_version = store_dispatch!( + self.update_config_operation_cas(operation, expected_resource_version) + )?; + let Some(resource_version) = resource_version else { + return Ok(KnownVersionUpdate::Conflict); + }; + let mut updated = operation.clone(); + updated.set_resource_version(resource_version); + Ok(KnownVersionUpdate::Changed(updated)) + } + + /// Repair operation query columns from the authoritative protobuf payload + /// without changing the payload or resource version. + pub async fn repair_config_operation_projection( + &self, + operation: &crate::storage_proto::StoredConfigUpdateOperation, + expected_resource_version: u64, + ) -> PersistenceResult { + store_dispatch!( + self.repair_config_operation_projection(operation, expected_resource_version) + ) + } + + /// Return pending operations for one sandbox. Terminal history is excluded + /// by SQL before protobuf payloads are decoded. + pub async fn list_pending_config_operations_for_scope( + &self, + scope: &str, + ) -> PersistenceResult> { + store_dispatch!(self.list_pending_config_operations_for_scope(scope))? + .into_iter() + .map(decode_record) + .collect() + } + + /// Return a bounded, stable batch of pending operations whose retry time + /// has arrived. + pub async fn list_due_config_update_operations( + &self, + now_ms: i64, + limit: u32, + ) -> PersistenceResult> { + store_dispatch!(self.list_due_config_update_operations(now_ms, limit))? + .into_iter() + .map(decode_record) + .collect() + } + + pub async fn count_pending_config_update_operations(&self) -> PersistenceResult { + store_dispatch!(self.count_pending_config_update_operations()) + } + /// Delete an object by id with compare-and-swap support. /// /// # Arguments diff --git a/crates/openshell-server/src/persistence/postgres.rs b/crates/openshell-server/src/persistence/postgres.rs index 0adf5f9a42..7ce54839f4 100644 --- a/crates/openshell-server/src/persistence/postgres.rs +++ b/crates/openshell-server/src/persistence/postgres.rs @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use super::{ - DraftChunkRecord, ObjectCursor, ObjectListQuery, ObjectRecord, PersistenceError, - PersistenceResult, PolicyRecord, WriteCondition, WriteResult, current_time_ms, map_db_error, - map_migrate_error, + AtomicSandboxProjection, DraftChunkRecord, ObjectCursor, ObjectListQuery, ObjectRecord, + PersistenceError, PersistenceResult, PolicyRecord, WriteCondition, WriteResult, + current_time_ms, map_db_error, map_migrate_error, }; use crate::policy_store::{ AtomicPolicyRevisionWrite, draft_chunk_payload_from_record, draft_chunk_record_from_parts, @@ -30,6 +30,118 @@ pub(super) fn embedded_migration_sql(version: i64) -> Option<&'static str> { use super::{DELETE_MANY_BATCH_SIZE, DRAFT_CHUNK_OBJECT_TYPE, POLICY_OBJECT_TYPE}; +async fn insert_update_operation_postgres( + tx: &mut sqlx::Transaction<'_, Postgres>, + record: &crate::storage_proto::StoredConfigUpdateOperation, + now_ms: i64, +) -> PersistenceResult<()> { + let metadata = record + .metadata + .as_ref() + .ok_or_else(|| PersistenceError::Encode("update operation metadata missing".to_string()))?; + let operation = record + .operation + .as_ref() + .ok_or_else(|| PersistenceError::Encode("update operation payload missing".to_string()))?; + let state = openshell_core::proto::ConfigUpdateOperationState::try_from(operation.state) + .unwrap_or_default(); + sqlx::query( + r" +INSERT INTO objects ( + object_type, id, name, workspace, scope, version, status, payload, + created_at_ms, updated_at_ms, labels, resource_version, next_attempt_at_ms +) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $9, '{}'::jsonb, 1, $10) +", + ) + .bind(crate::config_update_operation::CONFIG_UPDATE_OPERATION_OBJECT_TYPE) + .bind(&metadata.id) + .bind(&metadata.name) + .bind(&metadata.workspace) + .bind(&operation.sandbox_id) + .bind(Option::::None) + .bind(state.as_str_name()) + .bind(record.encode_to_vec()) + .bind(now_ms) + .bind(record.next_attempt_at_ms()) + .execute(&mut **tx) + .await + .map_err(|error| map_db_error(&error))?; + Ok(()) +} + +async fn lock_sandbox_config_fence( + tx: &mut sqlx::Transaction<'_, Postgres>, + sandbox_id: &str, +) -> PersistenceResult<()> { + sqlx::query( + "INSERT INTO sandbox_config_fences (sandbox_id) VALUES ($1) ON CONFLICT DO NOTHING", + ) + .bind(sandbox_id) + .execute(&mut **tx) + .await + .map_err(|error| map_db_error(&error))?; + sqlx::query("SELECT sandbox_id FROM sandbox_config_fences WHERE sandbox_id = $1 FOR UPDATE") + .bind(sandbox_id) + .fetch_one(&mut **tx) + .await + .map_err(|error| map_db_error(&error))?; + Ok(()) +} + +async fn operation_with_current_policy_target( + tx: &mut sqlx::Transaction<'_, Postgres>, + record: &crate::storage_proto::StoredConfigUpdateOperation, +) -> PersistenceResult { + let sandbox_id = record + .operation + .as_ref() + .ok_or_else(|| PersistenceError::Encode("update operation payload missing".to_string()))? + .sandbox_id + .as_str(); + let version: Option = sqlx::query_scalar( + "SELECT version FROM objects WHERE object_type = $1 AND scope = $2 ORDER BY version DESC LIMIT 1", + ) + .bind(POLICY_OBJECT_TYPE) + .bind(sandbox_id) + .fetch_optional(&mut **tx) + .await + .map_err(|error| map_db_error(&error))?; + let mut record = record.clone(); + record.target_policy_version = + version.map_or(0, |value| u32::try_from(value).unwrap_or(u32::MAX)); + Ok(record) +} + +async fn operation_with_current_settings_target( + tx: &mut sqlx::Transaction<'_, Postgres>, + record: &crate::storage_proto::StoredConfigUpdateOperation, + workspace: &str, + sandbox_name: &str, +) -> PersistenceResult { + let payload: Option> = sqlx::query_scalar( + "SELECT payload FROM objects WHERE object_type = $1 AND workspace = $2 AND name = $3", + ) + .bind(crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE) + .bind(workspace) + .bind(sandbox_name) + .fetch_optional(&mut **tx) + .await + .map_err(|error| map_db_error(&error))?; + let revision = payload + .as_deref() + .map(serde_json::from_slice::) + .transpose() + .map_err(|error| { + PersistenceError::Decode(format!("decode settings payload failed: {error}")) + })? + .and_then(|value| value.get("revision").and_then(serde_json::Value::as_u64)) + .unwrap_or(0); + let mut record = record.clone(); + record.target_settings_revision = revision; + Ok(record) +} + #[derive(Debug, Clone)] pub struct PostgresStore { pool: PgPool, @@ -304,6 +416,297 @@ RETURNING resource_version, created_at_ms, updated_at_ms } } + #[allow(clippy::too_many_arguments)] + pub async fn put_if_with_operation( + &self, + object_type: &str, + id: &str, + name: &str, + workspace: &str, + payload: &[u8], + condition: WriteCondition, + operation_record: &crate::storage_proto::StoredConfigUpdateOperation, + sandbox_projection: Option<&AtomicSandboxProjection<'_>>, + ) -> PersistenceResult { + let now_ms = current_time_ms(); + let mut tx = self + .pool + .begin() + .await + .map_err(|error| map_db_error(&error))?; + let sandbox_id = operation_record + .operation + .as_ref() + .ok_or_else(|| { + PersistenceError::Encode("update operation payload missing".to_string()) + })? + .sandbox_id + .clone(); + lock_sandbox_config_fence(&mut tx, &sandbox_id).await?; + let mut operation_record = + operation_with_current_policy_target(&mut tx, operation_record).await?; + let row = match condition { + WriteCondition::MustCreate => sqlx::query( + r" +INSERT INTO objects (object_type, id, name, workspace, payload, created_at_ms, updated_at_ms, labels, resource_version) +VALUES ($1, $2, $3, $4, $5, $6, $6, '{}'::jsonb, 1) +RETURNING resource_version, created_at_ms, updated_at_ms +", + ) + .bind(object_type) + .bind(id) + .bind(name) + .bind(workspace) + .bind(payload) + .bind(now_ms) + .fetch_one(&mut *tx) + .await + .map_err(|error| map_db_error(&error))?, + WriteCondition::MatchResourceVersion(expected) => sqlx::query( + r" +UPDATE objects +SET payload = $4, updated_at_ms = $5, resource_version = resource_version + 1 +WHERE object_type = $1 AND id = $2 AND resource_version = $3 +RETURNING resource_version, created_at_ms, updated_at_ms +", + ) + .bind(object_type) + .bind(id) + .bind(i64::try_from(expected).unwrap_or(i64::MAX)) + .bind(payload) + .bind(now_ms) + .fetch_optional(&mut *tx) + .await + .map_err(|error| map_db_error(&error))? + .ok_or(PersistenceError::Conflict { + current_resource_version: None, + })?, + WriteCondition::Unconditional => { + return Err(PersistenceError::Config( + "atomic settings operation requires a CAS condition".to_string(), + )); + } + }; + if let Some(projection) = sandbox_projection { + let row = sqlx::query( + r" +SELECT payload, resource_version +FROM objects +WHERE object_type = 'sandbox' AND id = $1 +FOR UPDATE +", + ) + .bind(projection.sandbox_id) + .fetch_optional(&mut *tx) + .await + .map_err(|error| map_db_error(&error))? + .ok_or_else(|| { + PersistenceError::Database(format!( + "sandbox object {} not found", + projection.sandbox_id + )) + })?; + let sandbox_payload: Vec = row.get("payload"); + let current_version: i64 = row.try_get("resource_version").unwrap_or(1); + let current_version = current_version.max(1).cast_unsigned(); + let (sandbox, changed) = projection.apply_and_sync_operation_response( + &sandbox_payload, + current_version, + &mut operation_record, + )?; + if changed { + let result = sqlx::query( + r" +UPDATE objects +SET payload = $2, updated_at_ms = $3, resource_version = resource_version + 1 +WHERE object_type = 'sandbox' AND id = $1 AND resource_version = $4 +", + ) + .bind(projection.sandbox_id) + .bind(sandbox.encode_to_vec()) + .bind(now_ms) + .bind(i64::try_from(current_version).unwrap_or(i64::MAX)) + .execute(&mut *tx) + .await + .map_err(|error| map_db_error(&error))?; + if result.rows_affected() != 1 { + return Err(PersistenceError::Conflict { + current_resource_version: Some(current_version), + }); + } + } + } + insert_update_operation_postgres(&mut tx, &operation_record, now_ms).await?; + tx.commit().await.map_err(|error| map_db_error(&error))?; + let resource_version: i64 = row.try_get("resource_version").unwrap_or(1); + Ok(WriteResult { + resource_version: resource_version.max(1).cast_unsigned(), + created_at_ms: row.get("created_at_ms"), + updated_at_ms: row.get("updated_at_ms"), + }) + } + + /// Track an unchanged request without allocating a new desired-state revision. + pub async fn insert_existing_config_operation( + &self, + record: &crate::storage_proto::StoredConfigUpdateOperation, + workspace: &str, + sandbox_name: &str, + ) -> PersistenceResult<()> { + let sandbox_id = &record + .operation + .as_ref() + .ok_or_else(|| { + PersistenceError::Encode("update operation payload missing".to_string()) + })? + .sandbox_id; + let mut tx = self + .pool + .begin() + .await + .map_err(|error| map_db_error(&error))?; + lock_sandbox_config_fence(&mut tx, sandbox_id).await?; + // Keep the dimension observed by the request. If it changed meanwhile, + // reconciliation supersedes this operation instead of claiming success. + let record = if record.response_policy_version != 0 { + operation_with_current_settings_target(&mut tx, record, workspace, sandbox_name).await? + } else { + operation_with_current_policy_target(&mut tx, record).await? + }; + insert_update_operation_postgres(&mut tx, &record, current_time_ms()).await?; + tx.commit().await.map_err(|error| map_db_error(&error))?; + Ok(()) + } + + pub async fn update_config_operation_cas( + &self, + record: &crate::storage_proto::StoredConfigUpdateOperation, + expected_resource_version: u64, + ) -> PersistenceResult> { + let metadata = record.metadata.as_ref().ok_or_else(|| { + PersistenceError::Encode("update operation metadata missing".to_string()) + })?; + let operation = record.operation.as_ref().ok_or_else(|| { + PersistenceError::Encode("update operation payload missing".to_string()) + })?; + let state = openshell_core::proto::ConfigUpdateOperationState::try_from(operation.state) + .unwrap_or_default(); + let row = sqlx::query( + r" +UPDATE objects +SET payload = $4, status = $5, next_attempt_at_ms = $6, + updated_at_ms = $7, resource_version = resource_version + 1 +WHERE object_type = $1 AND id = $2 AND resource_version = $3 +RETURNING resource_version +", + ) + .bind(crate::config_update_operation::CONFIG_UPDATE_OPERATION_OBJECT_TYPE) + .bind(&metadata.id) + .bind(i64::try_from(expected_resource_version).unwrap_or(i64::MAX)) + .bind(record.encode_to_vec()) + .bind(state.as_str_name()) + .bind(record.next_attempt_at_ms()) + .bind(current_time_ms()) + .fetch_optional(&self.pool) + .await + .map_err(|error| map_db_error(&error))?; + Ok(row.map(|row| { + let version: i64 = row.get("resource_version"); + version.max(1).cast_unsigned() + })) + } + + pub async fn repair_config_operation_projection( + &self, + record: &crate::storage_proto::StoredConfigUpdateOperation, + expected_resource_version: u64, + ) -> PersistenceResult { + let metadata = record.metadata.as_ref().ok_or_else(|| { + PersistenceError::Encode("update operation metadata missing".to_string()) + })?; + let operation = record.operation.as_ref().ok_or_else(|| { + PersistenceError::Encode("update operation payload missing".to_string()) + })?; + let state = openshell_core::proto::ConfigUpdateOperationState::try_from(operation.state) + .unwrap_or_default(); + let result = sqlx::query( + r" +UPDATE objects +SET scope = $4, status = $5, next_attempt_at_ms = $6 +WHERE object_type = $1 AND id = $2 AND resource_version = $3 +", + ) + .bind(crate::config_update_operation::CONFIG_UPDATE_OPERATION_OBJECT_TYPE) + .bind(&metadata.id) + .bind(i64::try_from(expected_resource_version).unwrap_or(i64::MAX)) + .bind(&operation.sandbox_id) + .bind(state.as_str_name()) + .bind(record.next_attempt_at_ms()) + .execute(&self.pool) + .await + .map_err(|error| map_db_error(&error))?; + Ok(result.rows_affected() == 1) + } + + pub async fn list_pending_config_operations_for_scope( + &self, + scope: &str, + ) -> PersistenceResult> { + let rows = sqlx::query( + r" +SELECT object_type, id, name, workspace, payload, created_at_ms, updated_at_ms, + labels, resource_version +FROM objects +WHERE object_type = $1 AND status = $2 AND scope = $3 +ORDER BY created_at_ms ASC, id ASC +", + ) + .bind(crate::config_update_operation::CONFIG_UPDATE_OPERATION_OBJECT_TYPE) + .bind(openshell_core::proto::ConfigUpdateOperationState::Pending.as_str_name()) + .bind(scope) + .fetch_all(&self.pool) + .await + .map_err(|error| map_db_error(&error))?; + Ok(rows.into_iter().map(row_to_object_record).collect()) + } + + pub async fn list_due_config_update_operations( + &self, + now_ms: i64, + limit: u32, + ) -> PersistenceResult> { + let rows = sqlx::query( + r" +SELECT object_type, id, name, workspace, payload, created_at_ms, updated_at_ms, + labels, resource_version +FROM objects +WHERE object_type = $1 AND status = $2 AND next_attempt_at_ms <= $3 +ORDER BY next_attempt_at_ms ASC, id ASC +LIMIT $4 +", + ) + .bind(crate::config_update_operation::CONFIG_UPDATE_OPERATION_OBJECT_TYPE) + .bind(openshell_core::proto::ConfigUpdateOperationState::Pending.as_str_name()) + .bind(now_ms) + .bind(i64::from(limit)) + .fetch_all(&self.pool) + .await + .map_err(|error| map_db_error(&error))?; + Ok(rows.into_iter().map(row_to_object_record).collect()) + } + + pub async fn count_pending_config_update_operations(&self) -> PersistenceResult { + let count: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM objects WHERE object_type = $1 AND status = $2", + ) + .bind(crate::config_update_operation::CONFIG_UPDATE_OPERATION_OBJECT_TYPE) + .bind(openshell_core::proto::ConfigUpdateOperationState::Pending.as_str_name()) + .fetch_one(&self.pool) + .await + .map_err(|error| map_db_error(&error))?; + Ok(count.max(0).cast_unsigned()) + } + pub async fn delete_if( &self, object_type: &str, @@ -1077,6 +1480,8 @@ ON CONFLICT DO NOTHING let wrapped_payload = policy_payload_from_record(&record)?; let mut tx = self.pool.begin().await.map_err(|e| map_db_error(&e))?; + lock_sandbox_config_fence(&mut tx, &write.sandbox_id).await?; + let row = sqlx::query( r" SELECT payload, resource_version @@ -1144,6 +1549,21 @@ VALUES ($1, $2, $3, $4, $5, $6, $7, $7, $8) .await .map_err(|e| map_db_error(&e))?; + if let Some(operation_record) = write.operation.as_ref() { + let sandbox_name = sandbox + .metadata + .as_ref() + .map_or("", |metadata| metadata.name.as_str()); + let operation_record = operation_with_current_settings_target( + &mut tx, + operation_record, + &write.workspace, + sandbox_name, + ) + .await?; + insert_update_operation_postgres(&mut tx, &operation_record, now_ms).await?; + } + sqlx::query( r" UPDATE objects diff --git a/crates/openshell-server/src/persistence/sqlite.rs b/crates/openshell-server/src/persistence/sqlite.rs index 0a7278d0ae..523eacbd4d 100644 --- a/crates/openshell-server/src/persistence/sqlite.rs +++ b/crates/openshell-server/src/persistence/sqlite.rs @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use super::{ - DraftChunkRecord, ObjectCursor, ObjectListQuery, ObjectRecord, PersistenceError, - PersistenceResult, PolicyRecord, WriteCondition, WriteResult, current_time_ms, map_db_error, - map_migrate_error, + AtomicSandboxProjection, DraftChunkRecord, ObjectCursor, ObjectListQuery, ObjectRecord, + PersistenceError, PersistenceResult, PolicyRecord, WriteCondition, WriteResult, + current_time_ms, map_db_error, map_migrate_error, }; use crate::policy_store::{ AtomicPolicyRevisionWrite, draft_chunk_payload_from_record, draft_chunk_record_from_parts, @@ -36,6 +36,113 @@ static IN_MEMORY_DB_SEQUENCE: AtomicU64 = AtomicU64::new(0); use super::{DELETE_MANY_BATCH_SIZE, DRAFT_CHUNK_OBJECT_TYPE, POLICY_OBJECT_TYPE}; +async fn insert_update_operation_sqlite( + tx: &mut sqlx::Transaction<'_, Sqlite>, + record: &crate::storage_proto::StoredConfigUpdateOperation, + now_ms: i64, +) -> PersistenceResult<()> { + let metadata = record + .metadata + .as_ref() + .ok_or_else(|| PersistenceError::Encode("update operation metadata missing".to_string()))?; + let operation = record + .operation + .as_ref() + .ok_or_else(|| PersistenceError::Encode("update operation payload missing".to_string()))?; + let state = openshell_core::proto::ConfigUpdateOperationState::try_from(operation.state) + .unwrap_or_default(); + sqlx::query( + r#" +INSERT INTO "objects" ( + "object_type", "id", "name", "workspace", "scope", "version", "status", "payload", + "created_at_ms", "updated_at_ms", "labels", "resource_version", "next_attempt_at_ms" +) +VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?9, '{}', 1, ?10) +"#, + ) + .bind(crate::config_update_operation::CONFIG_UPDATE_OPERATION_OBJECT_TYPE) + .bind(&metadata.id) + .bind(&metadata.name) + .bind(&metadata.workspace) + .bind(&operation.sandbox_id) + .bind(Option::::None) + .bind(state.as_str_name()) + .bind(record.encode_to_vec()) + .bind(now_ms) + .bind(record.next_attempt_at_ms()) + .execute(&mut **tx) + .await + .map_err(|error| map_db_error(&error))?; + Ok(()) +} + +async fn lock_sandbox_config_fence( + tx: &mut sqlx::Transaction<'_, Sqlite>, + sandbox_id: &str, +) -> PersistenceResult<()> { + sqlx::query( + r#"INSERT INTO "sandbox_config_fences" ("sandbox_id") VALUES (?1) ON CONFLICT DO NOTHING"#, + ) + .bind(sandbox_id) + .execute(&mut **tx) + .await + .map_err(|error| map_db_error(&error))?; + Ok(()) +} + +async fn operation_with_current_policy_target( + tx: &mut sqlx::Transaction<'_, Sqlite>, + record: &crate::storage_proto::StoredConfigUpdateOperation, +) -> PersistenceResult { + let sandbox_id = record + .operation + .as_ref() + .ok_or_else(|| PersistenceError::Encode("update operation payload missing".to_string()))? + .sandbox_id + .as_str(); + let version: Option = sqlx::query_scalar( + r#"SELECT "version" FROM "objects" WHERE "object_type" = ?1 AND "scope" = ?2 ORDER BY "version" DESC LIMIT 1"#, + ) + .bind(POLICY_OBJECT_TYPE) + .bind(sandbox_id) + .fetch_optional(&mut **tx) + .await + .map_err(|error| map_db_error(&error))?; + let mut record = record.clone(); + record.target_policy_version = + version.map_or(0, |value| u32::try_from(value).unwrap_or(u32::MAX)); + Ok(record) +} + +async fn operation_with_current_settings_target( + tx: &mut sqlx::Transaction<'_, Sqlite>, + record: &crate::storage_proto::StoredConfigUpdateOperation, + workspace: &str, + sandbox_name: &str, +) -> PersistenceResult { + let payload: Option> = sqlx::query_scalar( + r#"SELECT "payload" FROM "objects" WHERE "object_type" = ?1 AND "workspace" = ?2 AND "name" = ?3"#, + ) + .bind(crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE) + .bind(workspace) + .bind(sandbox_name) + .fetch_optional(&mut **tx) + .await + .map_err(|error| map_db_error(&error))?; + let revision = payload + .as_deref() + .map(serde_json::from_slice::) + .transpose() + .map_err(|error| { + PersistenceError::Decode(format!("decode settings payload failed: {error}")) + })? + .and_then(|value| value.get("revision").and_then(serde_json::Value::as_u64)) + .unwrap_or(0); + let mut record = record.clone(); + record.target_settings_revision = revision; + Ok(record) +} + #[derive(Debug, Clone)] pub struct SqliteStore { pool: SqlitePool, @@ -352,6 +459,297 @@ ON CONFLICT ("object_type", "workspace", "name") WHERE "name" IS NOT NULL DO UPD } } + #[allow(clippy::too_many_arguments)] + pub async fn put_if_with_operation( + &self, + object_type: &str, + id: &str, + name: &str, + workspace: &str, + payload: &[u8], + condition: WriteCondition, + operation_record: &crate::storage_proto::StoredConfigUpdateOperation, + sandbox_projection: Option<&AtomicSandboxProjection<'_>>, + ) -> PersistenceResult { + let now_ms = current_time_ms(); + let mut tx = self + .pool + .begin_with("BEGIN IMMEDIATE") + .await + .map_err(|error| map_db_error(&error))?; + let sandbox_id = operation_record + .operation + .as_ref() + .ok_or_else(|| { + PersistenceError::Encode("update operation payload missing".to_string()) + })? + .sandbox_id + .clone(); + lock_sandbox_config_fence(&mut tx, &sandbox_id).await?; + let mut operation_record = + operation_with_current_policy_target(&mut tx, operation_record).await?; + let resource_version = match condition { + WriteCondition::MustCreate => { + sqlx::query( + r#" +INSERT INTO "objects" ("object_type", "id", "name", "workspace", "payload", "created_at_ms", "updated_at_ms", "labels", "resource_version") +VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6, '{}', 1) +"#, + ) + .bind(object_type) + .bind(id) + .bind(name) + .bind(workspace) + .bind(payload) + .bind(now_ms) + .execute(&mut *tx) + .await + .map_err(|error| map_db_error(&error))?; + 1 + } + WriteCondition::MatchResourceVersion(expected) => { + let result = sqlx::query( + r#" +UPDATE "objects" +SET "payload" = ?4, "updated_at_ms" = ?5, "resource_version" = "resource_version" + 1 +WHERE "object_type" = ?1 AND "id" = ?2 AND "resource_version" = ?3 +"#, + ) + .bind(object_type) + .bind(id) + .bind(i64::try_from(expected).unwrap_or(i64::MAX)) + .bind(payload) + .bind(now_ms) + .execute(&mut *tx) + .await + .map_err(|error| map_db_error(&error))?; + if result.rows_affected() != 1 { + return Err(PersistenceError::Conflict { + current_resource_version: None, + }); + } + expected.saturating_add(1) + } + WriteCondition::Unconditional => { + return Err(PersistenceError::Config( + "atomic settings operation requires a CAS condition".to_string(), + )); + } + }; + if let Some(projection) = sandbox_projection { + let row = sqlx::query( + r#" +SELECT "payload", "resource_version" +FROM "objects" +WHERE "object_type" = 'sandbox' AND "id" = ?1 +"#, + ) + .bind(projection.sandbox_id) + .fetch_optional(&mut *tx) + .await + .map_err(|error| map_db_error(&error))? + .ok_or_else(|| { + PersistenceError::Database(format!( + "sandbox object {} not found", + projection.sandbox_id + )) + })?; + let sandbox_payload: Vec = row.get("payload"); + let current_version: i64 = row.try_get("resource_version").unwrap_or(1); + let current_version = current_version.max(1).cast_unsigned(); + let (sandbox, changed) = projection.apply_and_sync_operation_response( + &sandbox_payload, + current_version, + &mut operation_record, + )?; + if changed { + let result = sqlx::query( + r#" +UPDATE "objects" +SET "payload" = ?2, "updated_at_ms" = ?3, "resource_version" = "resource_version" + 1 +WHERE "object_type" = 'sandbox' AND "id" = ?1 AND "resource_version" = ?4 +"#, + ) + .bind(projection.sandbox_id) + .bind(sandbox.encode_to_vec()) + .bind(now_ms) + .bind(i64::try_from(current_version).unwrap_or(i64::MAX)) + .execute(&mut *tx) + .await + .map_err(|error| map_db_error(&error))?; + if result.rows_affected() != 1 { + return Err(PersistenceError::Conflict { + current_resource_version: Some(current_version), + }); + } + } + } + insert_update_operation_sqlite(&mut tx, &operation_record, now_ms).await?; + tx.commit().await.map_err(|error| map_db_error(&error))?; + Ok(WriteResult { + resource_version, + created_at_ms: now_ms, + updated_at_ms: now_ms, + }) + } + + /// Track an unchanged request without allocating a new desired-state revision. + pub async fn insert_existing_config_operation( + &self, + record: &crate::storage_proto::StoredConfigUpdateOperation, + workspace: &str, + sandbox_name: &str, + ) -> PersistenceResult<()> { + let sandbox_id = &record + .operation + .as_ref() + .ok_or_else(|| { + PersistenceError::Encode("update operation payload missing".to_string()) + })? + .sandbox_id; + let mut tx = self + .pool + .begin_with("BEGIN IMMEDIATE") + .await + .map_err(|error| map_db_error(&error))?; + lock_sandbox_config_fence(&mut tx, sandbox_id).await?; + // Keep the dimension observed by the request. If it changed meanwhile, + // reconciliation supersedes this operation instead of claiming success. + let record = if record.response_policy_version != 0 { + operation_with_current_settings_target(&mut tx, record, workspace, sandbox_name).await? + } else { + operation_with_current_policy_target(&mut tx, record).await? + }; + insert_update_operation_sqlite(&mut tx, &record, current_time_ms()).await?; + tx.commit().await.map_err(|error| map_db_error(&error))?; + Ok(()) + } + + pub async fn update_config_operation_cas( + &self, + record: &crate::storage_proto::StoredConfigUpdateOperation, + expected_resource_version: u64, + ) -> PersistenceResult> { + let metadata = record.metadata.as_ref().ok_or_else(|| { + PersistenceError::Encode("update operation metadata missing".to_string()) + })?; + let operation = record.operation.as_ref().ok_or_else(|| { + PersistenceError::Encode("update operation payload missing".to_string()) + })?; + let state = openshell_core::proto::ConfigUpdateOperationState::try_from(operation.state) + .unwrap_or_default(); + let result = sqlx::query( + r#" +UPDATE "objects" +SET "payload" = ?4, "status" = ?5, "next_attempt_at_ms" = ?6, + "updated_at_ms" = ?7, "resource_version" = "resource_version" + 1 +WHERE "object_type" = ?1 AND "id" = ?2 AND "resource_version" = ?3 +"#, + ) + .bind(crate::config_update_operation::CONFIG_UPDATE_OPERATION_OBJECT_TYPE) + .bind(&metadata.id) + .bind(i64::try_from(expected_resource_version).unwrap_or(i64::MAX)) + .bind(record.encode_to_vec()) + .bind(state.as_str_name()) + .bind(record.next_attempt_at_ms()) + .bind(current_time_ms()) + .execute(&self.pool) + .await + .map_err(|error| map_db_error(&error))?; + Ok((result.rows_affected() == 1).then(|| expected_resource_version.saturating_add(1))) + } + + pub async fn repair_config_operation_projection( + &self, + record: &crate::storage_proto::StoredConfigUpdateOperation, + expected_resource_version: u64, + ) -> PersistenceResult { + let metadata = record.metadata.as_ref().ok_or_else(|| { + PersistenceError::Encode("update operation metadata missing".to_string()) + })?; + let operation = record.operation.as_ref().ok_or_else(|| { + PersistenceError::Encode("update operation payload missing".to_string()) + })?; + let state = openshell_core::proto::ConfigUpdateOperationState::try_from(operation.state) + .unwrap_or_default(); + let result = sqlx::query( + r#" +UPDATE "objects" +SET "scope" = ?4, "status" = ?5, "next_attempt_at_ms" = ?6 +WHERE "object_type" = ?1 AND "id" = ?2 AND "resource_version" = ?3 +"#, + ) + .bind(crate::config_update_operation::CONFIG_UPDATE_OPERATION_OBJECT_TYPE) + .bind(&metadata.id) + .bind(i64::try_from(expected_resource_version).unwrap_or(i64::MAX)) + .bind(&operation.sandbox_id) + .bind(state.as_str_name()) + .bind(record.next_attempt_at_ms()) + .execute(&self.pool) + .await + .map_err(|error| map_db_error(&error))?; + Ok(result.rows_affected() == 1) + } + + pub async fn list_pending_config_operations_for_scope( + &self, + scope: &str, + ) -> PersistenceResult> { + let rows = sqlx::query( + r#" +SELECT "object_type", "id", "name", "workspace", "payload", "created_at_ms", "updated_at_ms", + "labels", "resource_version" +FROM "objects" +WHERE "object_type" = ?1 AND "status" = ?2 AND "scope" = ?3 +ORDER BY "created_at_ms" ASC, "id" ASC +"#, + ) + .bind(crate::config_update_operation::CONFIG_UPDATE_OPERATION_OBJECT_TYPE) + .bind(openshell_core::proto::ConfigUpdateOperationState::Pending.as_str_name()) + .bind(scope) + .fetch_all(&self.pool) + .await + .map_err(|error| map_db_error(&error))?; + Ok(rows.into_iter().map(row_to_object_record).collect()) + } + + pub async fn list_due_config_update_operations( + &self, + now_ms: i64, + limit: u32, + ) -> PersistenceResult> { + let rows = sqlx::query( + r#" +SELECT "object_type", "id", "name", "workspace", "payload", "created_at_ms", "updated_at_ms", + "labels", "resource_version" +FROM "objects" +WHERE "object_type" = ?1 AND "status" = ?2 AND "next_attempt_at_ms" <= ?3 +ORDER BY "next_attempt_at_ms" ASC, "id" ASC +LIMIT ?4 +"#, + ) + .bind(crate::config_update_operation::CONFIG_UPDATE_OPERATION_OBJECT_TYPE) + .bind(openshell_core::proto::ConfigUpdateOperationState::Pending.as_str_name()) + .bind(now_ms) + .bind(i64::from(limit)) + .fetch_all(&self.pool) + .await + .map_err(|error| map_db_error(&error))?; + Ok(rows.into_iter().map(row_to_object_record).collect()) + } + + pub async fn count_pending_config_update_operations(&self) -> PersistenceResult { + let count: i64 = sqlx::query_scalar( + r#"SELECT COUNT(*) FROM "objects" WHERE "object_type" = ?1 AND "status" = ?2"#, + ) + .bind(crate::config_update_operation::CONFIG_UPDATE_OPERATION_OBJECT_TYPE) + .bind(openshell_core::proto::ConfigUpdateOperationState::Pending.as_str_name()) + .fetch_one(&self.pool) + .await + .map_err(|error| map_db_error(&error))?; + Ok(count.max(0).cast_unsigned()) + } + pub async fn delete_if( &self, object_type: &str, @@ -1162,6 +1560,8 @@ ON CONFLICT DO NOTHING .await .map_err(|e| map_db_error(&e))?; + lock_sandbox_config_fence(&mut tx, &write.sandbox_id).await?; + let row = sqlx::query( r#" SELECT "payload", "resource_version" @@ -1228,6 +1628,21 @@ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?7, ?8) .await .map_err(|e| map_db_error(&e))?; + if let Some(operation_record) = write.operation.as_ref() { + let sandbox_name = sandbox + .metadata + .as_ref() + .map_or("", |metadata| metadata.name.as_str()); + let operation_record = operation_with_current_settings_target( + &mut tx, + operation_record, + &write.workspace, + sandbox_name, + ) + .await?; + insert_update_operation_sqlite(&mut tx, &operation_record, now_ms).await?; + } + sqlx::query( r#" UPDATE "objects" diff --git a/crates/openshell-server/src/persistence/tests.rs b/crates/openshell-server/src/persistence/tests.rs index 357192d87d..545eb7074b 100644 --- a/crates/openshell-server/src/persistence/tests.rs +++ b/crates/openshell-server/src/persistence/tests.rs @@ -2,7 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use super::{ - ObjectListQuery, ObjectType, PersistenceError, PolicyRecord, Store, generate_name, test_store, + AtomicSandboxProjection, ObjectId, ObjectListQuery, ObjectName, ObjectType, PersistenceError, + PolicyRecord, Store, generate_name, test_store, +}; +use crate::config_update_operation::{ + CommittedResponse, OperationDimension, OperationTarget, new_record, }; use crate::policy_store::{AtomicPolicyRevisionWrite, PolicyStoreExt}; use openshell_core::proto::datamodel::v1::ObjectMeta as ProtoObjectMeta; @@ -226,6 +230,20 @@ fn embedded_migrators_include_pagination_indexes() { } } +#[test] +fn embedded_migrators_include_config_operation_query_support() { + for (backend, migration) in [ + ("sqlite", super::sqlite::embedded_migration_sql(9)), + ("postgres", super::postgres::embedded_migration_sql(9)), + ] { + let sql = + migration.unwrap_or_else(|| panic!("{backend} migrator is missing migration 009")); + assert!(sql.contains("sandbox_config_fences")); + assert!(sql.contains("objects_type_status_due_idx")); + assert!(sql.contains("next_attempt_at_ms")); + } +} + #[tokio::test] async fn sqlite_in_memory_store_survives_pool_connection_replacement() { for url in ["sqlite::memory:", "sqlite://?mode=memory"] { @@ -1093,6 +1111,715 @@ fn policy_test_sandbox(id: &str, name: &str) -> Sandbox { } } +fn config_operation_for( + sandbox: &Sandbox, + policy_version: u32, + settings_revision: u64, +) -> crate::storage_proto::StoredConfigUpdateOperation { + let dimension = if policy_version == 0 { + OperationDimension::Settings + } else { + OperationDimension::Policy + }; + new_record( + sandbox, + "default", + "", + dimension, + None, + OperationTarget { + policy_version, + settings_revision, + }, + CommittedResponse::default(), + ) +} + +#[tokio::test] +async fn operation_cas_updates_sql_state_and_due_index_together() { + use openshell_core::proto::ConfigUpdateOperationState; + + let store = test_store().await; + let sandbox = policy_test_sandbox("operation-state-sandbox", "operation-state-sandbox"); + store.put_message(&sandbox).await.unwrap(); + let operation = config_operation_for(&sandbox, 0, 1); + let operation_id = operation.operation.as_ref().unwrap().operation_id.clone(); + store + .put_if_with_operation( + crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE, + "operation-state-settings", + sandbox.object_name(), + "default", + br#"{"revision":1,"settings":{}}"#, + super::WriteCondition::MustCreate, + &operation, + None, + ) + .await + .unwrap(); + + let pending = store + .list_pending_config_operations_for_scope(sandbox.object_id()) + .await + .unwrap(); + assert_eq!(pending.len(), 1); + let mut terminal = pending[0].clone(); + terminal.operation.as_mut().unwrap().state = ConfigUpdateOperationState::Applied.into(); + let version = terminal.metadata.as_ref().unwrap().resource_version; + let updated = store + .update_config_operation_cas(&terminal, version) + .await + .unwrap(); + assert!(matches!(updated, super::KnownVersionUpdate::Changed(_))); + assert!( + store + .list_pending_config_operations_for_scope(sandbox.object_id()) + .await + .unwrap() + .is_empty() + ); + let stored = store + .get_message::(&operation_id) + .await + .unwrap() + .unwrap(); + assert_eq!( + ConfigUpdateOperationState::try_from(stored.operation.unwrap().state).unwrap(), + ConfigUpdateOperationState::Applied + ); +} + +#[tokio::test] +async fn operation_projection_repair_decodes_authoritative_payload_without_version_churn() { + use openshell_core::proto::ConfigUpdateOperationState; + + let store = test_store().await; + let sandbox = policy_test_sandbox("operation-repair-sandbox", "operation-repair-sandbox"); + store.put_message(&sandbox).await.unwrap(); + let operation = config_operation_for(&sandbox, 0, 1); + let operation_id = operation.operation.as_ref().unwrap().operation_id.clone(); + store + .put_if_with_operation( + crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE, + "operation-repair-settings", + sandbox.object_name(), + "default", + br#"{"revision":1,"settings":{}}"#, + super::WriteCondition::MustCreate, + &operation, + None, + ) + .await + .unwrap(); + + let terminal = store + .update_message_cas::( + &operation_id, + 0, + |record| { + record.operation.as_mut().unwrap().state = + ConfigUpdateOperationState::Applied.into(); + }, + ) + .await + .unwrap(); + assert_eq!( + store + .list_pending_config_operations_for_scope(sandbox.object_id()) + .await + .unwrap() + .len(), + 1, + "generic legacy writes leave the SQL projection stale" + ); + + let version = terminal.metadata.as_ref().unwrap().resource_version; + assert!( + store + .repair_config_operation_projection(&terminal, version) + .await + .unwrap() + ); + assert!( + store + .list_pending_config_operations_for_scope(sandbox.object_id()) + .await + .unwrap() + .is_empty() + ); + let repaired = store + .get_message::(&operation_id) + .await + .unwrap() + .unwrap(); + assert_eq!(repaired.metadata.unwrap().resource_version, version); +} + +#[tokio::test] +async fn pending_operation_queries_are_scoped_bounded_and_due_ordered() { + let store = test_store().await; + let first_sandbox = policy_test_sandbox("operation-query-a", "operation-query-a"); + let second_sandbox = policy_test_sandbox("operation-query-b", "operation-query-b"); + store.put_message(&first_sandbox).await.unwrap(); + store.put_message(&second_sandbox).await.unwrap(); + + let mut first = config_operation_for(&first_sandbox, 0, 1); + first.next_attempt_time = Some(openshell_core::time::timestamp_from_millis(10).unwrap()); + let first_id = first.operation.as_ref().unwrap().operation_id.clone(); + let mut second = config_operation_for(&second_sandbox, 0, 1); + second.next_attempt_time = Some(openshell_core::time::timestamp_from_millis(10).unwrap()); + let second_id = second.operation.as_ref().unwrap().operation_id.clone(); + for (index, (sandbox, operation)) in [(&first_sandbox, &first), (&second_sandbox, &second)] + .into_iter() + .enumerate() + { + store + .put_if_with_operation( + crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE, + &format!("operation-query-settings-{index}"), + sandbox.object_name(), + "default", + br#"{"revision":1,"settings":{}}"#, + super::WriteCondition::MustCreate, + operation, + None, + ) + .await + .unwrap(); + } + + let scoped = store + .list_pending_config_operations_for_scope(first_sandbox.object_id()) + .await + .unwrap(); + assert_eq!(scoped.len(), 1); + assert_eq!(scoped[0].operation.as_ref().unwrap().operation_id, first_id); + + let due = store + .list_due_config_update_operations(i64::MAX, 1) + .await + .unwrap(); + assert_eq!(due.len(), 1); + let first_page_id = due[0].operation.as_ref().unwrap().operation_id.clone(); + assert!([&first_id, &second_id].contains(&&first_page_id)); + + let mut completed = due[0].clone(); + completed.operation.as_mut().unwrap().state = + openshell_core::proto::ConfigUpdateOperationState::Applied.into(); + let version = completed.metadata.as_ref().unwrap().resource_version; + store + .update_config_operation_cas(&completed, version) + .await + .unwrap(); + let next_page = store + .list_due_config_update_operations(i64::MAX, 1) + .await + .unwrap(); + assert_eq!(next_page.len(), 1); + let second_page_id = &next_page[0].operation.as_ref().unwrap().operation_id; + assert_ne!(second_page_id, &first_page_id); + assert!([&first_id, &second_id].contains(&second_page_id)); +} + +#[tokio::test] +async fn configuration_transactions_fill_the_other_target_dimension() { + let store = test_store().await; + + let settings_sandbox = policy_test_sandbox("target-settings-write", "target-settings-write"); + store.put_message(&settings_sandbox).await.unwrap(); + store + .put_policy_revision( + "target-settings-policy", + settings_sandbox.object_id(), + "default", + 1, + &SandboxPolicy::default().encode_to_vec(), + "target-settings-policy-hash", + ) + .await + .unwrap(); + let settings_operation = config_operation_for(&settings_sandbox, 0, 1); + let settings_operation_id = settings_operation + .operation + .as_ref() + .unwrap() + .operation_id + .clone(); + store + .put_if_with_operation( + crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE, + "target-settings-record", + settings_sandbox.object_name(), + "default", + br#"{"revision":1,"settings":{}}"#, + super::WriteCondition::MustCreate, + &settings_operation, + None, + ) + .await + .unwrap(); + let settings_operation = store + .get_message::(&settings_operation_id) + .await + .unwrap() + .unwrap(); + assert_eq!(settings_operation.target_policy_version, 1); + assert_eq!(settings_operation.target_settings_revision, 1); + + let policy_sandbox = policy_test_sandbox("target-policy-write", "target-policy-write"); + store.put_message(&policy_sandbox).await.unwrap(); + let settings_seed = config_operation_for(&policy_sandbox, 0, 2); + store + .put_if_with_operation( + crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE, + "target-policy-settings-record", + policy_sandbox.object_name(), + "default", + br#"{"revision":2,"settings":{}}"#, + super::WriteCondition::MustCreate, + &settings_seed, + None, + ) + .await + .unwrap(); + let current = store + .get_message::(policy_sandbox.object_id()) + .await + .unwrap() + .unwrap(); + let policy_operation = config_operation_for(&policy_sandbox, 1, 0); + let policy_operation_id = policy_operation + .operation + .as_ref() + .unwrap() + .operation_id + .clone(); + store + .put_policy_revision_atomic(&AtomicPolicyRevisionWrite { + id: "target-policy-revision".to_string(), + sandbox_id: policy_sandbox.object_id().to_string(), + workspace: "default".to_string(), + version: 1, + policy_payload: SandboxPolicy::default().encode_to_vec(), + policy_hash: "target-policy-hash".to_string(), + provenance: StdHashMap::new(), + expected_resource_version: current.metadata.as_ref().unwrap().resource_version, + annotations: StdHashMap::new(), + backfill_policy: None, + operation: Some(policy_operation), + }) + .await + .unwrap(); + let policy_operation = store + .get_message::(&policy_operation_id) + .await + .unwrap() + .unwrap(); + assert_eq!(policy_operation.target_policy_version, 1); + assert_eq!(policy_operation.target_settings_revision, 2); +} + +async fn settings_projection_uses_locked_sandbox_version( + store: Store, + requested: StdHashMap, +) { + let suffix = uuid::Uuid::new_v4(); + let sandbox_id = format!("settings-projection-{suffix}"); + let sandbox_name = format!("settings-projection-name-{suffix}"); + let sandbox = policy_test_sandbox(&sandbox_id, &sandbox_name); + store.put_message(&sandbox).await.unwrap(); + + let stale = store + .get_message::(&sandbox_id) + .await + .unwrap() + .unwrap(); + let mut concurrent = stale.clone(); + concurrent + .metadata + .as_mut() + .unwrap() + .annotations + .insert("concurrent".to_string(), "preserved".to_string()); + store.put_message(&concurrent).await.unwrap(); + let before = store + .get_message::(&sandbox_id) + .await + .unwrap() + .unwrap(); + let before_version = before.metadata.as_ref().unwrap().resource_version; + let projection_changes_sandbox = requested + .iter() + .any(|(key, value)| before.metadata.as_ref().unwrap().annotations.get(key) != Some(value)); + let operation = new_record( + &stale, + "default", + "", + OperationDimension::Settings, + None, + OperationTarget { + policy_version: 0, + settings_revision: 1, + }, + CommittedResponse { + settings_revision: 1, + annotations: requested.clone(), + ..Default::default() + }, + ); + let operation_id = operation.operation.as_ref().unwrap().operation_id.clone(); + + store + .put_if_with_operation( + crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE, + &format!("settings-projection-record-{suffix}"), + &sandbox_name, + "default", + br#"{"revision":1,"settings":{}}"#, + super::WriteCondition::MustCreate, + &operation, + Some(&AtomicSandboxProjection { + sandbox_id: &sandbox_id, + annotations: &requested, + expected_resource_version: 0, + }), + ) + .await + .unwrap(); + + let after = store + .get_message::(&sandbox_id) + .await + .unwrap() + .unwrap(); + let metadata = after.metadata.as_ref().unwrap(); + assert_eq!( + metadata.resource_version, + before_version + u64::from(projection_changes_sandbox) + ); + assert_eq!( + metadata.annotations.get("concurrent").map(String::as_str), + Some("preserved") + ); + for (key, value) in &requested { + assert_eq!(metadata.annotations.get(key), Some(value)); + } + + let stored_operation = store + .get_message::(&operation_id) + .await + .unwrap() + .unwrap(); + let response = crate::config_update_operation::response_from_record(&stored_operation).unwrap(); + assert_eq!(response.annotations, metadata.annotations); +} + +#[tokio::test] +async fn sqlite_settings_projection_uses_locked_sandbox_version() { + let store = test_store().await; + settings_projection_uses_locked_sandbox_version( + store.clone(), + StdHashMap::from([("requested".to_string(), "applied".to_string())]), + ) + .await; + settings_projection_uses_locked_sandbox_version(store, StdHashMap::new()).await; +} + +#[tokio::test] +#[ignore = "requires OPENSHELL_TEST_POSTGRES_URL pointing to a test database"] +async fn postgres_settings_projection_uses_locked_sandbox_version() { + let url = std::env::var("OPENSHELL_TEST_POSTGRES_URL").expect("test database URL"); + let store = Store::connect(&url).await.unwrap(); + settings_projection_uses_locked_sandbox_version( + store.clone(), + StdHashMap::from([("requested".to_string(), "applied".to_string())]), + ) + .await; + settings_projection_uses_locked_sandbox_version(store, StdHashMap::new()).await; +} + +#[tokio::test] +async fn operation_insert_failure_rolls_back_settings_write() { + let store = test_store().await; + let sandbox = policy_test_sandbox("operation-rollback", "operation-rollback"); + store.put_message(&sandbox).await.unwrap(); + let operation = new_record( + &sandbox, + "default", + "same-request", + OperationDimension::Settings, + None, + OperationTarget { + policy_version: 0, + settings_revision: 1, + }, + CommittedResponse::default(), + ); + store + .put_if_with_operation( + crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE, + "operation-rollback-settings", + sandbox.object_name(), + "default", + br#"{"revision":1,"settings":{}}"#, + super::WriteCondition::MustCreate, + &operation, + None, + ) + .await + .unwrap(); + let duplicate_operation = new_record( + &sandbox, + "default", + "same-request", + OperationDimension::Settings, + None, + OperationTarget { + policy_version: 0, + settings_revision: 2, + }, + CommittedResponse::default(), + ); + let error = store + .put_if_with_operation( + crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE, + "operation-rollback-settings", + sandbox.object_name(), + "default", + br#"{"revision":2,"settings":{}}"#, + super::WriteCondition::MatchResourceVersion(1), + &duplicate_operation, + None, + ) + .await + .unwrap_err(); + assert!(matches!(error, PersistenceError::UniqueViolation { .. })); + let settings = store + .get( + crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE, + "operation-rollback-settings", + ) + .await + .unwrap() + .unwrap(); + assert_eq!(settings.payload, br#"{"revision":1,"settings":{}}"#); + assert_eq!(settings.resource_version, 1); +} + +#[tokio::test] +async fn operation_insert_failure_rolls_back_policy_and_projection() { + let store = test_store().await; + let sandbox = policy_test_sandbox("policy-operation-rollback", "policy-operation-rollback"); + store.put_message(&sandbox).await.unwrap(); + let existing_operation = new_record( + &sandbox, + "default", + "duplicate-policy-request", + OperationDimension::Settings, + None, + OperationTarget { + policy_version: 0, + settings_revision: 1, + }, + CommittedResponse::default(), + ); + store + .put_if_with_operation( + crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE, + "policy-operation-rollback-settings", + sandbox.object_name(), + "default", + br#"{"revision":1,"settings":{}}"#, + super::WriteCondition::MustCreate, + &existing_operation, + None, + ) + .await + .unwrap(); + let before = store + .get_message::(sandbox.object_id()) + .await + .unwrap() + .unwrap(); + let duplicate_operation = new_record( + &sandbox, + "default", + "duplicate-policy-request", + OperationDimension::Policy, + None, + OperationTarget { + policy_version: 1, + settings_revision: 0, + }, + CommittedResponse::default(), + ); + let policy = SandboxPolicy::default(); + let error = store + .put_policy_revision_atomic(&AtomicPolicyRevisionWrite { + id: "policy-operation-rollback-revision".to_string(), + sandbox_id: sandbox.object_id().to_string(), + workspace: "default".to_string(), + version: 1, + policy_payload: policy.encode_to_vec(), + policy_hash: "rollback-hash".to_string(), + provenance: StdHashMap::new(), + expected_resource_version: before.metadata.as_ref().unwrap().resource_version, + annotations: StdHashMap::from([("changed".to_string(), "true".to_string())]), + backfill_policy: Some(policy), + operation: Some(duplicate_operation), + }) + .await + .unwrap_err(); + assert!(matches!(error, PersistenceError::UniqueViolation { .. })); + assert!( + store + .get_latest_policy(sandbox.object_id()) + .await + .unwrap() + .is_none() + ); + let after = store + .get_message::(sandbox.object_id()) + .await + .unwrap() + .unwrap(); + assert_eq!( + after.metadata.as_ref().unwrap().resource_version, + before.metadata.as_ref().unwrap().resource_version + ); + assert!(after.metadata.as_ref().unwrap().annotations.is_empty()); +} + +#[tokio::test] +#[ignore = "requires OPENSHELL_TEST_POSTGRES_URL pointing to a test database"] +async fn postgres_policy_and_settings_operations_allocate_serial_targets() { + let url = std::env::var("OPENSHELL_TEST_POSTGRES_URL").expect("test database URL"); + let first = Store::connect(&url).await.unwrap(); + let second = Store::connect(&url).await.unwrap(); + let sandbox_id = uuid::Uuid::new_v4().to_string(); + let sandbox = policy_test_sandbox(&sandbox_id, &sandbox_id); + first.put_message(&sandbox).await.unwrap(); + let current = first + .get_message::(&sandbox_id) + .await + .unwrap() + .unwrap(); + let settings_operation = config_operation_for(&sandbox, 0, 1); + let settings_operation_id = settings_operation + .operation + .as_ref() + .unwrap() + .operation_id + .clone(); + let policy_operation = config_operation_for(&sandbox, 1, 0); + let policy_operation_id = policy_operation + .operation + .as_ref() + .unwrap() + .operation_id + .clone(); + let barrier = std::sync::Arc::new(tokio::sync::Barrier::new(2)); + let settings_barrier = barrier.clone(); + let policy = SandboxPolicy::default(); + + let (settings_result, policy_result) = tokio::join!( + async { + settings_barrier.wait().await; + first + .put_if_with_operation( + crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE, + &uuid::Uuid::new_v4().to_string(), + sandbox.object_name(), + "default", + br#"{"revision":1,"settings":{}}"#, + super::WriteCondition::MustCreate, + &settings_operation, + None, + ) + .await + }, + async { + barrier.wait().await; + second + .put_policy_revision_atomic(&AtomicPolicyRevisionWrite { + id: uuid::Uuid::new_v4().to_string(), + sandbox_id: sandbox_id.clone(), + workspace: "default".to_string(), + version: 1, + policy_payload: policy.encode_to_vec(), + policy_hash: "serial-target".to_string(), + provenance: StdHashMap::new(), + expected_resource_version: current.metadata.as_ref().unwrap().resource_version, + annotations: StdHashMap::new(), + backfill_policy: None, + operation: Some(policy_operation), + }) + .await + } + ); + settings_result.unwrap(); + policy_result.unwrap(); + + let settings = first + .get_message::(&settings_operation_id) + .await + .unwrap() + .unwrap(); + let policy = first + .get_message::(&policy_operation_id) + .await + .unwrap() + .unwrap(); + let targets = [ + ( + settings.target_policy_version, + settings.target_settings_revision, + ), + ( + policy.target_policy_version, + policy.target_settings_revision, + ), + ]; + assert!(targets.contains(&(1, 1)), "committed targets: {targets:?}"); + assert_ne!(targets, [(0, 1), (1, 0)]); + + for policy_request in [false, true] { + let mut unchanged = if policy_request { + config_operation_for(&sandbox, 1, 0) + } else { + config_operation_for(&sandbox, 0, 1) + }; + unchanged.response_policy_version = u32::from(policy_request); + first + .insert_existing_config_operation(&unchanged, "default", sandbox.object_name()) + .await + .unwrap(); + let stored = first + .get_message::( + &unchanged.operation.as_ref().unwrap().operation_id, + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + ( + stored.target_policy_version, + stored.target_settings_revision + ), + (1, 1) + ); + assert!( + first + .list_pending_config_operations_for_scope(&sandbox_id) + .await + .unwrap() + .iter() + .any(|record| record.metadata.as_ref().unwrap().id + == stored.metadata.as_ref().unwrap().id) + ); + } +} + #[tokio::test] async fn initial_policy_history_is_insert_only() { assert_initial_policy_history_is_insert_only(&test_store().await).await; @@ -1203,6 +1930,7 @@ async fn policy_atomic_write_commits_revision_provenance_and_sandbox_projection( expected_resource_version: current_version, annotations: provenance.clone(), backfill_policy: Some(policy.clone()), + operation: None, }) .await .unwrap(); @@ -1272,6 +2000,7 @@ async fn policy_atomic_write_rolls_back_sandbox_when_revision_insert_conflicts() expected_resource_version: before_version, annotations: StdHashMap::from([("signature".to_string(), "new".to_string())]), backfill_policy: Some(policy), + operation: None, }) .await .unwrap_err(); @@ -1317,6 +2046,7 @@ async fn policy_atomic_write_persists_workspace() { expected_resource_version: current_version, annotations: StdHashMap::new(), backfill_policy: Some(policy), + operation: None, }) .await .unwrap(); diff --git a/crates/openshell-server/src/policy_store.rs b/crates/openshell-server/src/policy_store.rs index d2a62d7cad..f7928ca1d1 100644 --- a/crates/openshell-server/src/policy_store.rs +++ b/crates/openshell-server/src/policy_store.rs @@ -4,7 +4,7 @@ use crate::persistence::{ DraftChunkRecord, PersistenceError, PersistenceResult, PolicyRecord, SetResourceVersion, Store, }; -use crate::storage_proto::{DraftChunkPayload, PolicyRevisionPayload}; +use crate::storage_proto::{DraftChunkPayload, PolicyRevisionPayload, StoredConfigUpdateOperation}; use openshell_core::proto::{NetworkPolicyRule, Sandbox, SandboxPolicy as ProtoSandboxPolicy}; use prost::Message; use std::collections::HashMap; @@ -37,6 +37,7 @@ pub struct AtomicPolicyRevisionWrite { /// Populate the create-time baseline, or replace it while startup admission /// is blocked and no workload has consumed the static restrictions. pub backfill_policy: Option, + pub operation: Option, } pub fn policy_record_for_atomic_write( @@ -663,6 +664,7 @@ mod tests { .read_write .push("/new-static-path".to_string()); let write = AtomicPolicyRevisionWrite { + operation: None, id: "revision".to_string(), sandbox_id: "sandbox".to_string(), workspace: "default".to_string(), diff --git a/crates/openshell-server/src/provider_config_operation.rs b/crates/openshell-server/src/provider_config_operation.rs new file mode 100644 index 0000000000..9941630d95 --- /dev/null +++ b/crates/openshell-server/src/provider_config_operation.rs @@ -0,0 +1,757 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Durable, exact-target configuration operations, independent of delivery transport. +//! +//! Provider receipts project the common operation resource. Live installation +//! evidence remains session-bound; an applied operation records historical +//! completion and never substitutes for a fresh provider readiness evaluation. + +#![allow(clippy::result_large_err)] // Internal operation helpers preserve gRPC error details. + +use std::collections::HashMap; + +use openshell_core::proto::{ + ConfigApplyOutcome, ConfigComponent, ConfigSnapshotRevision, ConfigUpdateOperation, + ConfigUpdateOperationState, ObjectMeta, ProviderMutationKind, ProviderMutationReceipt, + ProviderReadinessReason, ProviderReadinessState, ProviderReadinessStatus, + config_snapshot_revision, +}; +use openshell_core::rpc_error::{self, ErrorDetails, StatusExt}; +use prost::Message; +use sha2::{Digest, Sha256}; +use tonic::{Code, Status}; + +use crate::persistence::{ObjectRecord, PersistenceError, Store, WriteCondition}; +use crate::storage_proto::StoredConfigUpdateOperation; + +/// Object-store namespace shared by configuration completion resources. +pub const CONFIG_UPDATE_OPERATION_OBJECT_TYPE: &str = "config_update_operation"; +const MAX_TRANSITION_RETRIES: usize = 8; + +/// Provider-specific view of one common configuration operation. +#[derive(Clone, Debug)] +pub struct ProviderOperation { + /// Immutable desired authority captured for this operation. + pub(crate) receipt: ProviderMutationReceipt, + /// A failed initial snapshot is never reconstructed into another target. + pub(crate) snapshot_reason: ProviderReadinessReason, + /// Durable historical outcome; callers must separately evaluate live readiness. + pub(crate) operation: ConfigUpdateOperation, +} + +fn storage_unavailable() -> Status { + // A provider mutation can already have committed when operation persistence + // fails. No retry hint is attached: repeating the mutation is not proven safe. + Status::with_error_details( + Code::Unavailable, + "configuration operation storage unavailable; the saved mutation may remain in effect", + ErrorDetails::with_error_info( + "CONFIG_OPERATION_STORAGE_UNCERTAIN", + rpc_error::ERROR_DOMAIN, + HashMap::new(), + ), + ) +} + +fn invalid_record() -> Status { + Status::with_error_details( + Code::Internal, + "configuration operation identity is inconsistent", + ErrorDetails::with_error_info( + "CONFIG_OPERATION_INVALID", + rpc_error::ERROR_DOMAIN, + HashMap::new(), + ), + ) +} + +fn persisted_time(receipt: &ProviderMutationReceipt) -> Result { + // Receipt identity includes the full canonical timestamp. Absence is not + // the Unix epoch, and reducing nanos to milliseconds would merge identities. + let timestamp = receipt.persisted_time.ok_or_else(invalid_record)?; + openshell_core::time::validate_timestamp(×tamp).map_err(|_| invalid_record())?; + Ok(timestamp) +} + +fn observation_id( + receipt: &ProviderMutationReceipt, + snapshot_reason: ProviderReadinessReason, +) -> Result { + let desired = receipt.desired.as_ref().ok_or_else(invalid_record)?; + let mut digest = Sha256::new(); + digest.update(b"openshell/provider-observation/v1\0"); + let target_bytes = desired.encode_to_vec(); + // The target contains only public identity and opaque revision fields. Its + // protobuf has no maps, so encoding is canonical. Length framing prevents + // component concatenation ambiguities across workspaces and provider names. + for component in [ + receipt.workspace.as_bytes(), + receipt.provider.as_bytes(), + target_bytes.as_slice(), + ] { + let length = u64::try_from(component.len()).map_err(|_| invalid_record())?; + digest.update(length.to_be_bytes()); + digest.update(component); + } + // A failed capture and its later successful repair are different targets; + // the original failed operation must remain immutable. + digest.update((snapshot_reason as i32).to_be_bytes()); + let mut bytes = [0_u8; 16]; + for (byte, hashed) in bytes.iter_mut().zip(digest.finalize()) { + *byte = hashed; + } + Ok(uuid::Builder::from_custom_bytes(bytes) + .into_uuid() + .to_string()) +} + +/// Record an exact provider target in the shared configuration-operation store. +/// +/// The caller captures the snapshot only after its provider mutation finishes. +/// This write does not roll back a preceding mutation on failure. An incomplete +/// snapshot is persisted as failed, retaining its original non-secret reason. +/// Observation-only requests reuse the original receipt for the same complete +/// target; source mutations retain their distinct caller-created receipt IDs. +pub async fn record_provider_operation( + store: &Store, + mut receipt: ProviderMutationReceipt, + snapshot_reason: ProviderReadinessReason, +) -> Result { + let observation = receipt.kind == ProviderMutationKind::Observe as i32; + if observation { + receipt.receipt_id = observation_id(&receipt, snapshot_reason)?; + } + let desired = receipt.desired.as_ref().ok_or_else(invalid_record)?; + if receipt.receipt_id.is_empty() + || receipt.workspace.is_empty() + || desired.sandbox_id.is_empty() + { + return Err(invalid_record()); + } + let persisted_time = persisted_time(&receipt)?; + let failed = snapshot_reason != ProviderReadinessReason::Unspecified; + let operation = ConfigUpdateOperation { + operation_id: receipt.receipt_id.clone(), + sandbox_id: desired.sandbox_id.clone(), + component: ConfigComponent::ProviderEnvironment.into(), + target_revision: Some(ConfigSnapshotRevision { + component: Some(config_snapshot_revision::Component::ProviderTarget( + desired.clone(), + )), + }), + state: if failed { + ConfigUpdateOperationState::Failed.into() + } else { + ConfigUpdateOperationState::Pending.into() + }, + outcome: if failed { + ConfigApplyOutcome::FailedClosed.into() + } else { + ConfigApplyOutcome::Unspecified.into() + }, + sanitized_error: if failed { + snapshot_reason.as_str_name().to_string() + } else { + String::new() + }, + created_time: Some(persisted_time), + updated_time: Some(persisted_time), + completed_time: failed.then_some(persisted_time), + }; + let stored = StoredConfigUpdateOperation { + metadata: Some(ObjectMeta { + id: receipt.receipt_id.clone(), + name: receipt.receipt_id.clone(), + workspace: receipt.workspace.clone(), + created_time: Some(persisted_time), + ..Default::default() + }), + operation: Some(operation), + provider_receipt: Some(receipt.clone()), + provider_snapshot_reason: snapshot_reason.into(), + ..Default::default() + }; + let result = store + .put_if( + CONFIG_UPDATE_OPERATION_OBJECT_TYPE, + &receipt.receipt_id, + &receipt.receipt_id, + &receipt.workspace, + &stored.encode_to_vec(), + None, + WriteCondition::MustCreate, + ) + .await; + match result { + Ok(_) => Ok(receipt), + Err(PersistenceError::UniqueViolation { .. }) if observation => { + // Concurrent observers race only on the insert. A conflict never + // updates the winner's timestamp, mutation identity, or outcome. + // Exact comparison also fails closed on an ID collision/corruption. + let existing = + get_provider_operation(store, &receipt.receipt_id, &receipt.workspace).await?; + if existing.receipt.kind != receipt.kind + || existing.receipt.provider != receipt.provider + || existing.receipt.desired != receipt.desired + || existing.snapshot_reason != snapshot_reason + { + return Err(invalid_record()); + } + Ok(existing.receipt) + } + Err(_) => Err(storage_unavailable()), + } +} + +// The record ID, receipt ID, and operation ID deliberately name the same +// durable identity; their distinct schema field names must compare equal. +#[allow(clippy::suspicious_operation_groupings)] +fn decode_provider_operation( + record: &ObjectRecord, +) -> Result<(StoredConfigUpdateOperation, ProviderOperation), Status> { + let stored = StoredConfigUpdateOperation::decode(record.payload.as_slice()) + .map_err(|_| invalid_record())?; + let receipt = stored + .provider_receipt + .as_ref() + .ok_or_else(invalid_record)?; + let operation = stored.operation.as_ref().ok_or_else(invalid_record)?; + let metadata = stored.metadata.as_ref().ok_or_else(invalid_record)?; + let desired = receipt.desired.as_ref().ok_or_else(invalid_record)?; + let persisted_time = persisted_time(receipt)?; + let snapshot_reason = ProviderReadinessReason::try_from(stored.provider_snapshot_reason) + .map_err(|_| invalid_record())?; + if record.id != receipt.receipt_id + || record.workspace != receipt.workspace + || metadata.id != record.id + || metadata.workspace != record.workspace + || operation.operation_id != record.id + || operation.sandbox_id != desired.sandbox_id + || operation.created_time != Some(persisted_time) + || metadata.created_time != Some(persisted_time) + || operation.component != ConfigComponent::ProviderEnvironment as i32 + || operation + .target_revision + .as_ref() + .and_then(|revision| revision.component.as_ref()) + != Some(&config_snapshot_revision::Component::ProviderTarget( + desired.clone(), + )) + || ConfigUpdateOperationState::try_from(operation.state).is_err() + { + return Err(invalid_record()); + } + let provider = ProviderOperation { + receipt: receipt.clone(), + snapshot_reason, + operation: operation.clone(), + }; + Ok((stored, provider)) +} + +async fn load_provider_operation( + store: &Store, + operation_id: &str, + workspace: &str, +) -> Result<(ObjectRecord, StoredConfigUpdateOperation, ProviderOperation), Status> { + let record = store + .get(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, operation_id) + .await + .map_err(|_| storage_unavailable())? + .filter(|record| record.workspace == workspace) + .ok_or_else(|| Status::not_found("provider operation not found"))?; + let (stored, provider) = decode_provider_operation(&record)?; + Ok((record, stored, provider)) +} + +/// Read a provider projection after the RPC has authorized the workspace. +/// +/// Looking up an operation from a different workspace returns the same result +/// as a missing operation and never reveals its receipt or desired target. +pub async fn get_provider_operation( + store: &Store, + operation_id: &str, + workspace: &str, +) -> Result { + let (_, _, provider) = load_provider_operation(store, operation_id, workspace).await?; + Ok(provider) +} + +fn terminal(state: ConfigUpdateOperationState) -> bool { + matches!( + state, + ConfigUpdateOperationState::Applied + | ConfigUpdateOperationState::Inactive + | ConfigUpdateOperationState::Failed + | ConfigUpdateOperationState::Superseded + | ConfigUpdateOperationState::Cancelled + ) +} + +fn completion( + status: &ProviderReadinessStatus, +) -> Result, Status> { + match ProviderReadinessState::try_from(status.state).map_err(|_| invalid_record())? { + ProviderReadinessState::Ready | ProviderReadinessState::Revoked => { + let desired = status + .receipt + .as_ref() + .and_then(|receipt| receipt.desired.as_ref()) + .ok_or_else(invalid_record)?; + let observed = status.observed.as_ref().ok_or_else(invalid_record)?; + // The RPC validates current session ownership and freshness. Check + // the complete target again before converting its result into a + // durable terminal transition; a partial install is never applied. + if status.reason != ProviderReadinessReason::Unspecified as i32 + || observed.reason != ProviderReadinessReason::Unspecified as i32 + || observed.attachment_epoch != desired.attachment_epoch + || observed.provider_env_revision != desired.provider_env_revision + || observed.config_revision != desired.config_revision + || observed.policy_hash != desired.policy_hash + || !observed.credentials_installed + || !observed.policy_active + || !observed.launch_environment_installed + || observed.process_instance_id.is_empty() + || observed.session_id.is_empty() + || status.network_instance_id.is_empty() + || (status.state == ProviderReadinessState::Revoked as i32) + != desired.provider_id.is_empty() + { + return Err(invalid_record()); + } + Ok(Some(( + ConfigUpdateOperationState::Applied, + ConfigApplyOutcome::Applied, + ))) + } + ProviderReadinessState::Superseded => Ok(Some(( + ConfigUpdateOperationState::Superseded, + ConfigApplyOutcome::IgnoredStale, + ))), + // Live installation failures can recover without changing the desired + // revision. They remain visible in the provider projection but do not + // terminate the operation or authorize a retry of the source mutation. + _ => Ok(None), + } +} + +/// Persist exact completion by CAS and attach its historical resource to a view. +/// +/// Call only after evaluating authenticated current-session evidence. The live +/// status is never changed by this function: expired or superseded evidence +/// cannot become ready because an earlier observation was durably applied. +pub async fn observe_provider_status( + store: &Store, + status: &mut ProviderReadinessStatus, +) -> Result<(), Status> { + let receipt = status.receipt.as_ref().ok_or_else(invalid_record)?.clone(); + let completion = completion(status)?; + for _ in 0..MAX_TRANSITION_RETRIES { + let (record, mut stored, provider) = + load_provider_operation(store, &receipt.receipt_id, &receipt.workspace).await?; + if provider.receipt != receipt { + return Err(invalid_record()); + } + let state = ConfigUpdateOperationState::try_from(provider.operation.state) + .map_err(|_| invalid_record())?; + let Some((terminal_state, outcome)) = completion.filter(|_| !terminal(state)) else { + status.operation = Some(provider.operation); + return Ok(()); + }; + // Capturing the desired snapshot failed permanently for this operation. + // A later read must not fabricate a different, successful target. + if provider.snapshot_reason != ProviderReadinessReason::Unspecified { + return Err(invalid_record()); + } + let operation = stored.operation.as_mut().ok_or_else(invalid_record)?; + operation.state = terminal_state.into(); + operation.outcome = outcome.into(); + operation.sanitized_error = if terminal_state == ConfigUpdateOperationState::Superseded { + ProviderReadinessReason::DesiredStateChanged + .as_str_name() + .to_string() + } else { + String::new() + }; + let completed_time = + openshell_core::time::timestamp_from_system_time(std::time::SystemTime::now()) + .map_err(|error| { + Status::internal(format!("create operation completion timestamp: {error}")) + })?; + operation.updated_time = Some(completed_time); + operation.completed_time = Some(completed_time); + let result = store + .put_if( + CONFIG_UPDATE_OPERATION_OBJECT_TYPE, + &record.id, + &record.name, + &record.workspace, + &stored.encode_to_vec(), + record.labels.as_deref(), + WriteCondition::MatchResourceVersion(record.resource_version), + ) + .await; + match result { + Ok(_) => { + status.operation = stored.operation; + return Ok(()); + } + // A competing observer may have completed this operation. Reload + // the authoritative row; a terminal outcome is immutable. + Err(PersistenceError::Conflict { .. }) => {} + Err(_) => return Err(storage_unavailable()), + } + } + Err(rpc_error::resource_version_conflict( + "configuration operation changed concurrently; query its status again", + None, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use openshell_core::proto::{ + ProviderDesiredIdentity, ProviderMutationKind, ProviderReadinessObservation, + }; + use uuid::Uuid; + + fn receipt() -> ProviderMutationReceipt { + ProviderMutationReceipt { + receipt_id: Uuid::new_v4().to_string(), + mutation_id: Uuid::new_v4().to_string(), + provider: "provider".to_string(), + workspace: "default".to_string(), + kind: ProviderMutationKind::Update.into(), + desired: Some(ProviderDesiredIdentity { + sandbox_id: Uuid::new_v4().to_string(), + sandbox: "sandbox".to_string(), + attachment_epoch: Uuid::new_v4().to_string(), + provider_id: Uuid::new_v4().to_string(), + provider_resource_version: 3, + provider_env_revision: 5, + config_revision: 7, + policy_hash: "policy".to_string(), + }), + persisted_time: Some(prost_types::Timestamp { + seconds: 1_700_000_000, + nanos: 123_456_789, + }), + } + } + + fn ready(receipt: &ProviderMutationReceipt) -> ProviderReadinessStatus { + let desired = receipt.desired.as_ref().unwrap(); + ProviderReadinessStatus { + receipt: Some(receipt.clone()), + state: ProviderReadinessState::Ready.into(), + network_instance_id: Uuid::new_v4().to_string(), + observed: Some(ProviderReadinessObservation { + session_id: Uuid::new_v4().to_string(), + sequence: 1, + attachment_epoch: desired.attachment_epoch.clone(), + provider_env_revision: desired.provider_env_revision, + config_revision: desired.config_revision, + policy_hash: desired.policy_hash.clone(), + credentials_installed: true, + policy_active: true, + launch_environment_installed: true, + process_instance_id: Uuid::new_v4().to_string(), + reason: ProviderReadinessReason::Unspecified.into(), + }), + ..Default::default() + } + } + + #[tokio::test] + async fn provider_receipt_uses_the_common_operation_namespace_and_exact_target() { + let store = crate::persistence::test_store().await; + let receipt = receipt(); + record_provider_operation( + &store, + receipt.clone(), + ProviderReadinessReason::Unspecified, + ) + .await + .unwrap(); + let operation = get_provider_operation(&store, &receipt.receipt_id, "default") + .await + .unwrap(); + assert_eq!(operation.receipt, receipt); + assert_eq!(operation.operation.operation_id, receipt.receipt_id); + assert_eq!(operation.operation.created_time, receipt.persisted_time); + assert_eq!(operation.operation.updated_time, receipt.persisted_time); + assert!(operation.operation.completed_time.is_none()); + assert_eq!( + operation.operation.state, + ConfigUpdateOperationState::Pending as i32 + ); + assert!( + store + .get("provider_mutation_receipt", &receipt.receipt_id) + .await + .unwrap() + .is_none() + ); + assert_eq!( + get_provider_operation(&store, &receipt.receipt_id, "other") + .await + .unwrap_err() + .code(), + Code::NotFound + ); + } + + #[tokio::test] + async fn incomplete_provider_target_stays_failed_after_later_installation() { + let store = crate::persistence::test_store().await; + let receipt = receipt(); + record_provider_operation( + &store, + receipt.clone(), + ProviderReadinessReason::SnapshotMismatch, + ) + .await + .unwrap(); + let mut status = ready(&receipt); + observe_provider_status(&store, &mut status).await.unwrap(); + let operation = status.operation.unwrap(); + assert_eq!(operation.state, ConfigUpdateOperationState::Failed as i32); + assert_eq!(operation.completed_time, receipt.persisted_time); + } + + #[tokio::test] + async fn receipt_timestamp_requires_presence_and_canonical_nanos() { + let store = crate::persistence::test_store().await; + for invalid_time in [ + None, + Some(prost_types::Timestamp { + seconds: 0, + nanos: -1, + }), + Some(prost_types::Timestamp { + seconds: openshell_core::time::MAX_TIMESTAMP_SECONDS + 1, + nanos: 0, + }), + ] { + let mut receipt = receipt(); + receipt.persisted_time = invalid_time; + let error = + record_provider_operation(&store, receipt, ProviderReadinessReason::Unspecified) + .await + .unwrap_err(); + assert_eq!(error.code(), Code::Internal); + } + assert_eq!( + store + .count_in_workspace(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, "default") + .await + .unwrap(), + 0 + ); + + // The Unix epoch is a valid explicit timestamp, not missing data. + let mut epoch = receipt(); + epoch.persisted_time = Some(prost_types::Timestamp::default()); + let recorded = + record_provider_operation(&store, epoch.clone(), ProviderReadinessReason::Unspecified) + .await + .unwrap(); + assert_eq!(recorded, epoch); + assert_eq!( + get_provider_operation(&store, &epoch.receipt_id, "default") + .await + .unwrap() + .receipt, + epoch + ); + } + + #[tokio::test] + async fn receipt_timestamp_nanos_are_part_of_exact_completion_identity() { + let store = crate::persistence::test_store().await; + let receipt = receipt(); + record_provider_operation( + &store, + receipt.clone(), + ProviderReadinessReason::Unspecified, + ) + .await + .unwrap(); + let mut changed = ready(&receipt); + changed + .receipt + .as_mut() + .unwrap() + .persisted_time + .as_mut() + .unwrap() + .nanos += 1; + assert_eq!( + observe_provider_status(&store, &mut changed) + .await + .unwrap_err() + .code(), + Code::Internal + ); + let stored = get_provider_operation(&store, &receipt.receipt_id, "default") + .await + .unwrap(); + assert_eq!(stored.receipt, receipt); + assert_eq!( + stored.operation.state, + ConfigUpdateOperationState::Pending as i32 + ); + assert!(stored.operation.completed_time.is_none()); + + let mut exact = ready(&receipt); + observe_provider_status(&store, &mut exact).await.unwrap(); + let completed = exact.operation.unwrap(); + assert_eq!(completed.created_time, receipt.persisted_time); + assert!(completed.completed_time.is_some()); + assert_eq!(completed.updated_time, completed.completed_time); + openshell_core::time::validate_timestamp(completed.completed_time.as_ref().unwrap()) + .unwrap(); + } + + #[tokio::test] + async fn stale_or_partial_provider_evidence_cannot_complete_an_operation() { + let store = crate::persistence::test_store().await; + let receipt = receipt(); + record_provider_operation( + &store, + receipt.clone(), + ProviderReadinessReason::Unspecified, + ) + .await + .unwrap(); + let mut stale = ready(&receipt); + stale.observed.as_mut().unwrap().config_revision += 1; + assert!(observe_provider_status(&store, &mut stale).await.is_err()); + let mut partial = ready(&receipt); + partial + .observed + .as_mut() + .unwrap() + .launch_environment_installed = false; + assert!(observe_provider_status(&store, &mut partial).await.is_err()); + assert_eq!( + get_provider_operation(&store, &receipt.receipt_id, "default") + .await + .unwrap() + .operation + .state, + ConfigUpdateOperationState::Pending as i32 + ); + } + + #[tokio::test] + async fn applied_history_never_upgrades_an_expired_live_view() { + let store = crate::persistence::test_store().await; + let receipt = receipt(); + record_provider_operation( + &store, + receipt.clone(), + ProviderReadinessReason::Unspecified, + ) + .await + .unwrap(); + let mut status = ready(&receipt); + observe_provider_status(&store, &mut status).await.unwrap(); + let before = store + .get(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, &receipt.receipt_id) + .await + .unwrap() + .unwrap(); + status.state = ProviderReadinessState::Pending.into(); + status.reason = ProviderReadinessReason::SupervisorLeaseExpired.into(); + observe_provider_status(&store, &mut status).await.unwrap(); + assert_eq!(status.state, ProviderReadinessState::Pending as i32); + assert_eq!( + status.operation.unwrap().state, + ConfigUpdateOperationState::Applied as i32 + ); + let after = store + .get(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, &receipt.receipt_id) + .await + .unwrap() + .unwrap(); + assert_eq!(before.resource_version, after.resource_version); + } + + #[tokio::test] + async fn competing_terminal_observers_preserve_the_first_committed_outcome() { + let store = crate::persistence::test_store().await; + let receipt = receipt(); + record_provider_operation( + &store, + receipt.clone(), + ProviderReadinessReason::Unspecified, + ) + .await + .unwrap(); + let mut applied = ready(&receipt); + let mut superseded = applied.clone(); + superseded.state = ProviderReadinessState::Superseded.into(); + superseded.reason = ProviderReadinessReason::DesiredStateChanged.into(); + let (first, second) = tokio::join!( + observe_provider_status(&store, &mut applied), + observe_provider_status(&store, &mut superseded), + ); + first.unwrap(); + second.unwrap(); + assert_eq!(applied.operation, superseded.operation); + let stored = get_provider_operation(&store, &receipt.receipt_id, "default") + .await + .unwrap(); + assert!(matches!( + ConfigUpdateOperationState::try_from(stored.operation.state).unwrap(), + ConfigUpdateOperationState::Applied | ConfigUpdateOperationState::Superseded + )); + } + + #[tokio::test] + async fn failed_observation_and_recovered_snapshot_have_distinct_immutable_receipts() { + let store = crate::persistence::test_store().await; + let mut observed = receipt(); + observed.kind = ProviderMutationKind::Observe.into(); + let failed = record_provider_operation( + &store, + observed.clone(), + ProviderReadinessReason::CredentialsWithheld, + ) + .await + .unwrap(); + observed.mutation_id = Uuid::new_v4().to_string(); + observed.persisted_time.as_mut().unwrap().nanos += 1; + let repeated = record_provider_operation( + &store, + observed.clone(), + ProviderReadinessReason::CredentialsWithheld, + ) + .await + .unwrap(); + assert_eq!(repeated, failed); + let repaired = + record_provider_operation(&store, observed, ProviderReadinessReason::Unspecified) + .await + .unwrap(); + assert_ne!(repaired.receipt_id, failed.receipt_id); + assert_eq!( + get_provider_operation(&store, &failed.receipt_id, "default") + .await + .unwrap() + .operation + .state, + ConfigUpdateOperationState::Failed as i32 + ); + assert_eq!( + store + .count_in_workspace(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, "default") + .await + .unwrap(), + 2 + ); + } +} diff --git a/crates/openshell-server/src/sandbox_watch.rs b/crates/openshell-server/src/sandbox_watch.rs index 3a62fce398..3ccca3694c 100644 --- a/crates/openshell-server/src/sandbox_watch.rs +++ b/crates/openshell-server/src/sandbox_watch.rs @@ -24,13 +24,16 @@ pub const DEFAULT_STORE_POLL_INTERVAL: Duration = Duration::from_secs(1); #[derive(Debug, Clone)] pub struct SandboxWatchBus { inner: Arc>>>, + all: broadcast::Sender, } impl SandboxWatchBus { #[must_use] pub fn new() -> Self { + let (all, _rx) = broadcast::channel(1024); Self { inner: Arc::new(Mutex::new(HashMap::new())), + all, } } @@ -50,6 +53,7 @@ impl SandboxWatchBus { pub fn notify(&self, sandbox_id: &str) { let tx = self.sender_for(sandbox_id); let _ = tx.send(()); + let _ = self.all.send(sandbox_id.to_string()); } /// Subscribe to sandbox updates. @@ -57,6 +61,11 @@ impl SandboxWatchBus { self.sender_for(sandbox_id).subscribe() } + /// Subscribe to sandbox ids whose persisted state changed. + pub fn subscribe_all(&self) -> broadcast::Receiver { + self.all.subscribe() + } + /// Remove the bus entry for the given sandbox id. /// /// This drops the broadcast sender, closing any active receivers with @@ -226,4 +235,12 @@ mod tests { shutdown_tx.send(true).unwrap(); } + + #[test] + fn sandbox_watch_bus_global_subscription_receives_changed_id() { + let bus = SandboxWatchBus::new(); + let mut rx = bus.subscribe_all(); + bus.notify("sb-global"); + assert_eq!(rx.try_recv().unwrap(), "sb-global"); + } } diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index b3a9979f89..dd2c8d94e2 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -150,6 +150,57 @@ impl ObjectWorkspace for StoredConfigComponentObservation { } } +impl StoredConfigUpdateOperation { + /// Missing retry time is immediately due; invalid stored time must never be claimed. + pub(crate) fn next_attempt_at_ms(&self) -> i64 { + self.next_attempt_time.as_ref().map_or(0, |time| { + openshell_core::time::timestamp_to_millis(time).unwrap_or(i64::MAX) + }) + } +} + +impl ObjectId for StoredConfigUpdateOperation { + fn object_id(&self) -> &str { + self.metadata.as_ref().map_or("", |m| m.id.as_str()) + } +} + +impl ObjectName for StoredConfigUpdateOperation { + fn object_name(&self) -> &str { + self.metadata.as_ref().map_or("", |m| m.name.as_str()) + } +} + +impl ObjectLabels for StoredConfigUpdateOperation { + fn object_labels(&self) -> Option> { + self.metadata.as_ref().map(|m| m.labels.clone()) + } +} + +impl SetResourceVersion for StoredConfigUpdateOperation { + fn set_resource_version(&mut self, version: u64) { + if let Some(meta) = self.metadata.as_mut() { + meta.resource_version = version; + } + } +} + +impl GetResourceVersion for StoredConfigUpdateOperation { + fn get_resource_version(&self) -> u64 { + self.metadata.as_ref().map_or(0, |m| m.resource_version) + } +} + +impl ObjectWorkspace for StoredConfigUpdateOperation { + fn object_workspace(&self) -> &str { + self.metadata.as_ref().map_or("", |m| m.workspace.as_str()) + } + + fn requires_workspace() -> bool { + true + } +} + #[cfg(test)] mod tests { use super::*; @@ -162,7 +213,7 @@ mod tests { const STORAGE_V1_SCHEMA_SHA256: &str = "d68401809d8cea445c35233ef32412bbd041cb2ac5acaf368a0d0bf74d2ddf17"; const PUBLIC_RPC_SCHEMA_SHA256: &str = - "708ad5971c5ed52a1e9294da62322b9864f38fa6d95add8e2df49523b99bee4f"; + "747d06a81abeed04dab0be0c82a0cd0a4b931fb2fd658eb229290b1376387054"; const DURABLE_SCHEMA_SHA256: &str = "965a8a09fa80168906a4f9cc6169d3623b98f5281c367695f2f2898230915dd0"; const PUBLIC_DURABLE_OVERLAP_SHA256: &str = @@ -573,12 +624,12 @@ mod tests { } assert_eq!( compiled_method_count, - 102 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len(), + 103 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len(), "classify every compiled RPC" ); assert_eq!( methods.len(), - 77 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len(), + 78 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len(), "inventory every public gateway RPC" ); assert_eq!( @@ -586,7 +637,7 @@ mod tests { .iter() .filter(|method| method.starts_with("openshell.v1.OpenShell/")) .count(), - 77 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len() + 78 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len() ); assert!(methods.iter().all(|method| !method.contains(".storage."))); @@ -630,7 +681,7 @@ mod tests { overlap_hash.as_str(), ), ( - (320, 26), + (322, 27), (93, 19), (80, 19), PUBLIC_RPC_SCHEMA_SHA256, diff --git a/crates/openshell-server/src/supervisor_session.rs b/crates/openshell-server/src/supervisor_session.rs index 6b14b4126a..7a75e843cb 100644 --- a/crates/openshell-server/src/supervisor_session.rs +++ b/crates/openshell-server/src/supervisor_session.rs @@ -19,6 +19,7 @@ use uuid::Uuid; #[cfg(test)] use openshell_core::proto::ConfigBootstrapResult; +use openshell_core::proto::SUPERVISOR_PROTOCOL_REVISION; use openshell_core::proto::{ ConfigApplyOutcome, ConfigBootstrap, ConfigComponent, ConfigComponentApplyResult, ConfigSnapshotRevision, ConfigUpdate, ConfigUpdateResult, GatewayMessage, @@ -31,10 +32,6 @@ use openshell_core::proto::{ config_update, gateway_message, open_shell_client, peer_relay_frame, relay_open, startup_config_prepared, supervisor_message, }; -use openshell_core::proto::{ - LEGACY_SUPERVISOR_PROTOCOL_REVISION, PREVIOUS_SUPERVISOR_PROTOCOL_REVISION, - SUPERVISOR_PROTOCOL_REVISION, -}; use openshell_core::transport_errors::is_expected_transport_close_status; use openshell_core::{ObjectId, ObjectWorkspace}; @@ -834,6 +831,7 @@ impl SupervisorSessionRegistry { &self, sandbox_id: &str, message: SupervisorConfigMessage, + require_acknowledgement: bool, ) -> DeliveryDisposition { let component_name = message.component_name(); let mut sessions = self.sessions.lock().unwrap(); @@ -848,7 +846,8 @@ impl SupervisorSessionRegistry { &mut session.config_sequences.provider_environment } }; - if delivery_state.in_flight.is_none() + if !require_acknowledgement + && delivery_state.in_flight.is_none() && delivery_state.last_acknowledged_fingerprint.as_ref() == Some(&config_message_fingerprint(&message)) { @@ -2599,15 +2598,10 @@ pub async fn handle_connect_supervisor( crate::auth::guard::ensure_sandbox_principal_scope(principal, &sandbox_id)?; } let sandbox = require_persisted_sandbox(&state.store, &sandbox_id).await?; - // Validate readiness identities before replacing a healthy session. Older - // supervisors remain usable but cannot assert provider installation. + // Validate readiness identities before replacing a healthy session. let provider_readiness = ProviderReadinessEvidence::from_hello(&hello)?; - let bootstrap_timeout = if stream_applies_config { - crate::config_delivery::REQUIRED_CONFIG_BOOTSTRAP_BUILD_TIMEOUT - } else { - crate::config_delivery::OPTIONAL_CONFIG_BOOTSTRAP_BUILD_TIMEOUT - }; + let bootstrap_timeout = crate::config_delivery::REQUIRED_CONFIG_BOOTSTRAP_BUILD_TIMEOUT; let mut repair_updates = matches!(image_policy_admission, ImagePolicyAdmission::Invalid) .then(|| state.sandbox_watch_bus.subscribe(&sandbox_id)); let repair_deadline = tokio::time::Instant::now() + STARTUP_POLICY_REPAIR_TIMEOUT; @@ -2854,28 +2848,13 @@ pub async fn handle_connect_supervisor( Ok(Response::new(stream)) } -fn validate_protocol_revision(sandbox_id: &str, supervisor_revision: u32) -> Result<(), Status> { - match supervisor_revision { - SUPERVISOR_PROTOCOL_REVISION => Ok(()), - PREVIOUS_SUPERVISOR_PROTOCOL_REVISION => { - counter!("openshell_supervisor_protocol_previous_sessions_total").increment(1); - warn!( - sandbox_id = %sandbox_id, - "supervisor session: Stage 1 supervisor is using polling compatibility" - ); - Ok(()) - } - LEGACY_SUPERVISOR_PROTOCOL_REVISION => { - counter!("openshell_supervisor_protocol_legacy_sessions_total").increment(1); - warn!( - sandbox_id = %sandbox_id, - "supervisor session: supervisor predates the protocol handshake; recreate the sandbox before the next gateway upgrade" - ); - Ok(()) - } - other => Err(Status::failed_precondition(format!( - "supervisor protocol revision mismatch: gateway requires {SUPERVISOR_PROTOCOL_REVISION}, supervisor offered {other}" - ))), +fn validate_protocol_revision(_sandbox_id: &str, supervisor_revision: u32) -> Result<(), Status> { + if supervisor_revision == SUPERVISOR_PROTOCOL_REVISION { + Ok(()) + } else { + Err(Status::failed_precondition(format!( + "supervisor protocol revision mismatch: gateway requires {SUPERVISOR_PROTOCOL_REVISION}, supervisor offered {supervisor_revision}" + ))) } } @@ -3520,6 +3499,7 @@ async fn record_component_apply_result( ) .increment(1); record_config_component_observation(state, sandbox_id, component, outcome, result).await?; + crate::config_update_operation::complete_from_apply_result(state, sandbox_id, result).await?; if component != ConfigComponent::SandboxConfig { return Ok(()); } @@ -3736,10 +3716,10 @@ mod tests { } #[test] - fn supervisor_protocol_revision_accepts_current_and_legacy_peers() { + fn supervisor_protocol_revision_accepts_only_current_peer() { assert!(validate_protocol_revision("sb-1", SUPERVISOR_PROTOCOL_REVISION).is_ok()); - assert!(validate_protocol_revision("sb-1", PREVIOUS_SUPERVISOR_PROTOCOL_REVISION).is_ok()); - assert!(validate_protocol_revision("sb-1", LEGACY_SUPERVISOR_PROTOCOL_REVISION).is_ok()); + assert!(validate_protocol_revision("sb-1", 1).is_err()); + assert!(validate_protocol_revision("sb-1", 0).is_err()); } #[test] @@ -3928,6 +3908,7 @@ mod tests { state.supervisor_sessions.deliver_config( "sb-bootstrap-ack", SupervisorConfigMessage::ProviderEnvironment(snapshot), + false ), DeliveryDisposition::SuppressedUnchanged ); @@ -3959,7 +3940,7 @@ mod tests { assert_eq!( state .supervisor_sessions - .deliver_config(sandbox_id, message.clone()), + .deliver_config(sandbox_id, message.clone(), false), DeliveryDisposition::Enqueued ); let Some(gateway_message::Payload::ConfigUpdate(update)) = @@ -4000,7 +3981,7 @@ mod tests { assert_eq!( state .supervisor_sessions - .deliver_config(sandbox_id, message), + .deliver_config(sandbox_id, message, false), DeliveryDisposition::Enqueued, "a non-durable admission must leave the revision eligible for repair" ); @@ -4045,7 +4026,7 @@ mod tests { assert_eq!( state .supervisor_sessions - .deliver_config(sandbox_id, message.clone()), + .deliver_config(sandbox_id, message.clone(), false), DeliveryDisposition::Enqueued ); let Some(gateway_message::Payload::ConfigUpdate(update)) = @@ -4270,57 +4251,16 @@ mod tests { } #[tokio::test] - async fn legacy_supervisor_without_protocol_revision_is_accepted() { + async fn rejects_supervisors_that_require_configuration_polling() { let state = state_with_sandbox("sb-legacy").await; - let mut harness = crate::grpc::test_support::connect_supervisor_stream( - &state, - "sb-legacy", - LEGACY_SUPERVISOR_PROTOCOL_REVISION, - ) - .await - .expect("legacy supervisor must connect"); - - let Some(gateway_message::Payload::SessionAccepted(accepted)) = - first_gateway_message(&mut harness).await.payload - else { - panic!("expected SessionAccepted"); - }; - assert_eq!( - accepted.protocol_revision, - LEGACY_SUPERVISOR_PROTOCOL_REVISION - ); - assert!( - state - .supervisor_sessions - .is_current_session("sb-legacy", &accepted.session_id) - ); - } - - #[tokio::test] - async fn stage_one_supervisor_uses_polling_compatibility() { - let state = state_with_sandbox("sb-stage-one").await; - let mut harness = crate::grpc::test_support::connect_supervisor_stream( - &state, - "sb-stage-one", - PREVIOUS_SUPERVISOR_PROTOCOL_REVISION, - ) - .await - .expect("Stage 1 supervisor must connect"); - - let Some(gateway_message::Payload::SessionAccepted(accepted)) = - first_gateway_message(&mut harness).await.payload - else { - panic!("expected SessionAccepted"); - }; - assert_eq!( - accepted.protocol_revision, - PREVIOUS_SUPERVISOR_PROTOCOL_REVISION - ); - assert!( - state - .supervisor_sessions - .is_current_session("sb-stage-one", &accepted.session_id) - ); + for revision in [0, 1, 2] { + let result = + crate::grpc::test_support::connect_supervisor_stream(&state, "sb-legacy", revision) + .await; + assert!( + matches!(result, Err(ref status) if status.code() == tonic::Code::FailedPrecondition) + ); + } } #[tokio::test] @@ -4677,6 +4617,7 @@ mod tests { .deliver( "missing", SupervisorConfigMessage::SandboxConfig(Box::default()), + false ) .await, DeliveryDisposition::NoActiveSession @@ -4728,11 +4669,23 @@ mod tests { assert_eq!( registry.deliver_config( "sb-1", - SupervisorConfigMessage::SandboxConfig(Box::new(snapshot)), + SupervisorConfigMessage::SandboxConfig(Box::new(snapshot.clone())), + false ), DeliveryDisposition::SuppressedUnchanged ); assert!(rx.try_recv().is_err()); + // A newly committed operation needs a result even when the current + // session has already acknowledged this exact snapshot. + assert_eq!( + registry.deliver_config( + "sb-1", + SupervisorConfigMessage::SandboxConfig(Box::new(snapshot)), + true, + ), + DeliveryDisposition::Enqueued + ); + assert!(rx.try_recv().is_ok()); } #[test] @@ -4757,6 +4710,7 @@ mod tests { provider_env_revision: 8, ..Default::default() }), + false ), DeliveryDisposition::Enqueued ); @@ -4847,7 +4801,7 @@ mod tests { &config_message_fingerprint(&message), )); assert_eq!( - registry.deliver_config("sb-1", message), + registry.deliver_config("sb-1", message, false), DeliveryDisposition::SuppressedUnchanged ); assert_eq!( @@ -4857,6 +4811,7 @@ mod tests { provider_env_revision: 12, ..snapshot })), + false, ), DeliveryDisposition::Enqueued ); @@ -4891,7 +4846,7 @@ mod tests { &fingerprint, )); assert_eq!( - registry.deliver_config("sb-1", message), + registry.deliver_config("sb-1", message, false), DeliveryDisposition::SuppressedUnchanged ); @@ -4901,7 +4856,7 @@ mod tests { ..snapshot.clone() }); assert_eq!( - registry.deliver_config("sb-1", policy_changed), + registry.deliver_config("sb-1", policy_changed, false), DeliveryDisposition::Enqueued ); assert!(rx.try_recv().is_ok()); @@ -4940,6 +4895,7 @@ mod tests { provider_attachment_epoch: "epoch-2".into(), ..snapshot }), + false, ), DeliveryDisposition::Enqueued ); @@ -4962,6 +4918,7 @@ mod tests { config_revision: 2, ..Default::default() })), + false ) .await, DeliveryDisposition::Enqueued @@ -4971,6 +4928,7 @@ mod tests { .deliver( "sb-1", SupervisorConfigMessage::SandboxConfig(Box::default()), + false ) .await, DeliveryDisposition::Coalesced @@ -4982,6 +4940,7 @@ mod tests { SupervisorConfigMessage::ProviderEnvironment( ProviderEnvironmentSnapshot::default(), ), + false ) .await, DeliveryDisposition::Enqueued @@ -5045,6 +5004,7 @@ mod tests { .deliver( "sb-1", SupervisorConfigMessage::SandboxConfig(Box::default()), + false ) .await, DeliveryDisposition::Enqueued @@ -5064,6 +5024,7 @@ mod tests { .deliver( "sb-1", SupervisorConfigMessage::SandboxConfig(Box::default()), + false ) .await, DeliveryDisposition::Enqueued @@ -5089,6 +5050,7 @@ mod tests { .deliver( "sb-1", SupervisorConfigMessage::SandboxConfig(Box::default()), + false ) .await, DeliveryDisposition::QueueFull @@ -5100,6 +5062,7 @@ mod tests { .deliver( "sb-1", SupervisorConfigMessage::SandboxConfig(Box::default()), + false ) .await, DeliveryDisposition::SessionClosed @@ -5127,6 +5090,7 @@ mod tests { .deliver( "sb-1", SupervisorConfigMessage::ProviderEnvironment(snapshot), + false ) .await, DeliveryDisposition::PayloadTooLarge diff --git a/crates/openshell-server/tests/common/mod.rs b/crates/openshell-server/tests/common/mod.rs index 414251734d..8186584dd6 100644 --- a/crates/openshell-server/tests/common/mod.rs +++ b/crates/openshell-server/tests/common/mod.rs @@ -9,6 +9,10 @@ #![allow(dead_code)] +use openshell_core::proto::{ + GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, +}; + use hyper_util::{ rt::{TokioExecutor, TokioIo}, server::conn::auto::Builder, @@ -19,8 +23,7 @@ use openshell_core::proto::{ ExchangeProviderSubjectTokenRequest, ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, GatewayMessage, GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, - GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest, - GetSandboxProviderEnvironmentResponse, GetSandboxRequest, HealthRequest, HealthResponse, + GetSandboxConfigResponse, GetSandboxRequest, HealthRequest, HealthResponse, IssueSandboxTokenRequest, IssueSandboxTokenResponse, ListProvidersRequest, ListProvidersResponse, ListSandboxesRequest, ListSandboxesResponse, PeerRelayFrame, ProviderResponse, RefreshSandboxTokenRequest, RefreshSandboxTokenResponse, RelayFrame, @@ -463,6 +466,17 @@ impl OpenShell for TestOpenShell { Ok(Response::new(ReceiverStream::new(rx))) } + #[allow(unused_qualifications)] + async fn get_config_update_operation( + &self, + _request: tonic::Request, + ) -> Result< + tonic::Response, + tonic::Status, + > { + Err(tonic::Status::unimplemented("unused")) + } + async fn update_config( &self, _request: tonic::Request, diff --git a/crates/openshell-server/tests/supervisor_relay_integration.rs b/crates/openshell-server/tests/supervisor_relay_integration.rs index 5856acbbda..8cc43d1d55 100644 --- a/crates/openshell-server/tests/supervisor_relay_integration.rs +++ b/crates/openshell-server/tests/supervisor_relay_integration.rs @@ -447,6 +447,17 @@ impl OpenShell for RelayGateway { ) -> Result, Status> { Err(Status::unimplemented("unused")) } + #[allow(unused_qualifications)] + async fn get_config_update_operation( + &self, + _request: tonic::Request, + ) -> Result< + tonic::Response, + tonic::Status, + > { + Err(tonic::Status::unimplemented("unused")) + } + async fn update_config( &self, _: tonic::Request, diff --git a/crates/openshell-supervisor-network/src/policy_local.rs b/crates/openshell-supervisor-network/src/policy_local.rs index 5afa4597ff..b03b8825ef 100644 --- a/crates/openshell-supervisor-network/src/policy_local.rs +++ b/crates/openshell-supervisor-network/src/policy_local.rs @@ -884,7 +884,7 @@ fn is_terminal_status(status: &str) -> bool { /// The polling cadence here is faster than `PROPOSAL_WAIT_POLL_INTERVAL` /// (which paces upstream gateway calls). This loop only reads in-memory /// state, so 200ms gives a responsive handoff to the agent's retry once -/// the supervisor's own policy poll catches up. +/// the supervisor's stream-delivered desired state catches up. async fn wait_for_local_policy_to_cover( ctx: &PolicyLocalContext, proposed_rule: &NetworkPolicyRule, diff --git a/crates/openshell-supervisor-network/src/run.rs b/crates/openshell-supervisor-network/src/run.rs index 1eba8a0742..8e09edaee7 100644 --- a/crates/openshell-supervisor-network/src/run.rs +++ b/crates/openshell-supervisor-network/src/run.rs @@ -153,7 +153,7 @@ pub struct Networking { pub proxy: Option, pub ca_file_paths: Option<(std::path::PathBuf, std::path::PathBuf)>, - /// Policy-local route context: shared with the orchestrator's policy poll + /// Policy-local route context: shared with the orchestrator's stream configuration loop /// loop so it can publish updated `SandboxPolicy` snapshots that the /// `policy.local` route handler returns to the workload. pub policy_local_ctx: Arc, @@ -202,7 +202,7 @@ pub async fn run_networking( #[cfg(target_os = "linux")] transparent_runtime: Option, network_mediation_source: Option>, ) -> Result { - // Build the policy-local route context. The orchestrator's policy poll + // Build the policy-local route context. The orchestrator's stream configuration loop // loop also holds an `Arc` clone (via `Networking::policy_local_ctx`) so // it can publish updated policy snapshots after a successful reload. let policy_local_ctx = Arc::new(PolicyLocalContext::new( diff --git a/crates/openshell-supervisor-process/src/supervisor_session.rs b/crates/openshell-supervisor-process/src/supervisor_session.rs index 6b4bb0b892..b6ffb1f8e7 100644 --- a/crates/openshell-supervisor-process/src/supervisor_session.rs +++ b/crates/openshell-supervisor-process/src/supervisor_session.rs @@ -15,6 +15,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; +use openshell_core::proto::SUPERVISOR_PROTOCOL_REVISION; use openshell_core::proto::open_shell_client::OpenShellClient; use openshell_core::proto::{ ConfigApplyFailure, ConfigApplyOutcome, ConfigBootstrap, ConfigBootstrapResult, @@ -25,10 +26,6 @@ use openshell_core::proto::{ config_snapshot_revision, config_update, gateway_message, relay_open, startup_config_prepared, supervisor_message, }; -use openshell_core::proto::{ - LEGACY_SUPERVISOR_PROTOCOL_REVISION, PREVIOUS_SUPERVISOR_PROTOCOL_REVISION, - SUPERVISOR_PROTOCOL_REVISION, -}; use openshell_isolation_interface::contract::{BoundaryLoopbackConnector, LoopbackTarget}; use openshell_ocsf::{ ActivityId, BaseEventBuilder, ConnectionInfo, Endpoint, EventContext, NetworkActivityBuilder, @@ -765,9 +762,6 @@ async fn run_prepared_session( .map_err(|_| "failed to queue configuration bootstrap result")?; } let config_sequences = Arc::new(Mutex::new(ConfigSequenceWatermarks::default())); - if prepared.protocol_revision != SUPERVISOR_PROTOCOL_REVISION { - config.ready_tx.send_replace(true); - } // Main loop: receive gateway messages + send heartbeats. let mut heartbeat_interval = @@ -820,24 +814,12 @@ async fn run_prepared_session( fn validate_gateway_protocol_revision( gateway_revision: u32, ) -> Result<(), Box> { - match gateway_revision { - SUPERVISOR_PROTOCOL_REVISION => Ok(()), - PREVIOUS_SUPERVISOR_PROTOCOL_REVISION => { - warn!( - "supervisor session: gateway uses Stage 1 stream semantics; polling remains active" - ); - Ok(()) - } - LEGACY_SUPERVISOR_PROTOCOL_REVISION => { - warn!( - "supervisor session: gateway predates the protocol handshake; upgrade the gateway before pinning newer supervisor images" - ); - Ok(()) - } - other => Err(format!( - "supervisor protocol revision mismatch: supervisor requires {SUPERVISOR_PROTOCOL_REVISION}, gateway offered {other}" - ) - .into()), + if gateway_revision == SUPERVISOR_PROTOCOL_REVISION { + Ok(()) + } else { + Err(format!( + "supervisor protocol revision mismatch: supervisor requires {SUPERVISOR_PROTOCOL_REVISION}, gateway offered {gateway_revision}" + ).into()) } } @@ -1403,10 +1385,9 @@ mod target_tests { use super::*; #[test] - fn gateway_protocol_revision_accepts_current_and_legacy_peers() { + fn gateway_protocol_revision_accepts_only_current_peer() { assert!(validate_gateway_protocol_revision(SUPERVISOR_PROTOCOL_REVISION).is_ok()); - assert!(validate_gateway_protocol_revision(PREVIOUS_SUPERVISOR_PROTOCOL_REVISION).is_ok()); - assert!(validate_gateway_protocol_revision(LEGACY_SUPERVISOR_PROTOCOL_REVISION).is_ok()); + assert!(validate_gateway_protocol_revision(SUPERVISOR_PROTOCOL_REVISION - 1).is_err()); } #[test] diff --git a/crates/openshell-supervisor/src/lib.rs b/crates/openshell-supervisor/src/lib.rs index ae9f7f4b71..0e6f4e9335 100644 --- a/crates/openshell-supervisor/src/lib.rs +++ b/crates/openshell-supervisor/src/lib.rs @@ -4052,11 +4052,38 @@ struct PendingStreamProvider { expires_at_ms: Option, } +#[derive(Debug)] +struct RejectedStreamSandbox { + configuration_instance_id: String, + requested_revision: openshell_core::proto::ConfigSnapshotRevision, + environment: EnvironmentIdentity, + error: String, + failure_mode: PolicyValidationFailureMode, + result: openshell_core::proto::ConfigComponentApplyResult, +} + +impl RejectedStreamSandbox { + fn matches( + &self, + snapshot: &openshell_core::grpc_client::SettingsPollResult, + requested_revision: &openshell_core::proto::ConfigSnapshotRevision, + environment: &EnvironmentIdentity, + error: &str, + ) -> bool { + self.configuration_instance_id == snapshot.configuration_instance_id + && self.requested_revision == *requested_revision + && self.environment == *environment + && self.error == error + && self.failure_mode == snapshot.policy_validation_failure_mode + } +} + #[derive(Debug)] struct StreamConfigurationState { active_environment: EnvironmentIdentity, pending_provider: Option, pending_sandbox: Option>, + rejected_sandbox: Option>, } impl StreamConfigurationState { @@ -4068,6 +4095,7 @@ impl StreamConfigurationState { .unwrap_or_default(), pending_provider: None, pending_sandbox: None, + rejected_sandbox: None, } } } @@ -4369,6 +4397,7 @@ async fn apply_stream_sandbox_snapshot( && current_stream_revision.as_ref() == Some(&requested_revision) && desired_environment == stream_state.active_environment { + stream_state.rejected_sandbox = None; return config_apply_result( ConfigComponent::SandboxConfig, requested_revision.clone(), @@ -4394,7 +4423,12 @@ async fn apply_stream_sandbox_snapshot( } else { &snapshot.configuration_error }; - return match apply_policy_validation_failure( + if let Some(rejected) = stream_state.rejected_sandbox.as_ref() + && rejected.matches(&snapshot, &requested_revision, &desired_environment, error) + { + return rejected.result.clone(); + } + let (result, cacheable) = match apply_policy_validation_failure( &ctx.opa_engine, snapshot.policy_validation_failure_mode, *has_last_valid_policy, @@ -4417,22 +4451,39 @@ async fn apply_stream_sandbox_snapshot( } else { ConfigApplyOutcome::FailedClosed }; - config_apply_result( - ConfigComponent::SandboxConfig, - requested_revision, - applied_revision, - outcome, - Some(("configuration_rejected", error.to_string(), true)), + ( + config_apply_result( + ConfigComponent::SandboxConfig, + requested_revision.clone(), + applied_revision, + outcome, + Some(("configuration_rejected", error.to_string(), true)), + ), + true, ) } - Err(failure) => config_apply_result( - ConfigComponent::SandboxConfig, - requested_revision, - None, - ConfigApplyOutcome::FailedClosed, - Some(("configuration_rejected", failure.to_string(), true)), + Err(failure) => ( + config_apply_result( + ConfigComponent::SandboxConfig, + requested_revision.clone(), + None, + ConfigApplyOutcome::FailedClosed, + Some(("configuration_rejected", failure.to_string(), true)), + ), + false, ), }; + if cacheable { + stream_state.rejected_sandbox = Some(Box::new(RejectedStreamSandbox { + configuration_instance_id: snapshot.configuration_instance_id, + requested_revision, + environment: desired_environment, + error: error.to_string(), + failure_mode: snapshot.policy_validation_failure_mode, + result: result.clone(), + })); + } + return result; } if desired_environment != stream_state.active_environment @@ -4638,6 +4689,7 @@ async fn apply_stream_sandbox_snapshot( match outcome { Ok(outcome) => { + stream_state.rejected_sandbox = None; if let Ok(generation) = ctx .opa_engine .generation_guard(ctx.opa_engine.current_generation()) @@ -7982,6 +8034,94 @@ network_policies: assert_eq!(current_settings, initial.settings); } + #[tokio::test] + async fn duplicate_rejected_stream_snapshot_does_not_advance_fail_closed_generation() { + use openshell_core::proto::{ConfigApplyOutcome, PolicySource}; + + let mut rejected = + settings_poll_result(Some(proto_policy_fixture()), 2, PolicySource::Sandbox); + rejected.config_revision = 200; + rejected.settings_revision = 2; + rejected.configuration_admitted = false; + rejected.configuration_error = "invalid policy".to_string(); + rejected.policy_validation_failure_mode = PolicyValidationFailureMode::FailClosed; + + let engine = + Arc::new(OpaEngine::from_proto(&proto_policy_fixture()).expect("build OPA engine")); + let initial_generation = engine.current_generation(); + let ctx = policy_poll_test_context( + engine, + LoadedPolicyOrigin::Gateway { + revision: None, + has_last_valid_policy: false, + }, + default_middleware_connector(), + ); + let (client, _polls, _reports) = scripted_policy_gateway(); + let mut config_revision = 0; + let mut stream_revision = None; + let mut policy_version = 0; + let mut policy_hash = String::new(); + let mut endpoint_policy = None; + let mut middleware_services = Vec::new(); + let mut extension_authentication_enabled = false; + let mut middleware_registry_status = MiddlewareRegistryStatus::Synchronized; + let mut current_settings = std::collections::HashMap::new(); + let mut stream_state = StreamConfigurationState::new(None); + let mut provider_revision = 0; + let mut has_last_valid_policy = false; + + let result = apply_stream_sandbox_snapshot( + &ctx, + &client, + rejected.clone(), + &mut config_revision, + &mut stream_revision, + &mut policy_version, + &mut policy_hash, + &mut endpoint_policy, + &mut middleware_services, + &mut extension_authentication_enabled, + &mut middleware_registry_status, + &mut current_settings, + &mut stream_state, + &mut provider_revision, + true, + &mut has_last_valid_policy, + ) + .await; + + assert_eq!( + ConfigApplyOutcome::try_from(result.outcome).unwrap(), + ConfigApplyOutcome::FailedClosed + ); + let rejected_generation = ctx.opa_engine.current_generation(); + assert!(rejected_generation > initial_generation); + + let duplicate = apply_stream_sandbox_snapshot( + &ctx, + &client, + rejected, + &mut config_revision, + &mut stream_revision, + &mut policy_version, + &mut policy_hash, + &mut endpoint_policy, + &mut middleware_services, + &mut extension_authentication_enabled, + &mut middleware_registry_status, + &mut current_settings, + &mut stream_state, + &mut provider_revision, + true, + &mut has_last_valid_policy, + ) + .await; + + assert_eq!(duplicate, result); + assert_eq!(ctx.opa_engine.current_generation(), rejected_generation); + } + #[tokio::test] async fn revision_two_stream_never_polls_gateway_settings() { let initial = settings_poll_result( diff --git a/docs/get-started/tutorials/microsoft-graph-provider-refresh.mdx b/docs/get-started/tutorials/microsoft-graph-provider-refresh.mdx index 4204e7ebfb..438ac5f10b 100644 --- a/docs/get-started/tutorials/microsoft-graph-provider-refresh.mdx +++ b/docs/get-started/tutorials/microsoft-graph-provider-refresh.mdx @@ -173,7 +173,7 @@ The request uses the [Microsoft Graph list messages API](https://learn.microsoft ## Update Running Sandboxes -Provider refresh updates the provider record at the gateway. Running sandboxes poll for provider environment revisions, but already-running processes keep the environment they started with. +Provider refresh updates the provider record at the gateway. Running sandboxes receive provider environment revisions over the supervisor configuration stream, but already-running processes keep the environment they started with. If you attach this provider to an existing sandbox or update provider credentials after a process has already started, launch a new process inside the sandbox before expecting `MS_GRAPH_ACCESS_TOKEN` to appear in that process environment. diff --git a/docs/observability/logging.mdx b/docs/observability/logging.mdx index 13ddfeda6d..36b2e4fdc5 100644 --- a/docs/observability/logging.mdx +++ b/docs/observability/logging.mdx @@ -176,10 +176,9 @@ A process launched inside the sandbox: OCSF PROC:LAUNCH [INFO] sleep(49) ``` -A policy reload after a settings change: +A policy reload after a stream-delivered configuration change: ```text -OCSF CONFIG:DETECTED [INFO] Settings poll: config change detected [old_revision:2915564174587774909 new_revision:11008534403127604466 policy_changed:true] OCSF CONFIG:LOADED [INFO] Policy reloaded successfully [policy_hash:0cc0c2b525573c07] ``` diff --git a/docs/providers/profiles.mdx b/docs/providers/profiles.mdx index 66f498ee11..d902a6963b 100644 --- a/docs/providers/profiles.mdx +++ b/docs/providers/profiles.mdx @@ -1053,7 +1053,7 @@ The API's `operation` field records the common operation's historical outcome; i ### Runtime Limitations -Running sandboxes periodically check for provider and policy changes. Use `--wait` or `sandbox provider status` to confirm when a saved change has taken effect. +Provider attach and detach update the persisted sandbox provider list. Running sandboxes receive provider environment and effective policy revisions over the supervisor configuration stream. Use `--wait` or `sandbox provider status` to confirm when a saved change has taken effect. The policy effect applies to future effective policy reads after the sandbox observes the update. The credential environment effect applies only to new process launches after the update is observed, such as later SSH, exec, or SFTP sessions. diff --git a/docs/reference/gateway-config.mdx b/docs/reference/gateway-config.mdx index 2b850e3f41..a6fd5f2de3 100644 --- a/docs/reference/gateway-config.mdx +++ b/docs/reference/gateway-config.mdx @@ -253,10 +253,9 @@ namespace = "openshell" # Required in raw TOML; Helm derives this from the gateway Service. grpc_endpoint = "https://openshell-gateway.openshell.svc:8080" default_image = "nvcr.io/nvidia/base/ubuntu:24.04" -# Defaults to the gateway version. The current internal supervisor protocol -# streams authoritative configuration snapshots. The immediately previous -# revision remains polling-compatible, and pre-handshake supervisors remain -# compatible for one release. Other revisions reject the session. +# Defaults to the gateway version. Gateway and supervisor require matching +# protocol revision 3 and stream authoritative configuration snapshots. +# Peers that require configuration polling are rejected. # supervisor_image = "ghcr.io/nvidia/openshell/supervisor:" client_tls_secret_name = "openshell-client-tls" service_account_name = "openshell-sandbox" @@ -590,8 +589,8 @@ image_pull_secrets = ["regcred"] sandbox_runtime_image_pull_policy = "if_not_present" # Defaults to the gateway version. Custom builds must match the gateway's # internal supervisor protocol revision; mismatched peers reject the session. -# Supervisors from releases before the handshake existed remain compatible for -# one release. +# Protocol revision 3 is required; older peers are rejected. +# After upgrading the gateway, recreate sandboxes started by the previous release. # supervisor_image = "ghcr.io/nvidia/openshell/supervisor:" supervisor_image_pull_policy = "if_not_present" @@ -745,10 +744,8 @@ sandbox_label = "docker-dev" grpc_endpoint = "https://127.0.0.1:17670" # Workload-side runtime. Defaults to the gateway version. # sandbox_runtime_image = "ghcr.io/nvidia/openshell/sandbox:" -# Supervisor runtime defaults to the gateway version. The current internal -# protocol streams authoritative configuration snapshots; the immediately -# previous revision uses polling compatibility. Pre-handshake supervisors -# remain compatible for one release. Other revisions reject the session. +# Supervisor runtime defaults to the gateway version. Both peers require +# protocol revision 3 and stream authoritative configuration snapshots. # supervisor_image = "ghcr.io/nvidia/openshell/supervisor:" # Unsafe operator override. Host bind mounts, including Docker local-driver # bind-backed volumes, expose gateway-host paths inside sandboxes and can @@ -812,11 +809,9 @@ ssh_socket_path = "/run/openshell/ssh.sock" stop_timeout_secs = 45 # Statically linked workload-side runtime. Defaults to the gateway version. # sandbox_runtime_image = "ghcr.io/nvidia/openshell/sandbox:" -# Dynamically linked supervisor runtime defaults to the gateway version. The -# current internal protocol streams authoritative configuration snapshots; the -# immediately previous revision uses polling compatibility. Pre-handshake -# supervisors remain compatible for one release. Other revisions reject the -# session. +# Dynamically linked supervisor runtime defaults to the gateway version. +# Both peers require protocol revision 3 and stream authoritative configuration +# snapshots. # supervisor_image = "ghcr.io/nvidia/openshell/supervisor:" # Unsafe operator override. Host bind mounts, including Podman local-driver # bind-backed volumes, expose gateway-host paths inside sandboxes and can diff --git a/docs/sandboxes/inference-routing.mdx b/docs/sandboxes/inference-routing.mdx index b0234e9686..8cf8a2c6bb 100644 --- a/docs/sandboxes/inference-routing.mdx +++ b/docs/sandboxes/inference-routing.mdx @@ -103,7 +103,8 @@ openshell sandbox provider attach inference-demo nvidia-prod openshell sandbox provider list inference-demo ``` -Running sandboxes poll for provider and effective-policy changes. Launch a new +Running sandboxes receive provider and effective-policy changes over the +supervisor configuration stream. Launch a new process after attachment so it receives the new credential placeholder: ```shell diff --git a/docs/sandboxes/policies.mdx b/docs/sandboxes/policies.mdx index e71728d4c0..20b1d68387 100644 --- a/docs/sandboxes/policies.mdx +++ b/docs/sandboxes/policies.mdx @@ -375,10 +375,12 @@ The incremental update surface is split into endpoint-level operations and metho | `--any-binary` | Explicitly acknowledges an existing rule that authorizes every binary. | Use for L7 appends instead of `--binary` when the target rule has no binary restriction. | | `--endpoint-path ` | Selects an exact existing endpoint path for L7 appends. | Resolve path ambiguity; pass `''` to select an endpoint without a path scope. | | `--dry-run` | Shows the merged policy locally and does not call the gateway. | Review the result before persisting it. | -| `--wait` | Polls until the sandbox reports that the new revision loaded. | Confirm the change took effect before continuing. | -| `--timeout ` | Sets the timeout for `--wait`. | Extend the wait window for slower sandboxes. | +| `--wait` | Asks the gateway to wait on the durable update operation until the exact revision is applied or reaches another terminal state. | Confirm the change took effect before continuing. | +| `--timeout ` | Sets the server-side timeout for `--wait`. A timeout does not roll back the committed update. | Extend the wait window for slower sandboxes. | `--wait` and `--dry-run` cannot be used together. +If the wait times out, the error includes the durable operation ID so a client +can query its later result without resubmitting the mutation. ### Add Endpoint Compared to Allow and Deny @@ -979,3 +981,25 @@ Explore related topics: - To learn about the built-in sandbox policy, refer to [Default Policy](/reference/default-policy). - To view the full field-by-field YAML definition, refer to the [Policy Schema Reference](/reference/policy-schema). - To review the default policy breakdown, refer to [Default Policy](/reference/default-policy). + +### Waiting for completion + +`openshell policy set --wait` waits for the durable update operation to finish. +Completion can mean applied, inactive, failed, superseded, or cancelled. An +applied operation can have a degraded outcome; the CLI reports that separately. +A wait timeout does not cancel the update or undo the saved policy. +OpenShell compares only the policy dimension when completing a policy operation, +so a concurrent settings change does not supersede an otherwise applied policy. +`superseded` means a newer policy revision replaced the requested revision. + +When a gateway-global policy is active, a sandbox policy update changes the +dormant sandbox-authored policy. Its operation can complete after the supervisor +accepts the resulting configuration snapshot, but the global policy remains the +effective source. Inspect the sandbox policy status and its `policy_source` +before assuming that the authored sandbox policy controls traffic. + +API clients select `WAIT_FOR_COMPLETION` and set `wait_timeout` to a protobuf +`Duration`. Inspect the returned operation's state and outcome before treating +it as successful application. Save its operation ID for later lookup. When using +`request_id`, retrying the same mutation with a different wait preference returns +the original operation without creating another policy revision. diff --git a/docs/sandboxes/policy-advisor.mdx b/docs/sandboxes/policy-advisor.mdx index 80884620d5..d494ae1c87 100644 --- a/docs/sandboxes/policy-advisor.mdx +++ b/docs/sandboxes/policy-advisor.mdx @@ -45,7 +45,7 @@ openshell settings delete --global \ --yes ``` -Set the value before creating a sandbox when you want the first denied request to include policy advisor guidance. Running sandboxes poll settings and can enable the surface after startup, but startup enablement gives the agent the clearest first-denial path. +Set the value before creating a sandbox when you want the first denied request to include policy advisor guidance. Running sandboxes receive setting updates over the supervisor configuration stream and can enable the surface after startup, but startup enablement gives the agent the clearest first-denial path. ## Approval Modes diff --git a/e2e/python/test_sandbox_api.py b/e2e/python/test_sandbox_api.py index 5885f376b2..87b9e8c493 100644 --- a/e2e/python/test_sandbox_api.py +++ b/e2e/python/test_sandbox_api.py @@ -64,7 +64,13 @@ def replay(method, request): request_id=str(uuid.uuid4()), ) updated = stub.UpdateConfig(update, timeout=30) - assert replay(stub.UpdateConfig, update) == updated + replayed_update = replay(stub.UpdateConfig, update) + assert replayed_update.version == updated.version + assert replayed_update.policy_hash == updated.policy_hash + assert replayed_update.settings_revision == updated.settings_revision + assert replayed_update.deleted == updated.deleted + assert replayed_update.annotations == updated.annotations + assert replayed_update.operation.operation_id == updated.operation.operation_id delete = openshell_pb2.DeleteSandboxRequest( name=name, workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=scope), diff --git a/e2e/rust/tests/live_policy_update.rs b/e2e/rust/tests/live_policy_update.rs index 3577e20a3c..f4cd709e74 100644 --- a/e2e/rust/tests/live_policy_update.rs +++ b/e2e/rust/tests/live_policy_update.rs @@ -12,7 +12,7 @@ //! //! These tests replace the Python e2e tests `test_live_policy_update_and_logs` //! and `test_live_policy_update_from_empty_network_policies`, which were flaky -//! due to hard-coded 90s poll timeouts. The Rust tests use the CLI's built-in +//! due to hard-coded 90s wait timeouts. The Rust tests use the CLI's built-in //! `--wait` flag for reliable synchronization. //! //! Note: the removed Python tests also covered `GetSandboxLogs` RPC and diff --git a/e2e/rust/tests/policy_activation.rs b/e2e/rust/tests/policy_activation.rs index 0491e80c3f..1531ea769e 100644 --- a/e2e/rust/tests/policy_activation.rs +++ b/e2e/rust/tests/policy_activation.rs @@ -317,11 +317,15 @@ binaries: String::from_utf8_lossy(&logs.stderr) ); assert_eq!( - logs.matches("credentialed endpoint 'api.example.com:443'") - .count(), + logs.matches("OCSF CONFIG:CONFIGURATION_ERROR").count(), 1, "unchanged startup rejection must be logged only once: {logs}" ); + assert!( + logs.matches("OCSF CONFIG:FAIL_CLOSED").count() <= 1, + "unchanged startup rejection must enter fail-closed at most once: {logs}" + ); + assert!(logs.contains("credentialed endpoint 'api.example.com:443'")); assert!(!logs.contains("Creating OPA engine from proto policy data")); let repaired = context.path().join("repaired.yaml"); std::fs::write( diff --git a/proto/openshell.proto b/proto/openshell.proto index 4d1c0136c6..2f36e305e7 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -459,6 +459,16 @@ service OpenShell { }; } + // Get a durable sandbox configuration update operation by id. + rpc GetConfigUpdateOperation(GetConfigUpdateOperationRequest) + returns (GetConfigUpdateOperationResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + // Get the load status of a specific policy version. rpc GetSandboxPolicyStatus(GetSandboxPolicyStatusRequest) returns (GetSandboxPolicyStatusResponse) { @@ -2669,6 +2679,19 @@ message UpdateConfigRequest { // sandbox metadata as a convenience projection. For setting-only updates, it // only merges them into sandbox metadata. map annotations = 9; + // Controls whether the RPC returns after commit or after the runtime reaches + // a terminal outcome, including failure, supersession, cancellation or inactivity. + // This is a completion mode, not a database consistency level. + // Unspecified preserves commit-only compatibility. + ConfigUpdateConsistency consistency = 12; + // Optional retry key scoped to the sandbox. Reusing a key returns the + // original operation and never creates another desired-state revision. + string idempotency_key = 13; + // Server-side wait bound for WAIT_FOR_COMPLETION. Zero uses 60 seconds. The + // operation remains durable and may complete after this RPC times out. + reserved 14; + reserved "wait_timeout_secs"; + google.protobuf.Duration wait_timeout = 114; // Required for sandbox-scoped updates and empty for global updates. string sandbox = 1; // Optional nonzero UUID for durable at-most-once admission. Successful results @@ -2676,6 +2699,21 @@ message UpdateConfigRequest { string request_id = 11; } +enum ConfigUpdateConsistency { + CONFIG_UPDATE_CONSISTENCY_UNSPECIFIED = 0; + CONFIG_UPDATE_CONSISTENCY_COMMIT_ONLY = 1; + CONFIG_UPDATE_CONSISTENCY_WAIT_FOR_COMPLETION = 2; +} + +message GetConfigUpdateOperationRequest { + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + string operation_id = 1; +} + +message GetConfigUpdateOperationResponse { + ConfigUpdateOperation operation = 1; +} + message PolicyMergeOperation { oneof operation { AddNetworkRule add_rule = 1; @@ -2747,6 +2785,9 @@ message UpdateConfigResponse { bool deleted = 4; // Sandbox metadata annotations after the update. Empty for global updates. map annotations = 5; + // Durable completion operation for a sandbox-scoped request, including an + // unchanged desired value. Omitted for global changes. + ConfigUpdateOperation operation = 6; } // Get sandbox policy status request. diff --git a/sdk/go/openshell/v1/internal/converter/setting.go b/sdk/go/openshell/v1/internal/converter/setting.go index 57a0ae1242..4267b88b92 100644 --- a/sdk/go/openshell/v1/internal/converter/setting.go +++ b/sdk/go/openshell/v1/internal/converter/setting.go @@ -6,6 +6,9 @@ package converter import ( "fmt" "slices" + "time" + + "google.golang.org/protobuf/types/known/durationpb" v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types" pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1" @@ -187,6 +190,14 @@ func ConfigUpdateToProto(cu *v1.ConfigUpdate) (*pb.UpdateConfigRequest, error) { Global: cu.Global, ExpectedResourceVersion: cu.ExpectedResourceVersion, Annotations: CopyStringMap(cu.Annotations), + IdempotencyKey: cu.IdempotencyKey, + WaitTimeout: durationpb.New(time.Duration(cu.WaitTimeoutSeconds) * time.Second), + } + switch cu.Consistency { + case v1.ConfigUpdateWaitForCompletion: + req.Consistency = pb.ConfigUpdateConsistency_CONFIG_UPDATE_CONSISTENCY_WAIT_FOR_COMPLETION + case v1.ConfigUpdateCommitOnly: + req.Consistency = pb.ConfigUpdateConsistency_CONFIG_UPDATE_CONSISTENCY_COMMIT_ONLY } if !cu.Global { req.Sandbox = cu.Name @@ -324,11 +335,21 @@ func ConfigUpdateResultFromProto(resp *pb.UpdateConfigResponse) *v1.ConfigUpdate if resp == nil { return nil } - return &v1.ConfigUpdateResult{ + result := &v1.ConfigUpdateResult{ Version: resp.GetVersion(), PolicyHash: resp.GetPolicyHash(), SettingsRevision: resp.GetSettingsRevision(), Deleted: resp.GetDeleted(), Annotations: CopyStringMap(resp.GetAnnotations()), } + if operation := resp.GetOperation(); operation != nil { + result.Operation = &v1.ConfigUpdateOperation{ + OperationID: operation.GetOperationId(), + SandboxID: operation.GetSandboxId(), + State: operation.GetState().String(), + Outcome: operation.GetOutcome().String(), + SanitizedError: operation.GetSanitizedError(), + } + } + return result } diff --git a/sdk/go/openshell/v1/types/setting.go b/sdk/go/openshell/v1/types/setting.go index 7dd8eff2f2..28c97f4b73 100644 --- a/sdk/go/openshell/v1/types/setting.go +++ b/sdk/go/openshell/v1/types/setting.go @@ -104,6 +104,31 @@ type ConfigUpdate struct { ExpectedResourceVersion uint64 // Annotations is caller-provided metadata for sandbox-scoped updates. Annotations map[string]string + // Consistency controls whether Update returns after commit or completion. + Consistency ConfigUpdateConsistency + // IdempotencyKey maps retries to the original durable operation. + IdempotencyKey string + // WaitTimeoutSeconds bounds WaitForCompletion on the server. Zero uses the server default. + WaitTimeoutSeconds uint32 +} + +// ConfigUpdateConsistency controls configuration mutation response timing. +type ConfigUpdateConsistency string + +const ( + // ConfigUpdateCommitOnly returns after desired state and its operation commit. + ConfigUpdateCommitOnly ConfigUpdateConsistency = "commit_only" + // ConfigUpdateWaitForCompletion waits for a durable terminal result, including failure. + ConfigUpdateWaitForCompletion ConfigUpdateConsistency = "wait_for_completion" +) + +// ConfigUpdateOperation records the durable state and outcome of a sandbox update. +type ConfigUpdateOperation struct { + OperationID string + SandboxID string + State string + Outcome string + SanitizedError string } // ConfigUpdateResult holds the result of a configuration update operation. @@ -119,4 +144,6 @@ type ConfigUpdateResult struct { Deleted bool // Annotations contains sandbox metadata annotations after the update. Annotations map[string]string + // Operation is present for sandbox-scoped requests, including unchanged values. + Operation *ConfigUpdateOperation } diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 9ad1065c68..7ca960d13f 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -779,6 +779,55 @@ func (ProviderEnvironmentValueClassification) EnumDescriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{11} } +type ConfigUpdateConsistency int32 + +const ( + ConfigUpdateConsistency_CONFIG_UPDATE_CONSISTENCY_UNSPECIFIED ConfigUpdateConsistency = 0 + ConfigUpdateConsistency_CONFIG_UPDATE_CONSISTENCY_COMMIT_ONLY ConfigUpdateConsistency = 1 + ConfigUpdateConsistency_CONFIG_UPDATE_CONSISTENCY_WAIT_FOR_COMPLETION ConfigUpdateConsistency = 2 +) + +// Enum value maps for ConfigUpdateConsistency. +var ( + ConfigUpdateConsistency_name = map[int32]string{ + 0: "CONFIG_UPDATE_CONSISTENCY_UNSPECIFIED", + 1: "CONFIG_UPDATE_CONSISTENCY_COMMIT_ONLY", + 2: "CONFIG_UPDATE_CONSISTENCY_WAIT_FOR_COMPLETION", + } + ConfigUpdateConsistency_value = map[string]int32{ + "CONFIG_UPDATE_CONSISTENCY_UNSPECIFIED": 0, + "CONFIG_UPDATE_CONSISTENCY_COMMIT_ONLY": 1, + "CONFIG_UPDATE_CONSISTENCY_WAIT_FOR_COMPLETION": 2, + } +) + +func (x ConfigUpdateConsistency) Enum() *ConfigUpdateConsistency { + p := new(ConfigUpdateConsistency) + *p = x + return p +} + +func (x ConfigUpdateConsistency) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConfigUpdateConsistency) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[12].Descriptor() +} + +func (ConfigUpdateConsistency) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[12] +} + +func (x ConfigUpdateConsistency) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConfigUpdateConsistency.Descriptor instead. +func (ConfigUpdateConsistency) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{12} +} + type ConfigurationAdmissionState int32 const ( @@ -815,11 +864,11 @@ func (x ConfigurationAdmissionState) String() string { } func (ConfigurationAdmissionState) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[12].Descriptor() + return file_openshell_proto_enumTypes[13].Descriptor() } func (ConfigurationAdmissionState) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[12] + return &file_openshell_proto_enumTypes[13] } func (x ConfigurationAdmissionState) Number() protoreflect.EnumNumber { @@ -828,7 +877,7 @@ func (x ConfigurationAdmissionState) Number() protoreflect.EnumNumber { // Deprecated: Use ConfigurationAdmissionState.Descriptor instead. func (ConfigurationAdmissionState) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{12} + return file_openshell_proto_rawDescGZIP(), []int{13} } // Policy load status. @@ -877,11 +926,11 @@ func (x PolicyStatus) String() string { } func (PolicyStatus) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[13].Descriptor() + return file_openshell_proto_enumTypes[14].Descriptor() } func (PolicyStatus) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[13] + return &file_openshell_proto_enumTypes[14] } func (x PolicyStatus) Number() protoreflect.EnumNumber { @@ -890,7 +939,7 @@ func (x PolicyStatus) Number() protoreflect.EnumNumber { // Deprecated: Use PolicyStatus.Descriptor instead. func (PolicyStatus) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{13} + return file_openshell_proto_rawDescGZIP(), []int{14} } // Service status enum. @@ -930,11 +979,11 @@ func (x ServiceStatus) String() string { } func (ServiceStatus) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[14].Descriptor() + return file_openshell_proto_enumTypes[15].Descriptor() } func (ServiceStatus) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[14] + return &file_openshell_proto_enumTypes[15] } func (x ServiceStatus) Number() protoreflect.EnumNumber { @@ -943,7 +992,7 @@ func (x ServiceStatus) Number() protoreflect.EnumNumber { // Deprecated: Use ServiceStatus.Descriptor instead. func (ServiceStatus) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{14} + return file_openshell_proto_rawDescGZIP(), []int{15} } // Workspace-scoped role for members. @@ -980,11 +1029,11 @@ func (x WorkspaceRole) String() string { } func (WorkspaceRole) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[15].Descriptor() + return file_openshell_proto_enumTypes[16].Descriptor() } func (WorkspaceRole) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[15] + return &file_openshell_proto_enumTypes[16] } func (x WorkspaceRole) Number() protoreflect.EnumNumber { @@ -993,7 +1042,7 @@ func (x WorkspaceRole) Number() protoreflect.EnumNumber { // Deprecated: Use WorkspaceRole.Descriptor instead. func (WorkspaceRole) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{15} + return file_openshell_proto_rawDescGZIP(), []int{16} } // Stable recovery action for the most recent provider credential refresh @@ -1039,11 +1088,11 @@ func (x ProviderCredentialRefreshRecoveryAction) String() string { } func (ProviderCredentialRefreshRecoveryAction) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[16].Descriptor() + return file_openshell_proto_enumTypes[17].Descriptor() } func (ProviderCredentialRefreshRecoveryAction) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[16] + return &file_openshell_proto_enumTypes[17] } func (x ProviderCredentialRefreshRecoveryAction) Number() protoreflect.EnumNumber { @@ -1052,7 +1101,7 @@ func (x ProviderCredentialRefreshRecoveryAction) Number() protoreflect.EnumNumbe // Deprecated: Use ProviderCredentialRefreshRecoveryAction.Descriptor instead. func (ProviderCredentialRefreshRecoveryAction) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{16} + return file_openshell_proto_rawDescGZIP(), []int{17} } // Result of a public delete, membership removal, or session revocation. @@ -1102,11 +1151,11 @@ func (x DeletionOutcome) String() string { } func (DeletionOutcome) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[17].Descriptor() + return file_openshell_proto_enumTypes[18].Descriptor() } func (DeletionOutcome) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[17] + return &file_openshell_proto_enumTypes[18] } func (x DeletionOutcome) Number() protoreflect.EnumNumber { @@ -1115,7 +1164,7 @@ func (x DeletionOutcome) Number() protoreflect.EnumNumber { // Deprecated: Use DeletionOutcome.Descriptor instead. func (DeletionOutcome) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{17} + return file_openshell_proto_rawDescGZIP(), []int{18} } // Last observed network result for a configured external tool endpoint. @@ -1176,11 +1225,11 @@ func (x EndpointResult) String() string { } func (EndpointResult) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[18].Descriptor() + return file_openshell_proto_enumTypes[19].Descriptor() } func (EndpointResult) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[18] + return &file_openshell_proto_enumTypes[19] } func (x EndpointResult) Number() protoreflect.EnumNumber { @@ -1189,7 +1238,7 @@ func (x EndpointResult) Number() protoreflect.EnumNumber { // Deprecated: Use EndpointResult.Descriptor instead. func (EndpointResult) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{18} + return file_openshell_proto_rawDescGZIP(), []int{19} } // IssueSandboxToken request. Empty body; identity is established by the @@ -11345,6 +11394,15 @@ type UpdateConfigRequest struct { // sandbox metadata as a convenience projection. For setting-only updates, it // only merges them into sandbox metadata. Annotations map[string]string `protobuf:"bytes,9,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Controls whether the RPC returns after commit or after the runtime reaches + // a terminal outcome, including failure, supersession, cancellation or inactivity. + // This is a completion mode, not a database consistency level. + // Unspecified preserves commit-only compatibility. + Consistency ConfigUpdateConsistency `protobuf:"varint,12,opt,name=consistency,proto3,enum=openshell.v1.ConfigUpdateConsistency" json:"consistency,omitempty"` + // Optional retry key scoped to the sandbox. Reusing a key returns the + // original operation and never creates another desired-state revision. + IdempotencyKey string `protobuf:"bytes,13,opt,name=idempotency_key,json=idempotencyKey,proto3" json:"idempotency_key,omitempty"` + WaitTimeout *durationpb.Duration `protobuf:"bytes,114,opt,name=wait_timeout,json=waitTimeout,proto3" json:"wait_timeout,omitempty"` // Required for sandbox-scoped updates and empty for global updates. Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` // Optional nonzero UUID for durable at-most-once admission. Successful results @@ -11447,6 +11505,27 @@ func (x *UpdateConfigRequest) GetAnnotations() map[string]string { return nil } +func (x *UpdateConfigRequest) GetConsistency() ConfigUpdateConsistency { + if x != nil { + return x.Consistency + } + return ConfigUpdateConsistency_CONFIG_UPDATE_CONSISTENCY_UNSPECIFIED +} + +func (x *UpdateConfigRequest) GetIdempotencyKey() string { + if x != nil { + return x.IdempotencyKey + } + return "" +} + +func (x *UpdateConfigRequest) GetWaitTimeout() *durationpb.Duration { + if x != nil { + return x.WaitTimeout + } + return nil +} + func (x *UpdateConfigRequest) GetSandbox() string { if x != nil { return x.Sandbox @@ -11461,6 +11540,102 @@ func (x *UpdateConfigRequest) GetRequestId() string { return "" } +type GetConfigUpdateOperationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + OperationId string `protobuf:"bytes,1,opt,name=operation_id,json=operationId,proto3" json:"operation_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetConfigUpdateOperationRequest) Reset() { + *x = GetConfigUpdateOperationRequest{} + mi := &file_openshell_proto_msgTypes[142] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetConfigUpdateOperationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetConfigUpdateOperationRequest) ProtoMessage() {} + +func (x *GetConfigUpdateOperationRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[142] + 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 GetConfigUpdateOperationRequest.ProtoReflect.Descriptor instead. +func (*GetConfigUpdateOperationRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{142} +} + +func (x *GetConfigUpdateOperationRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *GetConfigUpdateOperationRequest) GetOperationId() string { + if x != nil { + return x.OperationId + } + return "" +} + +type GetConfigUpdateOperationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Operation *ConfigUpdateOperation `protobuf:"bytes,1,opt,name=operation,proto3" json:"operation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetConfigUpdateOperationResponse) Reset() { + *x = GetConfigUpdateOperationResponse{} + mi := &file_openshell_proto_msgTypes[143] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetConfigUpdateOperationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetConfigUpdateOperationResponse) ProtoMessage() {} + +func (x *GetConfigUpdateOperationResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[143] + 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 GetConfigUpdateOperationResponse.ProtoReflect.Descriptor instead. +func (*GetConfigUpdateOperationResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{143} +} + +func (x *GetConfigUpdateOperationResponse) GetOperation() *ConfigUpdateOperation { + if x != nil { + return x.Operation + } + return nil +} + type PolicyMergeOperation struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Operation: @@ -11478,7 +11653,7 @@ type PolicyMergeOperation struct { func (x *PolicyMergeOperation) Reset() { *x = PolicyMergeOperation{} - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[144] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11490,7 +11665,7 @@ func (x *PolicyMergeOperation) String() string { func (*PolicyMergeOperation) ProtoMessage() {} func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[144] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11503,7 +11678,7 @@ func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyMergeOperation.ProtoReflect.Descriptor instead. func (*PolicyMergeOperation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{142} + return file_openshell_proto_rawDescGZIP(), []int{144} } func (x *PolicyMergeOperation) GetOperation() isPolicyMergeOperation_Operation { @@ -11617,7 +11792,7 @@ type AddNetworkRule struct { func (x *AddNetworkRule) Reset() { *x = AddNetworkRule{} - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[145] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11629,7 +11804,7 @@ func (x *AddNetworkRule) String() string { func (*AddNetworkRule) ProtoMessage() {} func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[145] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11642,7 +11817,7 @@ func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use AddNetworkRule.ProtoReflect.Descriptor instead. func (*AddNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{143} + return file_openshell_proto_rawDescGZIP(), []int{145} } func (x *AddNetworkRule) GetRuleName() string { @@ -11670,7 +11845,7 @@ type RemoveNetworkEndpoint struct { func (x *RemoveNetworkEndpoint) Reset() { *x = RemoveNetworkEndpoint{} - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[146] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11682,7 +11857,7 @@ func (x *RemoveNetworkEndpoint) String() string { func (*RemoveNetworkEndpoint) ProtoMessage() {} func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[146] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11695,7 +11870,7 @@ func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkEndpoint.ProtoReflect.Descriptor instead. func (*RemoveNetworkEndpoint) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{144} + return file_openshell_proto_rawDescGZIP(), []int{146} } func (x *RemoveNetworkEndpoint) GetRuleName() string { @@ -11728,7 +11903,7 @@ type RemoveNetworkRule struct { func (x *RemoveNetworkRule) Reset() { *x = RemoveNetworkRule{} - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[147] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11740,7 +11915,7 @@ func (x *RemoveNetworkRule) String() string { func (*RemoveNetworkRule) ProtoMessage() {} func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[147] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11753,7 +11928,7 @@ func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkRule.ProtoReflect.Descriptor instead. func (*RemoveNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{145} + return file_openshell_proto_rawDescGZIP(), []int{147} } func (x *RemoveNetworkRule) GetRuleName() string { @@ -11782,7 +11957,7 @@ type L7RuleTarget struct { func (x *L7RuleTarget) Reset() { *x = L7RuleTarget{} - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[148] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11794,7 +11969,7 @@ func (x *L7RuleTarget) String() string { func (*L7RuleTarget) ProtoMessage() {} func (x *L7RuleTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[148] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11807,7 +11982,7 @@ func (x *L7RuleTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use L7RuleTarget.ProtoReflect.Descriptor instead. func (*L7RuleTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{146} + return file_openshell_proto_rawDescGZIP(), []int{148} } func (x *L7RuleTarget) GetRuleName() string { @@ -11862,7 +12037,7 @@ type AddDenyRules struct { func (x *AddDenyRules) Reset() { *x = AddDenyRules{} - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[149] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11874,7 +12049,7 @@ func (x *AddDenyRules) String() string { func (*AddDenyRules) ProtoMessage() {} func (x *AddDenyRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[149] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11887,7 +12062,7 @@ func (x *AddDenyRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddDenyRules.ProtoReflect.Descriptor instead. func (*AddDenyRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{147} + return file_openshell_proto_rawDescGZIP(), []int{149} } func (x *AddDenyRules) GetDenyRules() []*sandboxv1.L7DenyRule { @@ -11914,7 +12089,7 @@ type AddAllowRules struct { func (x *AddAllowRules) Reset() { *x = AddAllowRules{} - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[150] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11926,7 +12101,7 @@ func (x *AddAllowRules) String() string { func (*AddAllowRules) ProtoMessage() {} func (x *AddAllowRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[150] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11939,7 +12114,7 @@ func (x *AddAllowRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddAllowRules.ProtoReflect.Descriptor instead. func (*AddAllowRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{148} + return file_openshell_proto_rawDescGZIP(), []int{150} } func (x *AddAllowRules) GetRules() []*sandboxv1.L7Rule { @@ -11966,7 +12141,7 @@ type RemoveNetworkBinary struct { func (x *RemoveNetworkBinary) Reset() { *x = RemoveNetworkBinary{} - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[151] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11978,7 +12153,7 @@ func (x *RemoveNetworkBinary) String() string { func (*RemoveNetworkBinary) ProtoMessage() {} func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[151] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11991,7 +12166,7 @@ func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkBinary.ProtoReflect.Descriptor instead. func (*RemoveNetworkBinary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{149} + return file_openshell_proto_rawDescGZIP(), []int{151} } func (x *RemoveNetworkBinary) GetRuleName() string { @@ -12020,14 +12195,17 @@ type UpdateConfigResponse struct { // True when a setting delete operation removed an existing key. Deleted bool `protobuf:"varint,4,opt,name=deleted,proto3" json:"deleted,omitempty"` // Sandbox metadata annotations after the update. Empty for global updates. - Annotations map[string]string `protobuf:"bytes,5,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Annotations map[string]string `protobuf:"bytes,5,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Durable completion operation for a sandbox-scoped request, including an + // unchanged desired value. Omitted for global changes. + Operation *ConfigUpdateOperation `protobuf:"bytes,6,opt,name=operation,proto3" json:"operation,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *UpdateConfigResponse) Reset() { *x = UpdateConfigResponse{} - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[152] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12039,7 +12217,7 @@ func (x *UpdateConfigResponse) String() string { func (*UpdateConfigResponse) ProtoMessage() {} func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[152] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12052,7 +12230,7 @@ func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigResponse.ProtoReflect.Descriptor instead. func (*UpdateConfigResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{150} + return file_openshell_proto_rawDescGZIP(), []int{152} } func (x *UpdateConfigResponse) GetVersion() uint32 { @@ -12090,6 +12268,13 @@ func (x *UpdateConfigResponse) GetAnnotations() map[string]string { return nil } +func (x *UpdateConfigResponse) GetOperation() *ConfigUpdateOperation { + if x != nil { + return x.Operation + } + return nil +} + // Get sandbox policy status request. type GetSandboxPolicyStatusRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -12106,7 +12291,7 @@ type GetSandboxPolicyStatusRequest struct { func (x *GetSandboxPolicyStatusRequest) Reset() { *x = GetSandboxPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[153] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12118,7 +12303,7 @@ func (x *GetSandboxPolicyStatusRequest) String() string { func (*GetSandboxPolicyStatusRequest) ProtoMessage() {} func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[153] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12131,7 +12316,7 @@ func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{151} + return file_openshell_proto_rawDescGZIP(), []int{153} } func (x *GetSandboxPolicyStatusRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -12175,7 +12360,7 @@ type GetSandboxPolicyStatusResponse struct { func (x *GetSandboxPolicyStatusResponse) Reset() { *x = GetSandboxPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[154] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12187,7 +12372,7 @@ func (x *GetSandboxPolicyStatusResponse) String() string { func (*GetSandboxPolicyStatusResponse) ProtoMessage() {} func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[154] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12200,7 +12385,7 @@ func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{152} + return file_openshell_proto_rawDescGZIP(), []int{154} } func (x *GetSandboxPolicyStatusResponse) GetRevision() *SandboxPolicyRevision { @@ -12237,7 +12422,7 @@ type ListSandboxPoliciesRequest struct { func (x *ListSandboxPoliciesRequest) Reset() { *x = ListSandboxPoliciesRequest{} - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[155] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12249,7 +12434,7 @@ func (x *ListSandboxPoliciesRequest) String() string { func (*ListSandboxPoliciesRequest) ProtoMessage() {} func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[155] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12262,7 +12447,7 @@ func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesRequest.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{153} + return file_openshell_proto_rawDescGZIP(), []int{155} } func (x *ListSandboxPoliciesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -12314,7 +12499,7 @@ type ListSandboxPoliciesResponse struct { func (x *ListSandboxPoliciesResponse) Reset() { *x = ListSandboxPoliciesResponse{} - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[156] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12326,7 +12511,7 @@ func (x *ListSandboxPoliciesResponse) String() string { func (*ListSandboxPoliciesResponse) ProtoMessage() {} func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[156] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12339,7 +12524,7 @@ func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesResponse.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{154} + return file_openshell_proto_rawDescGZIP(), []int{156} } func (x *ListSandboxPoliciesResponse) GetRevisions() []*SandboxPolicyRevision { @@ -12373,7 +12558,7 @@ type ReportPolicyStatusRequest struct { func (x *ReportPolicyStatusRequest) Reset() { *x = ReportPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[157] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12385,7 +12570,7 @@ func (x *ReportPolicyStatusRequest) String() string { func (*ReportPolicyStatusRequest) ProtoMessage() {} func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[157] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12398,7 +12583,7 @@ func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{155} + return file_openshell_proto_rawDescGZIP(), []int{157} } func (x *ReportPolicyStatusRequest) GetSandboxId() string { @@ -12438,7 +12623,7 @@ type ReportPolicyStatusResponse struct { func (x *ReportPolicyStatusResponse) Reset() { *x = ReportPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[158] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12450,7 +12635,7 @@ func (x *ReportPolicyStatusResponse) String() string { func (*ReportPolicyStatusResponse) ProtoMessage() {} func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[158] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12463,7 +12648,7 @@ func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{156} + return file_openshell_proto_rawDescGZIP(), []int{158} } type SandboxConfigurationAdmission struct { @@ -12481,7 +12666,7 @@ type SandboxConfigurationAdmission struct { func (x *SandboxConfigurationAdmission) Reset() { *x = SandboxConfigurationAdmission{} - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[159] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12493,7 +12678,7 @@ func (x *SandboxConfigurationAdmission) String() string { func (*SandboxConfigurationAdmission) ProtoMessage() {} func (x *SandboxConfigurationAdmission) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[159] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12506,7 +12691,7 @@ func (x *SandboxConfigurationAdmission) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxConfigurationAdmission.ProtoReflect.Descriptor instead. func (*SandboxConfigurationAdmission) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{157} + return file_openshell_proto_rawDescGZIP(), []int{159} } func (x *SandboxConfigurationAdmission) GetInstanceId() string { @@ -12570,7 +12755,7 @@ type ReportSandboxConfigurationRequest struct { func (x *ReportSandboxConfigurationRequest) Reset() { *x = ReportSandboxConfigurationRequest{} - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[160] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12582,7 +12767,7 @@ func (x *ReportSandboxConfigurationRequest) String() string { func (*ReportSandboxConfigurationRequest) ProtoMessage() {} func (x *ReportSandboxConfigurationRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[160] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12595,7 +12780,7 @@ func (x *ReportSandboxConfigurationRequest) ProtoReflect() protoreflect.Message // Deprecated: Use ReportSandboxConfigurationRequest.ProtoReflect.Descriptor instead. func (*ReportSandboxConfigurationRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{158} + return file_openshell_proto_rawDescGZIP(), []int{160} } func (x *ReportSandboxConfigurationRequest) GetSandboxId() string { @@ -12627,7 +12812,7 @@ type ReportSandboxConfigurationResponse struct { func (x *ReportSandboxConfigurationResponse) Reset() { *x = ReportSandboxConfigurationResponse{} - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[161] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12639,7 +12824,7 @@ func (x *ReportSandboxConfigurationResponse) String() string { func (*ReportSandboxConfigurationResponse) ProtoMessage() {} func (x *ReportSandboxConfigurationResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[161] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12652,7 +12837,7 @@ func (x *ReportSandboxConfigurationResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ReportSandboxConfigurationResponse.ProtoReflect.Descriptor instead. func (*ReportSandboxConfigurationResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{159} + return file_openshell_proto_rawDescGZIP(), []int{161} } // A versioned policy revision with metadata. @@ -12685,7 +12870,7 @@ type SandboxPolicyRevision struct { func (x *SandboxPolicyRevision) Reset() { *x = SandboxPolicyRevision{} - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[162] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12697,7 +12882,7 @@ func (x *SandboxPolicyRevision) String() string { func (*SandboxPolicyRevision) ProtoMessage() {} func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[162] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12710,7 +12895,7 @@ func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxPolicyRevision.ProtoReflect.Descriptor instead. func (*SandboxPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{160} + return file_openshell_proto_rawDescGZIP(), []int{162} } func (x *SandboxPolicyRevision) GetVersion() uint32 { @@ -12790,7 +12975,7 @@ type GetSandboxLogsRequest struct { func (x *GetSandboxLogsRequest) Reset() { *x = GetSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[163] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12802,7 +12987,7 @@ func (x *GetSandboxLogsRequest) String() string { func (*GetSandboxLogsRequest) ProtoMessage() {} func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[163] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12815,7 +13000,7 @@ func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*GetSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{161} + return file_openshell_proto_rawDescGZIP(), []int{163} } func (x *GetSandboxLogsRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -12873,7 +13058,7 @@ type PushSandboxLogsRequest struct { func (x *PushSandboxLogsRequest) Reset() { *x = PushSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[164] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12885,7 +13070,7 @@ func (x *PushSandboxLogsRequest) String() string { func (*PushSandboxLogsRequest) ProtoMessage() {} func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[164] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12898,7 +13083,7 @@ func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*PushSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{162} + return file_openshell_proto_rawDescGZIP(), []int{164} } func (x *PushSandboxLogsRequest) GetSandboxId() string { @@ -12924,7 +13109,7 @@ type PushSandboxLogsResponse struct { func (x *PushSandboxLogsResponse) Reset() { *x = PushSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[165] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12936,7 +13121,7 @@ func (x *PushSandboxLogsResponse) String() string { func (*PushSandboxLogsResponse) ProtoMessage() {} func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[165] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12949,7 +13134,7 @@ func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*PushSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{163} + return file_openshell_proto_rawDescGZIP(), []int{165} } // Get sandbox logs response. @@ -12965,7 +13150,7 @@ type GetSandboxLogsResponse struct { func (x *GetSandboxLogsResponse) Reset() { *x = GetSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[166] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12977,7 +13162,7 @@ func (x *GetSandboxLogsResponse) String() string { func (*GetSandboxLogsResponse) ProtoMessage() {} func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[166] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12990,7 +13175,7 @@ func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*GetSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{164} + return file_openshell_proto_rawDescGZIP(), []int{166} } func (x *GetSandboxLogsResponse) GetLogs() []*SandboxLogLine { @@ -13027,7 +13212,7 @@ type SupervisorMessage struct { func (x *SupervisorMessage) Reset() { *x = SupervisorMessage{} - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[167] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13039,7 +13224,7 @@ func (x *SupervisorMessage) String() string { func (*SupervisorMessage) ProtoMessage() {} func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[167] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13052,7 +13237,7 @@ func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorMessage.ProtoReflect.Descriptor instead. func (*SupervisorMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{165} + return file_openshell_proto_rawDescGZIP(), []int{167} } func (x *SupervisorMessage) GetPayload() isSupervisorMessage_Payload { @@ -13196,7 +13381,7 @@ type SupervisorRuntimeReady struct { func (x *SupervisorRuntimeReady) Reset() { *x = SupervisorRuntimeReady{} - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[168] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13208,7 +13393,7 @@ func (x *SupervisorRuntimeReady) String() string { func (*SupervisorRuntimeReady) ProtoMessage() {} func (x *SupervisorRuntimeReady) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[168] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13221,7 +13406,7 @@ func (x *SupervisorRuntimeReady) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorRuntimeReady.ProtoReflect.Descriptor instead. func (*SupervisorRuntimeReady) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{166} + return file_openshell_proto_rawDescGZIP(), []int{168} } // Envelope for gateway-to-supervisor messages on the ConnectSupervisor stream. @@ -13244,7 +13429,7 @@ type GatewayMessage struct { func (x *GatewayMessage) Reset() { *x = GatewayMessage{} - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[169] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13256,7 +13441,7 @@ func (x *GatewayMessage) String() string { func (*GatewayMessage) ProtoMessage() {} func (x *GatewayMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[169] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13269,7 +13454,7 @@ func (x *GatewayMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayMessage.ProtoReflect.Descriptor instead. func (*GatewayMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{167} + return file_openshell_proto_rawDescGZIP(), []int{169} } func (x *GatewayMessage) GetPayload() isGatewayMessage_Payload { @@ -13435,7 +13620,7 @@ type SupervisorHello struct { func (x *SupervisorHello) Reset() { *x = SupervisorHello{} - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[170] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13447,7 +13632,7 @@ func (x *SupervisorHello) String() string { func (*SupervisorHello) ProtoMessage() {} func (x *SupervisorHello) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[170] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13460,7 +13645,7 @@ func (x *SupervisorHello) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHello.ProtoReflect.Descriptor instead. func (*SupervisorHello) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{168} + return file_openshell_proto_rawDescGZIP(), []int{170} } func (x *SupervisorHello) GetSandboxId() string { @@ -13526,7 +13711,7 @@ type ImagePolicyDiscovery struct { func (x *ImagePolicyDiscovery) Reset() { *x = ImagePolicyDiscovery{} - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[171] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13538,7 +13723,7 @@ func (x *ImagePolicyDiscovery) String() string { func (*ImagePolicyDiscovery) ProtoMessage() {} func (x *ImagePolicyDiscovery) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[171] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13551,7 +13736,7 @@ func (x *ImagePolicyDiscovery) ProtoReflect() protoreflect.Message { // Deprecated: Use ImagePolicyDiscovery.ProtoReflect.Descriptor instead. func (*ImagePolicyDiscovery) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{169} + return file_openshell_proto_rawDescGZIP(), []int{171} } func (x *ImagePolicyDiscovery) GetResult() isImagePolicyDiscovery_Result { @@ -13627,7 +13812,7 @@ type StartupConfigCandidate struct { func (x *StartupConfigCandidate) Reset() { *x = StartupConfigCandidate{} - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[172] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13639,7 +13824,7 @@ func (x *StartupConfigCandidate) String() string { func (*StartupConfigCandidate) ProtoMessage() {} func (x *StartupConfigCandidate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[172] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13652,7 +13837,7 @@ func (x *StartupConfigCandidate) ProtoReflect() protoreflect.Message { // Deprecated: Use StartupConfigCandidate.ProtoReflect.Descriptor instead. func (*StartupConfigCandidate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{170} + return file_openshell_proto_rawDescGZIP(), []int{172} } func (x *StartupConfigCandidate) GetCandidateId() string { @@ -13706,7 +13891,7 @@ type StartupConfigPrepared struct { func (x *StartupConfigPrepared) Reset() { *x = StartupConfigPrepared{} - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[173] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13718,7 +13903,7 @@ func (x *StartupConfigPrepared) String() string { func (*StartupConfigPrepared) ProtoMessage() {} func (x *StartupConfigPrepared) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[173] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13731,7 +13916,7 @@ func (x *StartupConfigPrepared) ProtoReflect() protoreflect.Message { // Deprecated: Use StartupConfigPrepared.ProtoReflect.Descriptor instead. func (*StartupConfigPrepared) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{171} + return file_openshell_proto_rawDescGZIP(), []int{173} } func (x *StartupConfigPrepared) GetCandidateId() string { @@ -13819,7 +14004,7 @@ type SessionAccepted struct { func (x *SessionAccepted) Reset() { *x = SessionAccepted{} - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[174] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13831,7 +14016,7 @@ func (x *SessionAccepted) String() string { func (*SessionAccepted) ProtoMessage() {} func (x *SessionAccepted) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[174] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13844,7 +14029,7 @@ func (x *SessionAccepted) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionAccepted.ProtoReflect.Descriptor instead. func (*SessionAccepted) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{172} + return file_openshell_proto_rawDescGZIP(), []int{174} } func (x *SessionAccepted) GetSessionId() string { @@ -13886,7 +14071,7 @@ type ConfigBootstrap struct { func (x *ConfigBootstrap) Reset() { *x = ConfigBootstrap{} - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[175] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13898,7 +14083,7 @@ func (x *ConfigBootstrap) String() string { func (*ConfigBootstrap) ProtoMessage() {} func (x *ConfigBootstrap) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[175] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13911,7 +14096,7 @@ func (x *ConfigBootstrap) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigBootstrap.ProtoReflect.Descriptor instead. func (*ConfigBootstrap) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{173} + return file_openshell_proto_rawDescGZIP(), []int{175} } func (x *ConfigBootstrap) GetSandboxConfig() *sandboxv1.SandboxConfigSnapshot { @@ -13947,7 +14132,7 @@ type ConfigUpdate struct { func (x *ConfigUpdate) Reset() { *x = ConfigUpdate{} - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[176] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13959,7 +14144,7 @@ func (x *ConfigUpdate) String() string { func (*ConfigUpdate) ProtoMessage() {} func (x *ConfigUpdate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[176] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13972,7 +14157,7 @@ func (x *ConfigUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigUpdate.ProtoReflect.Descriptor instead. func (*ConfigUpdate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{174} + return file_openshell_proto_rawDescGZIP(), []int{176} } func (x *ConfigUpdate) GetUpdateId() string { @@ -14043,7 +14228,7 @@ type ConfigApplyFailure struct { func (x *ConfigApplyFailure) Reset() { *x = ConfigApplyFailure{} - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[177] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14055,7 +14240,7 @@ func (x *ConfigApplyFailure) String() string { func (*ConfigApplyFailure) ProtoMessage() {} func (x *ConfigApplyFailure) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[177] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14068,7 +14253,7 @@ func (x *ConfigApplyFailure) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigApplyFailure.ProtoReflect.Descriptor instead. func (*ConfigApplyFailure) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{175} + return file_openshell_proto_rawDescGZIP(), []int{177} } func (x *ConfigApplyFailure) GetCode() string { @@ -14108,7 +14293,7 @@ type ConfigComponentApplyResult struct { func (x *ConfigComponentApplyResult) Reset() { *x = ConfigComponentApplyResult{} - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[178] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14120,7 +14305,7 @@ func (x *ConfigComponentApplyResult) String() string { func (*ConfigComponentApplyResult) ProtoMessage() {} func (x *ConfigComponentApplyResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[178] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14133,7 +14318,7 @@ func (x *ConfigComponentApplyResult) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigComponentApplyResult.ProtoReflect.Descriptor instead. func (*ConfigComponentApplyResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{176} + return file_openshell_proto_rawDescGZIP(), []int{178} } func (x *ConfigComponentApplyResult) GetComponent() ConfigComponent { @@ -14188,7 +14373,7 @@ type ConfigUpdateResult struct { func (x *ConfigUpdateResult) Reset() { *x = ConfigUpdateResult{} - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[179] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14200,7 +14385,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[179] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14213,7 +14398,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{179} } func (x *ConfigUpdateResult) GetUpdateId() string { @@ -14256,7 +14441,7 @@ type ConfigBootstrapResult struct { func (x *ConfigBootstrapResult) Reset() { *x = ConfigBootstrapResult{} - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[180] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14268,7 +14453,7 @@ func (x *ConfigBootstrapResult) String() string { func (*ConfigBootstrapResult) ProtoMessage() {} func (x *ConfigBootstrapResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[180] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14281,7 +14466,7 @@ func (x *ConfigBootstrapResult) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigBootstrapResult.ProtoReflect.Descriptor instead. func (*ConfigBootstrapResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{178} + return file_openshell_proto_rawDescGZIP(), []int{180} } func (x *ConfigBootstrapResult) GetResults() []*ConfigComponentApplyResult { @@ -14309,7 +14494,7 @@ type SessionRejected struct { func (x *SessionRejected) Reset() { *x = SessionRejected{} - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[181] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14321,7 +14506,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[181] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14334,7 +14519,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{181} } func (x *SessionRejected) GetReason() string { @@ -14353,7 +14538,7 @@ type SupervisorHeartbeat struct { func (x *SupervisorHeartbeat) Reset() { *x = SupervisorHeartbeat{} - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[182] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14365,7 +14550,7 @@ func (x *SupervisorHeartbeat) String() string { func (*SupervisorHeartbeat) ProtoMessage() {} func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[182] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14378,7 +14563,7 @@ func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHeartbeat.ProtoReflect.Descriptor instead. func (*SupervisorHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{180} + return file_openshell_proto_rawDescGZIP(), []int{182} } // Gateway heartbeat. @@ -14390,7 +14575,7 @@ type GatewayHeartbeat struct { func (x *GatewayHeartbeat) Reset() { *x = GatewayHeartbeat{} - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[183] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14402,7 +14587,7 @@ func (x *GatewayHeartbeat) String() string { func (*GatewayHeartbeat) ProtoMessage() {} func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[183] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14415,7 +14600,7 @@ func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayHeartbeat.ProtoReflect.Descriptor instead. func (*GatewayHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{181} + return file_openshell_proto_rawDescGZIP(), []int{183} } // Terminal result reported before the supervisor shuts down. A successful RPC @@ -14432,7 +14617,7 @@ type ReportMainProcessExitRequest struct { func (x *ReportMainProcessExitRequest) Reset() { *x = ReportMainProcessExitRequest{} - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[184] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14444,7 +14629,7 @@ func (x *ReportMainProcessExitRequest) String() string { func (*ReportMainProcessExitRequest) ProtoMessage() {} func (x *ReportMainProcessExitRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[184] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14457,7 +14642,7 @@ func (x *ReportMainProcessExitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportMainProcessExitRequest.ProtoReflect.Descriptor instead. func (*ReportMainProcessExitRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{182} + return file_openshell_proto_rawDescGZIP(), []int{184} } func (x *ReportMainProcessExitRequest) GetSandboxId() string { @@ -14489,7 +14674,7 @@ type ReportMainProcessExitResponse struct { func (x *ReportMainProcessExitResponse) Reset() { *x = ReportMainProcessExitResponse{} - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[185] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14501,7 +14686,7 @@ func (x *ReportMainProcessExitResponse) String() string { func (*ReportMainProcessExitResponse) ProtoMessage() {} func (x *ReportMainProcessExitResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[185] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14514,7 +14699,7 @@ func (x *ReportMainProcessExitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportMainProcessExitResponse.ProtoReflect.Descriptor instead. func (*ReportMainProcessExitResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{183} + return file_openshell_proto_rawDescGZIP(), []int{185} } // Terminal-delivery completion reported after all expected foreground SSH @@ -14529,7 +14714,7 @@ type FinalizeMainProcessExitRequest struct { func (x *FinalizeMainProcessExitRequest) Reset() { *x = FinalizeMainProcessExitRequest{} - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[186] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14541,7 +14726,7 @@ func (x *FinalizeMainProcessExitRequest) String() string { func (*FinalizeMainProcessExitRequest) ProtoMessage() {} func (x *FinalizeMainProcessExitRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[186] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14554,7 +14739,7 @@ func (x *FinalizeMainProcessExitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeMainProcessExitRequest.ProtoReflect.Descriptor instead. func (*FinalizeMainProcessExitRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{184} + return file_openshell_proto_rawDescGZIP(), []int{186} } func (x *FinalizeMainProcessExitRequest) GetSandboxId() string { @@ -14579,7 +14764,7 @@ type FinalizeMainProcessExitResponse struct { func (x *FinalizeMainProcessExitResponse) Reset() { *x = FinalizeMainProcessExitResponse{} - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[187] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14591,7 +14776,7 @@ func (x *FinalizeMainProcessExitResponse) String() string { func (*FinalizeMainProcessExitResponse) ProtoMessage() {} func (x *FinalizeMainProcessExitResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[187] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14604,7 +14789,7 @@ func (x *FinalizeMainProcessExitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeMainProcessExitResponse.ProtoReflect.Descriptor instead. func (*FinalizeMainProcessExitResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{185} + return file_openshell_proto_rawDescGZIP(), []int{187} } // Gateway requests the supervisor to open a relay channel. @@ -14633,7 +14818,7 @@ type RelayOpen struct { func (x *RelayOpen) Reset() { *x = RelayOpen{} - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[188] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14645,7 +14830,7 @@ func (x *RelayOpen) String() string { func (*RelayOpen) ProtoMessage() {} func (x *RelayOpen) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[188] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14658,7 +14843,7 @@ func (x *RelayOpen) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpen.ProtoReflect.Descriptor instead. func (*RelayOpen) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{186} + return file_openshell_proto_rawDescGZIP(), []int{188} } func (x *RelayOpen) GetChannelId() string { @@ -14725,7 +14910,7 @@ type SshRelayTarget struct { func (x *SshRelayTarget) Reset() { *x = SshRelayTarget{} - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[189] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14737,7 +14922,7 @@ func (x *SshRelayTarget) String() string { func (*SshRelayTarget) ProtoMessage() {} func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[189] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14750,7 +14935,7 @@ func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use SshRelayTarget.ProtoReflect.Descriptor instead. func (*SshRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{187} + return file_openshell_proto_rawDescGZIP(), []int{189} } // TCP target dialed by the supervisor from inside the sandbox. @@ -14766,7 +14951,7 @@ type TcpRelayTarget struct { func (x *TcpRelayTarget) Reset() { *x = TcpRelayTarget{} - mi := &file_openshell_proto_msgTypes[188] + mi := &file_openshell_proto_msgTypes[190] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14778,7 +14963,7 @@ func (x *TcpRelayTarget) String() string { func (*TcpRelayTarget) ProtoMessage() {} func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[188] + mi := &file_openshell_proto_msgTypes[190] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14791,7 +14976,7 @@ func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpRelayTarget.ProtoReflect.Descriptor instead. func (*TcpRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{188} + return file_openshell_proto_rawDescGZIP(), []int{190} } func (x *TcpRelayTarget) GetHost() string { @@ -14819,7 +15004,7 @@ type RelayInit struct { func (x *RelayInit) Reset() { *x = RelayInit{} - mi := &file_openshell_proto_msgTypes[189] + mi := &file_openshell_proto_msgTypes[191] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14831,7 +15016,7 @@ func (x *RelayInit) String() string { func (*RelayInit) ProtoMessage() {} func (x *RelayInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[189] + mi := &file_openshell_proto_msgTypes[191] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14844,7 +15029,7 @@ func (x *RelayInit) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayInit.ProtoReflect.Descriptor instead. func (*RelayInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{189} + return file_openshell_proto_rawDescGZIP(), []int{191} } func (x *RelayInit) GetChannelId() string { @@ -14871,7 +15056,7 @@ type RelayFrame struct { func (x *RelayFrame) Reset() { *x = RelayFrame{} - mi := &file_openshell_proto_msgTypes[190] + mi := &file_openshell_proto_msgTypes[192] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14883,7 +15068,7 @@ func (x *RelayFrame) String() string { func (*RelayFrame) ProtoMessage() {} func (x *RelayFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[190] + mi := &file_openshell_proto_msgTypes[192] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14896,7 +15081,7 @@ func (x *RelayFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayFrame.ProtoReflect.Descriptor instead. func (*RelayFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{190} + return file_openshell_proto_rawDescGZIP(), []int{192} } func (x *RelayFrame) GetPayload() isRelayFrame_Payload { @@ -14956,7 +15141,7 @@ type PeerRelayInit struct { func (x *PeerRelayInit) Reset() { *x = PeerRelayInit{} - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[193] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14968,7 +15153,7 @@ func (x *PeerRelayInit) String() string { func (*PeerRelayInit) ProtoMessage() {} func (x *PeerRelayInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[193] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14981,7 +15166,7 @@ func (x *PeerRelayInit) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerRelayInit.ProtoReflect.Descriptor instead. func (*PeerRelayInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{191} + return file_openshell_proto_rawDescGZIP(), []int{193} } func (x *PeerRelayInit) GetSandboxId() string { @@ -15019,7 +15204,7 @@ type PeerRelayFrame struct { func (x *PeerRelayFrame) Reset() { *x = PeerRelayFrame{} - mi := &file_openshell_proto_msgTypes[192] + mi := &file_openshell_proto_msgTypes[194] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15031,7 +15216,7 @@ func (x *PeerRelayFrame) String() string { func (*PeerRelayFrame) ProtoMessage() {} func (x *PeerRelayFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[192] + mi := &file_openshell_proto_msgTypes[194] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15044,7 +15229,7 @@ func (x *PeerRelayFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerRelayFrame.ProtoReflect.Descriptor instead. func (*PeerRelayFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{192} + return file_openshell_proto_rawDescGZIP(), []int{194} } func (x *PeerRelayFrame) GetPayload() isPeerRelayFrame_Payload { @@ -15103,7 +15288,7 @@ type RelayOpenResult struct { func (x *RelayOpenResult) Reset() { *x = RelayOpenResult{} - mi := &file_openshell_proto_msgTypes[193] + mi := &file_openshell_proto_msgTypes[195] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15115,7 +15300,7 @@ func (x *RelayOpenResult) String() string { func (*RelayOpenResult) ProtoMessage() {} func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[193] + mi := &file_openshell_proto_msgTypes[195] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15128,7 +15313,7 @@ func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpenResult.ProtoReflect.Descriptor instead. func (*RelayOpenResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{193} + return file_openshell_proto_rawDescGZIP(), []int{195} } func (x *RelayOpenResult) GetChannelId() string { @@ -15165,7 +15350,7 @@ type RelayClose struct { func (x *RelayClose) Reset() { *x = RelayClose{} - mi := &file_openshell_proto_msgTypes[194] + mi := &file_openshell_proto_msgTypes[196] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15177,7 +15362,7 @@ func (x *RelayClose) String() string { func (*RelayClose) ProtoMessage() {} func (x *RelayClose) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[194] + mi := &file_openshell_proto_msgTypes[196] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15190,7 +15375,7 @@ func (x *RelayClose) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayClose.ProtoReflect.Descriptor instead. func (*RelayClose) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{194} + return file_openshell_proto_rawDescGZIP(), []int{196} } func (x *RelayClose) GetChannelId() string { @@ -15224,7 +15409,7 @@ type L7RequestSample struct { func (x *L7RequestSample) Reset() { *x = L7RequestSample{} - mi := &file_openshell_proto_msgTypes[195] + mi := &file_openshell_proto_msgTypes[197] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15236,7 +15421,7 @@ func (x *L7RequestSample) String() string { func (*L7RequestSample) ProtoMessage() {} func (x *L7RequestSample) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[195] + mi := &file_openshell_proto_msgTypes[197] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15249,7 +15434,7 @@ func (x *L7RequestSample) ProtoReflect() protoreflect.Message { // Deprecated: Use L7RequestSample.ProtoReflect.Descriptor instead. func (*L7RequestSample) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{195} + return file_openshell_proto_rawDescGZIP(), []int{197} } func (x *L7RequestSample) GetMethod() string { @@ -15323,7 +15508,7 @@ type DenialSummary struct { func (x *DenialSummary) Reset() { *x = DenialSummary{} - mi := &file_openshell_proto_msgTypes[196] + mi := &file_openshell_proto_msgTypes[198] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15335,7 +15520,7 @@ func (x *DenialSummary) String() string { func (*DenialSummary) ProtoMessage() {} func (x *DenialSummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[196] + mi := &file_openshell_proto_msgTypes[198] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15348,7 +15533,7 @@ func (x *DenialSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialSummary.ProtoReflect.Descriptor instead. func (*DenialSummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{196} + return file_openshell_proto_rawDescGZIP(), []int{198} } func (x *DenialSummary) GetSandboxId() string { @@ -15483,7 +15668,7 @@ type DenialGroupCount struct { func (x *DenialGroupCount) Reset() { *x = DenialGroupCount{} - mi := &file_openshell_proto_msgTypes[197] + mi := &file_openshell_proto_msgTypes[199] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15495,7 +15680,7 @@ func (x *DenialGroupCount) String() string { func (*DenialGroupCount) ProtoMessage() {} func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[197] + mi := &file_openshell_proto_msgTypes[199] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15508,7 +15693,7 @@ func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialGroupCount.ProtoReflect.Descriptor instead. func (*DenialGroupCount) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{197} + return file_openshell_proto_rawDescGZIP(), []int{199} } func (x *DenialGroupCount) GetDenyGroup() string { @@ -15541,7 +15726,7 @@ type NetworkActivitySummary struct { func (x *NetworkActivitySummary) Reset() { *x = NetworkActivitySummary{} - mi := &file_openshell_proto_msgTypes[198] + mi := &file_openshell_proto_msgTypes[200] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15553,7 +15738,7 @@ func (x *NetworkActivitySummary) String() string { func (*NetworkActivitySummary) ProtoMessage() {} func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[198] + mi := &file_openshell_proto_msgTypes[200] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15566,7 +15751,7 @@ func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkActivitySummary.ProtoReflect.Descriptor instead. func (*NetworkActivitySummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{198} + return file_openshell_proto_rawDescGZIP(), []int{200} } func (x *NetworkActivitySummary) GetNetworkActivityCount() uint32 { @@ -15654,7 +15839,7 @@ type PolicyChunk struct { func (x *PolicyChunk) Reset() { *x = PolicyChunk{} - mi := &file_openshell_proto_msgTypes[199] + mi := &file_openshell_proto_msgTypes[201] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15666,7 +15851,7 @@ func (x *PolicyChunk) String() string { func (*PolicyChunk) ProtoMessage() {} func (x *PolicyChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[199] + mi := &file_openshell_proto_msgTypes[201] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15679,7 +15864,7 @@ func (x *PolicyChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyChunk.ProtoReflect.Descriptor instead. func (*PolicyChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{199} + return file_openshell_proto_rawDescGZIP(), []int{201} } func (x *PolicyChunk) GetId() string { @@ -15867,7 +16052,7 @@ type DraftPolicyUpdate struct { func (x *DraftPolicyUpdate) Reset() { *x = DraftPolicyUpdate{} - mi := &file_openshell_proto_msgTypes[200] + mi := &file_openshell_proto_msgTypes[202] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15879,7 +16064,7 @@ func (x *DraftPolicyUpdate) String() string { func (*DraftPolicyUpdate) ProtoMessage() {} func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[200] + mi := &file_openshell_proto_msgTypes[202] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15892,7 +16077,7 @@ func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftPolicyUpdate.ProtoReflect.Descriptor instead. func (*DraftPolicyUpdate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{200} + return file_openshell_proto_rawDescGZIP(), []int{202} } func (x *DraftPolicyUpdate) GetDraftVersion() uint64 { @@ -15951,7 +16136,7 @@ type SubmitPolicyAnalysisRequest struct { func (x *SubmitPolicyAnalysisRequest) Reset() { *x = SubmitPolicyAnalysisRequest{} - mi := &file_openshell_proto_msgTypes[201] + mi := &file_openshell_proto_msgTypes[203] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15963,7 +16148,7 @@ func (x *SubmitPolicyAnalysisRequest) String() string { func (*SubmitPolicyAnalysisRequest) ProtoMessage() {} func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[201] + mi := &file_openshell_proto_msgTypes[203] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15976,7 +16161,7 @@ func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisRequest.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{201} + return file_openshell_proto_rawDescGZIP(), []int{203} } func (x *SubmitPolicyAnalysisRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16039,7 +16224,7 @@ type SubmitPolicyAnalysisResponse struct { func (x *SubmitPolicyAnalysisResponse) Reset() { *x = SubmitPolicyAnalysisResponse{} - mi := &file_openshell_proto_msgTypes[202] + mi := &file_openshell_proto_msgTypes[204] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16051,7 +16236,7 @@ func (x *SubmitPolicyAnalysisResponse) String() string { func (*SubmitPolicyAnalysisResponse) ProtoMessage() {} func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[202] + mi := &file_openshell_proto_msgTypes[204] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16064,7 +16249,7 @@ func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisResponse.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{202} + return file_openshell_proto_rawDescGZIP(), []int{204} } func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunks() uint32 { @@ -16109,7 +16294,7 @@ type GetDraftPolicyRequest struct { func (x *GetDraftPolicyRequest) Reset() { *x = GetDraftPolicyRequest{} - mi := &file_openshell_proto_msgTypes[203] + mi := &file_openshell_proto_msgTypes[205] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16121,7 +16306,7 @@ func (x *GetDraftPolicyRequest) String() string { func (*GetDraftPolicyRequest) ProtoMessage() {} func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[203] + mi := &file_openshell_proto_msgTypes[205] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16134,7 +16319,7 @@ func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyRequest.ProtoReflect.Descriptor instead. func (*GetDraftPolicyRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{203} + return file_openshell_proto_rawDescGZIP(), []int{205} } func (x *GetDraftPolicyRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16174,7 +16359,7 @@ type GetDraftPolicyResponse struct { func (x *GetDraftPolicyResponse) Reset() { *x = GetDraftPolicyResponse{} - mi := &file_openshell_proto_msgTypes[204] + mi := &file_openshell_proto_msgTypes[206] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16186,7 +16371,7 @@ func (x *GetDraftPolicyResponse) String() string { func (*GetDraftPolicyResponse) ProtoMessage() {} func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[204] + mi := &file_openshell_proto_msgTypes[206] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16199,7 +16384,7 @@ func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyResponse.ProtoReflect.Descriptor instead. func (*GetDraftPolicyResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{204} + return file_openshell_proto_rawDescGZIP(), []int{206} } func (x *GetDraftPolicyResponse) GetChunks() []*PolicyChunk { @@ -16250,7 +16435,7 @@ type ApproveDraftChunkRequest struct { func (x *ApproveDraftChunkRequest) Reset() { *x = ApproveDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[205] + mi := &file_openshell_proto_msgTypes[207] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16262,7 +16447,7 @@ func (x *ApproveDraftChunkRequest) String() string { func (*ApproveDraftChunkRequest) ProtoMessage() {} func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[205] + mi := &file_openshell_proto_msgTypes[207] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16275,7 +16460,7 @@ func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkRequest.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{205} + return file_openshell_proto_rawDescGZIP(), []int{207} } func (x *ApproveDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16325,7 +16510,7 @@ type ApproveDraftChunkResponse struct { func (x *ApproveDraftChunkResponse) Reset() { *x = ApproveDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[206] + mi := &file_openshell_proto_msgTypes[208] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16337,7 +16522,7 @@ func (x *ApproveDraftChunkResponse) String() string { func (*ApproveDraftChunkResponse) ProtoMessage() {} func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[206] + mi := &file_openshell_proto_msgTypes[208] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16350,7 +16535,7 @@ func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkResponse.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{206} + return file_openshell_proto_rawDescGZIP(), []int{208} } func (x *ApproveDraftChunkResponse) GetPolicyVersion() uint32 { @@ -16386,7 +16571,7 @@ type RejectDraftChunkRequest struct { func (x *RejectDraftChunkRequest) Reset() { *x = RejectDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[207] + mi := &file_openshell_proto_msgTypes[209] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16398,7 +16583,7 @@ func (x *RejectDraftChunkRequest) String() string { func (*RejectDraftChunkRequest) ProtoMessage() {} func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[207] + mi := &file_openshell_proto_msgTypes[209] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16411,7 +16596,7 @@ func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkRequest.ProtoReflect.Descriptor instead. func (*RejectDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{207} + return file_openshell_proto_rawDescGZIP(), []int{209} } func (x *RejectDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16457,7 +16642,7 @@ type RejectDraftChunkResponse struct { func (x *RejectDraftChunkResponse) Reset() { *x = RejectDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[208] + mi := &file_openshell_proto_msgTypes[210] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16469,7 +16654,7 @@ func (x *RejectDraftChunkResponse) String() string { func (*RejectDraftChunkResponse) ProtoMessage() {} func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[208] + mi := &file_openshell_proto_msgTypes[210] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16482,7 +16667,7 @@ func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkResponse.ProtoReflect.Descriptor instead. func (*RejectDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{208} + return file_openshell_proto_rawDescGZIP(), []int{210} } // Approve all pending chunks. @@ -16496,7 +16681,7 @@ type DraftChunkApproval struct { func (x *DraftChunkApproval) Reset() { *x = DraftChunkApproval{} - mi := &file_openshell_proto_msgTypes[209] + mi := &file_openshell_proto_msgTypes[211] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16508,7 +16693,7 @@ func (x *DraftChunkApproval) String() string { func (*DraftChunkApproval) ProtoMessage() {} func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[209] + mi := &file_openshell_proto_msgTypes[211] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16521,7 +16706,7 @@ func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftChunkApproval.ProtoReflect.Descriptor instead. func (*DraftChunkApproval) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{209} + return file_openshell_proto_rawDescGZIP(), []int{211} } func (x *DraftChunkApproval) GetChunkId() string { @@ -16557,7 +16742,7 @@ type ApproveAllDraftChunksRequest struct { func (x *ApproveAllDraftChunksRequest) Reset() { *x = ApproveAllDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[210] + mi := &file_openshell_proto_msgTypes[212] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16569,7 +16754,7 @@ func (x *ApproveAllDraftChunksRequest) String() string { func (*ApproveAllDraftChunksRequest) ProtoMessage() {} func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[210] + mi := &file_openshell_proto_msgTypes[212] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16582,7 +16767,7 @@ func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{210} + return file_openshell_proto_rawDescGZIP(), []int{212} } func (x *ApproveAllDraftChunksRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16637,7 +16822,7 @@ type ApproveAllDraftChunksResponse struct { func (x *ApproveAllDraftChunksResponse) Reset() { *x = ApproveAllDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[211] + mi := &file_openshell_proto_msgTypes[213] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16649,7 +16834,7 @@ func (x *ApproveAllDraftChunksResponse) String() string { func (*ApproveAllDraftChunksResponse) ProtoMessage() {} func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[211] + mi := &file_openshell_proto_msgTypes[213] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16662,7 +16847,7 @@ func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{211} + return file_openshell_proto_rawDescGZIP(), []int{213} } func (x *ApproveAllDraftChunksResponse) GetPolicyVersion() uint32 { @@ -16712,7 +16897,7 @@ type EditDraftChunkRequest struct { func (x *EditDraftChunkRequest) Reset() { *x = EditDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[212] + mi := &file_openshell_proto_msgTypes[214] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16724,7 +16909,7 @@ func (x *EditDraftChunkRequest) String() string { func (*EditDraftChunkRequest) ProtoMessage() {} func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[212] + mi := &file_openshell_proto_msgTypes[214] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16737,7 +16922,7 @@ func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkRequest.ProtoReflect.Descriptor instead. func (*EditDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{212} + return file_openshell_proto_rawDescGZIP(), []int{214} } func (x *EditDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16783,7 +16968,7 @@ type EditDraftChunkResponse struct { func (x *EditDraftChunkResponse) Reset() { *x = EditDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[213] + mi := &file_openshell_proto_msgTypes[215] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16795,7 +16980,7 @@ func (x *EditDraftChunkResponse) String() string { func (*EditDraftChunkResponse) ProtoMessage() {} func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[213] + mi := &file_openshell_proto_msgTypes[215] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16808,7 +16993,7 @@ func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkResponse.ProtoReflect.Descriptor instead. func (*EditDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{213} + return file_openshell_proto_rawDescGZIP(), []int{215} } // Reverse an approval (remove merged rule from active policy). @@ -16828,7 +17013,7 @@ type UndoDraftChunkRequest struct { func (x *UndoDraftChunkRequest) Reset() { *x = UndoDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[214] + mi := &file_openshell_proto_msgTypes[216] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16840,7 +17025,7 @@ func (x *UndoDraftChunkRequest) String() string { func (*UndoDraftChunkRequest) ProtoMessage() {} func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[214] + mi := &file_openshell_proto_msgTypes[216] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16853,7 +17038,7 @@ func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkRequest.ProtoReflect.Descriptor instead. func (*UndoDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{214} + return file_openshell_proto_rawDescGZIP(), []int{216} } func (x *UndoDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16896,7 +17081,7 @@ type UndoDraftChunkResponse struct { func (x *UndoDraftChunkResponse) Reset() { *x = UndoDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[215] + mi := &file_openshell_proto_msgTypes[217] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16908,7 +17093,7 @@ func (x *UndoDraftChunkResponse) String() string { func (*UndoDraftChunkResponse) ProtoMessage() {} func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[215] + mi := &file_openshell_proto_msgTypes[217] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16921,7 +17106,7 @@ func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkResponse.ProtoReflect.Descriptor instead. func (*UndoDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{215} + return file_openshell_proto_rawDescGZIP(), []int{217} } func (x *UndoDraftChunkResponse) GetPolicyVersion() uint32 { @@ -16953,7 +17138,7 @@ type ClearDraftChunksRequest struct { func (x *ClearDraftChunksRequest) Reset() { *x = ClearDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[216] + mi := &file_openshell_proto_msgTypes[218] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16965,7 +17150,7 @@ func (x *ClearDraftChunksRequest) String() string { func (*ClearDraftChunksRequest) ProtoMessage() {} func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[216] + mi := &file_openshell_proto_msgTypes[218] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16978,7 +17163,7 @@ func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ClearDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{216} + return file_openshell_proto_rawDescGZIP(), []int{218} } func (x *ClearDraftChunksRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17012,7 +17197,7 @@ type ClearDraftChunksResponse struct { func (x *ClearDraftChunksResponse) Reset() { *x = ClearDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[217] + mi := &file_openshell_proto_msgTypes[219] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17024,7 +17209,7 @@ func (x *ClearDraftChunksResponse) String() string { func (*ClearDraftChunksResponse) ProtoMessage() {} func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[217] + mi := &file_openshell_proto_msgTypes[219] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17037,7 +17222,7 @@ func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ClearDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{217} + return file_openshell_proto_rawDescGZIP(), []int{219} } func (x *ClearDraftChunksResponse) GetChunksCleared() uint32 { @@ -17059,7 +17244,7 @@ type GetDraftHistoryRequest struct { func (x *GetDraftHistoryRequest) Reset() { *x = GetDraftHistoryRequest{} - mi := &file_openshell_proto_msgTypes[218] + mi := &file_openshell_proto_msgTypes[220] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17071,7 +17256,7 @@ func (x *GetDraftHistoryRequest) String() string { func (*GetDraftHistoryRequest) ProtoMessage() {} func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[218] + mi := &file_openshell_proto_msgTypes[220] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17084,7 +17269,7 @@ func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryRequest.ProtoReflect.Descriptor instead. func (*GetDraftHistoryRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{218} + return file_openshell_proto_rawDescGZIP(), []int{220} } func (x *GetDraftHistoryRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17118,7 +17303,7 @@ type DraftHistoryEntry struct { func (x *DraftHistoryEntry) Reset() { *x = DraftHistoryEntry{} - mi := &file_openshell_proto_msgTypes[219] + mi := &file_openshell_proto_msgTypes[221] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17130,7 +17315,7 @@ func (x *DraftHistoryEntry) String() string { func (*DraftHistoryEntry) ProtoMessage() {} func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[219] + mi := &file_openshell_proto_msgTypes[221] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17143,7 +17328,7 @@ func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftHistoryEntry.ProtoReflect.Descriptor instead. func (*DraftHistoryEntry) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{219} + return file_openshell_proto_rawDescGZIP(), []int{221} } func (x *DraftHistoryEntry) GetEventTime() *timestamppb.Timestamp { @@ -17184,7 +17369,7 @@ type GetDraftHistoryResponse struct { func (x *GetDraftHistoryResponse) Reset() { *x = GetDraftHistoryResponse{} - mi := &file_openshell_proto_msgTypes[220] + mi := &file_openshell_proto_msgTypes[222] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17196,7 +17381,7 @@ func (x *GetDraftHistoryResponse) String() string { func (*GetDraftHistoryResponse) ProtoMessage() {} func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[220] + mi := &file_openshell_proto_msgTypes[222] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17209,7 +17394,7 @@ func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryResponse.ProtoReflect.Descriptor instead. func (*GetDraftHistoryResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{220} + return file_openshell_proto_rawDescGZIP(), []int{222} } func (x *GetDraftHistoryResponse) GetEntries() []*DraftHistoryEntry { @@ -17234,7 +17419,7 @@ type CreateWorkspaceRequest struct { func (x *CreateWorkspaceRequest) Reset() { *x = CreateWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[221] + mi := &file_openshell_proto_msgTypes[223] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17246,7 +17431,7 @@ func (x *CreateWorkspaceRequest) String() string { func (*CreateWorkspaceRequest) ProtoMessage() {} func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[221] + mi := &file_openshell_proto_msgTypes[223] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17259,7 +17444,7 @@ func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceRequest.ProtoReflect.Descriptor instead. func (*CreateWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{221} + return file_openshell_proto_rawDescGZIP(), []int{223} } func (x *CreateWorkspaceRequest) GetName() string { @@ -17293,7 +17478,7 @@ type CreateWorkspaceResponse struct { func (x *CreateWorkspaceResponse) Reset() { *x = CreateWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[222] + mi := &file_openshell_proto_msgTypes[224] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17305,7 +17490,7 @@ func (x *CreateWorkspaceResponse) String() string { func (*CreateWorkspaceResponse) ProtoMessage() {} func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[222] + mi := &file_openshell_proto_msgTypes[224] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17318,7 +17503,7 @@ func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceResponse.ProtoReflect.Descriptor instead. func (*CreateWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{222} + return file_openshell_proto_rawDescGZIP(), []int{224} } func (x *CreateWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -17339,7 +17524,7 @@ type GetWorkspaceRequest struct { func (x *GetWorkspaceRequest) Reset() { *x = GetWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[223] + mi := &file_openshell_proto_msgTypes[225] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17351,7 +17536,7 @@ func (x *GetWorkspaceRequest) String() string { func (*GetWorkspaceRequest) ProtoMessage() {} func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[223] + mi := &file_openshell_proto_msgTypes[225] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17364,7 +17549,7 @@ func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceRequest.ProtoReflect.Descriptor instead. func (*GetWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{223} + return file_openshell_proto_rawDescGZIP(), []int{225} } func (x *GetWorkspaceRequest) GetName() string { @@ -17384,7 +17569,7 @@ type GetWorkspaceResponse struct { func (x *GetWorkspaceResponse) Reset() { *x = GetWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[224] + mi := &file_openshell_proto_msgTypes[226] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17396,7 +17581,7 @@ func (x *GetWorkspaceResponse) String() string { func (*GetWorkspaceResponse) ProtoMessage() {} func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[224] + mi := &file_openshell_proto_msgTypes[226] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17409,7 +17594,7 @@ func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceResponse.ProtoReflect.Descriptor instead. func (*GetWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{224} + return file_openshell_proto_rawDescGZIP(), []int{226} } func (x *GetWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -17436,7 +17621,7 @@ type ListWorkspacesRequest struct { func (x *ListWorkspacesRequest) Reset() { *x = ListWorkspacesRequest{} - mi := &file_openshell_proto_msgTypes[225] + mi := &file_openshell_proto_msgTypes[227] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17448,7 +17633,7 @@ func (x *ListWorkspacesRequest) String() string { func (*ListWorkspacesRequest) ProtoMessage() {} func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[225] + mi := &file_openshell_proto_msgTypes[227] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17461,7 +17646,7 @@ func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesRequest.ProtoReflect.Descriptor instead. func (*ListWorkspacesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{225} + return file_openshell_proto_rawDescGZIP(), []int{227} } func (x *ListWorkspacesRequest) GetPageSize() int32 { @@ -17497,7 +17682,7 @@ type ListWorkspacesResponse struct { func (x *ListWorkspacesResponse) Reset() { *x = ListWorkspacesResponse{} - mi := &file_openshell_proto_msgTypes[226] + mi := &file_openshell_proto_msgTypes[228] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17509,7 +17694,7 @@ func (x *ListWorkspacesResponse) String() string { func (*ListWorkspacesResponse) ProtoMessage() {} func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[226] + mi := &file_openshell_proto_msgTypes[228] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17522,7 +17707,7 @@ func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesResponse.ProtoReflect.Descriptor instead. func (*ListWorkspacesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{226} + return file_openshell_proto_rawDescGZIP(), []int{228} } func (x *ListWorkspacesResponse) GetWorkspaces() []*datamodelv1.Workspace { @@ -17553,7 +17738,7 @@ type DeleteWorkspaceRequest struct { func (x *DeleteWorkspaceRequest) Reset() { *x = DeleteWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[227] + mi := &file_openshell_proto_msgTypes[229] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17565,7 +17750,7 @@ func (x *DeleteWorkspaceRequest) String() string { func (*DeleteWorkspaceRequest) ProtoMessage() {} func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[227] + mi := &file_openshell_proto_msgTypes[229] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17578,7 +17763,7 @@ func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceRequest.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{227} + return file_openshell_proto_rawDescGZIP(), []int{229} } func (x *DeleteWorkspaceRequest) GetName() string { @@ -17612,7 +17797,7 @@ type DeleteWorkspaceResponse struct { func (x *DeleteWorkspaceResponse) Reset() { *x = DeleteWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[228] + mi := &file_openshell_proto_msgTypes[230] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17624,7 +17809,7 @@ func (x *DeleteWorkspaceResponse) String() string { func (*DeleteWorkspaceResponse) ProtoMessage() {} func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[228] + mi := &file_openshell_proto_msgTypes[230] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17637,7 +17822,7 @@ func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceResponse.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{228} + return file_openshell_proto_rawDescGZIP(), []int{230} } func (x *DeleteWorkspaceResponse) GetOutcome() DeletionOutcome { @@ -17661,7 +17846,7 @@ type WorkspaceMember struct { func (x *WorkspaceMember) Reset() { *x = WorkspaceMember{} - mi := &file_openshell_proto_msgTypes[229] + mi := &file_openshell_proto_msgTypes[231] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17673,7 +17858,7 @@ func (x *WorkspaceMember) String() string { func (*WorkspaceMember) ProtoMessage() {} func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[229] + mi := &file_openshell_proto_msgTypes[231] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17686,7 +17871,7 @@ func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceMember.ProtoReflect.Descriptor instead. func (*WorkspaceMember) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{229} + return file_openshell_proto_rawDescGZIP(), []int{231} } func (x *WorkspaceMember) GetMetadata() *datamodelv1.ObjectMeta { @@ -17727,7 +17912,7 @@ type AddWorkspaceMemberRequest struct { func (x *AddWorkspaceMemberRequest) Reset() { *x = AddWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[230] + mi := &file_openshell_proto_msgTypes[232] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17739,7 +17924,7 @@ func (x *AddWorkspaceMemberRequest) String() string { func (*AddWorkspaceMemberRequest) ProtoMessage() {} func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[230] + mi := &file_openshell_proto_msgTypes[232] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17752,7 +17937,7 @@ func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{230} + return file_openshell_proto_rawDescGZIP(), []int{232} } func (x *AddWorkspaceMemberRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17793,7 +17978,7 @@ type AddWorkspaceMemberResponse struct { func (x *AddWorkspaceMemberResponse) Reset() { *x = AddWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[231] + mi := &file_openshell_proto_msgTypes[233] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17805,7 +17990,7 @@ func (x *AddWorkspaceMemberResponse) String() string { func (*AddWorkspaceMemberResponse) ProtoMessage() {} func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[231] + mi := &file_openshell_proto_msgTypes[233] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17818,7 +18003,7 @@ func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{231} + return file_openshell_proto_rawDescGZIP(), []int{233} } func (x *AddWorkspaceMemberResponse) GetMember() *WorkspaceMember { @@ -17844,7 +18029,7 @@ type RemoveWorkspaceMemberRequest struct { func (x *RemoveWorkspaceMemberRequest) Reset() { *x = RemoveWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[232] + mi := &file_openshell_proto_msgTypes[234] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17856,7 +18041,7 @@ func (x *RemoveWorkspaceMemberRequest) String() string { func (*RemoveWorkspaceMemberRequest) ProtoMessage() {} func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[232] + mi := &file_openshell_proto_msgTypes[234] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17869,7 +18054,7 @@ func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{232} + return file_openshell_proto_rawDescGZIP(), []int{234} } func (x *RemoveWorkspaceMemberRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17910,7 +18095,7 @@ type RemoveWorkspaceMemberResponse struct { func (x *RemoveWorkspaceMemberResponse) Reset() { *x = RemoveWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[233] + mi := &file_openshell_proto_msgTypes[235] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17922,7 +18107,7 @@ func (x *RemoveWorkspaceMemberResponse) String() string { func (*RemoveWorkspaceMemberResponse) ProtoMessage() {} func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[233] + mi := &file_openshell_proto_msgTypes[235] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17935,7 +18120,7 @@ func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{233} + return file_openshell_proto_rawDescGZIP(), []int{235} } func (x *RemoveWorkspaceMemberResponse) GetOutcome() DeletionOutcome { @@ -17962,7 +18147,7 @@ type ListWorkspaceMembersRequest struct { func (x *ListWorkspaceMembersRequest) Reset() { *x = ListWorkspaceMembersRequest{} - mi := &file_openshell_proto_msgTypes[234] + mi := &file_openshell_proto_msgTypes[236] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17974,7 +18159,7 @@ func (x *ListWorkspaceMembersRequest) String() string { func (*ListWorkspaceMembersRequest) ProtoMessage() {} func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[234] + mi := &file_openshell_proto_msgTypes[236] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17987,7 +18172,7 @@ func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersRequest.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{234} + return file_openshell_proto_rawDescGZIP(), []int{236} } func (x *ListWorkspaceMembersRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -18023,7 +18208,7 @@ type ListWorkspaceMembersResponse struct { func (x *ListWorkspaceMembersResponse) Reset() { *x = ListWorkspaceMembersResponse{} - mi := &file_openshell_proto_msgTypes[235] + mi := &file_openshell_proto_msgTypes[237] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18035,7 +18220,7 @@ func (x *ListWorkspaceMembersResponse) String() string { func (*ListWorkspaceMembersResponse) ProtoMessage() {} func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[235] + mi := &file_openshell_proto_msgTypes[237] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18048,7 +18233,7 @@ func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersResponse.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{235} + return file_openshell_proto_rawDescGZIP(), []int{237} } func (x *ListWorkspaceMembersResponse) GetMembers() []*WorkspaceMember { @@ -18083,7 +18268,7 @@ type ExtensionServiceCredential struct { func (x *ExtensionServiceCredential) Reset() { *x = ExtensionServiceCredential{} - mi := &file_openshell_proto_msgTypes[236] + mi := &file_openshell_proto_msgTypes[238] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18095,7 +18280,7 @@ func (x *ExtensionServiceCredential) String() string { func (*ExtensionServiceCredential) ProtoMessage() {} func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[236] + mi := &file_openshell_proto_msgTypes[238] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18108,7 +18293,7 @@ func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ExtensionServiceCredential.ProtoReflect.Descriptor instead. func (*ExtensionServiceCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{236} + return file_openshell_proto_rawDescGZIP(), []int{238} } func (x *ExtensionServiceCredential) GetServiceName() string { @@ -18145,7 +18330,7 @@ type EndpointObservation struct { func (x *EndpointObservation) Reset() { *x = EndpointObservation{} - mi := &file_openshell_proto_msgTypes[237] + mi := &file_openshell_proto_msgTypes[239] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18157,7 +18342,7 @@ func (x *EndpointObservation) String() string { func (*EndpointObservation) ProtoMessage() {} func (x *EndpointObservation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[237] + mi := &file_openshell_proto_msgTypes[239] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18170,7 +18355,7 @@ func (x *EndpointObservation) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointObservation.ProtoReflect.Descriptor instead. func (*EndpointObservation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{237} + return file_openshell_proto_rawDescGZIP(), []int{239} } func (x *EndpointObservation) GetEndpointId() string { @@ -18213,7 +18398,7 @@ type ReportEndpointStatusRequest struct { func (x *ReportEndpointStatusRequest) Reset() { *x = ReportEndpointStatusRequest{} - mi := &file_openshell_proto_msgTypes[238] + mi := &file_openshell_proto_msgTypes[240] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18225,7 +18410,7 @@ func (x *ReportEndpointStatusRequest) String() string { func (*ReportEndpointStatusRequest) ProtoMessage() {} func (x *ReportEndpointStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[238] + mi := &file_openshell_proto_msgTypes[240] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18238,7 +18423,7 @@ func (x *ReportEndpointStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportEndpointStatusRequest.ProtoReflect.Descriptor instead. func (*ReportEndpointStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{238} + return file_openshell_proto_rawDescGZIP(), []int{240} } func (x *ReportEndpointStatusRequest) GetSandboxId() string { @@ -18299,7 +18484,7 @@ type ReportEndpointStatusResponse struct { func (x *ReportEndpointStatusResponse) Reset() { *x = ReportEndpointStatusResponse{} - mi := &file_openshell_proto_msgTypes[239] + mi := &file_openshell_proto_msgTypes[241] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18311,7 +18496,7 @@ func (x *ReportEndpointStatusResponse) String() string { func (*ReportEndpointStatusResponse) ProtoMessage() {} func (x *ReportEndpointStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[239] + mi := &file_openshell_proto_msgTypes[241] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18324,7 +18509,7 @@ func (x *ReportEndpointStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportEndpointStatusResponse.ProtoReflect.Descriptor instead. func (*ReportEndpointStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{239} + return file_openshell_proto_rawDescGZIP(), []int{241} } // A configured endpoint and its last accepted network result in one record. @@ -18353,7 +18538,7 @@ type EndpointStatus struct { func (x *EndpointStatus) Reset() { *x = EndpointStatus{} - mi := &file_openshell_proto_msgTypes[240] + mi := &file_openshell_proto_msgTypes[242] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18365,7 +18550,7 @@ func (x *EndpointStatus) String() string { func (*EndpointStatus) ProtoMessage() {} func (x *EndpointStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[240] + mi := &file_openshell_proto_msgTypes[242] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18378,7 +18563,7 @@ func (x *EndpointStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointStatus.ProtoReflect.Descriptor instead. func (*EndpointStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{240} + return file_openshell_proto_rawDescGZIP(), []int{242} } func (x *EndpointStatus) GetEndpointId() string { @@ -18448,7 +18633,7 @@ type SandboxProvisioning struct { func (x *SandboxProvisioning) Reset() { *x = SandboxProvisioning{} - mi := &file_openshell_proto_msgTypes[241] + mi := &file_openshell_proto_msgTypes[243] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18460,7 +18645,7 @@ func (x *SandboxProvisioning) String() string { func (*SandboxProvisioning) ProtoMessage() {} func (x *SandboxProvisioning) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[241] + mi := &file_openshell_proto_msgTypes[243] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18473,7 +18658,7 @@ func (x *SandboxProvisioning) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxProvisioning.ProtoReflect.Descriptor instead. func (*SandboxProvisioning) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{241} + return file_openshell_proto_rawDescGZIP(), []int{243} } func (x *SandboxProvisioning) GetAttemptId() string { @@ -18566,7 +18751,7 @@ type SandboxServiceExposure struct { func (x *SandboxServiceExposure) Reset() { *x = SandboxServiceExposure{} - mi := &file_openshell_proto_msgTypes[242] + mi := &file_openshell_proto_msgTypes[244] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18578,7 +18763,7 @@ func (x *SandboxServiceExposure) String() string { func (*SandboxServiceExposure) ProtoMessage() {} func (x *SandboxServiceExposure) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[242] + mi := &file_openshell_proto_msgTypes[244] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18591,7 +18776,7 @@ func (x *SandboxServiceExposure) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxServiceExposure.ProtoReflect.Descriptor instead. func (*SandboxServiceExposure) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{242} + return file_openshell_proto_rawDescGZIP(), []int{244} } func (x *SandboxServiceExposure) GetService() string { @@ -19441,7 +19626,7 @@ const file_openshell_proto_rawDesc = "" + "\rexpires_after\x18f \x01(\v2\x19.google.protobuf.DurationR\fexpiresAfter\x12\x1d\n" + "\n" + "token_type\x18\x03 \x01(\tR\ttokenTypeJ\x04\b\x02\x10\x03R\n" + - "expires_in\"\xa9\x05\n" + + "expires_in\"\xf2\x06\n" + "\x13UpdateConfigRequest\x12R\n" + "\x0fworkspace_scope\x18\n" + " \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12;\n" + @@ -19453,13 +19638,21 @@ const file_openshell_proto_rawDesc = "" + "\x06global\x18\x06 \x01(\bR\x06global\x12M\n" + "\x10merge_operations\x18\a \x03(\v2\".openshell.v1.PolicyMergeOperationR\x0fmergeOperations\x12:\n" + "\x19expected_resource_version\x18\b \x01(\x04R\x17expectedResourceVersion\x12T\n" + - "\vannotations\x18\t \x03(\v22.openshell.v1.UpdateConfigRequest.AnnotationsEntryR\vannotations\x12\x18\n" + + "\vannotations\x18\t \x03(\v22.openshell.v1.UpdateConfigRequest.AnnotationsEntryR\vannotations\x12G\n" + + "\vconsistency\x18\f \x01(\x0e2%.openshell.v1.ConfigUpdateConsistencyR\vconsistency\x12'\n" + + "\x0fidempotency_key\x18\r \x01(\tR\x0eidempotencyKey\x12<\n" + + "\fwait_timeout\x18r \x01(\v2\x19.google.protobuf.DurationR\vwaitTimeout\x12\x18\n" + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1d\n" + "\n" + "request_id\x18\v \x01(\tR\trequestId\x1a>\n" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc7\x03\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x0e\x10\x0fR\x11wait_timeout_secs\"\x98\x01\n" + + "\x1fGetConfigUpdateOperationRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12!\n" + + "\foperation_id\x18\x01 \x01(\tR\voperationId\"e\n" + + " GetConfigUpdateOperationResponse\x12A\n" + + "\toperation\x18\x01 \x01(\v2#.openshell.v1.ConfigUpdateOperationR\toperation\"\xc7\x03\n" + "\x14PolicyMergeOperation\x129\n" + "\badd_rule\x18\x01 \x01(\v2\x1c.openshell.v1.AddNetworkRuleH\x00R\aaddRule\x12N\n" + "\x0fremove_endpoint\x18\x02 \x01(\v2#.openshell.v1.RemoveNetworkEndpointH\x00R\x0eremoveEndpoint\x12B\n" + @@ -19497,14 +19690,15 @@ const file_openshell_proto_rawDesc = "" + "\x13RemoveNetworkBinary\x12\x1b\n" + "\trule_name\x18\x01 \x01(\tR\bruleName\x12\x1f\n" + "\vbinary_path\x18\x02 \x01(\tR\n" + - "binaryPath\"\xaf\x02\n" + + "binaryPath\"\xf2\x02\n" + "\x14UpdateConfigResponse\x12\x18\n" + "\aversion\x18\x01 \x01(\rR\aversion\x12\x1f\n" + "\vpolicy_hash\x18\x02 \x01(\tR\n" + "policyHash\x12+\n" + "\x11settings_revision\x18\x03 \x01(\x04R\x10settingsRevision\x12\x18\n" + "\adeleted\x18\x04 \x01(\bR\adeleted\x12U\n" + - "\vannotations\x18\x05 \x03(\v23.openshell.v1.UpdateConfigResponse.AnnotationsEntryR\vannotations\x1a>\n" + + "\vannotations\x18\x05 \x03(\v23.openshell.v1.UpdateConfigResponse.AnnotationsEntryR\vannotations\x12A\n" + + "\toperation\x18\x06 \x01(\v2#.openshell.v1.ConfigUpdateOperationR\toperation\x1a>\n" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xbf\x01\n" + @@ -20091,7 +20285,11 @@ const file_openshell_proto_rawDesc = "" + "&ProviderEnvironmentValueClassification\x129\n" + "5PROVIDER_ENVIRONMENT_VALUE_CLASSIFICATION_UNSPECIFIED\x10\x00\x128\n" + "4PROVIDER_ENVIRONMENT_VALUE_CLASSIFICATION_NON_SECRET\x10\x01\x12?\n" + - ";PROVIDER_ENVIRONMENT_VALUE_CLASSIFICATION_STATIC_CREDENTIAL\x10\x02*\xcf\x01\n" + + ";PROVIDER_ENVIRONMENT_VALUE_CLASSIFICATION_STATIC_CREDENTIAL\x10\x02*\xa2\x01\n" + + "\x17ConfigUpdateConsistency\x12)\n" + + "%CONFIG_UPDATE_CONSISTENCY_UNSPECIFIED\x10\x00\x12)\n" + + "%CONFIG_UPDATE_CONSISTENCY_COMMIT_ONLY\x10\x01\x121\n" + + "-CONFIG_UPDATE_CONSISTENCY_WAIT_FOR_COMPLETION\x10\x02*\xcf\x01\n" + "\x1bConfigurationAdmissionState\x12-\n" + ")CONFIGURATION_ADMISSION_STATE_UNSPECIFIED\x10\x00\x12)\n" + "%CONFIGURATION_ADMISSION_STATE_PENDING\x10\x01\x12*\n" + @@ -20131,7 +20329,7 @@ const file_openshell_proto_rawDesc = "" + "&ENDPOINT_RESULT_CREDENTIAL_UNAVAILABLE\x10\x04\x12\x1e\n" + "\x1aENDPOINT_RESULT_TLS_FAILED\x10\x05\x12$\n" + " ENDPOINT_RESULT_TRANSPORT_FAILED\x10\x06\x12%\n" + - "!ENDPOINT_RESULT_UPSTREAM_REJECTED\x10\a2\xafU\n" + + "!ENDPOINT_RESULT_UPSTREAM_REJECTED\x10\a2\xcdV\n" + "\tOpenShell\x12Z\n" + "\x06Health\x12\x1b.openshell.v1.HealthRequest\x1a\x1c.openshell.v1.HealthResponse\"\x15\x82\xb5\x18\x11\n" + "\x0funauthenticated\x12i\n" + @@ -20225,7 +20423,9 @@ const file_openshell_proto_rawDesc = "" + "\x10GetGatewayConfig\x12-.openshell.sandbox.v1.GetGatewayConfigRequest\x1a..openshell.sandbox.v1.GetGatewayConfigResponse\"\x19\x82\xb5\x18\x15\n" + "\x06bearer\"\vconfig:read\x12v\n" + "\fUpdateConfig\x12!.openshell.v1.UpdateConfigRequest\x1a\".openshell.v1.UpdateConfigResponse\"\x1f\x82\xb5\x18\x1b\n" + - "\x04dual\x12\x05admin\"\fconfig:write\x12\x95\x01\n" + + "\x04dual\x12\x05admin\"\fconfig:write\x12\x9b\x01\n" + + "\x18GetConfigUpdateOperation\x12-.openshell.v1.GetConfigUpdateOperationRequest\x1a..openshell.v1.GetConfigUpdateOperationResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12\x95\x01\n" + "\x16GetSandboxPolicyStatus\x12+.openshell.v1.GetSandboxPolicyStatusRequest\x1a,.openshell.v1.GetSandboxPolicyStatusResponse\" \x82\xb5\x18\x1c\n" + "\x06bearer\x12\x04user\"\fsandbox:read\x12\x8c\x01\n" + "\x13ListSandboxPolicies\x12(.openshell.v1.ListSandboxPoliciesRequest\x1a).openshell.v1.ListSandboxPoliciesResponse\" \x82\xb5\x18\x1c\n" + @@ -20317,8 +20517,8 @@ func file_openshell_proto_rawDescGZIP() []byte { return file_openshell_proto_rawDescData } -var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 19) -var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 266) +var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 20) +var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 268) var file_openshell_proto_goTypes = []any{ (ExtensionKind)(0), // 0: openshell.v1.ExtensionKind (SandboxPhase)(0), // 1: openshell.v1.SandboxPhase @@ -20332,825 +20532,835 @@ var file_openshell_proto_goTypes = []any{ (ProviderCredentialRefreshStrategy)(0), // 9: openshell.v1.ProviderCredentialRefreshStrategy (ProviderProfileCategory)(0), // 10: openshell.v1.ProviderProfileCategory (ProviderEnvironmentValueClassification)(0), // 11: openshell.v1.ProviderEnvironmentValueClassification - (ConfigurationAdmissionState)(0), // 12: openshell.v1.ConfigurationAdmissionState - (PolicyStatus)(0), // 13: openshell.v1.PolicyStatus - (ServiceStatus)(0), // 14: openshell.v1.ServiceStatus - (WorkspaceRole)(0), // 15: openshell.v1.WorkspaceRole - (ProviderCredentialRefreshRecoveryAction)(0), // 16: openshell.v1.ProviderCredentialRefreshRecoveryAction - (DeletionOutcome)(0), // 17: openshell.v1.DeletionOutcome - (EndpointResult)(0), // 18: openshell.v1.EndpointResult - (*IssueSandboxTokenRequest)(nil), // 19: openshell.v1.IssueSandboxTokenRequest - (*IssueSandboxTokenResponse)(nil), // 20: openshell.v1.IssueSandboxTokenResponse - (*RefreshSandboxTokenRequest)(nil), // 21: openshell.v1.RefreshSandboxTokenRequest - (*RefreshSandboxTokenResponse)(nil), // 22: openshell.v1.RefreshSandboxTokenResponse - (*HealthRequest)(nil), // 23: openshell.v1.HealthRequest - (*HealthResponse)(nil), // 24: openshell.v1.HealthResponse - (*GetCurrentUserRequest)(nil), // 25: openshell.v1.GetCurrentUserRequest - (*GetCurrentUserResponse)(nil), // 26: openshell.v1.GetCurrentUserResponse - (*GetGatewayInfoRequest)(nil), // 27: openshell.v1.GetGatewayInfoRequest - (*GetGatewayInfoResponse)(nil), // 28: openshell.v1.GetGatewayInfoResponse - (*NegotiatedExtensionInfo)(nil), // 29: openshell.v1.NegotiatedExtensionInfo - (*ComputeDriverInfo)(nil), // 30: openshell.v1.ComputeDriverInfo - (*ComputeDriverCapabilities)(nil), // 31: openshell.v1.ComputeDriverCapabilities - (*ResourceCapabilities)(nil), // 32: openshell.v1.ResourceCapabilities - (*CpuResourceCapabilities)(nil), // 33: openshell.v1.CpuResourceCapabilities - (*MemoryResourceCapabilities)(nil), // 34: openshell.v1.MemoryResourceCapabilities - (*GpuResourceCapabilities)(nil), // 35: openshell.v1.GpuResourceCapabilities - (*Sandbox)(nil), // 36: openshell.v1.Sandbox - (*SandboxSpec)(nil), // 37: openshell.v1.SandboxSpec - (*ResourceRequirements)(nil), // 38: openshell.v1.ResourceRequirements - (*GpuResourceRequirements)(nil), // 39: openshell.v1.GpuResourceRequirements - (*SandboxTemplate)(nil), // 40: openshell.v1.SandboxTemplate - (*SandboxWorkloadTemplate)(nil), // 41: openshell.v1.SandboxWorkloadTemplate - (*SandboxWorkloadTemplateSpec)(nil), // 42: openshell.v1.SandboxWorkloadTemplateSpec - (*SandboxWorkloadConfig)(nil), // 43: openshell.v1.SandboxWorkloadConfig - (*SandboxResources)(nil), // 44: openshell.v1.SandboxResources - (*SandboxServiceLevel)(nil), // 45: openshell.v1.SandboxServiceLevel - (*SandboxStartup)(nil), // 46: openshell.v1.SandboxStartup - (*SandboxWorkloadTemplateProvenance)(nil), // 47: openshell.v1.SandboxWorkloadTemplateProvenance - (*SandboxStatus)(nil), // 48: openshell.v1.SandboxStatus - (*SandboxCondition)(nil), // 49: openshell.v1.SandboxCondition - (*PlatformEvent)(nil), // 50: openshell.v1.PlatformEvent - (*CreateSandboxRequest)(nil), // 51: openshell.v1.CreateSandboxRequest - (*CreateSandboxTemplateRequest)(nil), // 52: openshell.v1.CreateSandboxTemplateRequest - (*GetSandboxTemplateRequest)(nil), // 53: openshell.v1.GetSandboxTemplateRequest - (*ListSandboxTemplatesRequest)(nil), // 54: openshell.v1.ListSandboxTemplatesRequest - (*DeleteSandboxTemplateRequest)(nil), // 55: openshell.v1.DeleteSandboxTemplateRequest - (*SandboxTemplateResponse)(nil), // 56: openshell.v1.SandboxTemplateResponse - (*ListSandboxTemplatesResponse)(nil), // 57: openshell.v1.ListSandboxTemplatesResponse - (*DeleteSandboxTemplateResponse)(nil), // 58: openshell.v1.DeleteSandboxTemplateResponse - (*BeginRootfsTarStagingRequest)(nil), // 59: openshell.v1.BeginRootfsTarStagingRequest - (*BeginRootfsTarStagingResponse)(nil), // 60: openshell.v1.BeginRootfsTarStagingResponse - (*GetSandboxRequest)(nil), // 61: openshell.v1.GetSandboxRequest - (*ListSandboxesRequest)(nil), // 62: openshell.v1.ListSandboxesRequest - (*ListSandboxProvidersRequest)(nil), // 63: openshell.v1.ListSandboxProvidersRequest - (*AttachSandboxProviderRequest)(nil), // 64: openshell.v1.AttachSandboxProviderRequest - (*DetachSandboxProviderRequest)(nil), // 65: openshell.v1.DetachSandboxProviderRequest - (*DeleteSandboxRequest)(nil), // 66: openshell.v1.DeleteSandboxRequest - (*StopSandboxRequest)(nil), // 67: openshell.v1.StopSandboxRequest - (*StartSandboxRequest)(nil), // 68: openshell.v1.StartSandboxRequest - (*SandboxResponse)(nil), // 69: openshell.v1.SandboxResponse - (*ListSandboxesResponse)(nil), // 70: openshell.v1.ListSandboxesResponse - (*ListSandboxProvidersResponse)(nil), // 71: openshell.v1.ListSandboxProvidersResponse - (*AttachSandboxProviderResponse)(nil), // 72: openshell.v1.AttachSandboxProviderResponse - (*DetachSandboxProviderResponse)(nil), // 73: openshell.v1.DetachSandboxProviderResponse - (*ProviderDesiredIdentity)(nil), // 74: openshell.v1.ProviderDesiredIdentity - (*ConfigSnapshotRevision)(nil), // 75: openshell.v1.ConfigSnapshotRevision - (*SandboxConfigRevision)(nil), // 76: openshell.v1.SandboxConfigRevision - (*ConfigUpdateOperation)(nil), // 77: openshell.v1.ConfigUpdateOperation - (*ProviderMutationReceipt)(nil), // 78: openshell.v1.ProviderMutationReceipt - (*ProviderReadinessObservation)(nil), // 79: openshell.v1.ProviderReadinessObservation - (*ProviderReadinessStatus)(nil), // 80: openshell.v1.ProviderReadinessStatus - (*GetSandboxProviderStatusRequest)(nil), // 81: openshell.v1.GetSandboxProviderStatusRequest - (*GetSandboxProviderStatusResponse)(nil), // 82: openshell.v1.GetSandboxProviderStatusResponse - (*ReportProviderReadinessRequest)(nil), // 83: openshell.v1.ReportProviderReadinessRequest - (*ReportProviderReadinessResponse)(nil), // 84: openshell.v1.ReportProviderReadinessResponse - (*DeleteSandboxResponse)(nil), // 85: openshell.v1.DeleteSandboxResponse - (*CreateSshSessionRequest)(nil), // 86: openshell.v1.CreateSshSessionRequest - (*CreateSshSessionResponse)(nil), // 87: openshell.v1.CreateSshSessionResponse - (*ExposeServiceRequest)(nil), // 88: openshell.v1.ExposeServiceRequest - (*GetServiceRequest)(nil), // 89: openshell.v1.GetServiceRequest - (*ListServicesRequest)(nil), // 90: openshell.v1.ListServicesRequest - (*ListServicesResponse)(nil), // 91: openshell.v1.ListServicesResponse - (*DeleteServiceRequest)(nil), // 92: openshell.v1.DeleteServiceRequest - (*DeleteServiceResponse)(nil), // 93: openshell.v1.DeleteServiceResponse - (*ServiceEndpoint)(nil), // 94: openshell.v1.ServiceEndpoint - (*ServiceEndpointResponse)(nil), // 95: openshell.v1.ServiceEndpointResponse - (*RevokeSshSessionRequest)(nil), // 96: openshell.v1.RevokeSshSessionRequest - (*RevokeSshSessionResponse)(nil), // 97: openshell.v1.RevokeSshSessionResponse - (*ExecSandboxRequest)(nil), // 98: openshell.v1.ExecSandboxRequest - (*ExecSandboxStdout)(nil), // 99: openshell.v1.ExecSandboxStdout - (*ExecSandboxStderr)(nil), // 100: openshell.v1.ExecSandboxStderr - (*ExecSandboxExit)(nil), // 101: openshell.v1.ExecSandboxExit - (*ExecSandboxEvent)(nil), // 102: openshell.v1.ExecSandboxEvent - (*TcpForwardInit)(nil), // 103: openshell.v1.TcpForwardInit - (*TcpForwardFrame)(nil), // 104: openshell.v1.TcpForwardFrame - (*ExecSandboxInput)(nil), // 105: openshell.v1.ExecSandboxInput - (*ExecSandboxWindowResize)(nil), // 106: openshell.v1.ExecSandboxWindowResize - (*SshSession)(nil), // 107: openshell.v1.SshSession - (*WatchSandboxRequest)(nil), // 108: openshell.v1.WatchSandboxRequest - (*SandboxStreamEvent)(nil), // 109: openshell.v1.SandboxStreamEvent - (*SandboxLogLine)(nil), // 110: openshell.v1.SandboxLogLine - (*SandboxStreamWarning)(nil), // 111: openshell.v1.SandboxStreamWarning - (*CreateProviderRequest)(nil), // 112: openshell.v1.CreateProviderRequest - (*GetProviderRequest)(nil), // 113: openshell.v1.GetProviderRequest - (*ListProvidersRequest)(nil), // 114: openshell.v1.ListProvidersRequest - (*UpdateProviderRequest)(nil), // 115: openshell.v1.UpdateProviderRequest - (*DeleteProviderRequest)(nil), // 116: openshell.v1.DeleteProviderRequest - (*ProviderResponse)(nil), // 117: openshell.v1.ProviderResponse - (*ListProvidersResponse)(nil), // 118: openshell.v1.ListProvidersResponse - (*ListProviderProfilesRequest)(nil), // 119: openshell.v1.ListProviderProfilesRequest - (*GetProviderProfileRequest)(nil), // 120: openshell.v1.GetProviderProfileRequest - (*ProviderProfileImportItem)(nil), // 121: openshell.v1.ProviderProfileImportItem - (*ProviderProfileDiagnostic)(nil), // 122: openshell.v1.ProviderProfileDiagnostic - (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 123: openshell.v1.ProviderCredentialTokenGrantAudienceOverride - (*ProviderCredentialTokenGrantSubjectToken)(nil), // 124: openshell.v1.ProviderCredentialTokenGrantSubjectToken - (*ProviderCredentialTokenGrant)(nil), // 125: openshell.v1.ProviderCredentialTokenGrant - (*ProviderProfileCredential)(nil), // 126: openshell.v1.ProviderProfileCredential - (*ProviderCredentialRefreshMaterial)(nil), // 127: openshell.v1.ProviderCredentialRefreshMaterial - (*ProviderCredentialRefreshOutput)(nil), // 128: openshell.v1.ProviderCredentialRefreshOutput - (*ProviderCredentialRefresh)(nil), // 129: openshell.v1.ProviderCredentialRefresh - (*ProviderCredentialRefreshStatus)(nil), // 130: openshell.v1.ProviderCredentialRefreshStatus - (*ProviderProfileDiscovery)(nil), // 131: openshell.v1.ProviderProfileDiscovery - (*GetProviderRefreshStatusRequest)(nil), // 132: openshell.v1.GetProviderRefreshStatusRequest - (*GetProviderRefreshStatusResponse)(nil), // 133: openshell.v1.GetProviderRefreshStatusResponse - (*ConfigureProviderRefreshRequest)(nil), // 134: openshell.v1.ConfigureProviderRefreshRequest - (*ConfigureProviderRefreshResponse)(nil), // 135: openshell.v1.ConfigureProviderRefreshResponse - (*RotateProviderCredentialRequest)(nil), // 136: openshell.v1.RotateProviderCredentialRequest - (*RotateProviderCredentialResponse)(nil), // 137: openshell.v1.RotateProviderCredentialResponse - (*DeleteProviderRefreshRequest)(nil), // 138: openshell.v1.DeleteProviderRefreshRequest - (*DeleteProviderRefreshResponse)(nil), // 139: openshell.v1.DeleteProviderRefreshResponse - (*ProviderProfile)(nil), // 140: openshell.v1.ProviderProfile - (*ProviderProfileResponse)(nil), // 141: openshell.v1.ProviderProfileResponse - (*ListProviderProfilesResponse)(nil), // 142: openshell.v1.ListProviderProfilesResponse - (*ImportProviderProfilesRequest)(nil), // 143: openshell.v1.ImportProviderProfilesRequest - (*ImportProviderProfilesResponse)(nil), // 144: openshell.v1.ImportProviderProfilesResponse - (*UpdateProviderProfilesRequest)(nil), // 145: openshell.v1.UpdateProviderProfilesRequest - (*UpdateProviderProfilesResponse)(nil), // 146: openshell.v1.UpdateProviderProfilesResponse - (*LintProviderProfilesRequest)(nil), // 147: openshell.v1.LintProviderProfilesRequest - (*LintProviderProfilesResponse)(nil), // 148: openshell.v1.LintProviderProfilesResponse - (*DeleteProviderResponse)(nil), // 149: openshell.v1.DeleteProviderResponse - (*DeleteProviderProfileRequest)(nil), // 150: openshell.v1.DeleteProviderProfileRequest - (*DeleteProviderProfileResponse)(nil), // 151: openshell.v1.DeleteProviderProfileResponse - (*GetSandboxProviderEnvironmentRequest)(nil), // 152: openshell.v1.GetSandboxProviderEnvironmentRequest - (*StaticCredentialEndpointBinding)(nil), // 153: openshell.v1.StaticCredentialEndpointBinding - (*StaticCredentialBinding)(nil), // 154: openshell.v1.StaticCredentialBinding - (*GetSandboxProviderEnvironmentResponse)(nil), // 155: openshell.v1.GetSandboxProviderEnvironmentResponse - (*ProviderEnvironmentValue)(nil), // 156: openshell.v1.ProviderEnvironmentValue - (*ProviderEnvironmentSnapshot)(nil), // 157: openshell.v1.ProviderEnvironmentSnapshot - (*ExchangeProviderSubjectTokenRequest)(nil), // 158: openshell.v1.ExchangeProviderSubjectTokenRequest - (*ExchangeProviderSubjectTokenResponse)(nil), // 159: openshell.v1.ExchangeProviderSubjectTokenResponse - (*UpdateConfigRequest)(nil), // 160: openshell.v1.UpdateConfigRequest - (*PolicyMergeOperation)(nil), // 161: openshell.v1.PolicyMergeOperation - (*AddNetworkRule)(nil), // 162: openshell.v1.AddNetworkRule - (*RemoveNetworkEndpoint)(nil), // 163: openshell.v1.RemoveNetworkEndpoint - (*RemoveNetworkRule)(nil), // 164: openshell.v1.RemoveNetworkRule - (*L7RuleTarget)(nil), // 165: openshell.v1.L7RuleTarget - (*AddDenyRules)(nil), // 166: openshell.v1.AddDenyRules - (*AddAllowRules)(nil), // 167: openshell.v1.AddAllowRules - (*RemoveNetworkBinary)(nil), // 168: openshell.v1.RemoveNetworkBinary - (*UpdateConfigResponse)(nil), // 169: openshell.v1.UpdateConfigResponse - (*GetSandboxPolicyStatusRequest)(nil), // 170: openshell.v1.GetSandboxPolicyStatusRequest - (*GetSandboxPolicyStatusResponse)(nil), // 171: openshell.v1.GetSandboxPolicyStatusResponse - (*ListSandboxPoliciesRequest)(nil), // 172: openshell.v1.ListSandboxPoliciesRequest - (*ListSandboxPoliciesResponse)(nil), // 173: openshell.v1.ListSandboxPoliciesResponse - (*ReportPolicyStatusRequest)(nil), // 174: openshell.v1.ReportPolicyStatusRequest - (*ReportPolicyStatusResponse)(nil), // 175: openshell.v1.ReportPolicyStatusResponse - (*SandboxConfigurationAdmission)(nil), // 176: openshell.v1.SandboxConfigurationAdmission - (*ReportSandboxConfigurationRequest)(nil), // 177: openshell.v1.ReportSandboxConfigurationRequest - (*ReportSandboxConfigurationResponse)(nil), // 178: openshell.v1.ReportSandboxConfigurationResponse - (*SandboxPolicyRevision)(nil), // 179: openshell.v1.SandboxPolicyRevision - (*GetSandboxLogsRequest)(nil), // 180: openshell.v1.GetSandboxLogsRequest - (*PushSandboxLogsRequest)(nil), // 181: openshell.v1.PushSandboxLogsRequest - (*PushSandboxLogsResponse)(nil), // 182: openshell.v1.PushSandboxLogsResponse - (*GetSandboxLogsResponse)(nil), // 183: openshell.v1.GetSandboxLogsResponse - (*SupervisorMessage)(nil), // 184: openshell.v1.SupervisorMessage - (*SupervisorRuntimeReady)(nil), // 185: openshell.v1.SupervisorRuntimeReady - (*GatewayMessage)(nil), // 186: openshell.v1.GatewayMessage - (*SupervisorHello)(nil), // 187: openshell.v1.SupervisorHello - (*ImagePolicyDiscovery)(nil), // 188: openshell.v1.ImagePolicyDiscovery - (*StartupConfigCandidate)(nil), // 189: openshell.v1.StartupConfigCandidate - (*StartupConfigPrepared)(nil), // 190: openshell.v1.StartupConfigPrepared - (*SessionAccepted)(nil), // 191: openshell.v1.SessionAccepted - (*ConfigBootstrap)(nil), // 192: openshell.v1.ConfigBootstrap - (*ConfigUpdate)(nil), // 193: openshell.v1.ConfigUpdate - (*ConfigApplyFailure)(nil), // 194: openshell.v1.ConfigApplyFailure - (*ConfigComponentApplyResult)(nil), // 195: openshell.v1.ConfigComponentApplyResult - (*ConfigUpdateResult)(nil), // 196: openshell.v1.ConfigUpdateResult - (*ConfigBootstrapResult)(nil), // 197: openshell.v1.ConfigBootstrapResult - (*SessionRejected)(nil), // 198: openshell.v1.SessionRejected - (*SupervisorHeartbeat)(nil), // 199: openshell.v1.SupervisorHeartbeat - (*GatewayHeartbeat)(nil), // 200: openshell.v1.GatewayHeartbeat - (*ReportMainProcessExitRequest)(nil), // 201: openshell.v1.ReportMainProcessExitRequest - (*ReportMainProcessExitResponse)(nil), // 202: openshell.v1.ReportMainProcessExitResponse - (*FinalizeMainProcessExitRequest)(nil), // 203: openshell.v1.FinalizeMainProcessExitRequest - (*FinalizeMainProcessExitResponse)(nil), // 204: openshell.v1.FinalizeMainProcessExitResponse - (*RelayOpen)(nil), // 205: openshell.v1.RelayOpen - (*SshRelayTarget)(nil), // 206: openshell.v1.SshRelayTarget - (*TcpRelayTarget)(nil), // 207: openshell.v1.TcpRelayTarget - (*RelayInit)(nil), // 208: openshell.v1.RelayInit - (*RelayFrame)(nil), // 209: openshell.v1.RelayFrame - (*PeerRelayInit)(nil), // 210: openshell.v1.PeerRelayInit - (*PeerRelayFrame)(nil), // 211: openshell.v1.PeerRelayFrame - (*RelayOpenResult)(nil), // 212: openshell.v1.RelayOpenResult - (*RelayClose)(nil), // 213: openshell.v1.RelayClose - (*L7RequestSample)(nil), // 214: openshell.v1.L7RequestSample - (*DenialSummary)(nil), // 215: openshell.v1.DenialSummary - (*DenialGroupCount)(nil), // 216: openshell.v1.DenialGroupCount - (*NetworkActivitySummary)(nil), // 217: openshell.v1.NetworkActivitySummary - (*PolicyChunk)(nil), // 218: openshell.v1.PolicyChunk - (*DraftPolicyUpdate)(nil), // 219: openshell.v1.DraftPolicyUpdate - (*SubmitPolicyAnalysisRequest)(nil), // 220: openshell.v1.SubmitPolicyAnalysisRequest - (*SubmitPolicyAnalysisResponse)(nil), // 221: openshell.v1.SubmitPolicyAnalysisResponse - (*GetDraftPolicyRequest)(nil), // 222: openshell.v1.GetDraftPolicyRequest - (*GetDraftPolicyResponse)(nil), // 223: openshell.v1.GetDraftPolicyResponse - (*ApproveDraftChunkRequest)(nil), // 224: openshell.v1.ApproveDraftChunkRequest - (*ApproveDraftChunkResponse)(nil), // 225: openshell.v1.ApproveDraftChunkResponse - (*RejectDraftChunkRequest)(nil), // 226: openshell.v1.RejectDraftChunkRequest - (*RejectDraftChunkResponse)(nil), // 227: openshell.v1.RejectDraftChunkResponse - (*DraftChunkApproval)(nil), // 228: openshell.v1.DraftChunkApproval - (*ApproveAllDraftChunksRequest)(nil), // 229: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 230: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 231: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 232: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 233: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 234: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 235: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 236: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 237: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 238: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 239: openshell.v1.GetDraftHistoryResponse - (*CreateWorkspaceRequest)(nil), // 240: openshell.v1.CreateWorkspaceRequest - (*CreateWorkspaceResponse)(nil), // 241: openshell.v1.CreateWorkspaceResponse - (*GetWorkspaceRequest)(nil), // 242: openshell.v1.GetWorkspaceRequest - (*GetWorkspaceResponse)(nil), // 243: openshell.v1.GetWorkspaceResponse - (*ListWorkspacesRequest)(nil), // 244: openshell.v1.ListWorkspacesRequest - (*ListWorkspacesResponse)(nil), // 245: openshell.v1.ListWorkspacesResponse - (*DeleteWorkspaceRequest)(nil), // 246: openshell.v1.DeleteWorkspaceRequest - (*DeleteWorkspaceResponse)(nil), // 247: openshell.v1.DeleteWorkspaceResponse - (*WorkspaceMember)(nil), // 248: openshell.v1.WorkspaceMember - (*AddWorkspaceMemberRequest)(nil), // 249: openshell.v1.AddWorkspaceMemberRequest - (*AddWorkspaceMemberResponse)(nil), // 250: openshell.v1.AddWorkspaceMemberResponse - (*RemoveWorkspaceMemberRequest)(nil), // 251: openshell.v1.RemoveWorkspaceMemberRequest - (*RemoveWorkspaceMemberResponse)(nil), // 252: openshell.v1.RemoveWorkspaceMemberResponse - (*ListWorkspaceMembersRequest)(nil), // 253: openshell.v1.ListWorkspaceMembersRequest - (*ListWorkspaceMembersResponse)(nil), // 254: openshell.v1.ListWorkspaceMembersResponse - (*ExtensionServiceCredential)(nil), // 255: openshell.v1.ExtensionServiceCredential - (*EndpointObservation)(nil), // 256: openshell.v1.EndpointObservation - (*ReportEndpointStatusRequest)(nil), // 257: openshell.v1.ReportEndpointStatusRequest - (*ReportEndpointStatusResponse)(nil), // 258: openshell.v1.ReportEndpointStatusResponse - (*EndpointStatus)(nil), // 259: openshell.v1.EndpointStatus - (*SandboxProvisioning)(nil), // 260: openshell.v1.SandboxProvisioning - (*SandboxServiceExposure)(nil), // 261: openshell.v1.SandboxServiceExposure - nil, // 262: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 263: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 264: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 265: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 266: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry - nil, // 267: openshell.v1.PlatformEvent.MetadataEntry - nil, // 268: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 269: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 270: openshell.v1.SandboxResponse.ServiceUrlsEntry - nil, // 271: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 272: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 273: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry - nil, // 274: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 275: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 276: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 277: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry - nil, // 278: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 279: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 280: openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry - nil, // 281: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 282: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 283: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 284: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*timestamppb.Timestamp)(nil), // 285: google.protobuf.Timestamp - (*datamodelv1.ObjectMeta)(nil), // 286: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 287: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 288: google.protobuf.Struct - (*durationpb.Duration)(nil), // 289: google.protobuf.Duration - (*datamodelv1.WorkspaceSelector)(nil), // 290: openshell.datamodel.v1.WorkspaceSelector - (*datamodelv1.Provider)(nil), // 291: openshell.datamodel.v1.Provider - (sandboxv1.PolicySource)(0), // 292: openshell.sandbox.v1.PolicySource - (*sandboxv1.NetworkEndpoint)(nil), // 293: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 294: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 295: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 296: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 297: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 298: openshell.sandbox.v1.L7Rule - (*emptypb.Empty)(nil), // 299: google.protobuf.Empty - (*sandboxv1.SandboxConfigSnapshot)(nil), // 300: openshell.sandbox.v1.SandboxConfigSnapshot - (*datamodelv1.Workspace)(nil), // 301: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 302: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 303: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 304: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 305: openshell.sandbox.v1.GetGatewayConfigResponse + (ConfigUpdateConsistency)(0), // 12: openshell.v1.ConfigUpdateConsistency + (ConfigurationAdmissionState)(0), // 13: openshell.v1.ConfigurationAdmissionState + (PolicyStatus)(0), // 14: openshell.v1.PolicyStatus + (ServiceStatus)(0), // 15: openshell.v1.ServiceStatus + (WorkspaceRole)(0), // 16: openshell.v1.WorkspaceRole + (ProviderCredentialRefreshRecoveryAction)(0), // 17: openshell.v1.ProviderCredentialRefreshRecoveryAction + (DeletionOutcome)(0), // 18: openshell.v1.DeletionOutcome + (EndpointResult)(0), // 19: openshell.v1.EndpointResult + (*IssueSandboxTokenRequest)(nil), // 20: openshell.v1.IssueSandboxTokenRequest + (*IssueSandboxTokenResponse)(nil), // 21: openshell.v1.IssueSandboxTokenResponse + (*RefreshSandboxTokenRequest)(nil), // 22: openshell.v1.RefreshSandboxTokenRequest + (*RefreshSandboxTokenResponse)(nil), // 23: openshell.v1.RefreshSandboxTokenResponse + (*HealthRequest)(nil), // 24: openshell.v1.HealthRequest + (*HealthResponse)(nil), // 25: openshell.v1.HealthResponse + (*GetCurrentUserRequest)(nil), // 26: openshell.v1.GetCurrentUserRequest + (*GetCurrentUserResponse)(nil), // 27: openshell.v1.GetCurrentUserResponse + (*GetGatewayInfoRequest)(nil), // 28: openshell.v1.GetGatewayInfoRequest + (*GetGatewayInfoResponse)(nil), // 29: openshell.v1.GetGatewayInfoResponse + (*NegotiatedExtensionInfo)(nil), // 30: openshell.v1.NegotiatedExtensionInfo + (*ComputeDriverInfo)(nil), // 31: openshell.v1.ComputeDriverInfo + (*ComputeDriverCapabilities)(nil), // 32: openshell.v1.ComputeDriverCapabilities + (*ResourceCapabilities)(nil), // 33: openshell.v1.ResourceCapabilities + (*CpuResourceCapabilities)(nil), // 34: openshell.v1.CpuResourceCapabilities + (*MemoryResourceCapabilities)(nil), // 35: openshell.v1.MemoryResourceCapabilities + (*GpuResourceCapabilities)(nil), // 36: openshell.v1.GpuResourceCapabilities + (*Sandbox)(nil), // 37: openshell.v1.Sandbox + (*SandboxSpec)(nil), // 38: openshell.v1.SandboxSpec + (*ResourceRequirements)(nil), // 39: openshell.v1.ResourceRequirements + (*GpuResourceRequirements)(nil), // 40: openshell.v1.GpuResourceRequirements + (*SandboxTemplate)(nil), // 41: openshell.v1.SandboxTemplate + (*SandboxWorkloadTemplate)(nil), // 42: openshell.v1.SandboxWorkloadTemplate + (*SandboxWorkloadTemplateSpec)(nil), // 43: openshell.v1.SandboxWorkloadTemplateSpec + (*SandboxWorkloadConfig)(nil), // 44: openshell.v1.SandboxWorkloadConfig + (*SandboxResources)(nil), // 45: openshell.v1.SandboxResources + (*SandboxServiceLevel)(nil), // 46: openshell.v1.SandboxServiceLevel + (*SandboxStartup)(nil), // 47: openshell.v1.SandboxStartup + (*SandboxWorkloadTemplateProvenance)(nil), // 48: openshell.v1.SandboxWorkloadTemplateProvenance + (*SandboxStatus)(nil), // 49: openshell.v1.SandboxStatus + (*SandboxCondition)(nil), // 50: openshell.v1.SandboxCondition + (*PlatformEvent)(nil), // 51: openshell.v1.PlatformEvent + (*CreateSandboxRequest)(nil), // 52: openshell.v1.CreateSandboxRequest + (*CreateSandboxTemplateRequest)(nil), // 53: openshell.v1.CreateSandboxTemplateRequest + (*GetSandboxTemplateRequest)(nil), // 54: openshell.v1.GetSandboxTemplateRequest + (*ListSandboxTemplatesRequest)(nil), // 55: openshell.v1.ListSandboxTemplatesRequest + (*DeleteSandboxTemplateRequest)(nil), // 56: openshell.v1.DeleteSandboxTemplateRequest + (*SandboxTemplateResponse)(nil), // 57: openshell.v1.SandboxTemplateResponse + (*ListSandboxTemplatesResponse)(nil), // 58: openshell.v1.ListSandboxTemplatesResponse + (*DeleteSandboxTemplateResponse)(nil), // 59: openshell.v1.DeleteSandboxTemplateResponse + (*BeginRootfsTarStagingRequest)(nil), // 60: openshell.v1.BeginRootfsTarStagingRequest + (*BeginRootfsTarStagingResponse)(nil), // 61: openshell.v1.BeginRootfsTarStagingResponse + (*GetSandboxRequest)(nil), // 62: openshell.v1.GetSandboxRequest + (*ListSandboxesRequest)(nil), // 63: openshell.v1.ListSandboxesRequest + (*ListSandboxProvidersRequest)(nil), // 64: openshell.v1.ListSandboxProvidersRequest + (*AttachSandboxProviderRequest)(nil), // 65: openshell.v1.AttachSandboxProviderRequest + (*DetachSandboxProviderRequest)(nil), // 66: openshell.v1.DetachSandboxProviderRequest + (*DeleteSandboxRequest)(nil), // 67: openshell.v1.DeleteSandboxRequest + (*StopSandboxRequest)(nil), // 68: openshell.v1.StopSandboxRequest + (*StartSandboxRequest)(nil), // 69: openshell.v1.StartSandboxRequest + (*SandboxResponse)(nil), // 70: openshell.v1.SandboxResponse + (*ListSandboxesResponse)(nil), // 71: openshell.v1.ListSandboxesResponse + (*ListSandboxProvidersResponse)(nil), // 72: openshell.v1.ListSandboxProvidersResponse + (*AttachSandboxProviderResponse)(nil), // 73: openshell.v1.AttachSandboxProviderResponse + (*DetachSandboxProviderResponse)(nil), // 74: openshell.v1.DetachSandboxProviderResponse + (*ProviderDesiredIdentity)(nil), // 75: openshell.v1.ProviderDesiredIdentity + (*ConfigSnapshotRevision)(nil), // 76: openshell.v1.ConfigSnapshotRevision + (*SandboxConfigRevision)(nil), // 77: openshell.v1.SandboxConfigRevision + (*ConfigUpdateOperation)(nil), // 78: openshell.v1.ConfigUpdateOperation + (*ProviderMutationReceipt)(nil), // 79: openshell.v1.ProviderMutationReceipt + (*ProviderReadinessObservation)(nil), // 80: openshell.v1.ProviderReadinessObservation + (*ProviderReadinessStatus)(nil), // 81: openshell.v1.ProviderReadinessStatus + (*GetSandboxProviderStatusRequest)(nil), // 82: openshell.v1.GetSandboxProviderStatusRequest + (*GetSandboxProviderStatusResponse)(nil), // 83: openshell.v1.GetSandboxProviderStatusResponse + (*ReportProviderReadinessRequest)(nil), // 84: openshell.v1.ReportProviderReadinessRequest + (*ReportProviderReadinessResponse)(nil), // 85: openshell.v1.ReportProviderReadinessResponse + (*DeleteSandboxResponse)(nil), // 86: openshell.v1.DeleteSandboxResponse + (*CreateSshSessionRequest)(nil), // 87: openshell.v1.CreateSshSessionRequest + (*CreateSshSessionResponse)(nil), // 88: openshell.v1.CreateSshSessionResponse + (*ExposeServiceRequest)(nil), // 89: openshell.v1.ExposeServiceRequest + (*GetServiceRequest)(nil), // 90: openshell.v1.GetServiceRequest + (*ListServicesRequest)(nil), // 91: openshell.v1.ListServicesRequest + (*ListServicesResponse)(nil), // 92: openshell.v1.ListServicesResponse + (*DeleteServiceRequest)(nil), // 93: openshell.v1.DeleteServiceRequest + (*DeleteServiceResponse)(nil), // 94: openshell.v1.DeleteServiceResponse + (*ServiceEndpoint)(nil), // 95: openshell.v1.ServiceEndpoint + (*ServiceEndpointResponse)(nil), // 96: openshell.v1.ServiceEndpointResponse + (*RevokeSshSessionRequest)(nil), // 97: openshell.v1.RevokeSshSessionRequest + (*RevokeSshSessionResponse)(nil), // 98: openshell.v1.RevokeSshSessionResponse + (*ExecSandboxRequest)(nil), // 99: openshell.v1.ExecSandboxRequest + (*ExecSandboxStdout)(nil), // 100: openshell.v1.ExecSandboxStdout + (*ExecSandboxStderr)(nil), // 101: openshell.v1.ExecSandboxStderr + (*ExecSandboxExit)(nil), // 102: openshell.v1.ExecSandboxExit + (*ExecSandboxEvent)(nil), // 103: openshell.v1.ExecSandboxEvent + (*TcpForwardInit)(nil), // 104: openshell.v1.TcpForwardInit + (*TcpForwardFrame)(nil), // 105: openshell.v1.TcpForwardFrame + (*ExecSandboxInput)(nil), // 106: openshell.v1.ExecSandboxInput + (*ExecSandboxWindowResize)(nil), // 107: openshell.v1.ExecSandboxWindowResize + (*SshSession)(nil), // 108: openshell.v1.SshSession + (*WatchSandboxRequest)(nil), // 109: openshell.v1.WatchSandboxRequest + (*SandboxStreamEvent)(nil), // 110: openshell.v1.SandboxStreamEvent + (*SandboxLogLine)(nil), // 111: openshell.v1.SandboxLogLine + (*SandboxStreamWarning)(nil), // 112: openshell.v1.SandboxStreamWarning + (*CreateProviderRequest)(nil), // 113: openshell.v1.CreateProviderRequest + (*GetProviderRequest)(nil), // 114: openshell.v1.GetProviderRequest + (*ListProvidersRequest)(nil), // 115: openshell.v1.ListProvidersRequest + (*UpdateProviderRequest)(nil), // 116: openshell.v1.UpdateProviderRequest + (*DeleteProviderRequest)(nil), // 117: openshell.v1.DeleteProviderRequest + (*ProviderResponse)(nil), // 118: openshell.v1.ProviderResponse + (*ListProvidersResponse)(nil), // 119: openshell.v1.ListProvidersResponse + (*ListProviderProfilesRequest)(nil), // 120: openshell.v1.ListProviderProfilesRequest + (*GetProviderProfileRequest)(nil), // 121: openshell.v1.GetProviderProfileRequest + (*ProviderProfileImportItem)(nil), // 122: openshell.v1.ProviderProfileImportItem + (*ProviderProfileDiagnostic)(nil), // 123: openshell.v1.ProviderProfileDiagnostic + (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 124: openshell.v1.ProviderCredentialTokenGrantAudienceOverride + (*ProviderCredentialTokenGrantSubjectToken)(nil), // 125: openshell.v1.ProviderCredentialTokenGrantSubjectToken + (*ProviderCredentialTokenGrant)(nil), // 126: openshell.v1.ProviderCredentialTokenGrant + (*ProviderProfileCredential)(nil), // 127: openshell.v1.ProviderProfileCredential + (*ProviderCredentialRefreshMaterial)(nil), // 128: openshell.v1.ProviderCredentialRefreshMaterial + (*ProviderCredentialRefreshOutput)(nil), // 129: openshell.v1.ProviderCredentialRefreshOutput + (*ProviderCredentialRefresh)(nil), // 130: openshell.v1.ProviderCredentialRefresh + (*ProviderCredentialRefreshStatus)(nil), // 131: openshell.v1.ProviderCredentialRefreshStatus + (*ProviderProfileDiscovery)(nil), // 132: openshell.v1.ProviderProfileDiscovery + (*GetProviderRefreshStatusRequest)(nil), // 133: openshell.v1.GetProviderRefreshStatusRequest + (*GetProviderRefreshStatusResponse)(nil), // 134: openshell.v1.GetProviderRefreshStatusResponse + (*ConfigureProviderRefreshRequest)(nil), // 135: openshell.v1.ConfigureProviderRefreshRequest + (*ConfigureProviderRefreshResponse)(nil), // 136: openshell.v1.ConfigureProviderRefreshResponse + (*RotateProviderCredentialRequest)(nil), // 137: openshell.v1.RotateProviderCredentialRequest + (*RotateProviderCredentialResponse)(nil), // 138: openshell.v1.RotateProviderCredentialResponse + (*DeleteProviderRefreshRequest)(nil), // 139: openshell.v1.DeleteProviderRefreshRequest + (*DeleteProviderRefreshResponse)(nil), // 140: openshell.v1.DeleteProviderRefreshResponse + (*ProviderProfile)(nil), // 141: openshell.v1.ProviderProfile + (*ProviderProfileResponse)(nil), // 142: openshell.v1.ProviderProfileResponse + (*ListProviderProfilesResponse)(nil), // 143: openshell.v1.ListProviderProfilesResponse + (*ImportProviderProfilesRequest)(nil), // 144: openshell.v1.ImportProviderProfilesRequest + (*ImportProviderProfilesResponse)(nil), // 145: openshell.v1.ImportProviderProfilesResponse + (*UpdateProviderProfilesRequest)(nil), // 146: openshell.v1.UpdateProviderProfilesRequest + (*UpdateProviderProfilesResponse)(nil), // 147: openshell.v1.UpdateProviderProfilesResponse + (*LintProviderProfilesRequest)(nil), // 148: openshell.v1.LintProviderProfilesRequest + (*LintProviderProfilesResponse)(nil), // 149: openshell.v1.LintProviderProfilesResponse + (*DeleteProviderResponse)(nil), // 150: openshell.v1.DeleteProviderResponse + (*DeleteProviderProfileRequest)(nil), // 151: openshell.v1.DeleteProviderProfileRequest + (*DeleteProviderProfileResponse)(nil), // 152: openshell.v1.DeleteProviderProfileResponse + (*GetSandboxProviderEnvironmentRequest)(nil), // 153: openshell.v1.GetSandboxProviderEnvironmentRequest + (*StaticCredentialEndpointBinding)(nil), // 154: openshell.v1.StaticCredentialEndpointBinding + (*StaticCredentialBinding)(nil), // 155: openshell.v1.StaticCredentialBinding + (*GetSandboxProviderEnvironmentResponse)(nil), // 156: openshell.v1.GetSandboxProviderEnvironmentResponse + (*ProviderEnvironmentValue)(nil), // 157: openshell.v1.ProviderEnvironmentValue + (*ProviderEnvironmentSnapshot)(nil), // 158: openshell.v1.ProviderEnvironmentSnapshot + (*ExchangeProviderSubjectTokenRequest)(nil), // 159: openshell.v1.ExchangeProviderSubjectTokenRequest + (*ExchangeProviderSubjectTokenResponse)(nil), // 160: openshell.v1.ExchangeProviderSubjectTokenResponse + (*UpdateConfigRequest)(nil), // 161: openshell.v1.UpdateConfigRequest + (*GetConfigUpdateOperationRequest)(nil), // 162: openshell.v1.GetConfigUpdateOperationRequest + (*GetConfigUpdateOperationResponse)(nil), // 163: openshell.v1.GetConfigUpdateOperationResponse + (*PolicyMergeOperation)(nil), // 164: openshell.v1.PolicyMergeOperation + (*AddNetworkRule)(nil), // 165: openshell.v1.AddNetworkRule + (*RemoveNetworkEndpoint)(nil), // 166: openshell.v1.RemoveNetworkEndpoint + (*RemoveNetworkRule)(nil), // 167: openshell.v1.RemoveNetworkRule + (*L7RuleTarget)(nil), // 168: openshell.v1.L7RuleTarget + (*AddDenyRules)(nil), // 169: openshell.v1.AddDenyRules + (*AddAllowRules)(nil), // 170: openshell.v1.AddAllowRules + (*RemoveNetworkBinary)(nil), // 171: openshell.v1.RemoveNetworkBinary + (*UpdateConfigResponse)(nil), // 172: openshell.v1.UpdateConfigResponse + (*GetSandboxPolicyStatusRequest)(nil), // 173: openshell.v1.GetSandboxPolicyStatusRequest + (*GetSandboxPolicyStatusResponse)(nil), // 174: openshell.v1.GetSandboxPolicyStatusResponse + (*ListSandboxPoliciesRequest)(nil), // 175: openshell.v1.ListSandboxPoliciesRequest + (*ListSandboxPoliciesResponse)(nil), // 176: openshell.v1.ListSandboxPoliciesResponse + (*ReportPolicyStatusRequest)(nil), // 177: openshell.v1.ReportPolicyStatusRequest + (*ReportPolicyStatusResponse)(nil), // 178: openshell.v1.ReportPolicyStatusResponse + (*SandboxConfigurationAdmission)(nil), // 179: openshell.v1.SandboxConfigurationAdmission + (*ReportSandboxConfigurationRequest)(nil), // 180: openshell.v1.ReportSandboxConfigurationRequest + (*ReportSandboxConfigurationResponse)(nil), // 181: openshell.v1.ReportSandboxConfigurationResponse + (*SandboxPolicyRevision)(nil), // 182: openshell.v1.SandboxPolicyRevision + (*GetSandboxLogsRequest)(nil), // 183: openshell.v1.GetSandboxLogsRequest + (*PushSandboxLogsRequest)(nil), // 184: openshell.v1.PushSandboxLogsRequest + (*PushSandboxLogsResponse)(nil), // 185: openshell.v1.PushSandboxLogsResponse + (*GetSandboxLogsResponse)(nil), // 186: openshell.v1.GetSandboxLogsResponse + (*SupervisorMessage)(nil), // 187: openshell.v1.SupervisorMessage + (*SupervisorRuntimeReady)(nil), // 188: openshell.v1.SupervisorRuntimeReady + (*GatewayMessage)(nil), // 189: openshell.v1.GatewayMessage + (*SupervisorHello)(nil), // 190: openshell.v1.SupervisorHello + (*ImagePolicyDiscovery)(nil), // 191: openshell.v1.ImagePolicyDiscovery + (*StartupConfigCandidate)(nil), // 192: openshell.v1.StartupConfigCandidate + (*StartupConfigPrepared)(nil), // 193: openshell.v1.StartupConfigPrepared + (*SessionAccepted)(nil), // 194: openshell.v1.SessionAccepted + (*ConfigBootstrap)(nil), // 195: openshell.v1.ConfigBootstrap + (*ConfigUpdate)(nil), // 196: openshell.v1.ConfigUpdate + (*ConfigApplyFailure)(nil), // 197: openshell.v1.ConfigApplyFailure + (*ConfigComponentApplyResult)(nil), // 198: openshell.v1.ConfigComponentApplyResult + (*ConfigUpdateResult)(nil), // 199: openshell.v1.ConfigUpdateResult + (*ConfigBootstrapResult)(nil), // 200: openshell.v1.ConfigBootstrapResult + (*SessionRejected)(nil), // 201: openshell.v1.SessionRejected + (*SupervisorHeartbeat)(nil), // 202: openshell.v1.SupervisorHeartbeat + (*GatewayHeartbeat)(nil), // 203: openshell.v1.GatewayHeartbeat + (*ReportMainProcessExitRequest)(nil), // 204: openshell.v1.ReportMainProcessExitRequest + (*ReportMainProcessExitResponse)(nil), // 205: openshell.v1.ReportMainProcessExitResponse + (*FinalizeMainProcessExitRequest)(nil), // 206: openshell.v1.FinalizeMainProcessExitRequest + (*FinalizeMainProcessExitResponse)(nil), // 207: openshell.v1.FinalizeMainProcessExitResponse + (*RelayOpen)(nil), // 208: openshell.v1.RelayOpen + (*SshRelayTarget)(nil), // 209: openshell.v1.SshRelayTarget + (*TcpRelayTarget)(nil), // 210: openshell.v1.TcpRelayTarget + (*RelayInit)(nil), // 211: openshell.v1.RelayInit + (*RelayFrame)(nil), // 212: openshell.v1.RelayFrame + (*PeerRelayInit)(nil), // 213: openshell.v1.PeerRelayInit + (*PeerRelayFrame)(nil), // 214: openshell.v1.PeerRelayFrame + (*RelayOpenResult)(nil), // 215: openshell.v1.RelayOpenResult + (*RelayClose)(nil), // 216: openshell.v1.RelayClose + (*L7RequestSample)(nil), // 217: openshell.v1.L7RequestSample + (*DenialSummary)(nil), // 218: openshell.v1.DenialSummary + (*DenialGroupCount)(nil), // 219: openshell.v1.DenialGroupCount + (*NetworkActivitySummary)(nil), // 220: openshell.v1.NetworkActivitySummary + (*PolicyChunk)(nil), // 221: openshell.v1.PolicyChunk + (*DraftPolicyUpdate)(nil), // 222: openshell.v1.DraftPolicyUpdate + (*SubmitPolicyAnalysisRequest)(nil), // 223: openshell.v1.SubmitPolicyAnalysisRequest + (*SubmitPolicyAnalysisResponse)(nil), // 224: openshell.v1.SubmitPolicyAnalysisResponse + (*GetDraftPolicyRequest)(nil), // 225: openshell.v1.GetDraftPolicyRequest + (*GetDraftPolicyResponse)(nil), // 226: openshell.v1.GetDraftPolicyResponse + (*ApproveDraftChunkRequest)(nil), // 227: openshell.v1.ApproveDraftChunkRequest + (*ApproveDraftChunkResponse)(nil), // 228: openshell.v1.ApproveDraftChunkResponse + (*RejectDraftChunkRequest)(nil), // 229: openshell.v1.RejectDraftChunkRequest + (*RejectDraftChunkResponse)(nil), // 230: openshell.v1.RejectDraftChunkResponse + (*DraftChunkApproval)(nil), // 231: openshell.v1.DraftChunkApproval + (*ApproveAllDraftChunksRequest)(nil), // 232: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 233: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 234: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 235: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 236: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 237: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 238: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 239: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 240: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 241: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 242: openshell.v1.GetDraftHistoryResponse + (*CreateWorkspaceRequest)(nil), // 243: openshell.v1.CreateWorkspaceRequest + (*CreateWorkspaceResponse)(nil), // 244: openshell.v1.CreateWorkspaceResponse + (*GetWorkspaceRequest)(nil), // 245: openshell.v1.GetWorkspaceRequest + (*GetWorkspaceResponse)(nil), // 246: openshell.v1.GetWorkspaceResponse + (*ListWorkspacesRequest)(nil), // 247: openshell.v1.ListWorkspacesRequest + (*ListWorkspacesResponse)(nil), // 248: openshell.v1.ListWorkspacesResponse + (*DeleteWorkspaceRequest)(nil), // 249: openshell.v1.DeleteWorkspaceRequest + (*DeleteWorkspaceResponse)(nil), // 250: openshell.v1.DeleteWorkspaceResponse + (*WorkspaceMember)(nil), // 251: openshell.v1.WorkspaceMember + (*AddWorkspaceMemberRequest)(nil), // 252: openshell.v1.AddWorkspaceMemberRequest + (*AddWorkspaceMemberResponse)(nil), // 253: openshell.v1.AddWorkspaceMemberResponse + (*RemoveWorkspaceMemberRequest)(nil), // 254: openshell.v1.RemoveWorkspaceMemberRequest + (*RemoveWorkspaceMemberResponse)(nil), // 255: openshell.v1.RemoveWorkspaceMemberResponse + (*ListWorkspaceMembersRequest)(nil), // 256: openshell.v1.ListWorkspaceMembersRequest + (*ListWorkspaceMembersResponse)(nil), // 257: openshell.v1.ListWorkspaceMembersResponse + (*ExtensionServiceCredential)(nil), // 258: openshell.v1.ExtensionServiceCredential + (*EndpointObservation)(nil), // 259: openshell.v1.EndpointObservation + (*ReportEndpointStatusRequest)(nil), // 260: openshell.v1.ReportEndpointStatusRequest + (*ReportEndpointStatusResponse)(nil), // 261: openshell.v1.ReportEndpointStatusResponse + (*EndpointStatus)(nil), // 262: openshell.v1.EndpointStatus + (*SandboxProvisioning)(nil), // 263: openshell.v1.SandboxProvisioning + (*SandboxServiceExposure)(nil), // 264: openshell.v1.SandboxServiceExposure + nil, // 265: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 266: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 267: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 268: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 269: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + nil, // 270: openshell.v1.PlatformEvent.MetadataEntry + nil, // 271: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 272: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 273: openshell.v1.SandboxResponse.ServiceUrlsEntry + nil, // 274: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 275: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 276: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + nil, // 277: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 278: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 279: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 280: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + nil, // 281: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 282: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 283: openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry + nil, // 284: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 285: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 286: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 287: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*timestamppb.Timestamp)(nil), // 288: google.protobuf.Timestamp + (*datamodelv1.ObjectMeta)(nil), // 289: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 290: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 291: google.protobuf.Struct + (*durationpb.Duration)(nil), // 292: google.protobuf.Duration + (*datamodelv1.WorkspaceSelector)(nil), // 293: openshell.datamodel.v1.WorkspaceSelector + (*datamodelv1.Provider)(nil), // 294: openshell.datamodel.v1.Provider + (sandboxv1.PolicySource)(0), // 295: openshell.sandbox.v1.PolicySource + (*sandboxv1.NetworkEndpoint)(nil), // 296: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 297: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 298: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 299: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 300: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 301: openshell.sandbox.v1.L7Rule + (*emptypb.Empty)(nil), // 302: google.protobuf.Empty + (*sandboxv1.SandboxConfigSnapshot)(nil), // 303: openshell.sandbox.v1.SandboxConfigSnapshot + (*datamodelv1.Workspace)(nil), // 304: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 305: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 306: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 307: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 308: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 285, // 0: openshell.v1.IssueSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp - 285, // 1: openshell.v1.RefreshSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp - 255, // 2: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential - 285, // 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 - 30, // 6: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo - 29, // 7: openshell.v1.GetGatewayInfoResponse.extensions:type_name -> openshell.v1.NegotiatedExtensionInfo + 288, // 0: openshell.v1.IssueSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp + 288, // 1: openshell.v1.RefreshSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp + 258, // 2: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential + 288, // 3: openshell.v1.RefreshSandboxTokenResponse.sandbox_expiration_time:type_name -> google.protobuf.Timestamp + 15, // 4: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus + 15, // 5: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus + 31, // 6: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo + 30, // 7: openshell.v1.GetGatewayInfoResponse.extensions:type_name -> openshell.v1.NegotiatedExtensionInfo 0, // 8: openshell.v1.NegotiatedExtensionInfo.kind:type_name -> openshell.v1.ExtensionKind - 31, // 9: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities - 32, // 10: openshell.v1.ComputeDriverCapabilities.resource_capabilities:type_name -> openshell.v1.ResourceCapabilities - 33, // 11: openshell.v1.ResourceCapabilities.cpu:type_name -> openshell.v1.CpuResourceCapabilities - 34, // 12: openshell.v1.ResourceCapabilities.memory:type_name -> openshell.v1.MemoryResourceCapabilities - 35, // 13: openshell.v1.ResourceCapabilities.gpu:type_name -> openshell.v1.GpuResourceCapabilities - 286, // 14: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 37, // 15: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec - 48, // 16: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus - 47, // 17: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance - 262, // 18: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry - 40, // 19: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 287, // 20: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 38, // 21: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements - 39, // 22: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 263, // 23: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 264, // 24: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 265, // 25: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 288, // 26: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 288, // 27: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 286, // 28: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 42, // 29: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec - 43, // 30: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig - 288, // 31: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct - 45, // 32: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel - 266, // 33: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry - 44, // 34: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources - 39, // 35: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements - 46, // 36: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup - 289, // 37: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration - 49, // 38: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition + 32, // 9: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities + 33, // 10: openshell.v1.ComputeDriverCapabilities.resource_capabilities:type_name -> openshell.v1.ResourceCapabilities + 34, // 11: openshell.v1.ResourceCapabilities.cpu:type_name -> openshell.v1.CpuResourceCapabilities + 35, // 12: openshell.v1.ResourceCapabilities.memory:type_name -> openshell.v1.MemoryResourceCapabilities + 36, // 13: openshell.v1.ResourceCapabilities.gpu:type_name -> openshell.v1.GpuResourceCapabilities + 289, // 14: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 38, // 15: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec + 49, // 16: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus + 48, // 17: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance + 265, // 18: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 41, // 19: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate + 290, // 20: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 39, // 21: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements + 40, // 22: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements + 266, // 23: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 267, // 24: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 268, // 25: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 291, // 26: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 291, // 27: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 289, // 28: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 43, // 29: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec + 44, // 30: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig + 291, // 31: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct + 46, // 32: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel + 269, // 33: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + 45, // 34: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources + 40, // 35: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements + 47, // 36: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup + 292, // 37: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration + 50, // 38: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition 1, // 39: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 259, // 40: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus - 176, // 41: openshell.v1.SandboxStatus.configuration_admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 260, // 42: openshell.v1.SandboxStatus.provisioning:type_name -> openshell.v1.SandboxProvisioning - 285, // 43: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp - 285, // 44: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp - 267, // 45: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 290, // 46: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 37, // 47: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 268, // 48: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 269, // 49: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 261, // 50: openshell.v1.CreateSandboxRequest.service_exposures:type_name -> openshell.v1.SandboxServiceExposure - 290, // 51: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 41, // 52: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 290, // 53: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 54: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 55: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 41, // 56: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 41, // 57: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate - 17, // 58: openshell.v1.DeleteSandboxTemplateResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 290, // 59: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 285, // 60: openshell.v1.BeginRootfsTarStagingResponse.expiration_time:type_name -> google.protobuf.Timestamp - 290, // 61: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 62: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 63: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 64: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 65: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 66: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 67: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 68: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 36, // 69: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 270, // 70: openshell.v1.SandboxResponse.service_urls:type_name -> openshell.v1.SandboxResponse.ServiceUrlsEntry - 36, // 71: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 291, // 72: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 36, // 73: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 78, // 74: openshell.v1.AttachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt - 36, // 75: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 78, // 76: openshell.v1.DetachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt - 76, // 77: openshell.v1.ConfigSnapshotRevision.sandbox_config:type_name -> openshell.v1.SandboxConfigRevision - 74, // 78: openshell.v1.ConfigSnapshotRevision.provider_target:type_name -> openshell.v1.ProviderDesiredIdentity - 292, // 79: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource + 262, // 40: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus + 179, // 41: openshell.v1.SandboxStatus.configuration_admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 263, // 42: openshell.v1.SandboxStatus.provisioning:type_name -> openshell.v1.SandboxProvisioning + 288, // 43: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp + 288, // 44: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp + 270, // 45: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 293, // 46: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 38, // 47: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec + 271, // 48: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 272, // 49: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 264, // 50: openshell.v1.CreateSandboxRequest.service_exposures:type_name -> openshell.v1.SandboxServiceExposure + 293, // 51: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 42, // 52: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 293, // 53: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 54: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 55: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 42, // 56: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 42, // 57: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate + 18, // 58: openshell.v1.DeleteSandboxTemplateResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 293, // 59: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 288, // 60: openshell.v1.BeginRootfsTarStagingResponse.expiration_time:type_name -> google.protobuf.Timestamp + 293, // 61: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 62: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 63: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 64: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 65: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 66: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 67: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 68: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 37, // 69: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 273, // 70: openshell.v1.SandboxResponse.service_urls:type_name -> openshell.v1.SandboxResponse.ServiceUrlsEntry + 37, // 71: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 294, // 72: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 37, // 73: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 79, // 74: openshell.v1.AttachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 37, // 75: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 79, // 76: openshell.v1.DetachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 77, // 77: openshell.v1.ConfigSnapshotRevision.sandbox_config:type_name -> openshell.v1.SandboxConfigRevision + 75, // 78: openshell.v1.ConfigSnapshotRevision.provider_target:type_name -> openshell.v1.ProviderDesiredIdentity + 295, // 79: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource 5, // 80: openshell.v1.ConfigUpdateOperation.component:type_name -> openshell.v1.ConfigComponent - 75, // 81: openshell.v1.ConfigUpdateOperation.target_revision:type_name -> openshell.v1.ConfigSnapshotRevision + 76, // 81: openshell.v1.ConfigUpdateOperation.target_revision:type_name -> openshell.v1.ConfigSnapshotRevision 7, // 82: openshell.v1.ConfigUpdateOperation.state:type_name -> openshell.v1.ConfigUpdateOperationState 6, // 83: openshell.v1.ConfigUpdateOperation.outcome:type_name -> openshell.v1.ConfigApplyOutcome - 285, // 84: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp - 285, // 85: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp - 285, // 86: openshell.v1.ConfigUpdateOperation.completed_time:type_name -> google.protobuf.Timestamp + 288, // 84: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp + 288, // 85: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp + 288, // 86: openshell.v1.ConfigUpdateOperation.completed_time:type_name -> google.protobuf.Timestamp 2, // 87: openshell.v1.ProviderMutationReceipt.kind:type_name -> openshell.v1.ProviderMutationKind - 74, // 88: openshell.v1.ProviderMutationReceipt.desired:type_name -> openshell.v1.ProviderDesiredIdentity - 285, // 89: openshell.v1.ProviderMutationReceipt.persisted_time:type_name -> google.protobuf.Timestamp + 75, // 88: openshell.v1.ProviderMutationReceipt.desired:type_name -> openshell.v1.ProviderDesiredIdentity + 288, // 89: openshell.v1.ProviderMutationReceipt.persisted_time:type_name -> google.protobuf.Timestamp 4, // 90: openshell.v1.ProviderReadinessObservation.reason:type_name -> openshell.v1.ProviderReadinessReason - 78, // 91: openshell.v1.ProviderReadinessStatus.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 79, // 91: openshell.v1.ProviderReadinessStatus.receipt:type_name -> openshell.v1.ProviderMutationReceipt 3, // 92: openshell.v1.ProviderReadinessStatus.state:type_name -> openshell.v1.ProviderReadinessState 4, // 93: openshell.v1.ProviderReadinessStatus.reason:type_name -> openshell.v1.ProviderReadinessReason - 79, // 94: openshell.v1.ProviderReadinessStatus.observed:type_name -> openshell.v1.ProviderReadinessObservation - 285, // 95: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp - 285, // 96: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp - 77, // 97: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation - 290, // 98: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 80, // 99: openshell.v1.GetSandboxProviderStatusResponse.status:type_name -> openshell.v1.ProviderReadinessStatus - 79, // 100: openshell.v1.ReportProviderReadinessRequest.observation:type_name -> openshell.v1.ProviderReadinessObservation - 289, // 101: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration - 289, // 102: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration - 17, // 103: openshell.v1.DeleteSandboxResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 290, // 104: openshell.v1.CreateSshSessionRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 285, // 105: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp - 290, // 106: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 107: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 108: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 95, // 109: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 290, // 110: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 17, // 111: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 286, // 112: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 94, // 113: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 17, // 114: openshell.v1.RevokeSshSessionResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 290, // 115: openshell.v1.ExecSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 271, // 116: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 289, // 117: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration - 99, // 118: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 100, // 119: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 101, // 120: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 206, // 121: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 207, // 122: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 103, // 123: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 98, // 124: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 106, // 125: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 286, // 126: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 285, // 127: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp - 290, // 128: openshell.v1.WatchSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 285, // 129: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp - 36, // 130: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 110, // 131: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 50, // 132: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 111, // 133: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 219, // 134: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 285, // 135: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp - 272, // 136: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 290, // 137: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 291, // 138: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 290, // 139: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 140: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 141: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 291, // 142: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 273, // 143: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry - 290, // 144: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 291, // 145: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 78, // 146: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt - 291, // 147: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 290, // 148: openshell.v1.ListProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 149: openshell.v1.GetProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 140, // 150: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 289, // 151: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration - 123, // 152: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 80, // 94: openshell.v1.ProviderReadinessStatus.observed:type_name -> openshell.v1.ProviderReadinessObservation + 288, // 95: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp + 288, // 96: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp + 78, // 97: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation + 293, // 98: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 81, // 99: openshell.v1.GetSandboxProviderStatusResponse.status:type_name -> openshell.v1.ProviderReadinessStatus + 80, // 100: openshell.v1.ReportProviderReadinessRequest.observation:type_name -> openshell.v1.ProviderReadinessObservation + 292, // 101: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration + 292, // 102: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration + 18, // 103: openshell.v1.DeleteSandboxResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 293, // 104: openshell.v1.CreateSshSessionRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 288, // 105: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp + 293, // 106: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 107: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 108: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 96, // 109: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 293, // 110: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 18, // 111: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 289, // 112: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 95, // 113: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 18, // 114: openshell.v1.RevokeSshSessionResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 293, // 115: openshell.v1.ExecSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 274, // 116: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 292, // 117: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration + 100, // 118: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 101, // 119: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 102, // 120: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 209, // 121: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 210, // 122: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 104, // 123: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 99, // 124: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 107, // 125: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 289, // 126: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 288, // 127: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp + 293, // 128: openshell.v1.WatchSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 288, // 129: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp + 37, // 130: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 111, // 131: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 51, // 132: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 112, // 133: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 222, // 134: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 288, // 135: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp + 275, // 136: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 293, // 137: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 294, // 138: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 293, // 139: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 140: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 141: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 294, // 142: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 276, // 143: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + 293, // 144: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 294, // 145: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 79, // 146: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt + 294, // 147: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 293, // 148: openshell.v1.ListProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 149: openshell.v1.GetProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 141, // 150: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 292, // 151: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration + 124, // 152: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride 8, // 153: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType - 124, // 154: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken - 129, // 155: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 125, // 156: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 125, // 154: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 130, // 155: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 126, // 156: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant 9, // 157: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 289, // 158: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration - 289, // 159: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration - 127, // 160: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 128, // 161: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 292, // 158: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration + 292, // 159: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration + 128, // 160: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 129, // 161: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput 9, // 162: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 285, // 163: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp - 285, // 164: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp - 285, // 165: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp - 16, // 166: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 285, // 167: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp - 290, // 168: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 130, // 169: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 290, // 170: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 288, // 163: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp + 288, // 164: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp + 288, // 165: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp + 17, // 166: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 288, // 167: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp + 293, // 168: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 131, // 169: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 293, // 170: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 9, // 171: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 274, // 172: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 285, // 173: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp - 130, // 174: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 290, // 175: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 130, // 176: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 290, // 177: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 17, // 178: openshell.v1.DeleteProviderRefreshResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 277, // 172: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 288, // 173: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp + 131, // 174: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 293, // 175: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 131, // 176: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 293, // 177: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 18, // 178: openshell.v1.DeleteProviderRefreshResponse.outcome:type_name -> openshell.v1.DeletionOutcome 10, // 179: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 126, // 180: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 293, // 181: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 294, // 182: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 131, // 183: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 275, // 184: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 140, // 185: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 140, // 186: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 290, // 187: openshell.v1.ImportProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 121, // 188: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 122, // 189: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 140, // 190: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 290, // 191: openshell.v1.UpdateProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 121, // 192: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 122, // 193: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 140, // 194: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 290, // 195: openshell.v1.LintProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 121, // 196: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 122, // 197: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 17, // 198: openshell.v1.DeleteProviderResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 290, // 199: openshell.v1.DeleteProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 17, // 200: openshell.v1.DeleteProviderProfileResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 153, // 201: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 276, // 202: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 277, // 203: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry - 278, // 204: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 279, // 205: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 127, // 180: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 296, // 181: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 297, // 182: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 132, // 183: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 278, // 184: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 141, // 185: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 141, // 186: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 293, // 187: openshell.v1.ImportProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 122, // 188: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 123, // 189: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 141, // 190: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 293, // 191: openshell.v1.UpdateProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 122, // 192: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 123, // 193: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 141, // 194: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 293, // 195: openshell.v1.LintProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 122, // 196: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 123, // 197: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 18, // 198: openshell.v1.DeleteProviderResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 293, // 199: openshell.v1.DeleteProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 18, // 200: openshell.v1.DeleteProviderProfileResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 154, // 201: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 279, // 202: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 280, // 203: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + 281, // 204: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 282, // 205: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry 4, // 206: openshell.v1.GetSandboxProviderEnvironmentResponse.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason 11, // 207: openshell.v1.ProviderEnvironmentValue.classification:type_name -> openshell.v1.ProviderEnvironmentValueClassification - 154, // 208: openshell.v1.ProviderEnvironmentValue.static_credential_binding:type_name -> openshell.v1.StaticCredentialBinding - 285, // 209: openshell.v1.ProviderEnvironmentValue.expiration_time:type_name -> google.protobuf.Timestamp - 156, // 210: openshell.v1.ProviderEnvironmentSnapshot.values:type_name -> openshell.v1.ProviderEnvironmentValue - 280, // 211: openshell.v1.ProviderEnvironmentSnapshot.dynamic_credentials:type_name -> openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry + 155, // 208: openshell.v1.ProviderEnvironmentValue.static_credential_binding:type_name -> openshell.v1.StaticCredentialBinding + 288, // 209: openshell.v1.ProviderEnvironmentValue.expiration_time:type_name -> google.protobuf.Timestamp + 157, // 210: openshell.v1.ProviderEnvironmentSnapshot.values:type_name -> openshell.v1.ProviderEnvironmentValue + 283, // 211: openshell.v1.ProviderEnvironmentSnapshot.dynamic_credentials:type_name -> openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry 4, // 212: openshell.v1.ProviderEnvironmentSnapshot.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason - 289, // 213: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration - 290, // 214: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 287, // 215: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 295, // 216: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 161, // 217: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 281, // 218: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 162, // 219: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 163, // 220: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 164, // 221: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 166, // 222: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 167, // 223: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 168, // 224: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 296, // 225: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 294, // 226: openshell.v1.L7RuleTarget.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 297, // 227: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 165, // 228: openshell.v1.AddDenyRules.target:type_name -> openshell.v1.L7RuleTarget - 298, // 229: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 165, // 230: openshell.v1.AddAllowRules.target:type_name -> openshell.v1.L7RuleTarget - 282, // 231: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 290, // 232: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 179, // 233: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 290, // 234: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 179, // 235: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 13, // 236: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 12, // 237: openshell.v1.SandboxConfigurationAdmission.state:type_name -> openshell.v1.ConfigurationAdmissionState - 176, // 238: openshell.v1.ReportSandboxConfigurationRequest.admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 13, // 239: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 285, // 240: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp - 285, // 241: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp - 287, // 242: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 283, // 243: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 290, // 244: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 285, // 245: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp - 110, // 246: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 110, // 247: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 187, // 248: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 199, // 249: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 212, // 250: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 213, // 251: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 196, // 252: openshell.v1.SupervisorMessage.config_update_result:type_name -> openshell.v1.ConfigUpdateResult - 197, // 253: openshell.v1.SupervisorMessage.config_bootstrap_result:type_name -> openshell.v1.ConfigBootstrapResult - 190, // 254: openshell.v1.SupervisorMessage.startup_config_prepared:type_name -> openshell.v1.StartupConfigPrepared - 185, // 255: openshell.v1.SupervisorMessage.runtime_ready:type_name -> openshell.v1.SupervisorRuntimeReady - 191, // 256: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 198, // 257: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 200, // 258: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 205, // 259: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 213, // 260: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 193, // 261: openshell.v1.GatewayMessage.config_update:type_name -> openshell.v1.ConfigUpdate - 189, // 262: openshell.v1.GatewayMessage.startup_config_candidate:type_name -> openshell.v1.StartupConfigCandidate - 176, // 263: openshell.v1.GatewayMessage.configuration_admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 287, // 264: openshell.v1.SupervisorHello.image_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 188, // 265: openshell.v1.SupervisorHello.image_policy_discovery:type_name -> openshell.v1.ImagePolicyDiscovery - 299, // 266: openshell.v1.ImagePolicyDiscovery.missing:type_name -> google.protobuf.Empty - 299, // 267: openshell.v1.ImagePolicyDiscovery.invalid:type_name -> google.protobuf.Empty - 287, // 268: openshell.v1.ImagePolicyDiscovery.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 287, // 269: openshell.v1.StartupConfigCandidate.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 292, // 270: openshell.v1.StartupConfigCandidate.policy_source:type_name -> openshell.sandbox.v1.PolicySource - 299, // 271: openshell.v1.StartupConfigPrepared.unchanged:type_name -> google.protobuf.Empty - 287, // 272: openshell.v1.StartupConfigPrepared.prepared_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 194, // 273: openshell.v1.StartupConfigPrepared.failure:type_name -> openshell.v1.ConfigApplyFailure - 192, // 274: openshell.v1.SessionAccepted.bootstrap:type_name -> openshell.v1.ConfigBootstrap - 289, // 275: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration - 300, // 276: openshell.v1.ConfigBootstrap.sandbox_config:type_name -> openshell.sandbox.v1.SandboxConfigSnapshot - 157, // 277: openshell.v1.ConfigBootstrap.provider_environment:type_name -> openshell.v1.ProviderEnvironmentSnapshot - 300, // 278: openshell.v1.ConfigUpdate.sandbox_config:type_name -> openshell.sandbox.v1.SandboxConfigSnapshot - 157, // 279: openshell.v1.ConfigUpdate.provider_environment:type_name -> openshell.v1.ProviderEnvironmentSnapshot - 5, // 280: openshell.v1.ConfigComponentApplyResult.component:type_name -> openshell.v1.ConfigComponent - 75, // 281: openshell.v1.ConfigComponentApplyResult.requested_revision:type_name -> openshell.v1.ConfigSnapshotRevision - 75, // 282: openshell.v1.ConfigComponentApplyResult.applied_revision:type_name -> openshell.v1.ConfigSnapshotRevision - 6, // 283: openshell.v1.ConfigComponentApplyResult.outcome:type_name -> openshell.v1.ConfigApplyOutcome - 194, // 284: openshell.v1.ConfigComponentApplyResult.failure:type_name -> openshell.v1.ConfigApplyFailure - 195, // 285: openshell.v1.ConfigUpdateResult.result:type_name -> openshell.v1.ConfigComponentApplyResult - 176, // 286: openshell.v1.ConfigUpdateResult.admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 195, // 287: openshell.v1.ConfigBootstrapResult.results:type_name -> openshell.v1.ConfigComponentApplyResult - 176, // 288: openshell.v1.ConfigBootstrapResult.admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 206, // 289: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 207, // 290: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 208, // 291: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 205, // 292: openshell.v1.PeerRelayInit.relay_open:type_name -> openshell.v1.RelayOpen - 210, // 293: openshell.v1.PeerRelayFrame.init:type_name -> openshell.v1.PeerRelayInit - 285, // 294: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp - 285, // 295: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp - 214, // 296: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 216, // 297: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 296, // 298: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 285, // 299: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp - 285, // 300: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp - 285, // 301: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp - 285, // 302: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp - 287, // 303: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 287, // 304: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 290, // 305: openshell.v1.SubmitPolicyAnalysisRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 215, // 306: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 218, // 307: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 217, // 308: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 290, // 309: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 218, // 310: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 285, // 311: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp - 290, // 312: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 313: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 314: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 228, // 315: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 290, // 316: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 296, // 317: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 290, // 318: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 319: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 290, // 320: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 285, // 321: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp - 238, // 322: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 284, // 323: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 301, // 324: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 301, // 325: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 301, // 326: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 17, // 327: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 286, // 328: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 15, // 329: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 290, // 330: openshell.v1.AddWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 15, // 331: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 248, // 332: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 290, // 333: openshell.v1.RemoveWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 17, // 334: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 290, // 335: openshell.v1.ListWorkspaceMembersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 248, // 336: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 285, // 337: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp - 18, // 338: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult - 256, // 339: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation - 18, // 340: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult - 285, // 341: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp - 285, // 342: openshell.v1.SandboxProvisioning.configuration_change_time:type_name -> google.protobuf.Timestamp - 285, // 343: openshell.v1.SandboxProvisioning.first_rejection_time:type_name -> google.protobuf.Timestamp - 285, // 344: openshell.v1.SandboxProvisioning.deadline:type_name -> google.protobuf.Timestamp - 285, // 345: openshell.v1.SandboxProvisioning.timeout_time:type_name -> google.protobuf.Timestamp - 285, // 346: openshell.v1.SandboxProvisioning.cleanup_completed_time:type_name -> google.protobuf.Timestamp - 285, // 347: openshell.v1.SandboxProvisioning.cleanup_retry_time:type_name -> google.protobuf.Timestamp - 285, // 348: openshell.v1.SandboxProvisioning.attachment_change_time:type_name -> google.protobuf.Timestamp - 285, // 349: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 285, // 350: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 126, // 351: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 154, // 352: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 126, // 353: openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 23, // 354: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 25, // 355: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 27, // 356: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 51, // 357: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 59, // 358: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest - 61, // 359: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 62, // 360: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 52, // 361: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest - 53, // 362: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest - 54, // 363: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest - 55, // 364: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest - 63, // 365: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 64, // 366: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 65, // 367: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 81, // 368: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest - 66, // 369: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 67, // 370: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 68, // 371: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 86, // 372: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 88, // 373: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 89, // 374: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 90, // 375: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 92, // 376: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 96, // 377: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 98, // 378: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 104, // 379: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 105, // 380: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 112, // 381: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 113, // 382: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 114, // 383: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 119, // 384: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 120, // 385: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 143, // 386: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 145, // 387: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 147, // 388: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 115, // 389: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 132, // 390: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 134, // 391: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 136, // 392: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 138, // 393: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 116, // 394: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 150, // 395: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 302, // 396: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 303, // 397: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 160, // 398: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 170, // 399: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 172, // 400: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 174, // 401: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 257, // 402: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest - 83, // 403: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest - 177, // 404: openshell.v1.OpenShell.ReportSandboxConfiguration:input_type -> openshell.v1.ReportSandboxConfigurationRequest - 152, // 405: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 158, // 406: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 180, // 407: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 181, // 408: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 184, // 409: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 201, // 410: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 203, // 411: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest - 209, // 412: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 211, // 413: openshell.v1.OpenShell.PeerRelay:input_type -> openshell.v1.PeerRelayFrame - 83, // 414: openshell.v1.OpenShell.PeerReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest - 257, // 415: openshell.v1.OpenShell.PeerReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest - 81, // 416: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest - 108, // 417: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 220, // 418: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 222, // 419: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 224, // 420: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 226, // 421: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 229, // 422: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 231, // 423: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 233, // 424: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 235, // 425: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 237, // 426: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 19, // 427: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 21, // 428: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 240, // 429: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 242, // 430: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 244, // 431: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 246, // 432: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 249, // 433: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 251, // 434: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 253, // 435: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 24, // 436: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 26, // 437: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 28, // 438: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 69, // 439: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 60, // 440: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse - 69, // 441: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 70, // 442: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 56, // 443: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 56, // 444: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 57, // 445: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse - 58, // 446: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse - 71, // 447: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 72, // 448: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 73, // 449: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 82, // 450: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse - 85, // 451: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 69, // 452: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 69, // 453: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 87, // 454: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 95, // 455: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 95, // 456: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 91, // 457: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 93, // 458: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 97, // 459: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 102, // 460: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 104, // 461: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 102, // 462: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 117, // 463: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 117, // 464: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 118, // 465: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 142, // 466: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 141, // 467: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 144, // 468: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 146, // 469: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 148, // 470: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 117, // 471: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 133, // 472: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 135, // 473: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 137, // 474: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 139, // 475: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 149, // 476: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 151, // 477: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 304, // 478: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 305, // 479: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 169, // 480: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 171, // 481: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 173, // 482: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 175, // 483: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 258, // 484: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse - 84, // 485: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse - 178, // 486: openshell.v1.OpenShell.ReportSandboxConfiguration:output_type -> openshell.v1.ReportSandboxConfigurationResponse - 155, // 487: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 159, // 488: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 183, // 489: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 182, // 490: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 186, // 491: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 202, // 492: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 204, // 493: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse - 209, // 494: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 211, // 495: openshell.v1.OpenShell.PeerRelay:output_type -> openshell.v1.PeerRelayFrame - 84, // 496: openshell.v1.OpenShell.PeerReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse - 258, // 497: openshell.v1.OpenShell.PeerReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse - 82, // 498: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse - 109, // 499: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 221, // 500: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 223, // 501: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 225, // 502: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 227, // 503: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 230, // 504: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 232, // 505: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 234, // 506: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 236, // 507: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 239, // 508: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 20, // 509: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 22, // 510: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 241, // 511: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 243, // 512: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 245, // 513: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 247, // 514: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 250, // 515: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 252, // 516: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 254, // 517: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 436, // [436:518] is the sub-list for method output_type - 354, // [354:436] is the sub-list for method input_type - 354, // [354:354] is the sub-list for extension type_name - 354, // [354:354] is the sub-list for extension extendee - 0, // [0:354] is the sub-list for field type_name + 292, // 213: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration + 293, // 214: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 290, // 215: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 298, // 216: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 164, // 217: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 284, // 218: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 12, // 219: openshell.v1.UpdateConfigRequest.consistency:type_name -> openshell.v1.ConfigUpdateConsistency + 292, // 220: openshell.v1.UpdateConfigRequest.wait_timeout:type_name -> google.protobuf.Duration + 293, // 221: openshell.v1.GetConfigUpdateOperationRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 78, // 222: openshell.v1.GetConfigUpdateOperationResponse.operation:type_name -> openshell.v1.ConfigUpdateOperation + 165, // 223: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 166, // 224: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 167, // 225: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 169, // 226: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 170, // 227: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 171, // 228: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 299, // 229: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 297, // 230: openshell.v1.L7RuleTarget.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 300, // 231: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 168, // 232: openshell.v1.AddDenyRules.target:type_name -> openshell.v1.L7RuleTarget + 301, // 233: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 168, // 234: openshell.v1.AddAllowRules.target:type_name -> openshell.v1.L7RuleTarget + 285, // 235: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 78, // 236: openshell.v1.UpdateConfigResponse.operation:type_name -> openshell.v1.ConfigUpdateOperation + 293, // 237: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 182, // 238: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 293, // 239: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 182, // 240: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 14, // 241: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 13, // 242: openshell.v1.SandboxConfigurationAdmission.state:type_name -> openshell.v1.ConfigurationAdmissionState + 179, // 243: openshell.v1.ReportSandboxConfigurationRequest.admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 14, // 244: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 288, // 245: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp + 288, // 246: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp + 290, // 247: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 286, // 248: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 293, // 249: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 288, // 250: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp + 111, // 251: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 111, // 252: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 190, // 253: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 202, // 254: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 215, // 255: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 216, // 256: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 199, // 257: openshell.v1.SupervisorMessage.config_update_result:type_name -> openshell.v1.ConfigUpdateResult + 200, // 258: openshell.v1.SupervisorMessage.config_bootstrap_result:type_name -> openshell.v1.ConfigBootstrapResult + 193, // 259: openshell.v1.SupervisorMessage.startup_config_prepared:type_name -> openshell.v1.StartupConfigPrepared + 188, // 260: openshell.v1.SupervisorMessage.runtime_ready:type_name -> openshell.v1.SupervisorRuntimeReady + 194, // 261: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 201, // 262: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 203, // 263: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 208, // 264: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 216, // 265: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 196, // 266: openshell.v1.GatewayMessage.config_update:type_name -> openshell.v1.ConfigUpdate + 192, // 267: openshell.v1.GatewayMessage.startup_config_candidate:type_name -> openshell.v1.StartupConfigCandidate + 179, // 268: openshell.v1.GatewayMessage.configuration_admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 290, // 269: openshell.v1.SupervisorHello.image_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 191, // 270: openshell.v1.SupervisorHello.image_policy_discovery:type_name -> openshell.v1.ImagePolicyDiscovery + 302, // 271: openshell.v1.ImagePolicyDiscovery.missing:type_name -> google.protobuf.Empty + 302, // 272: openshell.v1.ImagePolicyDiscovery.invalid:type_name -> google.protobuf.Empty + 290, // 273: openshell.v1.ImagePolicyDiscovery.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 290, // 274: openshell.v1.StartupConfigCandidate.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 295, // 275: openshell.v1.StartupConfigCandidate.policy_source:type_name -> openshell.sandbox.v1.PolicySource + 302, // 276: openshell.v1.StartupConfigPrepared.unchanged:type_name -> google.protobuf.Empty + 290, // 277: openshell.v1.StartupConfigPrepared.prepared_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 197, // 278: openshell.v1.StartupConfigPrepared.failure:type_name -> openshell.v1.ConfigApplyFailure + 195, // 279: openshell.v1.SessionAccepted.bootstrap:type_name -> openshell.v1.ConfigBootstrap + 292, // 280: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration + 303, // 281: openshell.v1.ConfigBootstrap.sandbox_config:type_name -> openshell.sandbox.v1.SandboxConfigSnapshot + 158, // 282: openshell.v1.ConfigBootstrap.provider_environment:type_name -> openshell.v1.ProviderEnvironmentSnapshot + 303, // 283: openshell.v1.ConfigUpdate.sandbox_config:type_name -> openshell.sandbox.v1.SandboxConfigSnapshot + 158, // 284: openshell.v1.ConfigUpdate.provider_environment:type_name -> openshell.v1.ProviderEnvironmentSnapshot + 5, // 285: openshell.v1.ConfigComponentApplyResult.component:type_name -> openshell.v1.ConfigComponent + 76, // 286: openshell.v1.ConfigComponentApplyResult.requested_revision:type_name -> openshell.v1.ConfigSnapshotRevision + 76, // 287: openshell.v1.ConfigComponentApplyResult.applied_revision:type_name -> openshell.v1.ConfigSnapshotRevision + 6, // 288: openshell.v1.ConfigComponentApplyResult.outcome:type_name -> openshell.v1.ConfigApplyOutcome + 197, // 289: openshell.v1.ConfigComponentApplyResult.failure:type_name -> openshell.v1.ConfigApplyFailure + 198, // 290: openshell.v1.ConfigUpdateResult.result:type_name -> openshell.v1.ConfigComponentApplyResult + 179, // 291: openshell.v1.ConfigUpdateResult.admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 198, // 292: openshell.v1.ConfigBootstrapResult.results:type_name -> openshell.v1.ConfigComponentApplyResult + 179, // 293: openshell.v1.ConfigBootstrapResult.admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 209, // 294: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 210, // 295: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 211, // 296: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 208, // 297: openshell.v1.PeerRelayInit.relay_open:type_name -> openshell.v1.RelayOpen + 213, // 298: openshell.v1.PeerRelayFrame.init:type_name -> openshell.v1.PeerRelayInit + 288, // 299: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp + 288, // 300: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp + 217, // 301: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 219, // 302: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 299, // 303: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 288, // 304: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp + 288, // 305: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp + 288, // 306: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp + 288, // 307: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp + 290, // 308: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 290, // 309: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 293, // 310: openshell.v1.SubmitPolicyAnalysisRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 218, // 311: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 221, // 312: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 220, // 313: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 293, // 314: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 221, // 315: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 288, // 316: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp + 293, // 317: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 318: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 319: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 231, // 320: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 293, // 321: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 299, // 322: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 293, // 323: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 324: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 293, // 325: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 288, // 326: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp + 241, // 327: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 287, // 328: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 304, // 329: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 304, // 330: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 304, // 331: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 18, // 332: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 289, // 333: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 16, // 334: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 293, // 335: openshell.v1.AddWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 16, // 336: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 251, // 337: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 293, // 338: openshell.v1.RemoveWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 18, // 339: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 293, // 340: openshell.v1.ListWorkspaceMembersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 251, // 341: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 288, // 342: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp + 19, // 343: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult + 259, // 344: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation + 19, // 345: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult + 288, // 346: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp + 288, // 347: openshell.v1.SandboxProvisioning.configuration_change_time:type_name -> google.protobuf.Timestamp + 288, // 348: openshell.v1.SandboxProvisioning.first_rejection_time:type_name -> google.protobuf.Timestamp + 288, // 349: openshell.v1.SandboxProvisioning.deadline:type_name -> google.protobuf.Timestamp + 288, // 350: openshell.v1.SandboxProvisioning.timeout_time:type_name -> google.protobuf.Timestamp + 288, // 351: openshell.v1.SandboxProvisioning.cleanup_completed_time:type_name -> google.protobuf.Timestamp + 288, // 352: openshell.v1.SandboxProvisioning.cleanup_retry_time:type_name -> google.protobuf.Timestamp + 288, // 353: openshell.v1.SandboxProvisioning.attachment_change_time:type_name -> google.protobuf.Timestamp + 288, // 354: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 288, // 355: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 127, // 356: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 155, // 357: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 127, // 358: openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 24, // 359: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 26, // 360: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 28, // 361: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 52, // 362: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 60, // 363: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest + 62, // 364: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 63, // 365: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 53, // 366: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest + 54, // 367: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest + 55, // 368: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest + 56, // 369: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest + 64, // 370: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 65, // 371: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 66, // 372: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 82, // 373: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest + 67, // 374: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 68, // 375: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 69, // 376: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 87, // 377: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 89, // 378: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 90, // 379: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 91, // 380: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 93, // 381: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 97, // 382: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 99, // 383: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 105, // 384: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 106, // 385: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 113, // 386: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 114, // 387: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 115, // 388: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 120, // 389: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 121, // 390: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 144, // 391: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 146, // 392: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 148, // 393: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 116, // 394: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 133, // 395: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 135, // 396: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 137, // 397: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 139, // 398: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 117, // 399: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 151, // 400: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 305, // 401: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 306, // 402: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 161, // 403: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 162, // 404: openshell.v1.OpenShell.GetConfigUpdateOperation:input_type -> openshell.v1.GetConfigUpdateOperationRequest + 173, // 405: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 175, // 406: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 177, // 407: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 260, // 408: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest + 84, // 409: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest + 180, // 410: openshell.v1.OpenShell.ReportSandboxConfiguration:input_type -> openshell.v1.ReportSandboxConfigurationRequest + 153, // 411: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 159, // 412: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 183, // 413: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 184, // 414: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 187, // 415: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 204, // 416: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 206, // 417: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest + 212, // 418: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 214, // 419: openshell.v1.OpenShell.PeerRelay:input_type -> openshell.v1.PeerRelayFrame + 84, // 420: openshell.v1.OpenShell.PeerReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest + 260, // 421: openshell.v1.OpenShell.PeerReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest + 82, // 422: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest + 109, // 423: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 223, // 424: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 225, // 425: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 227, // 426: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 229, // 427: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 232, // 428: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 234, // 429: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 236, // 430: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 238, // 431: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 240, // 432: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 20, // 433: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 22, // 434: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 243, // 435: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 245, // 436: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 247, // 437: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 249, // 438: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 252, // 439: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 254, // 440: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 256, // 441: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 25, // 442: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 27, // 443: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 29, // 444: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 70, // 445: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 61, // 446: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse + 70, // 447: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 71, // 448: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 57, // 449: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 57, // 450: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 58, // 451: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse + 59, // 452: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse + 72, // 453: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 73, // 454: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 74, // 455: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 83, // 456: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse + 86, // 457: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 70, // 458: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 70, // 459: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 88, // 460: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 96, // 461: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 96, // 462: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 92, // 463: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 94, // 464: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 98, // 465: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 103, // 466: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 105, // 467: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 103, // 468: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 118, // 469: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 118, // 470: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 119, // 471: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 143, // 472: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 142, // 473: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 145, // 474: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 147, // 475: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 149, // 476: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 118, // 477: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 134, // 478: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 136, // 479: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 138, // 480: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 140, // 481: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 150, // 482: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 152, // 483: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 307, // 484: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 308, // 485: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 172, // 486: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 163, // 487: openshell.v1.OpenShell.GetConfigUpdateOperation:output_type -> openshell.v1.GetConfigUpdateOperationResponse + 174, // 488: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 176, // 489: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 178, // 490: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 261, // 491: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse + 85, // 492: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse + 181, // 493: openshell.v1.OpenShell.ReportSandboxConfiguration:output_type -> openshell.v1.ReportSandboxConfigurationResponse + 156, // 494: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 160, // 495: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 186, // 496: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 185, // 497: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 189, // 498: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 205, // 499: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 207, // 500: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse + 212, // 501: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 214, // 502: openshell.v1.OpenShell.PeerRelay:output_type -> openshell.v1.PeerRelayFrame + 85, // 503: openshell.v1.OpenShell.PeerReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse + 261, // 504: openshell.v1.OpenShell.PeerReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse + 83, // 505: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse + 110, // 506: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 224, // 507: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 226, // 508: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 228, // 509: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 230, // 510: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 233, // 511: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 235, // 512: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 237, // 513: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 239, // 514: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 242, // 515: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 21, // 516: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 23, // 517: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 244, // 518: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 246, // 519: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 248, // 520: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 250, // 521: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 253, // 522: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 255, // 523: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 257, // 524: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 442, // [442:525] is the sub-list for method output_type + 359, // [359:442] is the sub-list for method input_type + 359, // [359:359] is the sub-list for extension type_name + 359, // [359:359] is the sub-list for extension extendee + 0, // [0:359] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -21191,7 +21401,7 @@ func file_openshell_proto_init() { (*SandboxStreamEvent_Warning)(nil), (*SandboxStreamEvent_DraftPolicyUpdate)(nil), } - file_openshell_proto_msgTypes[142].OneofWrappers = []any{ + file_openshell_proto_msgTypes[144].OneofWrappers = []any{ (*PolicyMergeOperation_AddRule)(nil), (*PolicyMergeOperation_RemoveEndpoint)(nil), (*PolicyMergeOperation_RemoveRule)(nil), @@ -21199,8 +21409,8 @@ func file_openshell_proto_init() { (*PolicyMergeOperation_AddAllowRules)(nil), (*PolicyMergeOperation_RemoveBinary)(nil), } - file_openshell_proto_msgTypes[146].OneofWrappers = []any{} - file_openshell_proto_msgTypes[165].OneofWrappers = []any{ + file_openshell_proto_msgTypes[148].OneofWrappers = []any{} + file_openshell_proto_msgTypes[167].OneofWrappers = []any{ (*SupervisorMessage_Hello)(nil), (*SupervisorMessage_Heartbeat)(nil), (*SupervisorMessage_RelayOpenResult)(nil), @@ -21210,7 +21420,7 @@ func file_openshell_proto_init() { (*SupervisorMessage_StartupConfigPrepared)(nil), (*SupervisorMessage_RuntimeReady)(nil), } - file_openshell_proto_msgTypes[167].OneofWrappers = []any{ + file_openshell_proto_msgTypes[169].OneofWrappers = []any{ (*GatewayMessage_SessionAccepted)(nil), (*GatewayMessage_SessionRejected)(nil), (*GatewayMessage_Heartbeat)(nil), @@ -21220,29 +21430,29 @@ func file_openshell_proto_init() { (*GatewayMessage_StartupConfigCandidate)(nil), (*GatewayMessage_ConfigurationAdmission)(nil), } - file_openshell_proto_msgTypes[169].OneofWrappers = []any{ + file_openshell_proto_msgTypes[171].OneofWrappers = []any{ (*ImagePolicyDiscovery_Missing)(nil), (*ImagePolicyDiscovery_Invalid)(nil), (*ImagePolicyDiscovery_Policy)(nil), } - file_openshell_proto_msgTypes[171].OneofWrappers = []any{ + file_openshell_proto_msgTypes[173].OneofWrappers = []any{ (*StartupConfigPrepared_Unchanged)(nil), (*StartupConfigPrepared_PreparedPolicy)(nil), (*StartupConfigPrepared_Failure)(nil), } - file_openshell_proto_msgTypes[174].OneofWrappers = []any{ + file_openshell_proto_msgTypes[176].OneofWrappers = []any{ (*ConfigUpdate_SandboxConfig)(nil), (*ConfigUpdate_ProviderEnvironment)(nil), } - file_openshell_proto_msgTypes[186].OneofWrappers = []any{ + file_openshell_proto_msgTypes[188].OneofWrappers = []any{ (*RelayOpen_Ssh)(nil), (*RelayOpen_Tcp)(nil), } - file_openshell_proto_msgTypes[190].OneofWrappers = []any{ + file_openshell_proto_msgTypes[192].OneofWrappers = []any{ (*RelayFrame_Init)(nil), (*RelayFrame_Data)(nil), } - file_openshell_proto_msgTypes[192].OneofWrappers = []any{ + file_openshell_proto_msgTypes[194].OneofWrappers = []any{ (*PeerRelayFrame_Init)(nil), (*PeerRelayFrame_Data)(nil), } @@ -21251,8 +21461,8 @@ func file_openshell_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_openshell_proto_rawDesc), len(file_openshell_proto_rawDesc)), - NumEnums: 19, - NumMessages: 266, + NumEnums: 20, + NumMessages: 268, NumExtensions: 0, NumServices: 1, }, diff --git a/sdk/go/proto/openshellv1/openshell_grpc.pb.go b/sdk/go/proto/openshellv1/openshell_grpc.pb.go index e69a8f663f..6ca053664a 100644 --- a/sdk/go/proto/openshellv1/openshell_grpc.pb.go +++ b/sdk/go/proto/openshellv1/openshell_grpc.pb.go @@ -68,6 +68,7 @@ const ( OpenShell_GetSandboxConfig_FullMethodName = "/openshell.v1.OpenShell/GetSandboxConfig" OpenShell_GetGatewayConfig_FullMethodName = "/openshell.v1.OpenShell/GetGatewayConfig" OpenShell_UpdateConfig_FullMethodName = "/openshell.v1.OpenShell/UpdateConfig" + OpenShell_GetConfigUpdateOperation_FullMethodName = "/openshell.v1.OpenShell/GetConfigUpdateOperation" OpenShell_GetSandboxPolicyStatus_FullMethodName = "/openshell.v1.OpenShell/GetSandboxPolicyStatus" OpenShell_ListSandboxPolicies_FullMethodName = "/openshell.v1.OpenShell/ListSandboxPolicies" OpenShell_ReportPolicyStatus_FullMethodName = "/openshell.v1.OpenShell/ReportPolicyStatus" @@ -225,6 +226,8 @@ type OpenShellClient interface { GetGatewayConfig(ctx context.Context, in *sandboxv1.GetGatewayConfigRequest, opts ...grpc.CallOption) (*sandboxv1.GetGatewayConfigResponse, error) // Update settings or policy at sandbox or global scope. UpdateConfig(ctx context.Context, in *UpdateConfigRequest, opts ...grpc.CallOption) (*UpdateConfigResponse, error) + // Get a durable sandbox configuration update operation by id. + GetConfigUpdateOperation(ctx context.Context, in *GetConfigUpdateOperationRequest, opts ...grpc.CallOption) (*GetConfigUpdateOperationResponse, error) // Get the load status of a specific policy version. GetSandboxPolicyStatus(ctx context.Context, in *GetSandboxPolicyStatusRequest, opts ...grpc.CallOption) (*GetSandboxPolicyStatusResponse, error) // List policy history for a sandbox. @@ -811,6 +814,16 @@ func (c *openShellClient) UpdateConfig(ctx context.Context, in *UpdateConfigRequ return out, nil } +func (c *openShellClient) GetConfigUpdateOperation(ctx context.Context, in *GetConfigUpdateOperationRequest, opts ...grpc.CallOption) (*GetConfigUpdateOperationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetConfigUpdateOperationResponse) + err := c.cc.Invoke(ctx, OpenShell_GetConfigUpdateOperation_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *openShellClient) GetSandboxPolicyStatus(ctx context.Context, in *GetSandboxPolicyStatusRequest, opts ...grpc.CallOption) (*GetSandboxPolicyStatusResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetSandboxPolicyStatusResponse) @@ -1320,6 +1333,8 @@ type OpenShellServer interface { GetGatewayConfig(context.Context, *sandboxv1.GetGatewayConfigRequest) (*sandboxv1.GetGatewayConfigResponse, error) // Update settings or policy at sandbox or global scope. UpdateConfig(context.Context, *UpdateConfigRequest) (*UpdateConfigResponse, error) + // Get a durable sandbox configuration update operation by id. + GetConfigUpdateOperation(context.Context, *GetConfigUpdateOperationRequest) (*GetConfigUpdateOperationResponse, error) // Get the load status of a specific policy version. GetSandboxPolicyStatus(context.Context, *GetSandboxPolicyStatusRequest) (*GetSandboxPolicyStatusResponse, error) // List policy history for a sandbox. @@ -1576,6 +1591,9 @@ func (UnimplementedOpenShellServer) GetGatewayConfig(context.Context, *sandboxv1 func (UnimplementedOpenShellServer) UpdateConfig(context.Context, *UpdateConfigRequest) (*UpdateConfigResponse, error) { return nil, status.Error(codes.Unimplemented, "method UpdateConfig not implemented") } +func (UnimplementedOpenShellServer) GetConfigUpdateOperation(context.Context, *GetConfigUpdateOperationRequest) (*GetConfigUpdateOperationResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetConfigUpdateOperation not implemented") +} func (UnimplementedOpenShellServer) GetSandboxPolicyStatus(context.Context, *GetSandboxPolicyStatusRequest) (*GetSandboxPolicyStatusResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetSandboxPolicyStatus not implemented") } @@ -2489,6 +2507,24 @@ func _OpenShell_UpdateConfig_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _OpenShell_GetConfigUpdateOperation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetConfigUpdateOperationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetConfigUpdateOperation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetConfigUpdateOperation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetConfigUpdateOperation(ctx, req.(*GetConfigUpdateOperationRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _OpenShell_GetSandboxPolicyStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetSandboxPolicyStatusRequest) if err := dec(in); err != nil { @@ -3279,6 +3315,10 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "UpdateConfig", Handler: _OpenShell_UpdateConfig_Handler, }, + { + MethodName: "GetConfigUpdateOperation", + Handler: _OpenShell_GetConfigUpdateOperation_Handler, + }, { MethodName: "GetSandboxPolicyStatus", Handler: _OpenShell_GetSandboxPolicyStatus_Handler, diff --git a/sdk/typescript/src/client.test.ts b/sdk/typescript/src/client.test.ts index dcc85f328e..a431409860 100644 --- a/sdk/typescript/src/client.test.ts +++ b/sdk/typescript/src/client.test.ts @@ -22,7 +22,14 @@ import { SCOPE_NAMES, STATUS_NAMES, } from './client.js'; -import { OpenShell, SandboxPhase, ServiceStatus } from './gen/openshell_pb.js'; +import { + ConfigApplyOutcome, + ConfigUpdateConsistency, + ConfigUpdateOperationState, + OpenShell, + SandboxPhase, + ServiceStatus, +} from './gen/openshell_pb.js'; import { PolicySource, SettingScope } from './gen/sandbox_pb.js'; import type { ExecInteractiveSession, ExecInteractiveSessionControl } from './index.js'; @@ -1370,15 +1377,15 @@ describe('config / policy', () => { }); }); - it('setPolicy sends global=false + version pin and (wait) polls until the hash matches', async () => { + it('setPolicy preserves a degraded outcome after waiting for completion', async () => { let updateReq: { sandbox?: string; workspace?: string; global?: boolean; expectedResourceVersion?: bigint; policy?: unknown; + consistency?: ConfigUpdateConsistency; } = {}; - let configCalls = 0; const sandbox = client({ getSandbox: () => readySandbox('sb', 'sb-id'), updateConfig: (req) => { @@ -1388,20 +1395,12 @@ describe('config / policy', () => { policyHash: 'target', settingsRevision: 10n, deleted: false, - }; - }, - getSandboxConfig: () => { - configCalls += 1; - const policyHash = configCalls >= 2 ? 'target' : 'stale'; - return { - policy: { version: 1, networkPolicies: {} }, - version: 5, - policyHash, - settings: {}, - configRevision: 1n, - policySource: PolicySource.SANDBOX, - globalPolicyVersion: 0, - providerEnvRevision: 0n, + operation: { + operationId: 'operation-1', + state: ConfigUpdateOperationState.APPLIED, + outcome: ConfigApplyOutcome.DEGRADED, + sanitizedError: 'optional component unavailable', + }, }; }, }); @@ -1418,32 +1417,36 @@ describe('config / policy', () => { expect(updateReq.global).toBe(false); expect(updateReq.expectedResourceVersion).toBe(7n); expect(updateReq.policy).toBeDefined(); + expect(updateReq.consistency).toBe(ConfigUpdateConsistency.WAIT_FOR_COMPLETION); expect(result.version).toBe(5); expect(result.policyHash).toBe('target'); expect(result.settingsRevision).toBe('10'); - expect(configCalls).toBeGreaterThanOrEqual(2); + expect(result.operationId).toBe('operation-1'); + expect(result.operationState).toBe('applied'); + expect(result.operationOutcome).toBe('degraded'); + expect(result.operationError).toBe('optional component unavailable'); }); - // Fix #4 residual: setPolicy(..., {wait:true}) must not hang forever when the - // getConfig poll stalls. Each poll RPC is bounded by the remaining deadline, - // so a getSandboxConfig that never settles on its own is aborted and the wait - // rejects instead of pending forever. The handler resolves only on the call - // signal firing, proving the per-poll deadline (not the sleep loop) is what - // bounds the returned promise. - it('setPolicy wait rejects when the config poll stalls past the deadline', async () => { + it('setPolicy rejects a terminal failed durable operation', async () => { const sandbox = client({ getSandbox: () => readySandbox('sb', 'sb-id'), - updateConfig: () => ({ version: 5, policyHash: 'target', settingsRevision: 10n, deleted: false }), - getSandboxConfig: (_req, ctx) => - new Promise((_resolve, reject) => { - ctx.signal.addEventListener('abort', () => reject(new Error('aborted')), { once: true }); - }), + updateConfig: () => ({ + version: 5, + policyHash: 'target', + settingsRevision: 10n, + deleted: false, + operation: { + operationId: 'operation-2', + state: ConfigUpdateOperationState.FAILED, + sanitizedError: 'runtime rejected policy', + }, + }), }); - await expect( - sandbox.setPolicy('sb', { version: 1, networkPolicies: {} }, { wait: true, waitTimeoutSecs: 0.2 }), - ).rejects.toMatchObject({ code: 'connect' }); - }, 5000); + await expect(sandbox.setPolicy('sb', { version: 1, networkPolicies: {} }, { wait: true })).rejects.toThrow( + /runtime rejected policy/, + ); + }); it('setSetting upserts a single sandbox-scoped setting (global=false)', async () => { let req: { diff --git a/sdk/typescript/src/client.ts b/sdk/typescript/src/client.ts index b0595f0c0a..bb7b4d1de2 100644 --- a/sdk/typescript/src/client.ts +++ b/sdk/typescript/src/client.ts @@ -20,6 +20,9 @@ import { errorCode, fromConnect, SdkError } from './errors.js'; import type { Provider, WorkspaceSelectorSchema } from './gen/datamodel_pb.js'; import type { Sandbox, SandboxWorkloadTemplate, UpdateConfigResponse } from './gen/openshell_pb.js'; import { + ConfigApplyOutcome, + ConfigUpdateConsistency, + ConfigUpdateOperationState, type ExecSandboxInputSchema, OpenShell, SandboxPhase, @@ -414,9 +417,9 @@ export interface SandboxConfig { export interface SetPolicyOptions extends SandboxWorkspaceOptions { /** Pin the sandbox resource version for optimistic concurrency (u64 as string). */ expectedResourceVersion?: string; - /** Poll getConfig until the applied policy hash is observed. */ + /** Ask the gateway to wait for a durable terminal apply result. */ wait?: boolean; - /** Bound the `wait` poll (seconds). Default 60. */ + /** Bound the server-side wait in seconds. Default 60. */ waitTimeoutSecs?: number; } @@ -426,6 +429,20 @@ export interface UpdateConfigResult { /** u64 rendered as a string. */ settingsRevision: string; deleted: boolean; + operationId?: string; + operationState?: 'unspecified' | 'pending' | 'applied' | 'inactive' | 'failed' | 'superseded' | 'cancelled'; + /** Application may complete with a degraded outcome. */ + operationOutcome?: + | 'unspecified' + | 'applied' + | 'ignored_duplicate' + | 'ignored_stale' + | 'retained_local_override' + | 'degraded' + | 'failed_retained_last_known_good' + | 'failed_closed' + | 'unsupported'; + operationError?: string; } // ---- enum → lowercase string ----------------------------------------------- @@ -545,6 +562,16 @@ function updateConfigResult(resp: UpdateConfigResponse): UpdateConfigResult { policyHash: resp.policyHash, settingsRevision: resp.settingsRevision.toString(), deleted: resp.deleted, + ...(resp.operation + ? { + operationId: resp.operation.operationId, + operationState: (ConfigUpdateOperationState[resp.operation.state]?.toLowerCase() ?? + 'unspecified') as UpdateConfigResult['operationState'], + operationOutcome: (ConfigApplyOutcome[resp.operation.outcome]?.toLowerCase() ?? + 'unspecified') as UpdateConfigResult['operationOutcome'], + operationError: resp.operation.sanitizedError, + } + : {}), }; } @@ -1629,8 +1656,8 @@ export class SandboxClient { // Update the sandbox-scoped policy. Sandbox scope (global=false) may only // change network_policies; static fields must match the create-time policy or - // the gateway rejects the update. With `wait`, poll getConfig until the - // applied policy hash is observed. + // the gateway rejects the update. With `wait`, the gateway owns the durable + // wait and returns only after a terminal apply result. async setPolicy( name: string, policy: MessageInitShape, @@ -1642,10 +1669,26 @@ export class SandboxClient { policy, global: false, expectedResourceVersion: versionPin(options?.expectedResourceVersion), + consistency: options?.wait ? ConfigUpdateConsistency.WAIT_FOR_COMPLETION : ConfigUpdateConsistency.COMMIT_ONLY, + waitTimeout: durationFromMs(Math.max(0, (options?.waitTimeoutSecs ?? 60) * 1000)), }); const result = updateConfigResult(resp); - if (options?.wait) - await this.waitForPolicyHash(name, result.policyHash, options.waitTimeoutSecs, options.workspace); + if (options?.wait) { + if (!resp.operation) throw new SdkError('rpc', 'gateway omitted the requested apply operation'); + if ( + resp.operation.state === ConfigUpdateOperationState.FAILED || + resp.operation.state === ConfigUpdateOperationState.SUPERSEDED || + resp.operation.state === ConfigUpdateOperationState.CANCELLED + ) { + throw new SdkError( + 'rpc', + `policy update operation '${resp.operation.operationId}' did not apply: ${resp.operation.sanitizedError}`, + ); + } + if (![ConfigUpdateOperationState.APPLIED, ConfigUpdateOperationState.INACTIVE].includes(resp.operation.state)) { + throw new SdkError('rpc', `gateway returned non-terminal operation '${resp.operation.operationId}'`); + } + } return result; } catch (e) { throw e instanceof SdkError ? e : fromConnect(e); @@ -1672,37 +1715,6 @@ export class SandboxClient { throw fromConnect(e); } } - - // Poll getConfig until the applied policy hash is observed. Each poll RPC is - // bounded by the remaining deadline (deadlineOptions), so a stalled getConfig - // cannot make the returned promise outlive timeoutSecs. - private async waitForPolicyHash( - name: string, - policyHash: string, - timeoutSecs = 60, - workspace?: string, - ): Promise { - const deadline = Date.now() + timeoutSecs * 1000; - let delay = 100; - for (;;) { - let config: SandboxConfig; - const pollOptions = deadlineOptions(deadline - Date.now()); - try { - config = await this.getConfig(name, { ...pollOptions, workspace }); - } catch (e) { - if (pollOptions.signal?.aborted || Date.now() >= deadline) { - throw new SdkError('connect', `timed out waiting for policy '${policyHash}' on sandbox '${name}'`); - } - throw e instanceof SdkError ? e : fromConnect(e); - } - if (config.policyHash === policyHash) return; - if (Date.now() >= deadline) { - throw new SdkError('connect', `timed out waiting for policy '${policyHash}' on sandbox '${name}'`); - } - await waitSleep(delay, deadline); - delay = Math.min(delay * 2, 2000); - } - } } // ---- The client ------------------------------------------------------------ diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index a4ac652119..743622444e 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -19,7 +19,7 @@ The target deployment flow is: 4. The CLI registers a reachable gateway endpoint with `openshell gateway add`. 5. The gateway creates sandboxes through the selected compute driver. -If supervisor sessions fail with a protocol revision mismatch, check that custom supervisor images match the gateway release. Gateway and supervisor require the same internal protocol revision; authentication success does not make mismatched versions compatible. Supervisors that predate the handshake still connect for one release. The gateway logs a warning for each such session and counts them in the `openshell_supervisor_protocol_legacy_sessions_total` metric, so recreate those sandboxes before the next gateway upgrade. See the published [gateway configuration reference](https://docs.nvidia.com/openshell/latest/reference/gateway-config.md). +If supervisor sessions fail with a protocol revision mismatch, check that custom supervisor images match the gateway release. Gateway and supervisor require the same internal protocol revision; authentication success does not make mismatched versions compatible. Recreate sandboxes that were started by the previous gateway release after an upgrade. See the published [gateway configuration reference](https://docs.nvidia.com/openshell/latest/reference/gateway-config.md). The `openshell-gateway` composition crate explicitly installs its compiled Docker, Podman, Kubernetes, and VM registrations at startup; `openshell-server` diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index b6711e5140..df990fb916 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -540,7 +540,7 @@ Edit `current-policy.yaml` to allow the blocked actions. **For policy content au - Binary matching patterns - Ordered `network_middlewares`, host selection, HTTP request/response and WebSocket bindings, and `fail_open` or `fail_closed` behavior -`network_policies` and `network_middlewares` can be modified at runtime when the selected compute driver supports live policy updates. Use `--wait` to verify that the active runtime loaded the revision; do not infer enforcement from the gateway accepting the update. If `filesystem_policy`, `landlock`, or `process` need changes, the sandbox must be recreated. Built-in middleware such as `openshell/regex` needs no gateway registration. An operator-run middleware must already be registered under `[[openshell.supervisor.middleware]]`; changing that static registration requires a gateway restart. +`network_policies` and `network_middlewares` can be modified at runtime when the selected compute driver supports live policy updates. Use `--wait` to observe completion, then inspect whether the revision applied, applied with a degraded outcome, or finished without application. A wait timeout does not cancel the committed update. Do not infer enforcement from the gateway accepting the update. If `filesystem_policy`, `landlock`, or `process` need changes, the sandbox must be recreated. Built-in middleware such as `openshell/regex` needs no gateway registration. An operator-run middleware must already be registered under `[[openshell.supervisor.middleware]]`; changing that static registration requires a gateway restart. Middleware can inspect HTTP requests, HTTP responses, or client WebSocket text messages when the implementation advertises the matching binding. The built-in