From f06865d54b937af159735b00411233790503984e Mon Sep 17 00:00:00 2001 From: Shiju Date: Wed, 16 Sep 2026 23:17:32 +0530 Subject: [PATCH 1/4] feat(providers): report applied sandbox provider changes Record exact provider mutation targets in shared configuration operations. Require authenticated evidence that credentials, effective policy, and the workload launch environment have been installed before reporting readiness. Add bounded CLI and Rust SDK status and wait support, preserving ordinary revision-scoped references for existing processes. Verify new-client rotation and acknowledged detach revocation without external-stable resolver changes. Signed-off-by: Shiju --- architecture/gateway.md | 25 +- crates/openshell-cli/src/commands/mod.rs | 1 + crates/openshell-cli/src/commands/provider.rs | 137 +- .../src/commands/provider_readiness.rs | 545 +++ crates/openshell-cli/src/main.rs | 153 +- crates/openshell-cli/src/run.rs | 1 + .../tests/ensure_providers_integration.rs | 21 + .../openshell-cli/tests/mtls_integration.rs | 18 + .../tests/provider_commands_integration.rs | 1600 ++++++- .../sandbox_create_lifecycle_integration.rs | 18 + .../sandbox_name_fallback_integration.rs | 18 + crates/openshell-core/src/grpc_client.rs | 102 +- .../src/provider_credentials.rs | 70 +- .../src/contract.rs | 24 + .../src/boundary_protocol.rs | 13 +- .../openshell-sandbox-backend/src/runtime.rs | 194 +- .../openshell-sandbox/src/boundary_server.rs | 123 +- crates/openshell-sdk/README.md | 35 + crates/openshell-sdk/src/lib.rs | 1 + .../openshell-sdk/src/provider_readiness.rs | 765 ++++ crates/openshell-sdk/src/raw.rs | 17 +- crates/openshell-sdk/tests/client_mock.rs | 18 + crates/openshell-server/proto/storage.proto | 23 + .../src/config_update_operation.rs | 649 +++ crates/openshell-server/src/grpc/mod.rs | 31 +- crates/openshell-server/src/grpc/policy.rs | 125 +- crates/openshell-server/src/grpc/provider.rs | 573 ++- .../src/grpc/provider_readiness.rs | 612 +++ .../src/grpc/provider_readiness_tests.rs | 1308 ++++++ crates/openshell-server/src/grpc/sandbox.rs | 157 +- crates/openshell-server/src/lib.rs | 1 + crates/openshell-server/src/storage_proto.rs | 94 +- .../src/supervisor_session.rs | 98 +- crates/openshell-server/tests/common/mod.rs | 16 + .../tests/supervisor_relay_integration.rs | 16 + .../openshell-supervisor-network/src/opa.rs | 20 +- .../openshell-supervisor-network/src/proxy.rs | 1 + .../openshell-supervisor-network/src/run.rs | 2 +- .../src/supervisor_session.rs | 1 + crates/openshell-supervisor/src/lib.rs | 819 +++- .../src/provider_readiness.rs | 792 ++++ docs/providers/profiles.mdx | 49 +- docs/sandboxes/manage-providers.mdx | 12 + e2e/rust/Cargo.toml | 5 + e2e/rust/tests/provider_readiness.rs | 1655 +++++++ proto/openshell.proto | 236 + proto/sandbox.proto | 3 + .../v1/internal/converter/coverage_test.go | 7 +- sdk/go/proto/openshellv1/openshell.pb.go | 4014 ++++++++++++----- sdk/go/proto/openshellv1/openshell_grpc.pb.go | 82 + sdk/go/proto/sandboxv1/sandbox.pb.go | 19 +- skills/debug-inference/SKILL.md | 16 +- skills/openshell-cli/SKILL.md | 15 +- 53 files changed, 13766 insertions(+), 1584 deletions(-) create mode 100644 crates/openshell-cli/src/commands/provider_readiness.rs create mode 100644 crates/openshell-sdk/src/provider_readiness.rs create mode 100644 crates/openshell-server/src/config_update_operation.rs create mode 100644 crates/openshell-server/src/grpc/provider_readiness.rs create mode 100644 crates/openshell-server/src/grpc/provider_readiness_tests.rs create mode 100644 crates/openshell-supervisor/src/provider_readiness.rs create mode 100644 e2e/rust/tests/provider_readiness.rs diff --git a/architecture/gateway.md b/architecture/gateway.md index 6b465a0508..866fd9d1cb 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -352,6 +352,8 @@ Public RPC contracts and durable protobuf formats have separate ownership. The ` `ReportEndpointStatus` is a sandbox-authenticated public gateway RPC. Its request, response, and `EndpointObservation` messages belong only to the public closure. `EndpointStatus` and `EndpointResult` also belong to the durable closure because `Sandbox.status.endpoint_statuses` persists them. The repeated status field uses a new wire tag; stored sandboxes without it decode with an empty endpoint list and retain their lifecycle fields. A fixed payload encoded with the earlier sandbox schema verifies that no database rewrite is required. +`GetSandboxProviderStatus` and `ReportProviderReadiness` are unary public gateway RPCs. The first lets authorized users inspect a provider change; the second accepts installation reports only from the sandbox's current authenticated supervisor session. + The removed `NetworkBinary.harness` field remains reserved by number and name, so protobuf implementations cannot reuse its wire slot or source identifier. The durable-policy compatibility decoder reads the former boolean before Prost @@ -370,8 +372,10 @@ Go, Python, and TypeScript client generation inputs do not advertise them. | SQL materializations | `StoredPolicyRevision`, `StoredDraftChunk` | Server-only typed results assembled from indexed columns and decoded payloads; not public RPC messages. | | Public messages used directly as encoded storage roots | `Sandbox`, `SandboxWorkloadTemplate`, `Provider`, `Workspace`, `WorkspaceMember`, `SshSession`, `ServiceEndpoint` | The generated public type is also the persisted payload. `SshSession` is not in the current public RPC message closure. | | Embedded encoded root | `SandboxPolicy` | Stored in policy rows and inside the JSON settings envelope. | +| Configuration operation storage root | `StoredConfigUpdateOperation` | One common operation resource stores the exact provider target, receipt projection, snapshot failure reason, and historical outcome. | +| Public dependencies of an operation | `ConfigUpdateOperation`, `ProviderMutationReceipt`, `ProviderReadinessReason` | Their complete message and enum closures are durable contracts. | -The descriptor-derived test inventories the complete message and enum closure of the encoded durable roots and its intersection with the public RPC closure. The tables here record the reviewed roots and classifications. +The descriptor-derived test owns the complete public, durable, and intersecting inventories and their reviewed fingerprints. The tables here record their roots and classifications. A synthetic sandbox-spec byte fixture verifies that an absent server-owned attachment epoch decodes to the valid empty initial identity; direct and template creation tests separately require the gateway to replace any caller-supplied epoch. | Dual-purpose encoded root | Current decision | |---|---| @@ -383,6 +387,9 @@ The descriptor-derived test inventories the complete message and enum closure of | `SshSession` | Defer a storage twin; govern its complete dependency closure as durable. | | `ServiceEndpoint` | Defer a storage twin; govern its complete dependency closure as durable. | | `SandboxPolicy` | Defer a storage twin; govern its complete dependency closure as durable. | +| `ConfigUpdateOperation` | Persist the common historical outcome within `StoredConfigUpdateOperation`; govern its complete dependency closure as durable. | +| `ProviderMutationReceipt` | Persist the immutable provider projection within `StoredConfigUpdateOperation`; govern its complete dependency closure as durable. | +| `ProviderReadinessReason` | Persist only the closed snapshot failure category within the operation; govern its enum values as durable. | The public/storage overlap is deliberate for the current format. Storage twins for the public roots are deferred: introducing them would require a broad @@ -662,6 +669,22 @@ configuration, valid endpoint-bound static credentials from other attached providers, and the dynamic credential snapshot. Provider environment revisions include profile endpoint and binding changes. +The supervisor owns provider fetching, support negotiation, and credential resolution outside the workload. The authenticated sandbox boundary receives a revision and its prepared child environment from one snapshot; it preserves the issued placeholders without receiving the secret resolver or turning those placeholders into new references. + +Provider mutations return immutable per-sandbox receipts that separate saved desired state from observed runtime installation. A receipt pins sandbox and provider identity, the attachment epoch, and the exact provider/configuration/policy fingerprints. Attachment-set changes replace the epoch in the same sandbox CAS write; credential updates stage distinct backend objects before publishing their handles and provider revision. This prevents a published revision from referring to an unfinished in-place credential write. Update receipts retain the sandbox target set selected before publication. + +Each receipt projects a common configuration operation in the `config_update_operation` store; its receipt ID is the operation ID. The provider status path evaluates current installation evidence and records historical terminal outcomes through resource-version CAS. A previously applied operation does not bypass current session, freshness, or authority checks. The live provider projection can be pending after disconnection or superseded after another change even when historical operation state remains applied. Pending operations are evaluated through provider status queries; this path adds no background delivery engine or mutation replay contract. + +Provider mutation and operation-result writes are separate. A result-storage failure can follow a saved mutation and returns structured uncertainty without a rollback or safe-retry claim. A failed initial snapshot remains failed rather than acquiring a different target during a later lookup. Operations contain only identities, revisions, timestamps, and closed reason categories. + +Provider installation reports belong to the existing `ConnectSupervisor` session. Each report names that session, has an increasing sequence, and expires unless the supervisor reports again. Reconnection or disconnect invalidates prior observations; stored change records survive a gateway restart, but runtime evidence does not. Replaying an identical report cannot extend its lifetime. + +Reports and status also compare the supervisor instance with the sandbox's persisted current instance. A different supervisor becoming current invalidates an older connection, including one retained by another gateway replica. Observations stay local to the gateway holding the supervisor session; a status request reaching a replica without that session returns pending. Multi-replica deployments therefore retain the existing supervisor-session routing requirement. + +The supervisor reports success only after it installs the matching credentials, activates the effective policy, and receives an acknowledgment from the authenticated workload boundary that it installed the environment for future processes. Environment synchronization shares the process-launch lock, and its acknowledgment identifies the exact publication, including retries at the same provider revision. Failed policy installation cannot reuse evidence for a different installed policy. Ready and revoked statuses also recheck the requested sandbox, provider, attachment and configuration identities; revision fingerprints are compared only for equality. Revocation applies to future credential resolution and future processes. Requests already forwarded upstream can still finish. + +Ordinary static credentials retain revision-scoped references. After update readiness, a newly launched process receives the updated reference; an existing process keeps its original revision. Installation completion does not retarget that reference or establish that an old upstream key can be retired. + ## Provider Environment Resolution The gateway resolves only the providers attached to a sandbox. It combines each diff --git a/crates/openshell-cli/src/commands/mod.rs b/crates/openshell-cli/src/commands/mod.rs index 8d75cf5ae3..5158a4ac5d 100644 --- a/crates/openshell-cli/src/commands/mod.rs +++ b/crates/openshell-cli/src/commands/mod.rs @@ -4,3 +4,4 @@ pub mod common; pub mod gateway; pub mod provider; +pub mod provider_readiness; diff --git a/crates/openshell-cli/src/commands/provider.rs b/crates/openshell-cli/src/commands/provider.rs index b85fef0013..8658b26956 100644 --- a/crates/openshell-cli/src/commands/provider.rs +++ b/crates/openshell-cli/src/commands/provider.rs @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +use super::provider_readiness::{ + ProviderMutationExpectation, ProviderWaitOptions, finish_provider_mutation, +}; use crate::color::Colorize; use crate::commands::common::{ format_epoch_ms, format_optional_epoch_ms, parse_credential_expiry_pairs, @@ -19,9 +22,9 @@ use openshell_core::proto::{ LintProviderProfilesRequest, ListProviderProfilesRequest, ListProvidersRequest, ListSandboxProvidersRequest, Provider, ProviderCredentialRefreshRecoveryAction, ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy, - ProviderCredentialTokenGrantType, ProviderProfile, ProviderProfileDiagnostic, - ProviderProfileImportItem, RotateProviderCredentialRequest, UpdateProviderProfilesRequest, - UpdateProviderRequest, + ProviderCredentialTokenGrantType, ProviderMutationKind, ProviderProfile, + ProviderProfileDiagnostic, ProviderProfileImportItem, RotateProviderCredentialRequest, + UpdateProviderProfilesRequest, UpdateProviderRequest, }; use openshell_core::{ObjectId, ObjectName, ObjectWorkspace}; use openshell_providers::{ @@ -78,13 +81,16 @@ pub async fn sandbox_provider_list( Ok(()) } +/// Save a provider attachment and optionally wait for its installed authority. pub async fn sandbox_provider_attach( server: &str, name: &str, provider: &str, workspace: &str, tls: &TlsOptions, + readiness: ProviderWaitOptions<'_>, ) -> Result<()> { + readiness.validate()?; let mut client = grpc_client(server, tls).await?; // Fetch current sandbox to get resource_version for CAS @@ -94,7 +100,7 @@ pub async fn sandbox_provider_attach( workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await - .into_diagnostic()? + .map_err(|status| miette!("provider attachment lookup failed ({})", status.code()))? .into_inner() .sandbox .ok_or_else(|| miette::miette!("sandbox not found"))?; @@ -115,32 +121,43 @@ pub async fn sandbox_provider_attach( return Err(miette::miette!( "Failed to attach provider: sandbox was modified by another operation.\n\ Please retry the command." - ) - .with_source_code(status.message().to_string())); + )); } - Err(e) => return Err(e).into_diagnostic(), + Err(error) => return Err(miette!("provider attachment failed ({})", error.code())), }; - if response.attached { - println!( - "{} Attached provider {} to sandbox {}", - "✓".green().bold(), - provider, - name - ); - } else { - println!("Provider {provider} is already attached to sandbox {name}."); - } - Ok(()) + let receipt = response.receipt.ok_or_else(|| { + miette!( + "gateway did not return a provider receipt; saved attachment cannot establish readiness" + ) + })?; + let mutation_id = receipt.mutation_id.clone(); + finish_provider_mutation( + &client, + &mutation_id, + vec![receipt], + ProviderMutationExpectation { + workspace, + provider_name: provider, + kind: ProviderMutationKind::Attach, + sandbox: Some((name, sandbox.object_id())), + provider: None, + }, + readiness, + ) + .await } +/// Save a provider detachment and optionally wait for future authority revocation. pub async fn sandbox_provider_detach( server: &str, name: &str, provider: &str, workspace: &str, tls: &TlsOptions, + readiness: ProviderWaitOptions<'_>, ) -> Result<()> { + readiness.validate()?; let mut client = grpc_client(server, tls).await?; // Fetch current sandbox to get resource_version for CAS @@ -150,7 +167,7 @@ pub async fn sandbox_provider_detach( workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await - .into_diagnostic()? + .map_err(|status| miette!("provider detachment lookup failed ({})", status.code()))? .into_inner() .sandbox .ok_or_else(|| miette::miette!("sandbox not found"))?; @@ -171,23 +188,27 @@ pub async fn sandbox_provider_detach( return Err(miette::miette!( "Failed to detach provider: sandbox was modified by another operation.\n\ Please retry the command." - ) - .with_source_code(status.message().to_string())); + )); } - Err(e) => return Err(e).into_diagnostic(), + Err(error) => return Err(miette!("provider detachment failed ({})", error.code())), }; - if response.detached { - println!( - "{} Detached provider {} from sandbox {}", - "✓".green().bold(), - provider, - name - ); - } else { - println!("Provider {provider} was not attached to sandbox {name}."); - } - Ok(()) + let receipt = response.receipt.ok_or_else(|| miette!("gateway did not return a provider receipt; saved detachment cannot establish revocation"))?; + let mutation_id = receipt.mutation_id.clone(); + finish_provider_mutation( + &client, + &mutation_id, + vec![receipt], + ProviderMutationExpectation { + workspace, + provider_name: provider, + kind: ProviderMutationKind::Detach, + sandbox: Some((name, sandbox.object_id())), + provider: None, + }, + readiness, + ) + .await } fn print_provider_attachment_table(providers: &[Provider]) { @@ -722,11 +743,16 @@ async fn fetch_provider_profile( "provider profile '{requested}' not found; import a matching profile before using this provider type" ) } else { - miette::miette!(fallback_status.to_string()) + miette!("provider profile lookup failed ({})", fallback_status.code()) } })? } - Err(status) => return Err(miette::miette!(status.to_string())), + Err(status) => { + return Err(miette!( + "provider profile lookup failed ({})", + status.code() + )); + } }; Ok(response) @@ -2201,6 +2227,7 @@ fn print_provider_type_row( ); } +/// Credential update inputs and observation choices for all attached sandboxes. pub struct ProviderUpdateOptions<'a> { pub server: &'a str, pub name: &'a str, @@ -2211,8 +2238,11 @@ pub struct ProviderUpdateOptions<'a> { pub credential_expires_at: &'a [String], pub workspace: &'a str, pub tls: &'a TlsOptions, + /// Bound observation of the sandbox target set selected by the update. + pub readiness: ProviderWaitOptions<'a>, } +/// Update provider credentials and report each selected sandbox independently. pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> { let ProviderUpdateOptions { server, @@ -2224,7 +2254,9 @@ pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> { credential_expires_at, workspace, tls, + readiness, } = options; + readiness.validate()?; if from_existing && !credentials.is_empty() { return Err(miette::miette!( @@ -2262,7 +2294,7 @@ pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> { { None } - Err(status) => return Err(status).into_diagnostic(), + Err(status) => return Err(miette!("provider update lookup failed ({})", status.code())), }; if existing.is_none() && (from_existing || from_oidc_token) { @@ -2338,19 +2370,28 @@ pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> { workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), }) .await - .into_diagnostic()?; + .map_err(|error| miette!("provider update failed ({})", error.code()))?; - let provider = response - .into_inner() - .provider - .ok_or_else(|| miette::miette!("provider missing from response"))?; - - println!( - "{} Updated provider {}", - "✓".green().bold(), - provider.object_name() - ); - Ok(()) + let response = response.into_inner(); + if response.mutation_id.is_empty() { + return Err(miette!( + "gateway did not return a provider mutation receipt; saved credentials cannot establish readiness" + )); + } + finish_provider_mutation( + &client, + &response.mutation_id, + response.target_receipts, + ProviderMutationExpectation { + workspace, + provider_name: name, + kind: ProviderMutationKind::Update, + sandbox: None, + provider: response.provider.as_ref(), + }, + readiness, + ) + .await } pub async fn provider_delete( diff --git a/crates/openshell-cli/src/commands/provider_readiness.rs b/crates/openshell-cli/src/commands/provider_readiness.rs new file mode 100644 index 0000000000..d68d3a610d --- /dev/null +++ b/crates/openshell-cli/src/commands/provider_readiness.rs @@ -0,0 +1,545 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Secret-free provider receipts, status rendering, and bounded CLI waits. + +use crate::tls::{GrpcClient, TlsOptions, grpc_client}; +use futures::{StreamExt, stream}; +use miette::{IntoDiagnostic, Result, miette}; +use openshell_core::proto::{ + GetSandboxProviderStatusRequest, Provider, ProviderMutationKind, ProviderMutationReceipt, + ProviderReadinessReason, ProviderReadinessState, ProviderReadinessStatus, +}; +use openshell_sdk::provider_readiness::{ + ProviderWaitOutcome, persisted_status, provider_status, provider_wait_deadline, + validate_provider_receipt, wait_for_provider_status_until, +}; +use std::collections::HashSet; +use std::time::Duration; +use tokio::time::Instant; + +const PROVIDER_WAIT_CONCURRENCY: usize = 16; + +/// Output and deadline choices shared by provider mutation and status commands. +#[derive(Clone, Copy)] +pub struct ProviderWaitOptions<'a> { + /// Wait for current runtime installation after saving the desired mutation. + pub wait: bool, + /// One total deadline for all selected sandbox observations. + pub timeout: Duration, + /// CLI output format: table, JSON, or YAML. + pub output: &'a str, +} + +impl Default for ProviderWaitOptions<'_> { + fn default() -> Self { + Self { + wait: false, + timeout: Duration::from_secs(30), + output: "table", + } + } +} + +impl ProviderWaitOptions<'_> { + /// Reject invalid wait settings before performing a provider mutation. + pub fn validate(self) -> Result<()> { + provider_wait_deadline(self.timeout).into_diagnostic()?; + if !matches!(self.output, "table" | "json" | "yaml") { + return Err(miette!("unsupported provider status output format")); + } + Ok(()) + } +} + +struct DisplayStatus { + status: ProviderReadinessStatus, + outcome: &'static str, + complete: bool, +} + +/// Caller-selected authority that every returned mutation receipt must retain. +#[derive(Clone, Copy)] +pub(super) struct ProviderMutationExpectation<'a> { + /// Workspace explicitly selected by the command. + pub workspace: &'a str, + /// Provider name explicitly selected by the command. + pub provider_name: &'a str, + /// Operation submitted to the gateway. + pub kind: ProviderMutationKind, + /// Requested sandbox name and previously fetched object ID for attach/detach. + pub sandbox: Option<(&'a str, &'a str)>, + /// Published provider object returned by an update, including its revision. + pub provider: Option<&'a Provider>, +} + +fn validate_mutation_receipts( + mutation_id: &str, + receipts: &[ProviderMutationReceipt], + expected: &ProviderMutationExpectation<'_>, +) -> Result<()> { + let invalid = || miette!("gateway returned an invalid provider mutation receipt"); + if mutation_id.is_empty() || expected.workspace.is_empty() || expected.provider_name.is_empty() + { + return Err(invalid()); + } + let provider = match expected.kind { + ProviderMutationKind::Attach | ProviderMutationKind::Detach => { + if receipts.len() != 1 + || expected + .sandbox + .is_none_or(|(name, id)| name.is_empty() || id.is_empty()) + { + return Err(invalid()); + } + None + } + ProviderMutationKind::Update => { + let metadata = expected + .provider + .and_then(|provider| provider.metadata.as_ref()) + .ok_or_else(invalid)?; + if metadata.id.is_empty() + || metadata.name != expected.provider_name + || metadata.workspace != expected.workspace + { + return Err(invalid()); + } + Some(metadata) + } + ProviderMutationKind::Unspecified | ProviderMutationKind::Observe => return Err(invalid()), + }; + let mut receipt_ids = HashSet::new(); + let mut sandbox_ids = HashSet::new(); + for receipt in receipts { + validate_provider_receipt(receipt).into_diagnostic()?; + let desired = receipt.desired.as_ref().ok_or_else(invalid)?; + // Validate the complete batch before polling or printing saved intent. + // Otherwise a substituted receipt can redirect a wait to another scope. + if receipt.mutation_id != mutation_id + || receipt.workspace != expected.workspace + || receipt.provider_name != expected.provider_name + || receipt.kind != i32::from(expected.kind) + || !receipt_ids.insert(receipt.receipt_id.as_str()) + || !sandbox_ids.insert(desired.sandbox_id.as_str()) + || expected + .sandbox + .is_some_and(|(name, id)| desired.sandbox_name != name || desired.sandbox_id != id) + || provider.is_some_and(|provider| { + desired.provider_id != provider.id + || desired.provider_resource_version != provider.resource_version + }) + { + return Err(invalid()); + } + } + Ok(()) +} + +/// Display persisted targets and optionally wait for each original receipt. +/// +/// Every target is reported even when another target fails. Concurrency is +/// bounded and all requests share one deadline, including queued targets. +pub(super) async fn finish_provider_mutation( + client: &GrpcClient, + mutation_id: &str, + receipts: Vec, + expected: ProviderMutationExpectation<'_>, + options: ProviderWaitOptions<'_>, +) -> Result<()> { + options.validate()?; + validate_mutation_receipts(mutation_id, &receipts, &expected)?; + let deadline = provider_wait_deadline(options.timeout).into_diagnostic()?; + let statuses = receipts.into_iter().map(persisted_status).collect(); + let results = if options.wait { + wait_for_mutation_statuses(client, statuses, deadline).await + } else { + statuses + .into_iter() + .map(|status| DisplayStatus { + status, + outcome: "not_requested", + complete: false, + }) + .collect() + }; + print_statuses(mutation_id, &results, options.output)?; + if options.wait && results.iter().any(|result| !result.complete) { + return Err(miette!( + "provider readiness wait did not complete for every selected sandbox; inspect the reported outcomes" + )); + } + Ok(()) +} + +async fn wait_for_mutation_statuses( + client: &GrpcClient, + statuses: Vec, + deadline: Instant, +) -> Vec { + let mut pending = statuses.into_iter().enumerate().collect::>(); + let mut results = Vec::with_capacity(pending.len()); + while !pending.is_empty() && Instant::now() < deadline { + // Divide the remaining time among actual queued batches. When every + // target fits in one batch, healthy but slow RPCs get the full deadline; + // queued targets still get a turn if the preceding batch stalls. + let batches = + u32::try_from(pending.len().div_ceil(PROVIDER_WAIT_CONCURRENCY)).unwrap_or(u32::MAX); + let slice = deadline.saturating_duration_since(Instant::now()) / batches; + if slice.is_zero() { + break; + } + let round = stream::iter(pending.into_iter().map(|(index, status)| { + let mut client = client.clone(); + async move { + let slice_deadline = (Instant::now() + slice).min(deadline); + let result = + wait_for_provider_status_until(&mut client, &status, slice_deadline).await; + (index, status, result) + } + })) + .buffer_unordered(PROVIDER_WAIT_CONCURRENCY) + .collect::>() + .await; + pending = Vec::new(); + for (index, previous, result) in round { + match result { + // The SDK pins the receipt in this status and retains its last + // observation. Only unfinished targets enter the next round. + Ok(result) + if result.outcome == ProviderWaitOutcome::TimedOut + && Instant::now() < deadline => + { + pending.push((index, result.status)); + } + Ok(result) => { + results.push(( + index, + DisplayStatus { + status: result.status, + outcome: match result.outcome { + ProviderWaitOutcome::Complete => "complete", + ProviderWaitOutcome::TimedOut => "timed_out", + ProviderWaitOutcome::Terminal => "terminal", + }, + complete: result.outcome == ProviderWaitOutcome::Complete, + }, + )); + } + // A transport error cannot prove installation failure. Preserve + // the last observation and expose only the safe error category. + Err(_) => { + results.push(( + index, + DisplayStatus { + status: previous, + outcome: "observation_error", + complete: false, + }, + )); + } + } + } + } + results.extend(pending.into_iter().map(|(index, status)| { + ( + index, + DisplayStatus { + status, + outcome: "timed_out", + complete: false, + }, + ) + })); + // Fair polling must not change the command's stable target output order. + results.sort_unstable_by_key(|(index, _)| *index); + results.into_iter().map(|(_, result)| result).collect() +} + +/// Show desired/observed provider state, optionally waiting for that exact state. +pub async fn sandbox_provider_status( + server: &str, + name: &str, + provider: &str, + receipt_id: &str, + workspace: &str, + tls: &TlsOptions, + options: ProviderWaitOptions<'_>, +) -> Result<()> { + options.validate()?; + let mut client = grpc_client(server, tls).await?; + let deadline = provider_wait_deadline(options.timeout).into_diagnostic()?; + let status = tokio::time::timeout_at( + deadline, + provider_status( + &mut client, + GetSandboxProviderStatusRequest { + sandbox_name: name.to_string(), + provider_name: provider.to_string(), + receipt_id: receipt_id.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector(workspace)), + }, + ), + ) + .await + .map_err(|_| miette!("provider status request timed out"))? + .into_diagnostic()?; + let receipt = status + .receipt + .as_ref() + .ok_or_else(|| miette!("gateway returned no provider receipt"))?; + let mutation_id = receipt.mutation_id.clone(); + let result = if options.wait { + let result = wait_for_provider_status_until(&mut client, &status, deadline) + .await + .into_diagnostic()?; + DisplayStatus { + status: result.status, + outcome: match result.outcome { + ProviderWaitOutcome::Complete => "complete", + ProviderWaitOutcome::TimedOut => "timed_out", + ProviderWaitOutcome::Terminal => "terminal", + }, + complete: result.outcome == ProviderWaitOutcome::Complete, + } + } else { + DisplayStatus { + status, + outcome: "not_requested", + complete: false, + } + }; + let complete = result.complete; + print_statuses(&mutation_id, &[result], options.output)?; + if options.wait && !complete { + return Err(miette!( + "provider readiness wait did not complete; inspect the reported outcome" + )); + } + Ok(()) +} + +fn state_label(state: i32) -> &'static str { + match ProviderReadinessState::try_from(state) { + Ok(ProviderReadinessState::Persisted) => "persisted", + Ok(ProviderReadinessState::Pending) => "pending", + Ok(ProviderReadinessState::Ready) => "ready", + Ok(ProviderReadinessState::Withheld) => "withheld", + Ok(ProviderReadinessState::Revoked) => "revoked", + Ok(ProviderReadinessState::Failed) => "failed", + Ok(ProviderReadinessState::Superseded) => "superseded", + _ => "unknown", + } +} + +fn reason_label(reason: i32) -> String { + ProviderReadinessReason::try_from(reason).map_or_else( + |_| "unknown".to_string(), + |reason| { + reason + .as_str_name() + .trim_start_matches("PROVIDER_READINESS_REASON_") + .to_ascii_lowercase() + }, + ) +} + +fn receipt_json(receipt: &ProviderMutationReceipt) -> serde_json::Value { + let desired = receipt.desired.as_ref().map(|desired| { + serde_json::json!({ + "sandbox_id": desired.sandbox_id, + "sandbox_name": desired.sandbox_name, + "attachment_epoch": desired.attachment_epoch, + "provider_id": desired.provider_id, + "provider_resource_version": desired.provider_resource_version.to_string(), + "provider_env_revision": desired.provider_env_revision.to_string(), + "config_revision": desired.config_revision.to_string(), + "policy_hash": desired.policy_hash, + }) + }); + serde_json::json!({ + "receipt_id": receipt.receipt_id, + "mutation_id": receipt.mutation_id, + "provider_name": receipt.provider_name, + "workspace": receipt.workspace, + "kind": receipt.kind().as_str_name().trim_start_matches("PROVIDER_MUTATION_KIND_").to_ascii_lowercase(), + "desired": desired, + "persisted_at_ms": receipt.persisted_at_ms, + }) +} + +fn status_json(result: &DisplayStatus) -> serde_json::Value { + let observed = result.status.observed.as_ref().map(|observed| { + serde_json::json!({ + "session_id": observed.session_id, + "sequence": observed.sequence.to_string(), + "attachment_epoch": observed.attachment_epoch, + "provider_env_revision": observed.provider_env_revision.to_string(), + "config_revision": observed.config_revision.to_string(), + "policy_hash": observed.policy_hash, + "credentials_installed": observed.credentials_installed, + "policy_active": observed.policy_active, + "launch_environment_installed": observed.launch_environment_installed, + "process_instance_id": observed.process_instance_id, + "reason": reason_label(observed.reason), + }) + }); + serde_json::json!({ + "receipt": result.status.receipt.as_ref().map(receipt_json), + "state": state_label(result.status.state), + "reason": reason_label(result.status.reason), + "observed": observed, + "network_instance_id": result.status.network_instance_id, + "observed_at_ms": result.status.observed_at_ms, + "evaluated_at_ms": result.status.evaluated_at_ms, + "wait_outcome": result.outcome, + }) +} + +fn print_statuses(mutation_id: &str, results: &[DisplayStatus], output: &str) -> Result<()> { + let value = serde_json::json!({ + "mutation_id": mutation_id, + "targets": results.iter().map(status_json).collect::>(), + }); + if crate::output::print_output_single(output, &value, Clone::clone)? { + return Ok(()); + } + if results.is_empty() { + println!("Provider mutation {mutation_id} persisted; no attached sandboxes were selected."); + return Ok(()); + } + for result in results { + let Some(receipt) = result.status.receipt.as_ref() else { + continue; + }; + let sandbox = receipt + .desired + .as_ref() + .map_or("unknown", |desired| desired.sandbox_name.as_str()); + println!( + "{} / {}: {} ({})", + sandbox, + receipt.provider_name, + state_label(result.status.state), + reason_label(result.status.reason) + ); + println!( + " Receipt: {}", + if receipt.receipt_id.is_empty() { + "current state" + } else { + &receipt.receipt_id + } + ); + println!( + " Persisted: {}", + crate::commands::common::format_epoch_ms(receipt.persisted_at_ms) + ); + if let Some(observed) = result.status.observed.as_ref() { + println!( + " Installed: credentials={}, policy={}, future process environment={}", + observed.credentials_installed, + observed.policy_active, + observed.launch_environment_installed + ); + } + if result.outcome != "not_requested" { + println!(" Wait: {}", result.outcome); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use openshell_core::proto::ProviderDesiredIdentity; + + #[test] + fn mutation_receipts_reject_duplicate_targets_and_allow_initial_fingerprints() { + let provider = Provider { + metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { + id: "provider-id".to_string(), + name: "provider".to_string(), + workspace: "default".to_string(), + resource_version: 0, + ..Default::default() + }), + ..Default::default() + }; + let expected = ProviderMutationExpectation { + workspace: "default", + provider_name: "provider", + kind: ProviderMutationKind::Update, + sandbox: None, + provider: Some(&provider), + }; + let receipt = ProviderMutationReceipt { + receipt_id: "receipt".to_string(), + mutation_id: "mutation".to_string(), + provider_name: "provider".to_string(), + workspace: "default".to_string(), + kind: ProviderMutationKind::Update.into(), + persisted_at_ms: 1, + desired: Some(ProviderDesiredIdentity { + sandbox_id: "sandbox-id".to_string(), + sandbox_name: "sandbox".to_string(), + provider_id: "provider-id".to_string(), + ..Default::default() + }), + }; + assert!( + validate_mutation_receipts("mutation", std::slice::from_ref(&receipt), &expected) + .is_ok() + ); + assert!(validate_mutation_receipts("mutation", &[], &expected).is_ok()); + let mut duplicate = receipt.clone(); + duplicate.receipt_id = "other-receipt".to_string(); + assert!( + validate_mutation_receipts("mutation", &[receipt.clone(), duplicate], &expected) + .is_err() + ); + let mut duplicate = receipt.clone(); + duplicate + .desired + .as_mut() + .expect("desired identity") + .sandbox_id = "other-sandbox".to_string(); + assert!(validate_mutation_receipts("mutation", &[receipt, duplicate], &expected).is_err()); + assert!( + validate_mutation_receipts( + "mutation", + &[], + &ProviderMutationExpectation { + provider: None, + ..expected + } + ) + .is_err() + ); + } + + #[test] + fn structured_status_preserves_opaque_revisions_and_separates_timeout() { + let receipt = ProviderMutationReceipt { + desired: Some(ProviderDesiredIdentity { + provider_env_revision: u64::MAX, + ..Default::default() + }), + ..Default::default() + }; + let value = status_json(&DisplayStatus { + status: persisted_status(receipt), + outcome: "timed_out", + complete: false, + }); + assert_eq!(value["state"], "persisted"); + assert_eq!(value["wait_outcome"], "timed_out"); + assert_eq!( + value["receipt"]["desired"]["provider_env_revision"], + u64::MAX.to_string() + ); + assert!(value.get("environment").is_none()); + assert!(value.get("load_error").is_none()); + } +} diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index fdec4ce7e5..a17d4c2e92 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -944,6 +944,9 @@ enum ProviderCommands { /// Credential expiry (`KEY=TIMESTAMP`). Accepts epoch milliseconds or RFC3339. A zero timestamp clears expiry. #[arg(long = "credential-expires-at", value_name = "KEY=TIMESTAMP")] credential_expires_at: Vec, + + #[command(flatten)] + readiness: ProviderReadinessArgs, }, /// Delete providers by name. @@ -1624,6 +1627,32 @@ enum SandboxCommands { Template(SandboxTemplateCommands), } +/// Common observation flags; the deadline starts after the mutation is saved. +#[derive(clap::Args, Debug)] +struct ProviderReadinessArgs { + /// Wait until the sandbox applies the credentials, policy, and environment for new processes. + #[arg(long)] + wait: bool, + + /// Maximum wait in seconds after saving the change, shared by all selected sandboxes. + #[arg(long, default_value_t = 30, value_parser = clap::value_parser!(u64).range(1..=3600))] + timeout: u64, + + /// Output format; JSON and YAML include change IDs and results for each sandbox. + #[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table)] + output: OutputFormat, +} + +impl ProviderReadinessArgs { + fn options(&self) -> run::ProviderWaitOptions<'_> { + run::ProviderWaitOptions { + wait: self.wait, + timeout: std::time::Duration::from_secs(self.timeout), + output: self.output.as_str(), + } + } +} + #[derive(Subcommand, Debug)] enum SandboxProviderCommands { /// List providers attached to a sandbox. @@ -1648,6 +1677,9 @@ enum SandboxProviderCommands { /// Provider name to attach. #[arg(add = ArgValueCompleter::new(completers::complete_provider_names))] provider: String, + + #[command(flatten)] + readiness: ProviderReadinessArgs, }, /// Detach a provider from a sandbox. @@ -1660,6 +1692,28 @@ enum SandboxProviderCommands { /// Provider name to detach. #[arg(add = ArgValueCompleter::new(completers::complete_provider_names))] provider: String, + + #[command(flatten)] + readiness: ProviderReadinessArgs, + }, + + /// Check whether a sandbox has applied a provider change. + #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] + Status { + /// Sandbox name. + #[arg(add = ArgValueCompleter::new(completers::complete_sandbox_names))] + name: String, + + /// Provider whose attachment or revocation is being inspected. + #[arg(add = ArgValueCompleter::new(completers::complete_provider_names))] + provider: String, + + /// Change ID returned by attach, detach, or update; omitted means the latest saved state. + #[arg(long = "receipt")] + receipt: Option, + + #[command(flatten)] + readiness: ProviderReadinessArgs, }, } @@ -3356,23 +3410,50 @@ async fn run_async() -> Result<()> { ) .await?; } - SandboxProviderCommands::Attach { name, provider } => { + SandboxProviderCommands::Attach { + name, + provider, + readiness, + } => { run::sandbox_provider_attach( endpoint, &name, &provider, &cli.workspace, &tls, + readiness.options(), ) .await?; } - SandboxProviderCommands::Detach { name, provider } => { + SandboxProviderCommands::Detach { + name, + provider, + readiness, + } => { run::sandbox_provider_detach( endpoint, &name, &provider, &cli.workspace, &tls, + readiness.options(), + ) + .await?; + } + SandboxProviderCommands::Status { + name, + provider, + receipt, + readiness, + } => { + run::sandbox_provider_status( + endpoint, + &name, + &provider, + receipt.as_deref().unwrap_or_default(), + &cli.workspace, + &tls, + readiness.options(), ) .await?; } @@ -3733,6 +3814,7 @@ async fn run_async() -> Result<()> { credentials, config, credential_expires_at, + readiness, } => { run::provider_update(run::ProviderUpdateOptions { server: endpoint, @@ -3744,6 +3826,7 @@ async fn run_async() -> Result<()> { credential_expires_at: &credential_expires_at, workspace: &cli.workspace, tls: &tls, + readiness: readiness.options(), }) .await?; } @@ -4030,7 +4113,9 @@ mod tests { let Some(Commands::Sandbox { command: - Some(SandboxCommands::Provider(SandboxProviderCommands::Attach { name, provider })), + Some(SandboxCommands::Provider(SandboxProviderCommands::Attach { + name, provider, .. + })), }) = cli.command else { panic!("expected sandbox provider attach command"); @@ -4040,6 +4125,68 @@ mod tests { assert_eq!(provider, "work-github"); } + #[test] + fn provider_readiness_commands_have_bounded_waits_and_structured_output() { + for action in ["attach", "detach", "status"] { + let cli = Cli::try_parse_from([ + "openshell", + "sandbox", + "provider", + action, + "sandbox", + "provider", + "--wait", + "--timeout", + "45", + "--output", + "json", + ]) + .expect("readiness flags should parse"); + let Some(Commands::Sandbox { + command: Some(SandboxCommands::Provider(command)), + }) = cli.command + else { + panic!("expected provider command") + }; + let (SandboxProviderCommands::Attach { readiness, .. } + | SandboxProviderCommands::Detach { readiness, .. } + | SandboxProviderCommands::Status { readiness, .. }) = command + else { + panic!("expected readiness command"); + }; + assert!(readiness.wait); + assert_eq!(readiness.timeout, 45); + assert_eq!(readiness.output.as_str(), "json"); + } + for timeout in ["0", "3601"] { + assert!( + Cli::try_parse_from([ + "openshell", + "provider", + "update", + "provider", + "--wait", + "--timeout", + timeout, + ]) + .is_err() + ); + } + assert!( + Cli::try_parse_from([ + "openshell", + "sandbox", + "provider", + "status", + "sandbox", + "provider", + "--receipt", + "receipt-id", + ]) + .is_ok() + ); + } + #[test] fn completions_policy_flag_falls_back_to_file_paths() { let temp = tempfile::tempdir().expect("failed to create tempdir"); diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index 243e10d1bf..907bb55d47 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -32,6 +32,7 @@ pub use crate::commands::provider::{ provider_refresh_status, provider_rotate, provider_update, sandbox_provider_attach, sandbox_provider_detach, sandbox_provider_list, }; +pub use crate::commands::provider_readiness::{ProviderWaitOptions, sandbox_provider_status}; use crate::color::Colorize; use crate::policy_update::build_policy_update_plan; diff --git a/crates/openshell-cli/tests/ensure_providers_integration.rs b/crates/openshell-cli/tests/ensure_providers_integration.rs index 0f69287d97..450039c801 100644 --- a/crates/openshell-cli/tests/ensure_providers_integration.rs +++ b/crates/openshell-cli/tests/ensure_providers_integration.rs @@ -212,6 +212,24 @@ impl OpenShell for TestOpenShell { Ok(Response::new(GetGatewayConfigResponse::default())) } + async fn get_sandbox_provider_status( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "provider readiness is not exercised by this mock", + )) + } + + async fn report_provider_readiness( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "provider installation reports are not exercised by this mock", + )) + } + async fn get_sandbox_provider_environment( &self, _request: tonic::Request, @@ -293,6 +311,7 @@ impl OpenShell for TestOpenShell { providers.insert(provider_name, provider.clone()); Ok(Response::new(ProviderResponse { provider: Some(provider), + ..Default::default() })) } @@ -308,6 +327,7 @@ impl OpenShell for TestOpenShell { .ok_or_else(|| Status::not_found("provider not found"))?; Ok(Response::new(ProviderResponse { provider: Some(provider), + ..Default::default() })) } @@ -464,6 +484,7 @@ impl OpenShell for TestOpenShell { providers.insert(updated_name, updated.clone()); Ok(Response::new(ProviderResponse { provider: Some(updated), + ..Default::default() })) } async fn get_provider_refresh_status( diff --git a/crates/openshell-cli/tests/mtls_integration.rs b/crates/openshell-cli/tests/mtls_integration.rs index f916d704bd..6d43663d18 100644 --- a/crates/openshell-cli/tests/mtls_integration.rs +++ b/crates/openshell-cli/tests/mtls_integration.rs @@ -183,6 +183,24 @@ impl OpenShell for TestOpenShell { )) } + async fn get_sandbox_provider_status( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "provider readiness is not exercised by this mock", + )) + } + + async fn report_provider_readiness( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "provider installation reports are not exercised by this mock", + )) + } + async fn get_sandbox_provider_environment( &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 b7431bfc6d..f8cedfcc44 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -17,27 +17,60 @@ use openshell_core::proto::{ ExecSandboxInput, ExecSandboxRequest, GatewayMessage, GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderRefreshStatusRequest, GetProviderRefreshStatusResponse, GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, - GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, GetSandboxRequest, + GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, + GetSandboxProviderStatusRequest, GetSandboxProviderStatusResponse, GetSandboxRequest, HealthRequest, HealthResponse, ListProvidersRequest, ListProvidersResponse, ListSandboxProvidersRequest, ListSandboxProvidersResponse, ListSandboxesRequest, ListSandboxesResponse, Provider, ProviderCredentialRefresh, ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrant, - ProviderCredentialTokenGrantSubjectToken, ProviderCredentialTokenGrantType, ProviderProfile, - ProviderProfileCredential, ProviderProfileDiscovery, ProviderResponse, RevokeSshSessionRequest, - RevokeSshSessionResponse, RotateProviderCredentialRequest, RotateProviderCredentialResponse, - Sandbox, SandboxResponse, SandboxStreamEvent, ServiceStatus, SettingValue, SupervisorMessage, - UpdateProviderRequest, WatchSandboxRequest, + ProviderCredentialTokenGrantSubjectToken, ProviderCredentialTokenGrantType, + ProviderDesiredIdentity, ProviderMutationKind, ProviderMutationReceipt, ProviderProfile, + ProviderProfileCredential, ProviderProfileDiscovery, ProviderReadinessObservation, + ProviderReadinessReason, ProviderReadinessState, ProviderReadinessStatus, ProviderResponse, + RevokeSshSessionRequest, RevokeSshSessionResponse, RotateProviderCredentialRequest, + RotateProviderCredentialResponse, Sandbox, SandboxResponse, SandboxStreamEvent, ServiceStatus, + SettingValue, SupervisorMessage, UpdateProviderRequest, WatchSandboxRequest, }; use openshell_core::{ObjectId, ObjectName}; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::time::Duration; use tempfile::TempDir; use tokio::net::TcpListener; use tokio::sync::{Mutex, mpsc}; use tokio_stream::wrappers::TcpListenerStream; use tonic::transport::{Certificate as TlsCertificate, Identity, Server, ServerTlsConfig}; -use tonic::{Response, Status}; +use tonic::{Code, Response, Status}; + +type ReadinessScript = HashMap>; +type ReceiptCorruption = fn(&mut ProviderMutationReceipt); + +#[derive(Clone)] +enum ReadinessReply { + Status(Box), + DelayedStatus(Duration, Box), + Error(Code), + Hung, +} + +const READINESS_PROVIDER: &str = "readiness-provider"; +const SYNTHETIC_READINESS_CREDENTIAL: &str = "fixture-provider-credential"; +const SYNTHETIC_READINESS_BACKEND_ERROR: &str = "fixture-backend-authorization-details"; +const SYNTHETIC_PROFILE_BACKEND_ERROR: &str = "TESTLEAK"; + +fn selected_workspace( + scope: &Option, +) -> Option<&str> { + match scope.as_ref()?.selection.as_ref()? { + openshell_core::proto::datamodel::v1::workspace_selector::Selection::Workspace( + workspace, + ) => Some(workspace), + openshell_core::proto::datamodel::v1::workspace_selector::Selection::AllWorkspaces(_) => { + None + } + } +} #[derive(Clone, Default)] struct ProviderState { @@ -48,6 +81,10 @@ struct ProviderState { refresh_requests: Arc>>, provider_update_requests: Arc>>, deny_provider_reads: Arc, + fail_provider_reads: Arc, + fail_sandbox_reads: Arc, + profile_read_errors: Arc>>, + profile_read_requests: Arc>>, delete_provider_requests: Arc>>, delete_provider_profile_requests: Arc>>, fail_configure_refresh_message: Arc>>, @@ -56,6 +93,11 @@ struct ProviderState { fail_delete_provider_profile_message: Arc>>, sandbox_providers: Arc>>>, sandbox_provider_requests: Arc>>, + readiness_receipts: Arc>>, + readiness_scripts: Arc>, + readiness_requests: Arc>>, + readiness_sequence: Arc, + corrupt_mutation_receipt: Arc>>, global_settings: Arc>>, } @@ -102,6 +144,68 @@ struct TestOpenShell { state: ProviderState, } +impl TestOpenShell { + async fn provider_receipt( + &self, + sandbox_name: &str, + provider_name: &str, + workspace: &str, + kind: ProviderMutationKind, + mutation_id: Option<&str>, + ) -> ProviderMutationReceipt { + let sequence = self.state.readiness_sequence.fetch_add(1, Ordering::SeqCst) + 1; + let provider = self + .state + .providers + .lock() + .await + .get(provider_name) + .cloned(); + let detached = kind == ProviderMutationKind::Detach; + let mut receipt = ProviderMutationReceipt { + receipt_id: format!("receipt-{sequence}"), + mutation_id: mutation_id.map_or_else(|| format!("mutation-{sequence}"), str::to_string), + provider_name: provider_name.to_string(), + workspace: workspace.to_string(), + kind: kind.into(), + desired: Some(ProviderDesiredIdentity { + sandbox_id: format!("sb-{sandbox_name}"), + sandbox_name: sandbox_name.to_string(), + attachment_epoch: format!("attachment-{sandbox_name}"), + provider_id: if detached { + String::new() + } else { + provider + .as_ref() + .map_or_else(String::new, |provider| provider.object_id().to_string()) + }, + provider_resource_version: if detached { + 0 + } else { + provider + .as_ref() + .and_then(|provider| provider.metadata.as_ref()) + .map_or(0, |metadata| metadata.resource_version) + }, + provider_env_revision: sequence, + config_revision: 17, + policy_hash: "effective-policy".to_string(), + }), + persisted_at_ms: i64::try_from(sequence).unwrap(), + }; + let corrupt = *self.state.corrupt_mutation_receipt.lock().await; + if let Some(corrupt) = corrupt { + corrupt(&mut receipt); + } + self.state + .readiness_receipts + .lock() + .await + .insert(receipt.receipt_id.clone(), receipt.clone()); + receipt + } +} + #[tonic::async_trait] impl OpenShell for TestOpenShell { async fn report_endpoint_status( @@ -183,6 +287,9 @@ impl OpenShell for TestOpenShell { &self, request: tonic::Request, ) -> Result, Status> { + if self.state.fail_sandbox_reads.load(Ordering::SeqCst) { + return Err(Status::internal(SYNTHETIC_READINESS_BACKEND_ERROR)); + } let name = request.into_inner().name; // Return a minimal sandbox with metadata for CAS operations Ok(Response::new(SandboxResponse { @@ -246,6 +353,9 @@ impl OpenShell for TestOpenShell { request: tonic::Request, ) -> Result, Status> { let request = request.into_inner(); + let workspace = selected_workspace(&request.workspace_scope) + .filter(|workspace| !workspace.is_empty()) + .ok_or_else(|| Status::invalid_argument("one explicit workspace is required"))?; self.state .sandbox_provider_requests .lock() @@ -273,13 +383,24 @@ impl OpenShell for TestOpenShell { providers.push(request.provider_name.clone()); true }; + let provider_names = providers.clone(); + drop(sandbox_providers); + let receipt = self + .provider_receipt( + &request.sandbox_name, + &request.provider_name, + workspace, + ProviderMutationKind::Attach, + None, + ) + .await; let sandbox = Sandbox { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { name: request.sandbox_name, ..Default::default() }), spec: Some(openshell_core::proto::SandboxSpec { - providers: providers.clone(), + providers: provider_names, ..Default::default() }), ..Default::default() @@ -287,6 +408,7 @@ impl OpenShell for TestOpenShell { Ok(Response::new(AttachSandboxProviderResponse { sandbox: Some(sandbox), attached, + receipt: Some(receipt), })) } @@ -295,6 +417,9 @@ impl OpenShell for TestOpenShell { request: tonic::Request, ) -> Result, Status> { let request = request.into_inner(); + let workspace = selected_workspace(&request.workspace_scope) + .filter(|workspace| !workspace.is_empty()) + .ok_or_else(|| Status::invalid_argument("one explicit workspace is required"))?; self.state .sandbox_provider_requests .lock() @@ -310,13 +435,24 @@ impl OpenShell for TestOpenShell { let before_len = providers.len(); providers.retain(|name| name != &request.provider_name); let detached = providers.len() != before_len; + let provider_names = providers.clone(); + drop(sandbox_providers); + let receipt = self + .provider_receipt( + &request.sandbox_name, + &request.provider_name, + workspace, + ProviderMutationKind::Detach, + None, + ) + .await; let sandbox = Sandbox { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { name: request.sandbox_name, ..Default::default() }), spec: Some(openshell_core::proto::SandboxSpec { - providers: providers.clone(), + providers: provider_names, ..Default::default() }), ..Default::default() @@ -324,6 +460,7 @@ impl OpenShell for TestOpenShell { Ok(Response::new(DetachSandboxProviderResponse { sandbox: Some(sandbox), detached, + receipt: Some(receipt), })) } @@ -360,6 +497,97 @@ impl OpenShell for TestOpenShell { )) } + async fn get_sandbox_provider_status( + &self, + request: tonic::Request, + ) -> Result, Status> { + let request = request.into_inner(); + let workspace = selected_workspace(&request.workspace_scope) + .filter(|workspace| !workspace.is_empty()) + .ok_or_else(|| Status::invalid_argument("one explicit workspace is required"))?; + self.state + .readiness_requests + .lock() + .await + .push(request.clone()); + let receipts = self.state.readiness_receipts.lock().await; + let receipt = if request.receipt_id.is_empty() { + receipts + .values() + .filter(|receipt| { + receipt.workspace == workspace + && receipt.provider_name == request.provider_name + && receipt + .desired + .as_ref() + .is_some_and(|desired| desired.sandbox_name == request.sandbox_name) + }) + .max_by_key(|receipt| receipt.persisted_at_ms) + } else { + receipts + .get(&request.receipt_id) + .filter(|receipt| receipt.workspace == workspace) + } + .cloned() + .ok_or_else(|| Status::not_found("provider receipt not found"))?; + drop(receipts); + let scripted = self + .state + .readiness_scripts + .lock() + .await + .get_mut(&request.sandbox_name) + .and_then(|script| { + if script.len() > 1 { + script.pop_front() + } else { + script.front().cloned() + } + }); + let mut status = match scripted { + Some(ReadinessReply::Status(status)) => *status, + // The last scripted reply repeats its delay after cancellation, + // matching a gateway whose status calls are consistently slow. + Some(ReadinessReply::DelayedStatus(delay, status)) => { + tokio::time::sleep(delay).await; + *status + } + Some(ReadinessReply::Error(code)) => { + return Err(Status::new(code, SYNTHETIC_READINESS_BACKEND_ERROR)); + } + // No mock lock survives this await; the client deadline must cancel + // the request while retaining its previously observed status. + Some(ReadinessReply::Hung) => std::future::pending().await, + None => ProviderReadinessStatus { + state: ProviderReadinessState::Persisted.into(), + reason: ProviderReadinessReason::WaitingForSupervisor.into(), + ..Default::default() + }, + }; + let response_receipt = status.receipt.get_or_insert(receipt); + if let Some(observed) = status.observed.as_mut() { + let desired = response_receipt.desired.as_ref().unwrap(); + observed + .attachment_epoch + .clone_from(&desired.attachment_epoch); + observed.provider_env_revision = desired.provider_env_revision; + observed.config_revision = desired.config_revision; + observed.policy_hash.clone_from(&desired.policy_hash); + } + Ok(Response::new(GetSandboxProviderStatusResponse { + status: Some(status), + })) + } + + async fn report_provider_readiness( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "provider installation reports are not exercised by this mock", + )) + } + async fn create_ssh_session( &self, _request: tonic::Request, @@ -456,6 +684,7 @@ impl OpenShell for TestOpenShell { providers.insert(provider_name, provider.clone()); Ok(Response::new(ProviderResponse { provider: Some(provider), + ..Default::default() })) } @@ -463,6 +692,9 @@ impl OpenShell for TestOpenShell { &self, request: tonic::Request, ) -> Result, Status> { + if self.state.fail_provider_reads.load(Ordering::SeqCst) { + return Err(Status::internal(SYNTHETIC_READINESS_BACKEND_ERROR)); + } if self.state.deny_provider_reads.load(Ordering::SeqCst) { return Err(Status::permission_denied("scope 'provider:read' required")); } @@ -474,6 +706,7 @@ impl OpenShell for TestOpenShell { .ok_or_else(|| Status::not_found("provider not found"))?; Ok(Response::new(ProviderResponse { provider: Some(provider), + ..Default::default() })) } @@ -518,6 +751,21 @@ impl OpenShell for TestOpenShell { ) -> Result, Status> { let request = request.into_inner(); let id = request.id; + self.state + .profile_read_requests + .lock() + .await + .push(id.clone()); + let error_code = self + .state + .profile_read_errors + .lock() + .await + .get(&id) + .copied(); + if let Some(code) = error_code { + return Err(Status::new(code, SYNTHETIC_PROFILE_BACKEND_ERROR)); + } let scoped_profile = self .state .scoped_profiles @@ -657,8 +905,11 @@ impl OpenShell for TestOpenShell { &self, request: tonic::Request, ) -> Result, Status> { + let request = request.into_inner(); + let workspace = selected_workspace(&request.workspace_scope) + .filter(|workspace| !workspace.is_empty()) + .ok_or_else(|| Status::invalid_argument("one explicit workspace is required"))?; let provider = request - .into_inner() .provider .ok_or_else(|| Status::invalid_argument("provider is required"))?; self.state @@ -667,6 +918,16 @@ impl OpenShell for TestOpenShell { .await .push(provider.clone()); + let mut targets = self + .state + .sandbox_providers + .lock() + .await + .iter() + .filter(|(_, attached)| attached.iter().any(|name| name == provider.object_name())) + .map(|(sandbox_name, _)| sandbox_name.clone()) + .collect::>(); + targets.sort(); let mut providers = self.state.providers.lock().await; let existing = providers .get(provider.object_name()) @@ -709,9 +970,9 @@ impl OpenShell for TestOpenShell { name: provider_metadata.name, created_at_ms: existing_metadata.created_at_ms, labels: existing_metadata.labels, - resource_version: 0, + resource_version: existing_metadata.resource_version + 1, annotations: HashMap::new(), - workspace: String::new(), + workspace: workspace.to_string(), deletion_timestamp_ms: 0, }), r#type: existing.r#type, @@ -729,9 +990,29 @@ impl OpenShell for TestOpenShell { }, }; let updated_name = updated.object_name().to_string(); - providers.insert(updated_name, updated.clone()); + providers.insert(updated_name.clone(), updated.clone()); + drop(providers); + let mutation_id = format!( + "update-{}", + self.state.readiness_sequence.fetch_add(1, Ordering::SeqCst) + 1 + ); + let mut target_receipts = Vec::with_capacity(targets.len()); + for sandbox_name in targets { + target_receipts.push( + self.provider_receipt( + &sandbox_name, + &updated_name, + workspace, + ProviderMutationKind::Update, + Some(&mutation_id), + ) + .await, + ); + } Ok(Response::new(ProviderResponse { provider: Some(updated), + target_receipts, + mutation_id, })) } async fn get_provider_refresh_status( @@ -1171,7 +1452,7 @@ struct TestServer { endpoint: String, tls: TlsOptions, state: ProviderState, - _dir: TempDir, + tls_materials_dir: TempDir, } async fn run_server() -> TestServer { @@ -1218,10 +1499,1280 @@ async fn run_server() -> TestServer { endpoint, tls, state, - _dir: dir, + tls_materials_dir: dir, } } +async fn seed_readiness_provider(server: &TestServer) { + server.state.providers.lock().await.insert( + READINESS_PROVIDER.to_string(), + Provider { + metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { + id: format!("id-{READINESS_PROVIDER}"), + name: READINESS_PROVIDER.to_string(), + workspace: "default".to_string(), + resource_version: 1, + ..Default::default() + }), + r#type: "openai".to_string(), + credentials: HashMap::from([( + "OPENAI_API_KEY".to_string(), + SYNTHETIC_READINESS_CREDENTIAL.to_string(), + )]), + ..Default::default() + }, + ); +} + +fn readiness_status( + state: ProviderReadinessState, + reason: ProviderReadinessReason, + process_installed: bool, +) -> ProviderReadinessStatus { + ProviderReadinessStatus { + state: state.into(), + reason: reason.into(), + observed: Some(ProviderReadinessObservation { + session_id: "network-session".to_string(), + sequence: 1, + credentials_installed: true, + policy_active: true, + launch_environment_installed: process_installed, + process_instance_id: if process_installed { + "process-instance".to_string() + } else { + String::new() + }, + reason: reason.into(), + ..Default::default() + }), + network_instance_id: "network-instance".to_string(), + observed_at_ms: 1000, + evaluated_at_ms: 1000, + ..Default::default() + } +} + +async fn script_readiness( + server: &TestServer, + sandbox_name: &str, + responses: Vec>, +) { + server.state.readiness_scripts.lock().await.insert( + sandbox_name.to_string(), + responses + .into_iter() + .map(|response| match response { + Ok(status) => ReadinessReply::Status(Box::new(status)), + Err(code) => ReadinessReply::Error(code), + }) + .collect(), + ); +} + +async fn script_readiness_then_hang( + server: &TestServer, + sandbox_name: &str, + first_status: ProviderReadinessStatus, +) { + server.state.readiness_scripts.lock().await.insert( + sandbox_name.to_string(), + VecDeque::from([ + ReadinessReply::Status(Box::new(first_status)), + ReadinessReply::Hung, + ]), + ); +} + +async fn seed_readiness_receipt( + server: &TestServer, + sandbox_name: &str, + kind: ProviderMutationKind, +) -> ProviderMutationReceipt { + seed_readiness_provider(server).await; + TestOpenShell { + state: server.state.clone(), + } + .provider_receipt(sandbox_name, READINESS_PROVIDER, "default", kind, None) + .await +} + +async fn latest_readiness_receipt( + server: &TestServer, + sandbox_name: &str, +) -> ProviderMutationReceipt { + server + .state + .readiness_receipts + .lock() + .await + .values() + .filter(|receipt| { + receipt + .desired + .as_ref() + .is_some_and(|desired| desired.sandbox_name == sandbox_name) + }) + .max_by_key(|receipt| receipt.persisted_at_ms) + .cloned() + .expect("sandbox mutation receipt") +} + +fn provider_wait_options() -> run::ProviderWaitOptions<'static> { + run::ProviderWaitOptions { + wait: true, + timeout: Duration::from_secs(2), + output: "json", + } +} + +// A separate CLI process provides isolated stdout/stderr without redirecting +// the test runner's descriptors or sharing the user's gateway configuration. +async fn run_readiness_cli(server: &TestServer, args: &[&str]) -> std::process::Output { + let config_dir = tempfile::tempdir().unwrap(); + let tls_dir = config_dir + .path() + .join("openshell/gateways/provider-readiness/mtls"); + std::fs::create_dir_all(&tls_dir).unwrap(); + for filename in ["ca.crt", "tls.crt", "tls.key"] { + std::fs::copy( + server.tls_materials_dir.path().join(filename), + tls_dir.join(filename), + ) + .unwrap(); + } + let mut command = tokio::process::Command::new(env!("CARGO_BIN_EXE_openshell")); + for (key, _) in std::env::vars().filter(|(key, _)| key.starts_with("OPENSHELL_")) { + command.env_remove(key); + } + command + .args([ + "--gateway", + "provider-readiness", + "--gateway-endpoint", + &server.endpoint, + "--color", + "never", + ]) + .args(args) + .env("XDG_CONFIG_HOME", config_dir.path()) + .kill_on_drop(true); + tokio::time::timeout(Duration::from_secs(8), command.output()) + .await + .expect("bounded readiness CLI process") + .expect("readiness CLI output") +} + +fn assert_readiness_output_redacted(output: &std::process::Output) { + for bytes in [&output.stdout, &output.stderr] { + let text = String::from_utf8_lossy(bytes); + assert!(!text.contains(SYNTHETIC_READINESS_CREDENTIAL)); + assert!(!text.contains(SYNTHETIC_READINESS_BACKEND_ERROR)); + assert!(!text.contains(SYNTHETIC_PROFILE_BACKEND_ERROR)); + } +} + +#[tokio::test] +async fn provider_readiness_mutations_reject_unbound_receipts_before_output_or_polling() { + let server = run_server().await; + seed_readiness_provider(&server).await; + let corruptions: &[(&str, ReceiptCorruption)] = &[ + ("empty", |receipt| { + *receipt = ProviderMutationReceipt::default(); + }), + ("receipt_id", |receipt| receipt.receipt_id.clear()), + ("mutation_id", |receipt| receipt.mutation_id.clear()), + ("workspace", |receipt| { + receipt.workspace = "other".to_string(); + }), + ("provider", |receipt| { + receipt.provider_name = "other".to_string(); + }), + ("kind", |receipt| { + receipt.kind = ProviderMutationKind::Observe.into(); + }), + ("sandbox_name", |receipt| { + receipt + .desired + .as_mut() + .expect("desired identity") + .sandbox_name = "other".to_string(); + }), + ("sandbox_id", |receipt| { + receipt + .desired + .as_mut() + .expect("desired identity") + .sandbox_id = "other-id".to_string(); + }), + ("desired", |receipt| receipt.desired = None), + ("timestamp", |receipt| receipt.persisted_at_ms = 0), + ("provider_presence", |receipt| { + let detached = receipt.kind == i32::from(ProviderMutationKind::Detach); + receipt + .desired + .as_mut() + .expect("desired identity") + .provider_id = if detached { + "unexpected-provider".to_string() + } else { + String::new() + }; + }), + ]; + for action in ["attach", "detach"] { + let state = if action == "attach" { + ProviderReadinessState::Ready + } else { + ProviderReadinessState::Revoked + }; + script_readiness( + &server, + "receipt-target", + vec![Ok(readiness_status( + state, + ProviderReadinessReason::Unspecified, + true, + ))], + ) + .await; + for wait in [false, true] { + for (field, corrupt) in corruptions { + *server.state.corrupt_mutation_receipt.lock().await = Some(*corrupt); + let mut args = vec![ + "sandbox", + "provider", + action, + "receipt-target", + READINESS_PROVIDER, + "--timeout", + "1", + "--output", + "json", + ]; + if wait { + args.push("--wait"); + } + let output = run_readiness_cli(&server, &args).await; + assert!(!output.status.success(), "{action}: {field}, wait={wait}"); + assert!( + output.stdout.is_empty(), + "invalid receipt was printed: {field}" + ); + assert_readiness_output_redacted(&output); + assert!(server.state.readiness_requests.lock().await.is_empty()); + } + } + } +} + +#[tokio::test] +async fn provider_readiness_update_rejects_unbound_batch_identity() { + let server = run_server().await; + seed_readiness_provider(&server).await; + server.state.sandbox_providers.lock().await.insert( + "update-target".to_string(), + vec![READINESS_PROVIDER.to_string()], + ); + script_readiness( + &server, + "update-target", + vec![Ok(readiness_status( + ProviderReadinessState::Ready, + ProviderReadinessReason::Unspecified, + true, + ))], + ) + .await; + let corruptions: &[(&str, ReceiptCorruption)] = &[ + ("mutation_id", |receipt| { + receipt.mutation_id = "another-update".to_string(); + }), + ("workspace", |receipt| { + receipt.workspace = "other".to_string(); + }), + ("provider_name", |receipt| { + receipt.provider_name = "other".to_string(); + }), + ("kind", |receipt| { + receipt.kind = ProviderMutationKind::Observe.into(); + }), + ("provider_id", |receipt| { + receipt + .desired + .as_mut() + .expect("desired identity") + .provider_id = "other-id".to_string(); + }), + ("provider_revision", |receipt| { + receipt + .desired + .as_mut() + .expect("desired identity") + .provider_resource_version = u64::MAX; + }), + ]; + for wait in [false, true] { + for (field, corrupt) in corruptions { + *server.state.corrupt_mutation_receipt.lock().await = Some(*corrupt); + let mut args = vec![ + "provider", + "update", + READINESS_PROVIDER, + "--credential", + "OPENAI_API_KEY=updated-fixture", + "--timeout", + "1", + "--output", + "json", + ]; + if wait { + args.push("--wait"); + } + let output = run_readiness_cli(&server, &args).await; + assert!(!output.status.success(), "{field}, wait={wait}"); + assert!( + output.stdout.is_empty(), + "invalid batch was printed: {field}" + ); + assert_readiness_output_redacted(&output); + assert!(server.state.readiness_requests.lock().await.is_empty()); + } + } +} + +#[tokio::test] +async fn provider_readiness_update_with_no_targets_preserves_saved_success() { + let server = run_server().await; + seed_readiness_provider(&server).await; + for wait in [false, true] { + let mut args = vec![ + "provider", + "update", + READINESS_PROVIDER, + "--credential", + "OPENAI_API_KEY=updated-fixture", + "--output", + "json", + ]; + if wait { + args.push("--wait"); + } + let output = run_readiness_cli(&server, &args).await; + assert!(output.status.success()); + let value: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("saved update JSON"); + assert_eq!(value["targets"], serde_json::json!([])); + assert!( + !value["mutation_id"] + .as_str() + .expect("mutation ID") + .is_empty() + ); + assert!(server.state.readiness_requests.lock().await.is_empty()); + } +} + +#[tokio::test] +async fn provider_readiness_update_redacts_exact_and_alias_profile_lookup_errors() { + let server = run_server().await; + seed_readiness_provider(&server).await; + for (profile, failed_lookup, expected_lookups) in [ + ("openai", "openai", vec!["openai"]), + ("gh", "github", vec!["gh", "github"]), + ] { + server + .state + .providers + .lock() + .await + .get_mut(READINESS_PROVIDER) + .expect("seeded provider") + .r#type = profile.to_string(); + let mut errors = server.state.profile_read_errors.lock().await; + errors.clear(); + errors.insert(failed_lookup.to_string(), Code::Internal); + drop(errors); + for source in ["--from-existing", "--from-oidc-token"] { + server.state.profile_read_requests.lock().await.clear(); + let output = run_readiness_cli( + &server, + &[ + "provider", + "update", + READINESS_PROVIDER, + source, + "--wait", + "--output", + "json", + ], + ) + .await; + assert!(!output.status.success()); + assert_readiness_output_redacted(&output); + assert!( + String::from_utf8_lossy(&output.stderr).contains("provider profile lookup failed") + ); + assert_eq!( + *server.state.profile_read_requests.lock().await, + expected_lookups + ); + assert!( + server + .state + .provider_update_requests + .lock() + .await + .is_empty() + ); + } + } +} + +async fn assert_later_readiness_targets_are_polled( + blocked_targets: usize, + blocked_reply: ReadinessReply, + expected_state: &str, +) { + let server = run_server().await; + seed_readiness_provider(&server).await; + for index in 0..17 { + let sandbox = format!("sandbox-{index:02}"); + server + .state + .sandbox_providers + .lock() + .await + .insert(sandbox.clone(), vec![READINESS_PROVIDER.to_string()]); + script_readiness( + &server, + &sandbox, + vec![Ok(readiness_status( + ProviderReadinessState::Ready, + ProviderReadinessReason::Unspecified, + true, + ))], + ) + .await; + } + for index in 0..blocked_targets { + server.state.readiness_scripts.lock().await.insert( + format!("sandbox-{index:02}"), + VecDeque::from([blocked_reply.clone()]), + ); + } + let started = std::time::Instant::now(); + let output = run_readiness_cli( + &server, + &[ + "provider", + "update", + READINESS_PROVIDER, + "--credential", + "OPENAI_API_KEY=updated-fixture", + "--wait", + "--timeout", + "1", + "--output", + "json", + ], + ) + .await; + assert!(!output.status.success()); + assert!(started.elapsed() < Duration::from_secs(4)); + assert_readiness_output_redacted(&output); + let value: serde_json::Value = serde_json::from_slice(&output.stdout).expect("target outcomes"); + let targets = value["targets"].as_array().expect("target array"); + assert_eq!(targets.len(), 17); + let requests = server.state.readiness_requests.lock().await; + for (index, target) in targets.iter().enumerate() { + let sandbox = format!("sandbox-{index:02}"); + assert_eq!(target["receipt"]["desired"]["sandbox_name"], sandbox); + if index < blocked_targets { + assert_eq!(target["wait_outcome"], "timed_out"); + assert_eq!(target["state"], expected_state); + } else { + assert_eq!(target["wait_outcome"], "complete"); + } + let target_requests = requests + .iter() + .filter(|request| request.sandbox_name == sandbox) + .collect::>(); + assert!(!target_requests.is_empty(), "{sandbox} was never queried"); + if index >= blocked_targets { + assert_eq!( + target_requests.len(), + 1, + "completed targets leave the queue" + ); + } + for request in target_requests { + assert_eq!( + request.receipt_id, + target["receipt"]["receipt_id"] + .as_str() + .expect("receipt ID") + ); + assert_eq!(request.provider_name, READINESS_PROVIDER); + assert_eq!( + selected_workspace(&request.workspace_scope), + Some("default") + ); + } + } +} + +#[tokio::test] +async fn provider_readiness_update_polls_later_targets_while_first_is_hung() { + assert_later_readiness_targets_are_polled(1, ReadinessReply::Hung, "persisted").await; +} + +#[tokio::test] +async fn provider_readiness_update_polls_later_targets_while_first_batch_is_pending() { + assert_later_readiness_targets_are_polled( + 16, + ReadinessReply::Status(Box::new(readiness_status( + ProviderReadinessState::Pending, + ProviderReadinessReason::WaitingForProcess, + false, + ))), + "pending", + ) + .await; +} + +#[tokio::test] +async fn provider_readiness_update_polls_later_targets_while_first_batch_is_hung() { + assert_later_readiness_targets_are_polled(16, ReadinessReply::Hung, "persisted").await; +} + +async fn assert_slow_readiness_targets_complete(target_count: usize, delay: Duration) { + let server = run_server().await; + seed_readiness_provider(&server).await; + for index in 0..target_count { + let sandbox = format!("slow-sandbox-{index:02}"); + server + .state + .sandbox_providers + .lock() + .await + .insert(sandbox.clone(), vec![READINESS_PROVIDER.to_string()]); + server.state.readiness_scripts.lock().await.insert( + sandbox, + VecDeque::from([ReadinessReply::DelayedStatus( + delay, + Box::new(readiness_status( + ProviderReadinessState::Ready, + ProviderReadinessReason::Unspecified, + true, + )), + )]), + ); + } + let started = std::time::Instant::now(); + let output = run_readiness_cli( + &server, + &[ + "provider", + "update", + READINESS_PROVIDER, + "--credential", + "OPENAI_API_KEY=updated-fixture", + "--wait", + "--timeout", + "2", + "--output", + "json", + ], + ) + .await; + assert!( + output.status.success(), + "healthy status replies that fit the shared deadline must complete: {}", + String::from_utf8_lossy(&output.stdout) + ); + assert!(started.elapsed() < Duration::from_secs(4)); + assert_readiness_output_redacted(&output); + let value: serde_json::Value = serde_json::from_slice(&output.stdout).expect("target outcomes"); + let targets = value["targets"].as_array().expect("target array"); + assert_eq!(targets.len(), target_count); + let requests = server.state.readiness_requests.lock().await; + for (index, target) in targets.iter().enumerate() { + let sandbox = format!("slow-sandbox-{index:02}"); + assert_eq!(target["receipt"]["desired"]["sandbox_name"], sandbox); + assert_eq!(target["wait_outcome"], "complete"); + assert_eq!( + requests + .iter() + .filter(|request| request.sandbox_name == sandbox) + .count(), + 1, + "a healthy response must not be discarded and retried" + ); + } +} + +#[tokio::test] +async fn provider_readiness_update_wait_allows_slow_single_target() { + assert_slow_readiness_targets_complete(1, Duration::from_millis(1200)).await; +} + +#[tokio::test] +async fn provider_readiness_update_wait_allows_slow_queued_targets() { + assert_slow_readiness_targets_complete(17, Duration::from_millis(700)).await; +} + +#[tokio::test] +async fn provider_readiness_attach_wait_observes_pending_then_ready() { + let server = run_server().await; + seed_readiness_provider(&server).await; + script_readiness( + &server, + "pending-sandbox", + vec![ + Ok(readiness_status( + ProviderReadinessState::Pending, + ProviderReadinessReason::WaitingForProcess, + false, + )), + Ok(readiness_status( + ProviderReadinessState::Ready, + ProviderReadinessReason::Unspecified, + true, + )), + ], + ) + .await; + + run::sandbox_provider_attach( + &server.endpoint, + "pending-sandbox", + READINESS_PROVIDER, + "default", + &server.tls, + provider_wait_options(), + ) + .await + .expect("attachment waits through pending state"); + + let requests = server.state.readiness_requests.lock().await; + assert_eq!(requests.len(), 2); + assert!(!requests[0].receipt_id.is_empty()); + assert!( + requests + .iter() + .all(|request| request.receipt_id == requests[0].receipt_id) + ); + assert!( + requests + .iter() + .all(|request| selected_workspace(&request.workspace_scope) == Some("default")) + ); +} + +#[tokio::test] +async fn provider_readiness_attach_wait_times_out_without_process_ack() { + let server = run_server().await; + seed_readiness_provider(&server).await; + script_readiness( + &server, + "missing-process", + vec![Ok(readiness_status( + ProviderReadinessState::Pending, + ProviderReadinessReason::WaitingForProcess, + false, + ))], + ) + .await; + + let output = run_readiness_cli( + &server, + &[ + "sandbox", + "provider", + "attach", + "missing-process", + READINESS_PROVIDER, + "--wait", + "--timeout", + "1", + "--output", + "json", + ], + ) + .await; + + assert!(!output.status.success()); + assert_readiness_output_redacted(&output); + let value: serde_json::Value = serde_json::from_slice(&output.stdout).expect("readiness JSON"); + let target = &value["targets"][0]; + assert_eq!(target["state"], "pending"); + assert_eq!(target["reason"], "waiting_for_process"); + assert_eq!(target["wait_outcome"], "timed_out"); + assert_eq!(target["observed"]["launch_environment_installed"], false); + assert!(!server.state.readiness_requests.lock().await.is_empty()); +} + +#[tokio::test] +async fn provider_readiness_status_wait_supersedes_changed_desired_authority() { + let server = run_server().await; + seed_readiness_provider(&server).await; + run::sandbox_provider_attach( + &server.endpoint, + "changed-authority", + READINESS_PROVIDER, + "default", + &server.tls, + run::ProviderWaitOptions::default(), + ) + .await + .unwrap(); + let original = latest_readiness_receipt(&server, "changed-authority").await; + let mut newer = original.clone(); + newer.desired.as_mut().unwrap().provider_env_revision += 1; + let mut newer_status = readiness_status( + ProviderReadinessState::Ready, + ProviderReadinessReason::Unspecified, + true, + ); + newer_status.receipt = Some(newer); + script_readiness( + &server, + "changed-authority", + vec![ + Ok(readiness_status( + ProviderReadinessState::Pending, + ProviderReadinessReason::WaitingForProcess, + false, + )), + Ok(newer_status), + ], + ) + .await; + + let output = run_readiness_cli( + &server, + &[ + "sandbox", + "provider", + "status", + "changed-authority", + READINESS_PROVIDER, + "--receipt", + &original.receipt_id, + "--wait", + "--output", + "json", + ], + ) + .await; + + assert!(!output.status.success()); + assert_readiness_output_redacted(&output); + let value: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("superseded readiness JSON"); + let target = &value["targets"][0]; + assert_eq!(target["state"], "superseded"); + assert_eq!(target["reason"], "desired_state_changed"); + assert_eq!(target["wait_outcome"], "terminal"); + assert_eq!(target["receipt"]["receipt_id"], original.receipt_id); + assert_eq!( + target["receipt"]["desired"]["provider_env_revision"], + original + .desired + .as_ref() + .unwrap() + .provider_env_revision + .to_string() + ); + let requests = server.state.readiness_requests.lock().await; + assert_eq!(requests.len(), 2); + assert!( + requests + .iter() + .all(|request| request.receipt_id == original.receipt_id) + ); +} + +#[tokio::test] +async fn provider_readiness_status_without_wait_rejects_missing_process_ack() { + let server = run_server().await; + let receipt = + seed_readiness_receipt(&server, "unproved-ready", ProviderMutationKind::Attach).await; + script_readiness( + &server, + "unproved-ready", + vec![Ok(readiness_status( + ProviderReadinessState::Ready, + ProviderReadinessReason::Unspecified, + false, + ))], + ) + .await; + + let output = run_readiness_cli( + &server, + &[ + "sandbox", + "provider", + "status", + "unproved-ready", + READINESS_PROVIDER, + "--receipt", + &receipt.receipt_id, + "--output", + "json", + ], + ) + .await; + + assert!(!output.status.success()); + assert_readiness_output_redacted(&output); + assert!(output.stdout.is_empty()); + assert!(String::from_utf8_lossy(&output.stderr).contains("invalid provider readiness status")); + assert_eq!(server.state.readiness_requests.lock().await.len(), 1); +} + +#[tokio::test] +async fn provider_readiness_status_without_wait_rejects_ready_for_detach() { + let server = run_server().await; + let receipt = + seed_readiness_receipt(&server, "unrevoked-detach", ProviderMutationKind::Detach).await; + script_readiness( + &server, + "unrevoked-detach", + vec![Ok(readiness_status( + ProviderReadinessState::Ready, + ProviderReadinessReason::Unspecified, + true, + ))], + ) + .await; + + let output = run_readiness_cli( + &server, + &[ + "sandbox", + "provider", + "status", + "unrevoked-detach", + READINESS_PROVIDER, + "--receipt", + &receipt.receipt_id, + "--output", + "json", + ], + ) + .await; + + assert!(!output.status.success()); + assert_readiness_output_redacted(&output); + assert!(output.stdout.is_empty()); + assert!(String::from_utf8_lossy(&output.stderr).contains("invalid provider readiness status")); + assert_eq!(server.state.readiness_requests.lock().await.len(), 1); +} + +#[tokio::test] +async fn provider_readiness_status_wait_completes_from_first_ready_response() { + let server = run_server().await; + let receipt = + seed_readiness_receipt(&server, "first-ready", ProviderMutationKind::Attach).await; + script_readiness_then_hang( + &server, + "first-ready", + readiness_status( + ProviderReadinessState::Ready, + ProviderReadinessReason::Unspecified, + true, + ), + ) + .await; + + let output = run_readiness_cli( + &server, + &[ + "sandbox", + "provider", + "status", + "first-ready", + READINESS_PROVIDER, + "--receipt", + &receipt.receipt_id, + "--wait", + "--timeout", + "1", + "--output", + "json", + ], + ) + .await; + + assert!(output.status.success()); + assert_readiness_output_redacted(&output); + let value: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("completed readiness JSON"); + let target = &value["targets"][0]; + assert_eq!(target["state"], "ready"); + assert_eq!(target["wait_outcome"], "complete"); + assert_eq!(target["receipt"]["receipt_id"], receipt.receipt_id); + assert_eq!(target["observed"]["launch_environment_installed"], true); + assert_eq!(server.state.readiness_requests.lock().await.len(), 1); +} + +#[tokio::test] +async fn provider_readiness_status_wait_timeout_preserves_first_pending_response() { + let server = run_server().await; + let receipt = + seed_readiness_receipt(&server, "first-pending", ProviderMutationKind::Attach).await; + script_readiness_then_hang( + &server, + "first-pending", + readiness_status( + ProviderReadinessState::Pending, + ProviderReadinessReason::WaitingForProcess, + false, + ), + ) + .await; + + let output = run_readiness_cli( + &server, + &[ + "sandbox", + "provider", + "status", + "first-pending", + READINESS_PROVIDER, + "--receipt", + &receipt.receipt_id, + "--wait", + "--timeout", + "1", + "--output", + "json", + ], + ) + .await; + + assert!(!output.status.success()); + assert_readiness_output_redacted(&output); + let value: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("timed-out readiness JSON"); + let target = &value["targets"][0]; + assert_eq!(target["state"], "pending"); + assert_eq!(target["reason"], "waiting_for_process"); + assert_eq!(target["wait_outcome"], "timed_out"); + assert_eq!(target["receipt"]["receipt_id"], receipt.receipt_id); + assert_eq!(target["observed"]["session_id"], "network-session"); + assert_eq!(target["observed"]["launch_environment_installed"], false); + let requests = server.state.readiness_requests.lock().await; + assert_eq!(requests.len(), 2); + assert!( + requests + .iter() + .all(|request| request.receipt_id == receipt.receipt_id) + ); +} + +#[tokio::test] +async fn provider_readiness_mutations_redact_sandbox_lookup_errors() { + let server = run_server().await; + seed_readiness_provider(&server).await; + server + .state + .fail_sandbox_reads + .store(true, Ordering::SeqCst); + + for operation in ["attach", "detach"] { + let output = run_readiness_cli( + &server, + &[ + "sandbox", + "provider", + operation, + "read-failed", + READINESS_PROVIDER, + "--wait", + "--output", + "json", + ], + ) + .await; + + assert!(!output.status.success()); + assert_readiness_output_redacted(&output); + assert!( + String::from_utf8_lossy(&output.stderr) + .to_ascii_lowercase() + .contains("internal") + ); + } + assert!( + server + .state + .sandbox_provider_requests + .lock() + .await + .is_empty() + ); + assert!(server.state.readiness_requests.lock().await.is_empty()); +} + +#[tokio::test] +async fn provider_readiness_update_redacts_provider_lookup_errors() { + let server = run_server().await; + seed_readiness_provider(&server).await; + server + .state + .fail_provider_reads + .store(true, Ordering::SeqCst); + + let output = run_readiness_cli( + &server, + &[ + "provider", + "update", + READINESS_PROVIDER, + "--config", + "region=test", + "--wait", + "--output", + "json", + ], + ) + .await; + + assert!(!output.status.success()); + assert_readiness_output_redacted(&output); + assert!( + String::from_utf8_lossy(&output.stderr) + .to_ascii_lowercase() + .contains("internal") + ); + assert!( + server + .state + .provider_update_requests + .lock() + .await + .is_empty() + ); + assert!(server.state.readiness_requests.lock().await.is_empty()); +} + +#[tokio::test] +async fn provider_readiness_status_rejects_wrong_first_receipt() { + let server = run_server().await; + seed_readiness_provider(&server).await; + run::sandbox_provider_attach( + &server.endpoint, + "wrong-receipt", + READINESS_PROVIDER, + "default", + &server.tls, + run::ProviderWaitOptions::default(), + ) + .await + .unwrap(); + let original = latest_readiness_receipt(&server, "wrong-receipt").await; + let mut replacement = original.clone(); + replacement.receipt_id = "another-mutation-receipt".to_string(); + replacement.mutation_id = "another-mutation".to_string(); + let mut status = readiness_status( + ProviderReadinessState::Ready, + ProviderReadinessReason::Unspecified, + true, + ); + status.receipt = Some(replacement); + script_readiness(&server, "wrong-receipt", vec![Ok(status)]).await; + + let error = run::sandbox_provider_status( + &server.endpoint, + "wrong-receipt", + READINESS_PROVIDER, + &original.receipt_id, + "default", + &server.tls, + provider_wait_options(), + ) + .await + .expect_err("a replacement receipt cannot satisfy the requested receipt"); + + assert!( + error + .to_string() + .contains("invalid provider readiness status") + ); + assert_eq!(server.state.readiness_requests.lock().await.len(), 1); +} + +#[tokio::test] +async fn provider_readiness_detach_wait_requires_revoked() { + let server = run_server().await; + seed_readiness_provider(&server).await; + server.state.sandbox_providers.lock().await.insert( + "detach-sandbox".to_string(), + vec![READINESS_PROVIDER.to_string()], + ); + script_readiness( + &server, + "detach-sandbox", + vec![Ok(readiness_status( + ProviderReadinessState::Ready, + ProviderReadinessReason::Unspecified, + true, + ))], + ) + .await; + + let error = run::sandbox_provider_detach( + &server.endpoint, + "detach-sandbox", + READINESS_PROVIDER, + "default", + &server.tls, + provider_wait_options(), + ) + .await + .expect_err("READY cannot establish detachment completion"); + assert!( + error + .to_string() + .contains("readiness wait did not complete") + ); + script_readiness( + &server, + "detach-sandbox", + vec![ + Ok(readiness_status( + ProviderReadinessState::Pending, + ProviderReadinessReason::WaitingForProcess, + false, + )), + Ok(readiness_status( + ProviderReadinessState::Revoked, + ProviderReadinessReason::Unspecified, + true, + )), + ], + ) + .await; + + // A repeated detach still returns a receipt and waits for revoked authority. + run::sandbox_provider_detach( + &server.endpoint, + "detach-sandbox", + READINESS_PROVIDER, + "default", + &server.tls, + provider_wait_options(), + ) + .await + .expect("revoked receipt completes detachment"); + let requests = server.state.readiness_requests.lock().await; + assert_eq!(requests.len(), 3); + assert!( + requests + .iter() + .all(|request| selected_workspace(&request.workspace_scope) == Some("default")) + ); +} + +#[tokio::test] +async fn provider_readiness_update_reports_every_target_failure() { + let server = run_server().await; + seed_readiness_provider(&server).await; + for sandbox in ["network-failed", "unsupported", "transport-failed"] { + server + .state + .sandbox_providers + .lock() + .await + .insert(sandbox.to_string(), vec![READINESS_PROVIDER.to_string()]); + } + script_readiness( + &server, + "network-failed", + vec![Ok(readiness_status( + ProviderReadinessState::Failed, + ProviderReadinessReason::CredentialInstallFailed, + false, + ))], + ) + .await; + script_readiness( + &server, + "unsupported", + vec![Ok(readiness_status( + ProviderReadinessState::Withheld, + ProviderReadinessReason::UnsupportedSupervisor, + false, + ))], + ) + .await; + script_readiness(&server, "transport-failed", vec![Err(Code::Internal)]).await; + + let output = run_readiness_cli( + &server, + &[ + "provider", + "update", + READINESS_PROVIDER, + "--config", + "region=test", + "--wait", + "--timeout", + "1", + "--output", + "json", + ], + ) + .await; + + assert!(!output.status.success()); + assert_readiness_output_redacted(&output); + let value: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("multi-target readiness JSON"); + let targets: HashMap<_, _> = value["targets"] + .as_array() + .unwrap() + .iter() + .map(|target| { + ( + target["receipt"]["desired"]["sandbox_name"] + .as_str() + .unwrap(), + target, + ) + }) + .collect(); + assert_eq!(targets.len(), 3); + assert_eq!(targets["network-failed"]["state"], "failed"); + assert_eq!( + targets["network-failed"]["reason"], + "credential_install_failed" + ); + assert_eq!(targets["unsupported"]["state"], "withheld"); + assert_eq!(targets["unsupported"]["reason"], "unsupported_supervisor"); + assert_eq!( + targets["transport-failed"]["wait_outcome"], + "observation_error" + ); + assert!(!value["mutation_id"].as_str().unwrap().is_empty()); + assert!( + targets + .values() + .all(|target| target["receipt"]["mutation_id"] == value["mutation_id"]) + ); + let requests = server.state.readiness_requests.lock().await; + assert_eq!(requests.len(), 3); + assert!( + requests + .iter() + .all(|request| selected_workspace(&request.workspace_scope) == Some("default")) + ); +} + async fn install_test_profile(ts: &TestServer, id: &str, credential_key: &str) { ts.state.profiles.lock().await.insert( id.to_string(), @@ -1275,6 +2826,7 @@ async fn provider_update_preserves_stored_type_and_profile_workspace_when_readab credential_expires_at: &[], workspace: "default", tls: &ts.tls, + readiness: run::ProviderWaitOptions::default(), }) .await .expect("provider update"); @@ -1396,6 +2948,7 @@ async fn provider_cli_run_functions_support_full_crud_flow() { credential_expires_at: &[], workspace: "default", tls: &ts.tls, + readiness: run::ProviderWaitOptions::default(), }) .await .expect("provider update"); @@ -1871,6 +3424,7 @@ async fn sandbox_provider_cli_run_functions_wire_requests_and_idempotent_results "work-github", "default", &ts.tls, + run::ProviderWaitOptions::default(), ) .await .expect("sandbox provider attach"); @@ -1880,6 +3434,7 @@ async fn sandbox_provider_cli_run_functions_wire_requests_and_idempotent_results "work-github", "default", &ts.tls, + run::ProviderWaitOptions::default(), ) .await .expect("sandbox provider attach is idempotent"); @@ -1892,6 +3447,7 @@ async fn sandbox_provider_cli_run_functions_wire_requests_and_idempotent_results "work-github", "default", &ts.tls, + run::ProviderWaitOptions::default(), ) .await .expect("sandbox provider detach"); @@ -1901,6 +3457,7 @@ async fn sandbox_provider_cli_run_functions_wire_requests_and_idempotent_results "work-github", "default", &ts.tls, + run::ProviderWaitOptions::default(), ) .await .expect("sandbox provider detach is idempotent"); @@ -1945,14 +3502,12 @@ async fn sandbox_provider_attach_cli_surfaces_server_errors() { "missing-provider", "default", &ts.tls, + run::ProviderWaitOptions::default(), ) .await .expect_err("missing provider should fail"); - assert!( - err.to_string().contains("provider not found"), - "unexpected error: {err}" - ); + assert!(err.to_string().contains("provider attachment failed")); assert_eq!( ts.state.sandbox_provider_requests.lock().await.as_slice(), [SandboxProviderRequestLog::Attach { @@ -2364,6 +3919,7 @@ async fn provider_update_from_existing_uses_profile_discovery() { credential_expires_at: &[], workspace: "default", tls: &ts.tls, + readiness: run::ProviderWaitOptions::default(), }) .await .expect("profile-backed provider update --from-existing"); @@ -2436,6 +3992,7 @@ async fn provider_update_from_existing_preserves_global_profile_scope() { credential_expires_at: &[], workspace: "default", tls: &ts.tls, + readiness: run::ProviderWaitOptions::default(), }) .await .expect("global profile-backed provider update --from-existing"); @@ -2513,6 +4070,7 @@ async fn provider_update_from_oidc_token_preserves_global_profile_scope() { credential_expires_at: &[], workspace: "default", tls: &ts.tls, + readiness: run::ProviderWaitOptions::default(), }) .await .expect_err("unnamed test gateway should stop after profile validation"); diff --git a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs index d3cccefb76..4aabb16298 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -362,6 +362,24 @@ impl OpenShell for TestOpenShell { })) } + async fn get_sandbox_provider_status( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "provider readiness is not exercised by this mock", + )) + } + + async fn report_provider_readiness( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "provider installation reports are not exercised by this mock", + )) + } + async fn get_sandbox_provider_environment( &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 d040501590..90daa41929 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -245,6 +245,24 @@ impl OpenShell for TestOpenShell { Ok(Response::new(GetGatewayConfigResponse::default())) } + async fn get_sandbox_provider_status( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "provider readiness is not exercised by this mock", + )) + } + + async fn report_provider_readiness( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "provider installation reports are not exercised by this mock", + )) + } + async fn get_sandbox_provider_environment( &self, _request: tonic::Request, diff --git a/crates/openshell-core/src/grpc_client.rs b/crates/openshell-core/src/grpc_client.rs index af40578159..6152141f4b 100644 --- a/crates/openshell-core/src/grpc_client.rs +++ b/crates/openshell-core/src/grpc_client.rs @@ -27,10 +27,11 @@ use crate::proto::{ DenialSummary, EndpointObservation as ProtoEndpointObservation, EndpointResult as ProtoEndpointResult, ExchangeProviderSubjectTokenRequest, GetDraftPolicyRequest, GetSandboxConfigRequest, GetSandboxProviderEnvironmentRequest, - IssueSandboxTokenRequest, NetworkActivitySummary, PolicyChunk, PolicySource, PolicyStatus, - RefreshSandboxTokenRequest, ReportEndpointStatusRequest, ReportPolicyStatusRequest, - SandboxPolicy as ProtoSandboxPolicy, SubmitPolicyAnalysisRequest, SubmitPolicyAnalysisResponse, - UpdateConfigRequest, open_shell_client::OpenShellClient, workspace_selector, + GetSandboxProviderEnvironmentResponse, IssueSandboxTokenRequest, NetworkActivitySummary, + PolicyChunk, PolicySource, PolicyStatus, RefreshSandboxTokenRequest, + ReportEndpointStatusRequest, ReportPolicyStatusRequest, SandboxPolicy as ProtoSandboxPolicy, + SubmitPolicyAnalysisRequest, SubmitPolicyAnalysisResponse, UpdateConfigRequest, + open_shell_client::OpenShellClient, workspace_selector, }; use crate::sandbox_env; use miette::{IntoDiagnostic, Result, WrapErr}; @@ -390,6 +391,26 @@ pub async fn connect_channel_pub(endpoint: &str) -> Result { connect_channel(endpoint).await } +/// Report installed provider state for the current authenticated supervisor session. +/// +/// The observation must carry the session ID returned by `ConnectSupervisor`. +/// Reconnects start a new report sequence; retries preserve the complete report. +pub async fn report_provider_readiness( + endpoint: &str, + sandbox_id: &str, + observation: crate::proto::ProviderReadinessObservation, +) -> Result { + let mut client = connect(endpoint).await?; + client + .report_provider_readiness(crate::proto::ReportProviderReadinessRequest { + sandbox_id: sandbox_id.to_string(), + observation: Some(observation), + }) + .await + .map(tonic::Response::into_inner) + .into_diagnostic() +} + /// Background task that renews the sandbox JWT at ~80% of its remaining /// lifetime. The new token replaces the value in [`TOKEN_SLOT`], so all /// in-flight and future clients pick it up on their next request. The @@ -941,9 +962,9 @@ pub async fn sync_policy_and_fetch_snapshot( /// Fetch provider environment variables for a sandbox from `OpenShell` server via gRPC. /// -/// Returns a map of environment variable names to values derived from provider -/// credentials configured on the sandbox. Returns an empty map if the sandbox -/// has no providers or the call fails. +/// Returns the credential snapshot and its exact readiness identity. An empty +/// environment represents a sandbox without provider credentials. Transport +/// failure returns an error so callers can revoke credentials and retry. pub async fn fetch_provider_environment( endpoint: &str, sandbox_id: &str, @@ -960,15 +981,66 @@ pub async fn fetch_provider_environment( .await .into_diagnostic()?; - let inner = response.into_inner(); - Ok(ProviderEnvironmentResult { + Ok(provider_environment_result(response.into_inner())) +} + +/// Preserve snapshot authority and fail closed on unknown delivery reasons. +fn provider_environment_result( + inner: GetSandboxProviderEnvironmentResponse, +) -> ProviderEnvironmentResult { + ProviderEnvironmentResult { environment: inner.environment, provider_env_revision: inner.provider_env_revision, + provider_attachment_epoch: inner.provider_attachment_epoch, + policy_hash: inner.policy_hash, + readiness_reason: crate::proto::ProviderReadinessReason::try_from(inner.readiness_reason) + .unwrap_or(crate::proto::ProviderReadinessReason::CredentialsWithheld), credential_expires_at_ms: inner.credential_expires_at_ms, dynamic_credentials: inner.dynamic_credentials, static_credential_bindings: inner.static_credential_bindings, non_secret_environment_keys: inner.non_secret_environment_keys, - }) + } +} + +#[cfg(test)] +mod provider_environment_tests { + use super::*; + + #[test] + fn provider_environment_preserves_readiness_identity() { + let result = provider_environment_result(GetSandboxProviderEnvironmentResponse { + environment: HashMap::from([("TOKEN".to_string(), "synthetic".to_string())]), + provider_env_revision: 42, + provider_attachment_epoch: "attachment-epoch".to_string(), + policy_hash: "binding-policy".to_string(), + readiness_reason: crate::proto::ProviderReadinessReason::CredentialsWithheld.into(), + ..Default::default() + }); + assert_eq!(result.provider_env_revision, 42); + assert_eq!(result.provider_attachment_epoch, "attachment-epoch"); + assert_eq!(result.policy_hash, "binding-policy"); + assert_eq!( + result.readiness_reason, + crate::proto::ProviderReadinessReason::CredentialsWithheld + ); + assert_eq!( + result.environment.get("TOKEN").map(String::as_str), + Some("synthetic") + ); + } + + #[test] + fn provider_readiness_unknown_delivery_reason_is_withheld() { + let result = provider_environment_result(GetSandboxProviderEnvironmentResponse { + policy_hash: "binding-policy".to_string(), + readiness_reason: i32::MAX, + ..Default::default() + }); + assert_eq!( + result.readiness_reason, + crate::proto::ProviderReadinessReason::CredentialsWithheld + ); + } } pub async fn exchange_provider_subject_token( @@ -1047,6 +1119,8 @@ pub struct SettingsPollResult { /// When `policy_source` is `Global`, the version of the global policy revision. pub global_policy_version: u32, pub provider_env_revision: u64, + /// Attachment identity captured with this effective configuration. + pub provider_attachment_epoch: String, pub supervisor_middleware_services: Vec, /// Workspace the sandbox belongs to. pub workspace: String, @@ -1067,6 +1141,7 @@ fn settings_poll_result(inner: crate::proto::GetSandboxConfigResponse) -> Settin settings: inner.settings, global_policy_version: inner.global_policy_version, provider_env_revision: inner.provider_env_revision, + provider_attachment_epoch: inner.provider_attachment_epoch, supervisor_middleware_services: inner.supervisor_middleware_services, workspace: inner.workspace, policy_validation_failure_mode: inner @@ -1120,9 +1195,16 @@ mod settings_poll_tests { } } +/// Credential material and the authority snapshot that produced its bindings. pub struct ProviderEnvironmentResult { pub environment: HashMap, pub provider_env_revision: u64, + /// Attachment identity captured with the delivered credential records. + pub provider_attachment_epoch: String, + /// Effective policy used to derive the delivered endpoint bindings. + pub policy_hash: String, + /// Closed failure category; withheld material cannot establish readiness. + pub readiness_reason: crate::proto::ProviderReadinessReason, pub credential_expires_at_ms: HashMap, pub dynamic_credentials: HashMap, pub static_credential_bindings: HashMap, diff --git a/crates/openshell-core/src/provider_credentials.rs b/crates/openshell-core/src/provider_credentials.rs index 4d893fa362..beec21de37 100644 --- a/crates/openshell-core/src/provider_credentials.rs +++ b/crates/openshell-core/src/provider_credentials.rs @@ -17,11 +17,27 @@ const MAX_RETAINED_CREDENTIAL_GENERATIONS: usize = 8; #[derive(Debug, Clone, Default)] pub struct ProviderCredentialSnapshot { + /// Identifies this local installation, including repairs at the same revision. + pub installation_id: String, pub revision: u64, pub child_env: HashMap, pub dynamic_credentials: HashMap, } +/// One atomic workload-facing snapshot and its local installation identity. +/// +/// Only the prepared environment crosses an isolation boundary; resolver +/// material remains in the supervisor. The identity distinguishes an empty +/// fail-closed snapshot from a later repair with the same provider revision. +pub struct ChildEnvironmentSnapshot { + /// Opaque identity of the installed supervisor snapshot. + pub installation_id: String, + /// Opaque provider content fingerprint, never an ordered counter. + pub revision: u64, + /// Prepared environment used by future workload processes. + pub environment: HashMap, +} + #[derive(Debug)] struct ProviderCredentialStateInner { current: Arc, @@ -89,6 +105,7 @@ impl ProviderCredentialState { revision, ); let snapshot = Arc::new(ProviderCredentialSnapshot { + installation_id: uuid::Uuid::new_v4().to_string(), revision, child_env, dynamic_credentials, @@ -136,6 +153,7 @@ impl ProviderCredentialState { &stable_handles, ); let snapshot = Arc::new(ProviderCredentialSnapshot { + installation_id: uuid::Uuid::new_v4().to_string(), revision, child_env, dynamic_credentials, @@ -178,6 +196,7 @@ impl ProviderCredentialState { /// or holding the gateway-side resolver material. pub fn from_child_env_snapshot(revision: u64, child_env: HashMap) -> Self { let snapshot = Arc::new(ProviderCredentialSnapshot { + installation_id: uuid::Uuid::new_v4().to_string(), revision, child_env, dynamic_credentials: HashMap::new(), @@ -221,6 +240,7 @@ impl ProviderCredentialState { } inner.current = Arc::new(ProviderCredentialSnapshot { + installation_id: uuid::Uuid::new_v4().to_string(), revision, child_env, dynamic_credentials: HashMap::new(), @@ -386,6 +406,7 @@ impl ProviderCredentialState { inner.suppressed_keys.insert(key.to_string()); let mut env = (*inner.current).clone(); env.child_env.remove(key); + env.installation_id = uuid::Uuid::new_v4().to_string(); inner.current = Arc::new(env); } @@ -431,6 +452,20 @@ impl ProviderCredentialState { Ok(Self::resolve_child_env_snapshot(&inner)) } + /// Capture installation identity and prepared environment under one lock. + pub fn child_environment_snapshot(&self) -> std::io::Result { + let inner = self + .inner + .read() + .map_err(|_| std::io::Error::other("provider credential state poisoned"))?; + let (revision, environment) = Self::resolve_child_env_snapshot(&inner); + Ok(ChildEnvironmentSnapshot { + installation_id: inner.current.installation_id.clone(), + revision, + environment, + }) + } + fn resolve_child_env_snapshot( inner: &ProviderCredentialStateInner, ) -> (u64, HashMap) { @@ -491,8 +526,9 @@ impl ProviderCredentialState { /// Compare and install a workload-facing environment snapshot. /// /// Provider environment revisions are opaque content identities, not - /// ordered counters. The expected revision makes retries idempotent while - /// rejecting updates based on a stale view of the boundary state. + /// ordered counters. The expected revision rejects a different current + /// fingerprint. Callers must also fence publication order when a failed + /// refresh and its repair can share that fingerprint. pub fn compare_and_install_child_env_snapshot( &self, expected_revision: u64, @@ -503,14 +539,19 @@ impl ProviderCredentialState { .inner .write() .map_err(|_| std::io::Error::other("provider credential state poisoned"))?; - if revision == inner.current.revision || expected_revision != inner.current.revision { + if expected_revision != inner.current.revision { return Ok(inner.current.revision); } + // A failed refresh can clear the map without changing its provider + // fingerprint. A matching expectation must therefore install even an + // equal revision. Boundary publication ordering fences delayed retries. + for key in &inner.suppressed_keys { child_env.remove(key); } inner.current = Arc::new(ProviderCredentialSnapshot { + installation_id: uuid::Uuid::new_v4().to_string(), revision, child_env, dynamic_credentials: HashMap::new(), @@ -587,6 +628,7 @@ impl ProviderCredentialState { } inner.current = Arc::new(ProviderCredentialSnapshot { + installation_id: uuid::Uuid::new_v4().to_string(), revision, child_env, dynamic_credentials, @@ -652,6 +694,7 @@ impl ProviderCredentialState { child_env.remove(key); } inner.current = Arc::new(ProviderCredentialSnapshot { + installation_id: uuid::Uuid::new_v4().to_string(), revision, child_env, dynamic_credentials, @@ -711,6 +754,7 @@ impl ProviderCredentialState { let dynamic_credentials = dynamic_credentials.unwrap_or_else(|| inner.current.dynamic_credentials.clone()); inner.current = Arc::new(ProviderCredentialSnapshot { + installation_id: uuid::Uuid::new_v4().to_string(), revision, child_env: HashMap::new(), dynamic_credentials, @@ -2427,6 +2471,26 @@ mod tests { assert!(env.is_empty(), "an empty snapshot must revoke the old env"); } + #[test] + fn child_environment_repair_replaces_an_empty_map_at_the_same_revision() { + let state = ProviderCredentialState::from_child_env_snapshot(6, HashMap::new()); + let failed = state.child_environment_snapshot().unwrap(); + state + .compare_and_install_child_env_snapshot( + 6, + 6, + HashMap::from([("TOKEN".to_string(), "reference".to_string())]), + ) + .unwrap(); + let repaired = state.child_environment_snapshot().unwrap(); + assert_ne!(failed.installation_id, repaired.installation_id); + assert_eq!(repaired.revision, 6); + assert_eq!( + repaired.environment.get("TOKEN").map(String::as_str), + Some("reference") + ); + } + #[test] fn poisoned_environment_update_returns_error_without_recovering_state() { let state = ProviderCredentialState::from_child_env_snapshot(4, HashMap::new()); diff --git a/crates/openshell-isolation-interface/src/contract.rs b/crates/openshell-isolation-interface/src/contract.rs index f671dd99e9..8e68d48c77 100644 --- a/crates/openshell-isolation-interface/src/contract.rs +++ b/crates/openshell-isolation-interface/src/contract.rs @@ -762,6 +762,30 @@ pub struct ExecSpec { pub trait BoundaryExec: Send + Sync { /// Spawn `spec` inside the boundary, returning an owned session. async fn exec(&self, spec: ExecSpec) -> Result; + + /// Install the current provider environment for future process launches. + /// + /// Success requires an authenticated acknowledgment from the running + /// boundary. Implementations serialize this operation with exec so an + /// older publication cannot replace the acknowledged environment. + async fn synchronize_provider_environment( + &self, + ) -> Result { + Err(BackendError::Unsupported( + "provider environment installation acknowledgment is unavailable".to_string(), + )) + } +} + +/// Evidence that the running workload boundary installed one provider snapshot. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProviderEnvironmentInstallation { + /// Local supervisor snapshot that produced the acknowledged environment. + pub installation_id: String, + /// Opaque provider content fingerprint. + pub revision: u64, + /// Authenticated and confirmed workload boundary session. + pub session_id: SandboxSessionId, } // ============================================================================ diff --git a/crates/openshell-sandbox-backend/src/boundary_protocol.rs b/crates/openshell-sandbox-backend/src/boundary_protocol.rs index e619d68dc2..73668daae1 100644 --- a/crates/openshell-sandbox-backend/src/boundary_protocol.rs +++ b/crates/openshell-sandbox-backend/src/boundary_protocol.rs @@ -536,7 +536,8 @@ pub enum Request { provider_env: std::collections::HashMap, }, UpdateProviderEnvironment { - expected_revision: u64, + /// Ordered publication within this authenticated boundary session. + generation: u64, revision: u64, provider_env: std::collections::HashMap, }, @@ -634,12 +635,12 @@ impl fmt::Debug for Request { ) .finish(), Self::UpdateProviderEnvironment { - expected_revision, + generation, revision, provider_env, } => formatter .debug_struct("UpdateProviderEnvironment") - .field("expected_revision", expected_revision) + .field("generation", generation) .field("revision", revision) .field( "provider_env_keys", @@ -710,9 +711,13 @@ pub enum Response { Started { process_id: String, provider_env_revision: u64, + provider_env_generation: u64, }, ProviderEnvironmentUpdated { revision: u64, + generation: u64, + /// True only for this installed request or its exact idempotent replay. + applied: bool, }, ProcessAttached { terminal: bool, @@ -1298,7 +1303,7 @@ mod tests { second.insert("A".to_string(), "1".to_string()); second.insert("B".to_string(), "2".to_string()); let build = |provider_env| Request::UpdateProviderEnvironment { - expected_revision: 1, + generation: 1, revision: 2, provider_env, }; diff --git a/crates/openshell-sandbox-backend/src/runtime.rs b/crates/openshell-sandbox-backend/src/runtime.rs index 86964fc3c8..9855c1c449 100644 --- a/crates/openshell-sandbox-backend/src/runtime.rs +++ b/crates/openshell-sandbox-backend/src/runtime.rs @@ -25,8 +25,8 @@ use openshell_isolation_interface::contract::{ BoundaryInput, BoundaryLoopbackConnector, BoundaryOutput, BoundaryProcess, BoundarySignal, BoundaryTerminal, ConfirmedBoundary, ExecSession, ExecSpec, IsolationBackend, LoopbackTarget, MediationTiming, NetworkMediationSource, PendingDnsQuery, PendingTcpOpen, ProcessAttachment, - ReadyBoundary, RunningBoundary, SandboxContext, TcpOpenDecision, TcpOpenDenial, - VerifiedBackendDescriptor, + ProviderEnvironmentInstallation, ReadyBoundary, RunningBoundary, SandboxContext, + TcpOpenDecision, TcpOpenDenial, VerifiedBackendDescriptor, }; use tokio::io::{AsyncReadExt, AsyncWriteExt}; #[cfg(unix)] @@ -372,7 +372,8 @@ impl ReadyBoundary for RemoteReady { .await?; let Response::Started { process_id, - provider_env_revision, + provider_env_generation, + .. } = response else { return Err(unexpected_response("started", &response)); @@ -386,7 +387,7 @@ impl ReadyBoundary for RemoteReady { exec: Arc::new(RemoteExec { client: self.client.clone(), provider_credentials: self.provider_credentials, - boundary_revision: tokio::sync::Mutex::new(provider_env_revision), + publication_generation: tokio::sync::Mutex::new(provider_env_generation), }), loopback_connector: Arc::new(RemoteLoopbackConnector { client: self.client, @@ -551,30 +552,39 @@ async fn pump_process_responses( struct RemoteExec { client: Arc, provider_credentials: openshell_core::provider_credentials::ProviderCredentialState, - boundary_revision: tokio::sync::Mutex, + // Shared by proactive synchronization and exec. Reserve generations before + // dispatch so a timed-out request cannot overwrite a newer publication. + publication_generation: tokio::sync::Mutex, } -#[async_trait] -impl BoundaryExec for RemoteExec { - async fn exec(&self, spec: ExecSpec) -> Result { - let mut boundary_revision = self.boundary_revision.lock().await; +impl RemoteExec { + async fn synchronize( + &self, + generation: &mut u64, + ) -> Result { for _ in 0..3 { - let (revision, provider_env) = self + let snapshot = self .provider_credentials - .child_env_snapshot_with_gcp_resolved() + .child_environment_snapshot() .map_err(|error| { BackendError::Process(format!("snapshot provider environment: {error}")) })?; + *generation = generation.checked_add(1).ok_or_else(|| { + BackendError::Process("provider environment publication exhausted".to_string()) + })?; + let requested_generation = *generation; let response = self .client .call_idempotent(Request::UpdateProviderEnvironment { - expected_revision: *boundary_revision, - revision, - provider_env, + generation: requested_generation, + revision: snapshot.revision, + provider_env: snapshot.environment, }) .await?; let Response::ProviderEnvironmentUpdated { revision: effective_revision, + generation: effective_generation, + applied, } = response else { return Err(unexpected_response( @@ -582,9 +592,18 @@ impl BoundaryExec for RemoteExec { &response, )); }; - *boundary_revision = effective_revision; - if effective_revision == revision { - return open_exec_session(self.client.clone(), spec).await; + *generation = (*generation).max(effective_generation); + if applied + && effective_generation == requested_generation + && effective_revision == snapshot.revision + { + // The acknowledgment is for the exact request, including its + // map, even when a repaired map reuses a provider fingerprint. + return Ok(ProviderEnvironmentInstallation { + installation_id: snapshot.installation_id, + revision: snapshot.revision, + session_id: self.client.runtime_descriptor.session_id, + }); } } Err(BackendError::Process( @@ -593,6 +612,22 @@ impl BoundaryExec for RemoteExec { } } +#[async_trait] +impl BoundaryExec for RemoteExec { + async fn exec(&self, spec: ExecSpec) -> Result { + let mut generation = self.publication_generation.lock().await; + self.synchronize(&mut generation).await?; + open_exec_session(self.client.clone(), spec).await + } + + async fn synchronize_provider_environment( + &self, + ) -> Result { + let mut generation = self.publication_generation.lock().await; + self.synchronize(&mut generation).await + } +} + struct RemoteLoopbackConnector { client: Arc, } @@ -1796,6 +1831,7 @@ mod tests { requests: Arc, mediation_failures: Arc, mediation_ready: bool, + provider_environment_generation: u64, } type TestGrpcStream = Pin< @@ -1824,6 +1860,7 @@ mod tests { let wait_for_half_close = self.wait_for_half_close; let requests = self.requests.clone(); let mediation_ready = self.mediation_ready; + let provider_environment_generation = self.provider_environment_generation; let (outbound, outbound_rx) = tokio::sync::mpsc::channel(1); tokio::spawn(async move { let mut frame = Vec::new(); @@ -1885,9 +1922,15 @@ mod tests { } Request::Terminate { .. } => Response::Terminated, Request::TerminateBoundary => Response::BoundaryTerminated, - Request::UpdateProviderEnvironment { revision, .. } => { - Response::ProviderEnvironmentUpdated { revision } - } + Request::UpdateProviderEnvironment { + revision, + generation, + .. + } => Response::ProviderEnvironmentUpdated { + revision, + generation: generation.max(provider_environment_generation), + applied: generation > provider_environment_generation, + }, Request::Resize { .. } => Response::Resized, Request::LoopbackConnect { .. } => Response::PortConnected, Request::StartAgent { @@ -1896,6 +1939,7 @@ mod tests { } => Response::Started { process_id: "test-generation:main:0".to_string(), provider_env_revision, + provider_env_generation: provider_environment_generation, }, Request::AcceptNetwork => Response::Error { kind: crate::boundary_protocol::BoundaryErrorKind::Unavailable, @@ -1960,6 +2004,7 @@ mod tests { requests: requests.clone(), mediation_failures: Arc::new(std::sync::atomic::AtomicUsize::new(0)), mediation_ready: false, + provider_environment_generation: 0, }; let server = tokio::spawn(async move { let (stream, _) = listener.accept().await.unwrap(); @@ -2020,6 +2065,7 @@ mod tests { requests: server_requests.clone(), mediation_failures: Arc::new(std::sync::atomic::AtomicUsize::new(0)), mediation_ready: false, + provider_environment_generation: 0, }; tokio::spawn(async move { tonic::transport::Server::builder() @@ -2094,6 +2140,7 @@ mod tests { requests: server_requests.clone(), mediation_failures: server_failures.clone(), mediation_ready: true, + provider_environment_generation: 0, }; tokio::spawn(async move { tonic::transport::Server::builder() @@ -2147,6 +2194,7 @@ mod tests { requests, mediation_failures: Arc::new(std::sync::atomic::AtomicUsize::new(0)), mediation_ready: false, + provider_environment_generation: 0, }; let server = tokio::spawn(async move { let (stream, _) = listener.accept().await.unwrap(); @@ -2319,6 +2367,7 @@ mod tests { requests: Arc::new(std::sync::atomic::AtomicUsize::new(0)), mediation_failures: Arc::new(std::sync::atomic::AtomicUsize::new(0)), mediation_ready: false, + provider_environment_generation: 0, }; tonic::transport::Server::builder() .add_service(IsolationBoundaryServer::new(service)) @@ -2638,6 +2687,7 @@ mod tests { requests: handled.clone(), mediation_failures: Arc::new(std::sync::atomic::AtomicUsize::new(0)), mediation_ready: false, + provider_environment_generation: 0, }; let server = tokio::spawn(async move { loop { @@ -2696,6 +2746,110 @@ mod tests { server.abort(); } + #[tokio::test] + async fn reconstructed_backend_resumes_the_running_boundary_publication_generation() { + let certificate = test_certificate(); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let requests = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let service = TestGrpcBoundary { + wait_for_half_close: false, + expected_token: "a".repeat(32), + requests: requests.clone(), + mediation_failures: Arc::new(std::sync::atomic::AtomicUsize::new(0)), + mediation_ready: false, + provider_environment_generation: 50, + }; + let server = tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + let stream = tokio_rustls::TlsAcceptor::from(certificate.server_config) + .accept(stream) + .await + .unwrap(); + tonic::transport::Server::builder() + .add_service(IsolationBoundaryServer::new(service)) + .serve_with_incoming(tokio_stream::iter([Ok::<_, std::io::Error>(TestTlsIo( + Box::new(stream), + ))])) + .await + .unwrap(); + }); + let credentials = + openshell_core::provider_credentials::ProviderCredentialState::from_child_env_snapshot( + 6, + HashMap::new(), + ); + let context = sandbox(); + let ready = Box::new(RemoteReady { + client: Arc::new(BoundaryClient::new( + tls_runtime_descriptor(address, certificate.client_tls), + test_bearer(&"a".repeat(32)), + )), + agent: context.agent, + policy: context.policy, + sandbox_id: context.sandbox_id, + ca_file_paths: Arc::new(std::sync::Mutex::new(None)), + provider_credentials: credentials.clone(), + }); + let running = ready.start_agent().await.unwrap(); + assert_eq!(requests.load(Ordering::Acquire), 1); + let installed = running + .exec() + .synchronize_provider_environment() + .await + .unwrap(); + assert_eq!( + requests.load(Ordering::Acquire), + 2, + "the first update must advance the returned generation, without rejected catch-up calls" + ); + assert_eq!( + installed.installation_id, + credentials.snapshot().installation_id + ); + assert_eq!(installed.session_id, test_session_id()); + server.abort(); + let _ = server.await; + } + + #[tokio::test] + async fn provider_environment_acknowledges_the_exact_local_snapshot_and_checked_generation() { + let certificate = test_certificate(); + let (address, server) = spawn_tls_boundary(certificate.server_config, "a".repeat(32)).await; + let credentials = + openshell_core::provider_credentials::ProviderCredentialState::from_child_env_snapshot( + 6, + HashMap::new(), + ); + let client = Arc::new(BoundaryClient::new( + tls_runtime_descriptor(address, certificate.client_tls), + test_bearer(&"a".repeat(32)), + )); + let exec = RemoteExec { + client, + provider_credentials: credentials.clone(), + publication_generation: tokio::sync::Mutex::new(0), + }; + let empty = exec.synchronize_provider_environment().await.unwrap(); + credentials + .install_child_env_snapshot(6, HashMap::from([("TOKEN".into(), "restored".into())])); + let repaired = exec.synchronize_provider_environment().await.unwrap(); + assert_eq!(empty.revision, repaired.revision); + assert_ne!(empty.installation_id, repaired.installation_id); + assert_eq!( + repaired.installation_id, + credentials.snapshot().installation_id + ); + assert_eq!(*exec.publication_generation.lock().await, 2); + *exec.publication_generation.lock().await = u64::MAX; + assert!( + exec.synchronize_provider_environment().await.is_err(), + "publication exhaustion must fail before sending another request" + ); + server.abort(); + let _ = server.await; + } + #[tokio::test] async fn tls_tcp_flushes_large_control_requests_before_reading_response() { let certificate = test_certificate(); diff --git a/crates/openshell-sandbox/src/boundary_server.rs b/crates/openshell-sandbox/src/boundary_server.rs index 3326570d96..bc00b55f33 100644 --- a/crates/openshell-sandbox/src/boundary_server.rs +++ b/crates/openshell-sandbox/src/boundary_server.rs @@ -1863,10 +1863,10 @@ mod linux { provider_env, ), Request::UpdateProviderEnvironment { - expected_revision, + generation, revision, provider_env, - } => self.update_provider_environment(expected_revision, revision, provider_env), + } => self.update_provider_environment(generation, revision, provider_env), Request::Wait { process_id } => self.wait(&process_id), Request::Signal { process_id, signal } => self.signal(&process_id, signal), Request::Terminate { process_id } => self.terminate(&process_id), @@ -2336,6 +2336,7 @@ mod linux { Response::Started { process_id: process.process_id(), provider_env_revision: process.provider_credentials.snapshot().revision, + provider_env_generation: *lock(&process.provider_environment_generation), } } else { guest_error( @@ -2400,12 +2401,13 @@ mod linux { Response::Started { process_id, provider_env_revision, + provider_env_generation: 0, } } fn update_provider_environment( &self, - expected_revision: u64, + generation: u64, revision: u64, provider_env: std::collections::HashMap, ) -> Response { @@ -2419,14 +2421,34 @@ mod linux { }; process.clone() }; + // The session-scoped publication order is separate from opaque + // provider fingerprints. Holding it through installation prevents + // a delayed request from replacing a newer same-revision repair. + let mut installed_generation = lock(&process.provider_environment_generation); + let current = match process.provider_credentials.child_environment_snapshot() { + Ok(snapshot) => snapshot, + Err(error) => return guest_error(BoundaryErrorKind::Process, error.to_string()), + }; + if generation <= *installed_generation { + return Response::ProviderEnvironmentUpdated { + revision: current.revision, + generation: *installed_generation, + applied: false, + }; + } let revision = match process .provider_credentials - .compare_and_install_child_env_snapshot(expected_revision, revision, provider_env) + .compare_and_install_child_env_snapshot(current.revision, revision, provider_env) { Ok(revision) => revision, Err(error) => return guest_error(BoundaryErrorKind::Process, error.to_string()), }; - Response::ProviderEnvironmentUpdated { revision } + *installed_generation = generation; + Response::ProviderEnvironmentUpdated { + revision, + generation, + applied: true, + } } fn wait(&self, process_id: &str) -> Response { @@ -2699,6 +2721,7 @@ mod linux { attached: Arc, boundary_runtime: Arc, provider_credentials: ProviderCredentialState, + provider_environment_generation: Mutex, } struct ManagedProcessLaunch { @@ -2792,6 +2815,7 @@ mod linux { attached: Arc::new(AtomicBool::new(false)), boundary_runtime, provider_credentials, + provider_environment_generation: Mutex::new(0), }) } @@ -4598,13 +4622,14 @@ mod linux { let Response::Started { process_id, provider_env_revision: 0, + provider_env_generation: 0, } = start() else { panic!("initial start did not succeed"); }; let update = RequestEnvelope::new(Request::UpdateProviderEnvironment { - expected_revision: 0, + generation: 1, revision: 7, provider_env: std::collections::HashMap::from([( "REPLAY_TEST".to_string(), @@ -4614,11 +4639,19 @@ mod linux { .expect("build replayed update"); assert_eq!( boundary.dispatch(update.clone()), - Response::ProviderEnvironmentUpdated { revision: 7 } + Response::ProviderEnvironmentUpdated { + revision: 7, + generation: 1, + applied: true, + } ); assert_eq!( boundary.dispatch(update.clone()), - Response::ProviderEnvironmentUpdated { revision: 7 }, + Response::ProviderEnvironmentUpdated { + revision: 7, + generation: 1, + applied: true, + }, "the same request ID and payload must replay its recorded response" ); let mut changed = RequestEnvelope::new(Request::Terminate { @@ -4662,6 +4695,7 @@ mod linux { Response::Started { process_id: process_id.clone(), provider_env_revision: 7, + provider_env_generation: 1, } ); @@ -4684,9 +4718,44 @@ mod linux { Response::Error { kind, .. } if kind == BoundaryErrorKind::Denied )); + // Failed refresh and recovery can retain the same provider + // fingerprint. Distinct publications must still replace the map, + // while a delayed older clear must never undo the repair. + assert_eq!( + boundary.update_provider_environment(2, 7, Default::default()), + Response::ProviderEnvironmentUpdated { + revision: 7, + generation: 2, + applied: true + } + ); + assert_eq!( + boundary.update_provider_environment( + 3, + 7, + std::collections::HashMap::from([( + "REPLAY_TEST".to_string(), + "reconnected".to_string() + ),]) + ), + Response::ProviderEnvironmentUpdated { + revision: 7, + generation: 3, + applied: true + } + ); + assert_eq!( + boundary.update_provider_environment(2, 7, Default::default()), + Response::ProviderEnvironmentUpdated { + revision: 7, + generation: 3, + applied: false + } + ); + let exec_spec = ExecSpecWire { program: "/bin/sh".to_string(), - args: vec!["-c".to_string(), "printf reconnected".to_string()], + args: vec!["-c".to_string(), "printf '%s' \"$REPLAY_TEST\"".to_string()], env: Vec::new(), workdir: None, pty: false, @@ -4869,19 +4938,24 @@ mod linux { Response::Started { process_id: process.process_id(), provider_env_revision: 0, + provider_env_generation: 0, } ); assert_eq!( boundary.update_provider_environment( - 0, + 1, 2, std::collections::HashMap::from([( "ROTATED_TOKEN".to_string(), "refreshed".to_string(), )]), ), - Response::ProviderEnvironmentUpdated { revision: 2 } + Response::ProviderEnvironmentUpdated { + revision: 2, + generation: 1, + applied: true, + } ); assert_eq!( boundary.update_provider_environment( @@ -4892,11 +4966,19 @@ mod linux { "stale".to_string(), )]), ), - Response::ProviderEnvironmentUpdated { revision: 2 } + Response::ProviderEnvironmentUpdated { + revision: 2, + generation: 1, + applied: false, + } ); assert_eq!( boundary.update_provider_environment(2, 1, std::collections::HashMap::new()), - Response::ProviderEnvironmentUpdated { revision: 1 }, + Response::ProviderEnvironmentUpdated { + revision: 1, + generation: 2, + applied: true, + }, "a numerically smaller opaque revision must revoke the environment" ); assert_eq!( @@ -4908,12 +4990,20 @@ mod linux { "out-of-order".to_string(), )]), ), - Response::ProviderEnvironmentUpdated { revision: 1 }, - "a stale expected revision must not overwrite current state" + Response::ProviderEnvironmentUpdated { + revision: 1, + generation: 2, + applied: false, + }, + "a stale publication must not overwrite current state" ); assert_eq!( boundary.update_provider_environment(1, 1, std::collections::HashMap::new()), - Response::ProviderEnvironmentUpdated { revision: 1 }, + Response::ProviderEnvironmentUpdated { + revision: 1, + generation: 2, + applied: false, + }, "a duplicate update must be idempotent" ); @@ -4938,6 +5028,7 @@ mod linux { Response::Started { process_id: process.process_id(), provider_env_revision: 1, + provider_env_generation: 2, }, "a replacement control must resume from the boundary's current revision" ); diff --git a/crates/openshell-sdk/README.md b/crates/openshell-sdk/README.md index 3adbcba375..517e6841cb 100644 --- a/crates/openshell-sdk/README.md +++ b/crates/openshell-sdk/README.md @@ -114,6 +114,40 @@ let _sandbox = client # } ``` +## Wait for a provider change + +Provider attach, detach, and update responses include a `ProviderMutationReceipt`: a saved record identifying the exact change requested for one sandbox. Pass that record to `provider_readiness::wait_for_provider` to wait until the current sandbox runtime confirms it applied the change. Detach completes with `Revoked`; attach and update complete with `Ready`. + +```rust +use std::time::Duration; +use openshell_sdk::{OpenShellClient, raw::ProviderMutationReceipt}; +use openshell_sdk::provider_readiness::{ + ProviderWaitOutcome, wait_for_provider, +}; + +async fn wait_for_change( + client: &OpenShellClient, + change: &ProviderMutationReceipt, +) -> Result<(), Box> { + let mut grpc = client.raw_grpc_fresh().await?; + let result = wait_for_provider(&mut grpc, change, Duration::from_secs(30)).await?; + match result.outcome { + ProviderWaitOutcome::Complete => println!("The sandbox applied the change."), + ProviderWaitOutcome::TimedOut => println!("Still waiting; check the same change again."), + ProviderWaitOutcome::Terminal => println!("The change failed, was withheld, or was replaced."), + } + Ok(()) +} +``` + +The result preserves the last known status when the deadline expires. A later change cannot satisfy a wait for the original request. `provider_status` queries once, and `wait_for_provider_until` accepts a shared deadline for waiting on the sandboxes selected by one provider update. Status responses contain configuration identities and safe reason categories, without credentials or raw installation errors. + +For ordinary static credentials, launch a new client after update readiness to receive the updated reference. Existing processes keep their revision-scoped references; a successful wait does not retarget them or prove that the old upstream key can be retired. After detach completes, retained references cannot resolve and new processes do not receive them. + +The status's `operation` field is the common operation's historical outcome, keyed by the receipt ID. These helpers complete from the live provider state and its matching evidence; a historical applied operation cannot override a disconnected, expired, or superseded live result. + +These helpers use the raw client's authentication slot. They do not perform OIDC refresh themselves. Follow the raw-client refresh guidance above if a request returns `Unauthenticated`, then resume waiting for the same change ID. + ## Modules | Module | Purpose | @@ -129,6 +163,7 @@ let _sandbox = client | `pagination` | Lazy `Pager` and response `Page`. | | `types` | Curated request/response types and proto conversions. | | `raw` | Escape hatch re-exporting the generated tonic clients. | +| `provider_readiness` | Check and wait for an exact provider change to take effect. | ## Notes diff --git a/crates/openshell-sdk/src/lib.rs b/crates/openshell-sdk/src/lib.rs index b94b211f2f..57752af15a 100644 --- a/crates/openshell-sdk/src/lib.rs +++ b/crates/openshell-sdk/src/lib.rs @@ -37,6 +37,7 @@ pub mod edge_tunnel; pub mod error; pub mod oidc; pub mod pagination; +pub mod provider_readiness; pub mod raw; pub mod refresh; pub mod transport; diff --git a/crates/openshell-sdk/src/provider_readiness.rs b/crates/openshell-sdk/src/provider_readiness.rs new file mode 100644 index 0000000000..c28bf245bf --- /dev/null +++ b/crates/openshell-sdk/src/provider_readiness.rs @@ -0,0 +1,765 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Receipt-bound provider status and bounded waits over the raw provider API. +//! +//! A wait pins the original desired identity. It cannot succeed by following a +//! later mutation, and its deadline covers RPC execution as well as polling. + +use crate::raw::GrpcClient; +use openshell_core::proto::{ + GetSandboxProviderStatusRequest, ProviderMutationKind, ProviderMutationReceipt, + ProviderReadinessReason, ProviderReadinessState, ProviderReadinessStatus, +}; +use std::future::Future; +use std::time::Duration; +use thiserror::Error; +use tokio::time::Instant; +use tonic::service::Interceptor; +use tonic::service::interceptor::InterceptedService; +use tonic::transport::Channel; + +/// Maximum duration accepted by provider waits. +pub const MAX_PROVIDER_WAIT: Duration = Duration::from_hours(1); + +/// Completion of a bounded wait, separate from the gateway's installed state. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ProviderWaitOutcome { + /// The original receipt's authority was installed or revoked as requested. + Complete, + /// The deadline elapsed; the last observed state remains authoritative. + TimedOut, + /// Installation failed, was withheld, or the desired authority was superseded. + Terminal, +} + +/// Last safe status and the reason a provider wait returned. +#[derive(Clone, Debug)] +pub struct ProviderWaitResult { + /// Status for the original receipt, including on timeout. + pub status: ProviderReadinessStatus, + /// Whether installation completed, failed, or exhausted the deadline. + pub outcome: ProviderWaitOutcome, +} + +/// Errors safe to display without including raw secret-bearing RPC messages. +#[derive(Clone, Debug, Error)] +pub enum ProviderReadinessError { + /// Provider waits require a positive timeout of at most one hour. + #[error("provider wait timeout must be greater than zero and at most 3600 seconds")] + InvalidTimeout, + /// A receipt must identify a sandbox, workspace, provider, and desired state. + #[error("provider readiness receipt is incomplete")] + InvalidReceipt, + /// The gateway returned no usable status or an unknown protocol state. + #[error("gateway returned an invalid provider readiness status")] + InvalidStatus, + /// The RPC failed; only its protocol code is exposed. + #[error("provider readiness request failed ({code})")] + Rpc { + /// gRPC status code without the server's diagnostic message or metadata. + code: tonic::Code, + }, +} + +/// Query a provider receipt or reconstruct current state when its ID is empty. +/// A receipt ID selects its provider when the request omits the provider name. +/// +/// # Errors +/// Returns a safe protocol error if the gateway is unavailable or omits status. +pub async fn provider_status( + client: &mut GrpcClient>, + request: GetSandboxProviderStatusRequest, +) -> Result +where + I: Interceptor + Clone + Send + Sync, +{ + let expected = request.clone(); + let status = client + .get_sandbox_provider_status(request) + .await + .map_err(|error| ProviderReadinessError::Rpc { code: error.code() })? + .into_inner() + .status + .ok_or(ProviderReadinessError::InvalidStatus)?; + validate_status_for_request(&status, &expected)?; + Ok(status) +} + +fn validate_status_for_request( + status: &ProviderReadinessStatus, + expected: &GetSandboxProviderStatusRequest, +) -> Result<(), ProviderReadinessError> { + validate_status(status)?; + let receipt = status + .receipt + .as_ref() + .ok_or(ProviderReadinessError::InvalidStatus)?; + // Either a receipt ID or a provider name must select the authority. Every + // supplied selector must still bind the response to that authority. + if (expected.receipt_id.is_empty() && expected.provider_name.is_empty()) + || (!expected.receipt_id.is_empty() && receipt.receipt_id != expected.receipt_id) + || (!expected.provider_name.is_empty() && receipt.provider_name != expected.provider_name) + || expected.workspace_scope.as_ref().is_some_and(|scope| { + !matches!( + scope.selection.as_ref(), + Some(openshell_core::proto::workspace_selector::Selection::Workspace(workspace)) + if workspace == &receipt.workspace + ) + }) + || receipt + .desired + .as_ref() + .is_none_or(|desired| desired.sandbox_name != expected.sandbox_name) + { + return Err(ProviderReadinessError::InvalidStatus); + } + Ok(()) +} + +/// Wait for the exact authority named by a provider receipt. +/// +/// Detach requires `REVOKED`; attach and update require `READY`. A reconstructed +/// observation waits for ready or revoked according to its original provider ID. +/// This raw helper uses the authentication slot on the supplied client. +/// +/// # Errors +/// Rejects an invalid timeout/receipt, malformed responses, or failed RPCs. +pub async fn wait_for_provider( + client: &mut GrpcClient>, + receipt: &ProviderMutationReceipt, + timeout: Duration, +) -> Result +where + I: Interceptor + Clone + Send + Sync, +{ + let deadline = provider_wait_deadline(timeout)?; + wait_for_provider_until(client, receipt, deadline).await +} + +/// Wait using a shared deadline, so a multi-sandbox update has one total bound. +/// +/// # Errors +/// Returns safe protocol errors without retaining the underlying RPC message. +pub async fn wait_for_provider_until( + client: &mut GrpcClient>, + receipt: &ProviderMutationReceipt, + deadline: Instant, +) -> Result +where + I: Interceptor + Clone + Send + Sync, +{ + wait_with(receipt, deadline, Duration::from_millis(250), |request| { + let mut client = client.clone(); + async move { provider_status(&mut client, request).await } + }) + .await +} + +/// Continue a bounded wait from a status that has already been observed. +/// +/// Completed observations return immediately. A later failed or timed-out RPC +/// cannot discard the last observed pending state and replace it with saved intent. +/// +/// # Errors +/// Rejects malformed receipts/statuses and reports only safe protocol errors. +pub async fn wait_for_provider_status_until( + client: &mut GrpcClient>, + status: &ProviderReadinessStatus, + deadline: Instant, +) -> Result +where + I: Interceptor + Clone + Send + Sync, +{ + let receipt = status + .receipt + .as_ref() + .ok_or(ProviderReadinessError::InvalidStatus)?; + wait_with_initial_status( + receipt, + status.clone(), + deadline, + Duration::from_millis(250), + |request| { + let mut client = client.clone(); + async move { provider_status(&mut client, request).await } + }, + ) + .await +} + +/// Validate a timeout and create a deadline shared across all selected sandboxes. +/// +/// # Errors +/// A zero timeout or a timeout over one hour is rejected before mutation. +pub fn provider_wait_deadline(timeout: Duration) -> Result { + if timeout.is_zero() || timeout > MAX_PROVIDER_WAIT { + return Err(ProviderReadinessError::InvalidTimeout); + } + Ok(Instant::now() + timeout) +} + +/// Represent saved intent before any runtime observation has been fetched. +#[must_use] +pub fn persisted_status(receipt: ProviderMutationReceipt) -> ProviderReadinessStatus { + ProviderReadinessStatus { + receipt: Some(receipt), + state: ProviderReadinessState::Persisted.into(), + reason: ProviderReadinessReason::WaitingForSupervisor.into(), + ..Default::default() + } +} + +fn validate_status(status: &ProviderReadinessStatus) -> Result<(), ProviderReadinessError> { + if status.receipt.is_none() + || matches!( + ProviderReadinessState::try_from(status.state), + Err(_) | Ok(ProviderReadinessState::Unspecified) + ) + || ProviderReadinessReason::try_from(status.reason).is_err() + { + return Err(ProviderReadinessError::InvalidStatus); + } + let receipt = status + .receipt + .as_ref() + .ok_or(ProviderReadinessError::InvalidStatus)?; + validate_provider_receipt(receipt).map_err(|_| ProviderReadinessError::InvalidStatus)?; + let state = ProviderReadinessState::try_from(status.state) + .map_err(|_| ProviderReadinessError::InvalidStatus)?; + if matches!( + state, + ProviderReadinessState::Ready | ProviderReadinessState::Revoked + ) { + if state != completion_state(receipt).map_err(|_| ProviderReadinessError::InvalidStatus)? { + return Err(ProviderReadinessError::InvalidStatus); + } + // A status read has the same installation contract as a wait. Enum + // labels alone must never report ready or revoked to a direct caller. + validate_completion(status)?; + } + Ok(()) +} + +fn completion_state( + receipt: &ProviderMutationReceipt, +) -> Result { + let desired = receipt + .desired + .as_ref() + .ok_or(ProviderReadinessError::InvalidReceipt)?; + let attached = !desired.provider_id.is_empty(); + match ProviderMutationKind::try_from(receipt.kind) { + Ok(ProviderMutationKind::Attach | ProviderMutationKind::Update) if attached => { + Ok(ProviderReadinessState::Ready) + } + Ok(ProviderMutationKind::Detach) if !attached => Ok(ProviderReadinessState::Revoked), + Ok(ProviderMutationKind::Observe) => Ok(if attached { + ProviderReadinessState::Ready + } else { + ProviderReadinessState::Revoked + }), + _ => Err(ProviderReadinessError::InvalidReceipt), + } +} + +/// Validate saved receipt identity without fetching a runtime observation. +/// +/// Initial attachment epochs may be empty and revision fingerprints may be zero; +/// the mutation kind must still agree with the presence of a provider identity. +/// +/// # Errors +/// Rejects incomplete receipts and inconsistent mutation/provider identities. +pub fn validate_provider_receipt( + receipt: &ProviderMutationReceipt, +) -> Result<(), ProviderReadinessError> { + let desired = receipt + .desired + .as_ref() + .ok_or(ProviderReadinessError::InvalidReceipt)?; + if receipt.receipt_id.is_empty() + || receipt.mutation_id.is_empty() + || receipt.persisted_at_ms <= 0 + || desired.sandbox_id.is_empty() + || desired.sandbox_name.is_empty() + || receipt.provider_name.is_empty() + || receipt.workspace.is_empty() + || matches!( + ProviderMutationKind::try_from(receipt.kind), + Err(_) | Ok(ProviderMutationKind::Unspecified) + ) + { + return Err(ProviderReadinessError::InvalidReceipt); + } + completion_state(receipt)?; + Ok(()) +} + +fn request_for_receipt( + receipt: &ProviderMutationReceipt, +) -> Result { + validate_provider_receipt(receipt)?; + let desired = receipt + .desired + .as_ref() + .ok_or(ProviderReadinessError::InvalidReceipt)?; + Ok(GetSandboxProviderStatusRequest { + sandbox_name: desired.sandbox_name.clone(), + provider_name: receipt.provider_name.clone(), + receipt_id: receipt.receipt_id.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector( + &receipt.workspace, + )), + }) +} + +fn disposition( + receipt: &ProviderMutationReceipt, + status: &mut ProviderReadinessStatus, +) -> Result, ProviderReadinessError> { + validate_status(status)?; + let observed_receipt = status + .receipt + .as_ref() + .ok_or(ProviderReadinessError::InvalidStatus)?; + // Status reconstruction creates an immutable observation receipt too. The + // full receipt must remain fixed; a later snapshot cannot move this target. + if observed_receipt.desired != receipt.desired + || observed_receipt.provider_name != receipt.provider_name + || observed_receipt.workspace != receipt.workspace + || (!receipt.receipt_id.is_empty() + && (observed_receipt.receipt_id != receipt.receipt_id + || observed_receipt.mutation_id != receipt.mutation_id + || observed_receipt.kind != receipt.kind + || observed_receipt.persisted_at_ms != receipt.persisted_at_ms)) + { + status.receipt = Some(receipt.clone()); + status.state = ProviderReadinessState::Superseded.into(); + status.reason = ProviderReadinessReason::DesiredStateChanged.into(); + return Ok(Some(ProviderWaitOutcome::Terminal)); + } + let state = ProviderReadinessState::try_from(status.state) + .map_err(|_| ProviderReadinessError::InvalidStatus)?; + let revoked = completion_state(receipt)? == ProviderReadinessState::Revoked; + match state { + ProviderReadinessState::Ready if !revoked => { + validate_completion(status)?; + Ok(Some(ProviderWaitOutcome::Complete)) + } + ProviderReadinessState::Revoked if revoked => { + validate_completion(status)?; + Ok(Some(ProviderWaitOutcome::Complete)) + } + ProviderReadinessState::Withheld + | ProviderReadinessState::Failed + | ProviderReadinessState::Superseded => Ok(Some(ProviderWaitOutcome::Terminal)), + ProviderReadinessState::Ready | ProviderReadinessState::Revoked => { + Err(ProviderReadinessError::InvalidStatus) + } + _ => Ok(None), + } +} + +fn validate_completion(status: &ProviderReadinessStatus) -> Result<(), ProviderReadinessError> { + let desired = status + .receipt + .as_ref() + .and_then(|receipt| receipt.desired.as_ref()) + .ok_or(ProviderReadinessError::InvalidStatus)?; + let observed = status + .observed + .as_ref() + .ok_or(ProviderReadinessError::InvalidStatus)?; + if desired.policy_hash.is_empty() + || status.network_instance_id.is_empty() + || observed.session_id.is_empty() + || observed.process_instance_id.is_empty() + || !observed.credentials_installed + || !observed.policy_active + || !observed.launch_environment_installed + || observed.reason != i32::from(ProviderReadinessReason::Unspecified) + || status.reason != i32::from(ProviderReadinessReason::Unspecified) + || 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 + { + return Err(ProviderReadinessError::InvalidStatus); + } + Ok(()) +} + +async fn wait_with( + receipt: &ProviderMutationReceipt, + deadline: Instant, + interval: Duration, + fetch: F, +) -> Result +where + F: FnMut(GetSandboxProviderStatusRequest) -> Fut, + Fut: Future>, +{ + wait_with_initial_status( + receipt, + persisted_status(receipt.clone()), + deadline, + interval, + fetch, + ) + .await +} + +async fn wait_with_initial_status( + receipt: &ProviderMutationReceipt, + mut last: ProviderReadinessStatus, + deadline: Instant, + interval: Duration, + mut fetch: F, +) -> Result +where + F: FnMut(GetSandboxProviderStatusRequest) -> Fut, + Fut: Future>, +{ + let request = request_for_receipt(receipt)?; + if let Some(outcome) = disposition(receipt, &mut last)? { + return Ok(ProviderWaitResult { + status: last, + outcome, + }); + } + loop { + if Instant::now() >= deadline { + return Ok(ProviderWaitResult { + status: last, + outcome: ProviderWaitOutcome::TimedOut, + }); + } + // The timeout also cancels an RPC that never responds; sleeping alone + // between polls cannot enforce a bounded wait against an unavailable peer. + match tokio::time::timeout_at(deadline, fetch(request.clone())).await { + Ok(result) => last = result?, + Err(_) => { + return Ok(ProviderWaitResult { + status: last, + outcome: ProviderWaitOutcome::TimedOut, + }); + } + } + if let Some(outcome) = disposition(receipt, &mut last)? { + return Ok(ProviderWaitResult { + status: last, + outcome, + }); + } + tokio::time::sleep_until((Instant::now() + interval).min(deadline)).await; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use openshell_core::proto::ProviderDesiredIdentity; + + fn receipt(kind: ProviderMutationKind) -> ProviderMutationReceipt { + ProviderMutationReceipt { + receipt_id: "receipt".into(), + mutation_id: "mutation".into(), + provider_name: "provider".into(), + workspace: "default".into(), + persisted_at_ms: 1, + kind: kind.into(), + desired: Some(ProviderDesiredIdentity { + sandbox_id: "sandbox-id".into(), + sandbox_name: "sandbox".into(), + provider_id: if kind == ProviderMutationKind::Detach { + String::new() + } else { + "provider-id".into() + }, + attachment_epoch: "attachment-epoch".into(), + policy_hash: "policy-hash".into(), + provider_env_revision: u64::MAX, + ..Default::default() + }), + } + } + + fn completed_status( + receipt: &ProviderMutationReceipt, + state: ProviderReadinessState, + ) -> ProviderReadinessStatus { + let desired = receipt.desired.as_ref().unwrap(); + ProviderReadinessStatus { + receipt: Some(receipt.clone()), + state: state.into(), + network_instance_id: "network-instance".into(), + observed: Some(openshell_core::proto::ProviderReadinessObservation { + session_id: "session".into(), + process_instance_id: "process-instance".into(), + 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, + ..Default::default() + }), + ..Default::default() + } + } + + #[test] + fn direct_status_accepts_receipt_only_and_explicit_provider_selectors() { + let receipt = receipt(ProviderMutationKind::Attach); + let status = completed_status(&receipt, ProviderReadinessState::Ready); + let mut request = GetSandboxProviderStatusRequest { + sandbox_name: "sandbox".into(), + receipt_id: receipt.receipt_id.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + ..Default::default() + }; + assert!(validate_status_for_request(&status, &request).is_ok()); + + request.provider_name = receipt.provider_name; + assert!(validate_status_for_request(&status, &request).is_ok()); + + request.receipt_id.clear(); + assert!(validate_status_for_request(&status, &request).is_ok()); + } + + #[test] + fn direct_status_rejects_mismatched_explicit_selectors() { + let receipt = receipt(ProviderMutationKind::Attach); + let status = completed_status(&receipt, ProviderReadinessState::Ready); + let request = GetSandboxProviderStatusRequest { + sandbox_name: "sandbox".into(), + receipt_id: receipt.receipt_id, + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + ..Default::default() + }; + for invalid in [ + GetSandboxProviderStatusRequest { + receipt_id: String::new(), + ..request.clone() + }, + GetSandboxProviderStatusRequest { + provider_name: "other-provider".into(), + ..request.clone() + }, + GetSandboxProviderStatusRequest { + receipt_id: "other-receipt".into(), + ..request.clone() + }, + GetSandboxProviderStatusRequest { + sandbox_name: "other-sandbox".into(), + ..request.clone() + }, + GetSandboxProviderStatusRequest { + workspace_scope: Some(openshell_core::proto::workspace_selector("other-workspace")), + ..request + }, + ] { + assert!(matches!( + validate_status_for_request(&status, &invalid), + Err(ProviderReadinessError::InvalidStatus) + )); + } + } + + #[tokio::test] + async fn pending_process_waits_until_exact_receipt_is_ready() { + let receipt = receipt(ProviderMutationKind::Attach); + let mut calls = 0; + let result = wait_with( + &receipt, + Instant::now() + Duration::from_secs(1), + Duration::ZERO, + |_| { + calls += 1; + let mut status = completed_status(&receipt, ProviderReadinessState::Ready); + status.state = if calls == 1 { + ProviderReadinessState::Pending + } else { + ProviderReadinessState::Ready + } + .into(); + status.reason = if calls == 1 { + ProviderReadinessReason::WaitingForProcess + } else { + ProviderReadinessReason::Unspecified + } + .into(); + async { Ok(status) } + }, + ) + .await + .unwrap(); + assert_eq!(calls, 2); + assert_eq!(result.outcome, ProviderWaitOutcome::Complete); + } + + #[tokio::test] + async fn deadline_bounds_a_hung_status_rpc() { + let receipt = receipt(ProviderMutationKind::Update); + let result = wait_with( + &receipt, + Instant::now() + Duration::from_millis(10), + Duration::ZERO, + |_| std::future::pending(), + ) + .await + .unwrap(); + assert_eq!(result.outcome, ProviderWaitOutcome::TimedOut); + assert_eq!( + result.status.state, + i32::from(ProviderReadinessState::Persisted) + ); + } + + #[test] + fn different_authority_is_superseded_even_with_a_smaller_fingerprint() { + let receipt = receipt(ProviderMutationKind::Update); + let mut newer_receipt = receipt.clone(); + newer_receipt + .desired + .as_mut() + .unwrap() + .provider_env_revision = 1; + let mut status = completed_status(&newer_receipt, ProviderReadinessState::Ready); + assert_eq!( + disposition(&receipt, &mut status).unwrap(), + Some(ProviderWaitOutcome::Terminal) + ); + assert_eq!(status.state, i32::from(ProviderReadinessState::Superseded)); + } + + #[test] + fn detach_requires_revocation_and_installation_failure_is_terminal() { + let receipt = receipt(ProviderMutationKind::Detach); + let mut status = completed_status(&receipt, ProviderReadinessState::Ready); + assert!(disposition(&receipt, &mut status).is_err()); + status.state = ProviderReadinessState::Revoked.into(); + assert_eq!( + disposition(&receipt, &mut status).unwrap(), + Some(ProviderWaitOutcome::Complete) + ); + status.state = ProviderReadinessState::Failed.into(); + status.reason = ProviderReadinessReason::CredentialInstallFailed.into(); + assert_eq!( + disposition(&receipt, &mut status).unwrap(), + Some(ProviderWaitOutcome::Terminal) + ); + } + + #[test] + fn initial_empty_epoch_requires_a_complete_policy_identity() { + let mut receipt = receipt(ProviderMutationKind::Observe); + receipt.desired.as_mut().unwrap().attachment_epoch.clear(); + let mut status = completed_status(&receipt, ProviderReadinessState::Ready); + assert_eq!( + disposition(&receipt, &mut status).unwrap(), + Some(ProviderWaitOutcome::Complete) + ); + + receipt.desired.as_mut().unwrap().policy_hash.clear(); + let mut status = completed_status(&receipt, ProviderReadinessState::Ready); + assert!(disposition(&receipt, &mut status).is_err()); + } + + #[test] + fn direct_status_rejects_missing_installation_and_inconsistent_authority() { + let attached = receipt(ProviderMutationKind::Attach); + let mut status = completed_status(&attached, ProviderReadinessState::Ready); + status + .observed + .as_mut() + .unwrap() + .launch_environment_installed = false; + assert!(validate_status(&status).is_err()); + + let mut missing_provider = attached; + missing_provider + .desired + .as_mut() + .unwrap() + .provider_id + .clear(); + assert!( + validate_status(&completed_status( + &missing_provider, + ProviderReadinessState::Ready + )) + .is_err() + ); + + let mut detach = receipt(ProviderMutationKind::Detach); + detach.desired.as_mut().unwrap().provider_id = "still-attached".into(); + assert!( + validate_status(&completed_status(&detach, ProviderReadinessState::Revoked)).is_err() + ); + + let mut observe = receipt(ProviderMutationKind::Observe); + observe.desired.as_mut().unwrap().provider_id.clear(); + assert!( + validate_status(&completed_status(&observe, ProviderReadinessState::Revoked)).is_ok() + ); + } + + #[tokio::test] + async fn initial_completed_status_returns_without_another_rpc() { + let receipt = receipt(ProviderMutationKind::Attach); + let status = completed_status(&receipt, ProviderReadinessState::Ready); + let result = wait_with_initial_status( + &receipt, + status, + Instant::now() + Duration::from_millis(10), + Duration::ZERO, + |_| std::future::pending(), + ) + .await + .unwrap(); + assert_eq!(result.outcome, ProviderWaitOutcome::Complete); + } + + #[tokio::test] + async fn initial_pending_status_survives_a_hung_followup_rpc() { + let receipt = receipt(ProviderMutationKind::Attach); + let mut status = persisted_status(receipt.clone()); + status.state = ProviderReadinessState::Pending.into(); + status.reason = ProviderReadinessReason::WaitingForProcess.into(); + let result = wait_with_initial_status( + &receipt, + status.clone(), + Instant::now() + Duration::from_millis(10), + Duration::ZERO, + |_| std::future::pending(), + ) + .await + .unwrap(); + assert_eq!(result.outcome, ProviderWaitOutcome::TimedOut); + assert_eq!(result.status, status); + } + + #[test] + fn rejects_unbounded_waits_and_unknown_states() { + assert!(provider_wait_deadline(Duration::ZERO).is_err()); + assert!(provider_wait_deadline(MAX_PROVIDER_WAIT + Duration::from_secs(1)).is_err()); + let receipt = receipt(ProviderMutationKind::Attach); + let mut status = persisted_status(receipt.clone()); + status.state = 99; + assert!(disposition(&receipt, &mut status).is_err()); + } + + #[test] + fn claimed_ready_without_process_acknowledgment_is_invalid() { + let receipt = receipt(ProviderMutationKind::Attach); + let mut status = completed_status(&receipt, ProviderReadinessState::Ready); + status + .observed + .as_mut() + .unwrap() + .launch_environment_installed = false; + assert!(disposition(&receipt, &mut status).is_err()); + } +} diff --git a/crates/openshell-sdk/src/raw.rs b/crates/openshell-sdk/src/raw.rs index 3b1bb6605a..dd3b7b165b 100644 --- a/crates/openshell-sdk/src/raw.rs +++ b/crates/openshell-sdk/src/raw.rs @@ -23,13 +23,16 @@ pub use openshell_core::proto::open_shell_client::OpenShellClient as GrpcClient; pub use openshell_core::proto::{ CreateSandboxRequest, CreateSandboxTemplateRequest, CreateWorkspaceRequest, DeleteSandboxRequest, DeleteSandboxTemplateRequest, DeleteWorkspaceRequest, ExecSandboxRequest, - GetSandboxRequest, GetSandboxTemplateRequest, GetWorkspaceRequest, HealthRequest, - ListProvidersRequest, ListSandboxTemplatesRequest, ListSandboxesRequest, ListWorkspacesRequest, - Sandbox, SandboxPhase as ProtoSandboxPhase, SandboxResources, SandboxServiceLevel, - SandboxSpec as ProtoSandboxSpec, SandboxStartup, SandboxTemplate, SandboxTemplateResponse, - SandboxWorkloadConfig, SandboxWorkloadTemplate, SandboxWorkloadTemplateProvenance, - SandboxWorkloadTemplateSpec, ServiceStatus as ProtoServiceStatus, StartSandboxRequest, - StopSandboxRequest, Workspace, + GetSandboxProviderStatusRequest, GetSandboxProviderStatusResponse, GetSandboxRequest, + GetSandboxTemplateRequest, GetWorkspaceRequest, HealthRequest, ListProvidersRequest, + ListSandboxTemplatesRequest, ListSandboxesRequest, ListWorkspacesRequest, + ProviderDesiredIdentity, ProviderMutationKind, ProviderMutationReceipt, + ProviderReadinessObservation, ProviderReadinessReason, ProviderReadinessState, + ProviderReadinessStatus, Sandbox, SandboxPhase as ProtoSandboxPhase, SandboxResources, + SandboxServiceLevel, SandboxSpec as ProtoSandboxSpec, SandboxStartup, SandboxTemplate, + SandboxTemplateResponse, SandboxWorkloadConfig, SandboxWorkloadTemplate, + SandboxWorkloadTemplateProvenance, SandboxWorkloadTemplateSpec, + ServiceStatus as ProtoServiceStatus, StartSandboxRequest, StopSandboxRequest, Workspace, }; /// Type alias for the gRPC client wrapped in the SDK's auth interceptor. diff --git a/crates/openshell-sdk/tests/client_mock.rs b/crates/openshell-sdk/tests/client_mock.rs index 6202f0bc2c..e9a29d94b3 100644 --- a/crates/openshell-sdk/tests/client_mock.rs +++ b/crates/openshell-sdk/tests/client_mock.rs @@ -693,6 +693,24 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("unused")) } + async fn get_sandbox_provider_status( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "provider readiness is not exercised by this mock", + )) + } + + async fn report_provider_readiness( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "provider installation reports are not exercised by this mock", + )) + } + async fn get_sandbox_provider_environment( &self, _: tonic::Request, diff --git a/crates/openshell-server/proto/storage.proto b/crates/openshell-server/proto/storage.proto index 2ca7946f83..49ce6ec4d7 100644 --- a/crates/openshell-server/proto/storage.proto +++ b/crates/openshell-server/proto/storage.proto @@ -74,6 +74,29 @@ message StoredProviderProfile { openshell.v1.ProviderProfile profile = 2; } +// Durable, non-secret progress for a sandbox-scoped desired-state mutation. +// The target_* fields let a recovery worker bind the exact effective snapshot +// revision after the desired-state transaction commits. +message StoredConfigUpdateOperation { + openshell.datamodel.v1.ObjectMeta metadata = 1; + openshell.v1.ConfigUpdateOperation operation = 2; + uint32 target_policy_version = 3; + uint64 target_settings_revision = 4; + openshell.v1.SandboxPhase initial_phase = 5; + string idempotency_key = 6; + uint32 attempt_count = 7; + int64 next_attempt_at_ms = 8; + uint32 response_policy_version = 9; + string response_policy_hash = 10; + uint64 response_settings_revision = 11; + bool response_deleted = 12; + map response_annotations = 13; + // Immutable provider mutation intent associated with this operation. + openshell.v1.ProviderMutationReceipt provider_receipt = 14; + // Closed reason captured when resolving the operation's provider snapshot. + openshell.v1.ProviderReadinessReason provider_snapshot_reason = 15; +} + // Stored payload for a policy revision row in the generic objects table. message PolicyRevisionPayload { // Serialized policy contents. diff --git a/crates/openshell-server/src/config_update_operation.rs b/crates/openshell-server/src/config_update_operation.rs new file mode 100644 index 0000000000..1f1622c618 --- /dev/null +++ b/crates/openshell-server/src/config_update_operation.rs @@ -0,0 +1,649 @@ +// 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, ObjectType, PersistenceError, Store, WriteCondition, current_time_ms, +}; +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; + +impl ObjectType for StoredConfigUpdateOperation { + fn object_type() -> &'static str { + CONFIG_UPDATE_OPERATION_OBJECT_TYPE + } +} + +/// 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 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_name.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 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_at_ms: receipt.persisted_at_ms, + updated_at_ms: receipt.persisted_at_ms, + completed_at_ms: if failed { receipt.persisted_at_ms } else { 0 }, + }; + let stored = StoredConfigUpdateOperation { + metadata: Some(ObjectMeta { + id: receipt.receipt_id.clone(), + name: receipt.receipt_id.clone(), + workspace: receipt.workspace.clone(), + created_at_ms: receipt.persisted_at_ms, + ..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_name != receipt.provider_name + || 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 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.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() + }; + operation.updated_at_ms = current_time_ms(); + operation.completed_at_ms = operation.updated_at_ms; + 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_name: "provider".to_string(), + workspace: "default".to_string(), + kind: ProviderMutationKind::Update.into(), + desired: Some(ProviderDesiredIdentity { + sandbox_id: Uuid::new_v4().to_string(), + sandbox_name: "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_at_ms: current_time_ms(), + } + } + + 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.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(); + assert_eq!( + status.operation.unwrap().state, + ConfigUpdateOperationState::Failed as i32 + ); + } + + #[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_at_ms += 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/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index 0418ea7685..ba00169828 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -6,6 +6,7 @@ mod auth_rpc; pub mod policy; pub mod provider; +pub mod provider_readiness; mod sandbox; pub use sandbox::mint_persisted_authentication; mod service; @@ -36,7 +37,8 @@ use openshell_core::proto::{ GetProviderRefreshStatusResponse, GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxLogsRequest, GetSandboxLogsResponse, GetSandboxPolicyStatusRequest, GetSandboxPolicyStatusResponse, - GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, GetSandboxRequest, + GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, + GetSandboxProviderStatusRequest, GetSandboxProviderStatusResponse, GetSandboxRequest, GetSandboxTemplateRequest, GetServiceRequest, GetWorkspaceRequest, GetWorkspaceResponse, GpuResourceCapabilities, HealthRequest, HealthResponse, ImportProviderProfilesRequest, ImportProviderProfilesResponse, IssueSandboxTokenRequest, IssueSandboxTokenResponse, @@ -51,12 +53,13 @@ use openshell_core::proto::{ RefreshSandboxTokenResponse, RejectDraftChunkRequest, RejectDraftChunkResponse, RelayFrame, RemoveWorkspaceMemberRequest, RemoveWorkspaceMemberResponse, ReportEndpointStatusRequest, ReportEndpointStatusResponse, ReportMainProcessExitRequest, ReportMainProcessExitResponse, - ReportPolicyStatusRequest, ReportPolicyStatusResponse, ResourceCapabilities, - RevokeSshSessionRequest, RevokeSshSessionResponse, RotateProviderCredentialRequest, - RotateProviderCredentialResponse, SandboxResponse, SandboxTemplateResponse, - ServiceEndpointResponse, ServiceStatus, StartSandboxRequest, StopSandboxRequest, - SubmitPolicyAnalysisRequest, SubmitPolicyAnalysisResponse, SupervisorMessage, TcpForwardFrame, - UndoDraftChunkRequest, UndoDraftChunkResponse, UpdateConfigRequest, UpdateConfigResponse, + ReportPolicyStatusRequest, ReportPolicyStatusResponse, ReportProviderReadinessRequest, + ReportProviderReadinessResponse, ResourceCapabilities, RevokeSshSessionRequest, + RevokeSshSessionResponse, RotateProviderCredentialRequest, RotateProviderCredentialResponse, + SandboxResponse, SandboxTemplateResponse, ServiceEndpointResponse, ServiceStatus, + StartSandboxRequest, StopSandboxRequest, SubmitPolicyAnalysisRequest, + SubmitPolicyAnalysisResponse, SupervisorMessage, TcpForwardFrame, UndoDraftChunkRequest, + UndoDraftChunkResponse, UpdateConfigRequest, UpdateConfigResponse, UpdateProviderProfilesRequest, UpdateProviderProfilesResponse, UpdateProviderRequest, WatchSandboxRequest, open_shell_server::OpenShell, }; @@ -336,6 +339,20 @@ impl OpenShell for OpenShellService { sandbox::handle_detach_sandbox_provider(&self.state, request).await } + async fn get_sandbox_provider_status( + &self, + request: Request, + ) -> Result, Status> { + provider_readiness::handle_get_sandbox_provider_status(&self.state, request).await + } + + async fn report_provider_readiness( + &self, + request: Request, + ) -> Result, Status> { + provider_readiness::handle_report_provider_readiness(&self.state, request).await + } + async fn delete_sandbox( &self, request: Request, diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 8abdfd36d0..3b02bdd616 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -51,11 +51,11 @@ use openshell_core::proto::{ GetSandboxPolicyStatusRequest, GetSandboxPolicyStatusResponse, GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, ListSandboxPoliciesRequest, ListSandboxPoliciesResponse, PolicyChunk, PolicyMergeOperation, - PolicySource, PolicyStatus, PushSandboxLogsRequest, PushSandboxLogsResponse, - RejectDraftChunkRequest, RejectDraftChunkResponse, ReportPolicyStatusRequest, - ReportPolicyStatusResponse, SandboxLogLine, SandboxPolicyRevision, SettingScope, SettingValue, - SubmitPolicyAnalysisRequest, SubmitPolicyAnalysisResponse, UndoDraftChunkRequest, - UndoDraftChunkResponse, UpdateConfigRequest, UpdateConfigResponse, + PolicySource, PolicyStatus, ProviderReadinessReason, PushSandboxLogsRequest, + PushSandboxLogsResponse, RejectDraftChunkRequest, RejectDraftChunkResponse, + ReportPolicyStatusRequest, ReportPolicyStatusResponse, SandboxLogLine, SandboxPolicyRevision, + SettingScope, SettingValue, SubmitPolicyAnalysisRequest, SubmitPolicyAnalysisResponse, + UndoDraftChunkRequest, UndoDraftChunkResponse, UpdateConfigRequest, UpdateConfigResponse, }; use openshell_core::proto::{ L7DenyRule, L7Rule, NetworkBinary, NetworkEndpoint, NetworkPolicyRule, Provider, Sandbox, @@ -2499,6 +2499,16 @@ pub(super) async fn handle_get_sandbox_config( let sandbox = super::sandbox::fetch_and_authorize_sandbox(state, &principal, &sandbox_id).await?; + Ok(Response::new(load_sandbox_config(state, &sandbox).await?)) +} + +/// Resolve the same effective configuration for authenticated RPCs and trusted +/// readiness snapshots. Callers must authorize external access before entering. +pub(super) async fn load_sandbox_config( + state: &Arc, + sandbox: &Sandbox, +) -> Result { + let sandbox_id = sandbox.object_id().to_string(); let workspace = sandbox.object_workspace().to_string(); let sandbox_provider_names = sandbox .spec @@ -2710,7 +2720,7 @@ pub(super) async fn handle_get_sandbox_config( ) .await?; - Ok(Response::new(GetSandboxConfigResponse { + Ok(GetSandboxConfigResponse { policy, version, policy_hash, @@ -2727,7 +2737,12 @@ pub(super) async fn handle_get_sandbox_config( .as_str() .to_string(), extension_authentication_enabled: state.sandbox_jwt_issuer.is_some(), - })) + provider_attachment_epoch: sandbox + .spec + .as_ref() + .map(|spec| spec.provider_attachment_epoch.clone()) + .unwrap_or_default(), + }) } #[cfg(test)] @@ -3241,6 +3256,20 @@ pub(super) async fn handle_get_sandbox_provider_environment( .await .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? .ok_or_else(|| Status::not_found("sandbox not found"))?; + Ok(Response::new( + load_sandbox_provider_environment(state, &sandbox, supports_static_credential_bindings) + .await?, + )) +} + +/// Materialize a privileged provider snapshot after the caller has authorized +/// access. Omission reasons remain separate from a legitimately empty snapshot. +pub(super) async fn load_sandbox_provider_environment( + state: &Arc, + sandbox: &Sandbox, + supports_static_credential_bindings: bool, +) -> Result { + let sandbox_id = sandbox.object_id().to_string(); let workspace = sandbox.object_workspace().to_string(); let spec = sandbox @@ -3263,7 +3292,7 @@ pub(super) async fn handle_get_sandbox_provider_environment( state.as_ref(), &provider_profile_catalog, &workspace, - &sandbox, + sandbox, &sandbox_id, ) .await?; @@ -3291,6 +3320,8 @@ pub(super) async fn handle_get_sandbox_provider_environment( ) .await?; + let mut readiness_reason = provider_environment.readiness_reason; + if supports_static_credential_bindings { let unbound_static_keys = provider_environment .static_credential_keys @@ -3302,6 +3333,9 @@ pub(super) async fn handle_get_sandbox_provider_environment( }) .cloned() .collect::>(); + if !unbound_static_keys.is_empty() { + readiness_reason = ProviderReadinessReason::CredentialsWithheld; + } for key in unbound_static_keys { warn!( sandbox_id = %sandbox_id, @@ -3313,6 +3347,9 @@ pub(super) async fn handle_get_sandbox_provider_environment( provider_environment.static_credential_keys.remove(&key); } } else { + if !provider_environment.static_credential_keys.is_empty() { + readiness_reason = ProviderReadinessReason::UnsupportedSupervisor; + } for key in &provider_environment.static_credential_keys { provider_environment.environment.remove(key); provider_environment.credential_expires_at_ms.remove(key); @@ -3335,14 +3372,17 @@ pub(super) async fn handle_get_sandbox_provider_environment( .cloned() .collect(); - Ok(Response::new(GetSandboxProviderEnvironmentResponse { + Ok(GetSandboxProviderEnvironmentResponse { environment: provider_environment.environment, provider_env_revision, credential_expires_at_ms: provider_environment.credential_expires_at_ms, dynamic_credentials: provider_environment.dynamic_credentials, static_credential_bindings: provider_environment.static_credential_bindings, non_secret_environment_keys, - })) + provider_attachment_epoch: spec.provider_attachment_epoch.clone(), + policy_hash: deterministic_policy_hash(&effective_policy), + readiness_reason: readiness_reason.into(), + }) } // --------------------------------------------------------------------------- @@ -11333,6 +11373,63 @@ mod tests { assert_eq!(v2_env.get("GITHUB_TOKEN"), Some(&"ghp-test".to_string())); } + #[tokio::test] + async fn provider_readiness_snapshot_uses_baseline_static_binding_contract() { + let state = test_server_state().await; + state + .store + .put_message(&test_provider("work-github", "github")) + .await + .unwrap(); + let mut sandbox = test_sandbox( + "sb-static-ready", + "static-ready", + test_policy_with_rule("sandbox_only", "sandbox.example.com"), + vec!["work-github".to_string()], + ); + let attachment_epoch = uuid::Uuid::new_v4().to_string(); + sandbox.spec.as_mut().unwrap().provider_attachment_epoch = attachment_epoch.clone(); + state.store.put_message(&sandbox).await.unwrap(); + + // Static binding support is sufficient to install this snapshot. The + // environment and policy must describe the same attachment authority. + let response = handle_get_sandbox_provider_environment( + &state, + with_user(Request::new(GetSandboxProviderEnvironmentRequest { + sandbox_id: "sb-static-ready".to_string(), + supports_static_credential_bindings: true, + })), + ) + .await + .unwrap() + .into_inner(); + let config = load_sandbox_config(&state, &sandbox).await.unwrap(); + + assert_eq!( + response.readiness_reason, + ProviderReadinessReason::Unspecified as i32 + ); + assert_eq!(response.provider_attachment_epoch, attachment_epoch); + assert_eq!( + response.provider_attachment_epoch, + config.provider_attachment_epoch + ); + assert_eq!(response.provider_env_revision, config.provider_env_revision); + assert_eq!(response.policy_hash, config.policy_hash); + assert!(response.environment.contains_key("GITHUB_TOKEN")); + assert!( + response + .static_credential_bindings + .contains_key("GITHUB_TOKEN") + ); + assert!( + !response + .non_secret_environment_keys + .iter() + .any(|key| key == "GITHUB_TOKEN") + ); + } + #[tokio::test] async fn provider_environment_withholds_static_credentials_from_legacy_supervisors() { use openshell_core::proto::GetSandboxProviderEnvironmentRequest; @@ -11367,6 +11464,10 @@ mod tests { assert!(!response.environment.contains_key("GITHUB_TOKEN")); assert!(response.static_credential_bindings.is_empty()); + assert_eq!( + response.readiness_reason, + ProviderReadinessReason::UnsupportedSupervisor as i32 + ); } #[tokio::test] @@ -11406,6 +11507,10 @@ mod tests { .into_inner(); assert!(!response.environment.contains_key("OPENAI_API_KEY")); + assert_eq!( + response.readiness_reason, + ProviderReadinessReason::CredentialsWithheld as i32 + ); assert!( !response .static_credential_bindings diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index e3cb3f6b20..28369834b7 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -63,6 +63,9 @@ fn redact_provider_credentials(mut provider: Provider) -> Provider { #[derive(Debug, Clone, Default, PartialEq)] pub(super) struct ProviderEnvironment { + /// A closed reason for omitted injectable material. An empty environment + /// alone cannot distinguish successful revocation from withheld authority. + pub readiness_reason: openshell_core::proto::ProviderReadinessReason, pub environment: HashMap, pub credential_expires_at_ms: HashMap, pub dynamic_credentials: HashMap, @@ -443,7 +446,6 @@ async fn update_provider_record_validating( candidate.object_name(), candidate.object_workspace(), candidate.object_id(), - &removed_credential_handles, &updated_credential_values, &existing_handles, ) @@ -452,9 +454,6 @@ async fn update_provider_record_validating( candidate.credential_handles.remove(key); candidate.credentials.remove(key); } - for key in credential_update.deferred_store_values.keys() { - candidate.credentials.remove(key); - } if credentials.is_some_and(crate::credentials::CredentialRuntime::stores_provider_credentials) { for key in updated_credential_values.keys() { candidate.credentials.remove(key); @@ -521,16 +520,25 @@ async fn update_provider_record_validating( } }; - finish_provider_credential_update( + // The provider CAS already excludes these handles. Keep the committed + // result and its receipts available if retirement fails; the unused + // backend objects still require cleanup. + if let Err(err) = finish_provider_credential_update( credentials, candidate.object_name(), candidate.object_workspace(), candidate.object_id(), credential_update, &removed_credential_handles, - &existing_handles, ) - .await?; + .await + { + warn!( + provider_name = %candidate.object_name(), + code = ?err.code(), + "failed to retire unused provider credentials after publication" + ); + } // Update resource_version from successful write if let Some(metadata) = candidate.metadata.as_mut() { @@ -773,16 +781,17 @@ fn credential_handles_removed_by_update( #[derive(Debug, Clone, Default)] struct ProviderCredentialUpdate { pre_stored_handles: HashMap, - deferred_store_values: HashMap, replaced_handles: HashMap, } +// Each candidate owns distinct backend objects before its provider CAS. A +// published resource version therefore identifies fully stored credentials, +// and a concurrent loser cannot overwrite the winner's credential values. async fn prepare_provider_credential_update( credentials: Option<&crate::credentials::CredentialRuntime>, provider_name: &str, workspace: &str, provider_id: &str, - _removed_handles: &HashMap, updated_values: &HashMap, existing_handles: &HashMap, ) -> Result { @@ -793,42 +802,41 @@ async fn prepare_provider_credential_update( return Ok(ProviderCredentialUpdate::default()); } - let mut update = ProviderCredentialUpdate::default(); - let mut values_requiring_new_handles = HashMap::new(); - for (credential_key, value) in updated_values { - match existing_handles.get(credential_key) { - Some(existing_handle) if credentials.storage_owns_handle(existing_handle) => { - update - .deferred_store_values - .insert(credential_key.clone(), value.clone()); - } - Some(replaced_handle) => { - values_requiring_new_handles.insert(credential_key.clone(), value.clone()); - update - .replaced_handles - .insert(credential_key.clone(), replaced_handle.clone()); - } - None => { - values_requiring_new_handles.insert(credential_key.clone(), value.clone()); - } - } - } - - if !values_requiring_new_handles.is_empty() { - update.pre_stored_handles = credentials - .store_provider_credentials( - provider_name, - workspace, - provider_id, - &values_requiring_new_handles, - &HashMap::new(), + let object_id = uuid::Uuid::new_v4().to_string(); + let pre_stored_handles = credentials + .store_provider_credentials_with_object_id( + provider_name, + workspace, + provider_id, + &object_id, + updated_values, + &HashMap::new(), + ) + .await + .map_err(|err| { + Status::new( + err.code(), + "credential storage failed before provider publication", ) - .await?; - } + })?; + let replaced_handles = updated_values + .keys() + .filter_map(|key| { + existing_handles + .get(key) + .map(|handle| (key.clone(), handle.clone())) + }) + .collect(); - Ok(update) + Ok(ProviderCredentialUpdate { + pre_stored_handles, + replaced_handles, + }) } +// Retire only handles replaced by the successful provider CAS. Readers of an +// older record can fail resolution after retirement, but cannot resolve its +// handles to credential values from a different provider resource version. async fn finish_provider_credential_update( credentials: Option<&crate::credentials::CredentialRuntime>, provider_name: &str, @@ -836,7 +844,6 @@ async fn finish_provider_credential_update( provider_id: &str, update: ProviderCredentialUpdate, removed_handles: &HashMap, - existing_handles: &HashMap, ) -> Result<(), Status> { let Some(credentials) = credentials else { return Ok(()); @@ -845,18 +852,6 @@ async fn finish_provider_credential_update( return Ok(()); } - if !update.deferred_store_values.is_empty() { - credentials - .store_provider_credentials( - provider_name, - workspace, - provider_id, - &update.deferred_store_values, - existing_handles, - ) - .await?; - } - let mut handles_to_delete = removed_handles.clone(); handles_to_delete.extend(update.replaced_handles); if !handles_to_delete.is_empty() { @@ -867,17 +862,21 @@ async fn finish_provider_credential_update( provider_id, &handles_to_delete, ) - .await?; + .await + .map_err(|err| { + Status::new( + err.code(), + "credential retirement failed after provider publication", + ) + })?; } Ok(()) } -// TODO(credential-drivers): A gateway crash between CAS success and -// finish_provider_credential_update leaves replaced/removed credential handles -// orphaned in the backing store. This best-effort cleanup only covers pre-CAS -// failures. A background reconciliation loop should be added to detect and -// reclaim orphaned handles. +// A failed CAS never owns the published handles. Best-effort cleanup removes +// only this candidate's staged objects; a crash may leave unused backend objects +// but cannot change the credential data named by the committed provider record. async fn cleanup_pre_stored_provider_credentials( credentials: Option<&crate::credentials::CredentialRuntime>, provider_name: &str, @@ -897,7 +896,7 @@ async fn cleanup_pre_stored_provider_credentials( { warn!( provider_name = %provider_name, - error = %err, + code = ?err.code(), "failed to clean up staged provider credentials after provider update failure" ); } @@ -1117,6 +1116,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin let mut expires = HashMap::new(); let mut static_credential_bindings = HashMap::new(); let mut static_credential_keys = HashSet::new(); + let mut readiness_reason = openshell_core::proto::ProviderReadinessReason::Unspecified; let now_ms = crate::persistence::current_time_ms(); validate_provider_environment_records_unique_at(store, catalog, records, now_ms).await?; let registry = openshell_providers::ProviderRegistry::new(); @@ -1198,6 +1198,8 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin key = %key, "withholding provider credential not declared by resolved profile" ); + readiness_reason = + openshell_core::proto::ProviderReadinessReason::CredentialsWithheld; continue; } if is_non_injectable_provider_credential(provider, key) @@ -1221,6 +1223,8 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin key = %key, "withholding static provider credential from endpointless profile" ); + readiness_reason = + openshell_core::proto::ProviderReadinessReason::CredentialsWithheld; continue; } let expires_at_ms = provider @@ -1235,6 +1239,8 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin expires_at_ms, "skipping expired provider credential" ); + readiness_reason = + openshell_core::proto::ProviderReadinessReason::CredentialExpired; continue; } if expires_at_ms > 0 { @@ -1271,6 +1277,15 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin let resolved_refs = credentials .resolve_provider_handles(provider, now_ms) .await?; + // Expired handles are removed by the credential runtime before values + // reach this loop. Preserve omission evidence without exposing handles. + if provider.credential_handles.keys().any(|key| { + !is_non_injectable_provider_credential(provider, key) + && !broker_only_credential_keys.contains(key) + && !resolved_refs.values.contains_key(key) + }) { + readiness_reason = openshell_core::proto::ProviderReadinessReason::CredentialExpired; + } for (key, value) in resolved_refs.values { if accepted_stored_credential_keys .as_ref() @@ -1281,6 +1296,8 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin key = %key, "withholding provider credential handle not declared by resolved profile" ); + readiness_reason = + openshell_core::proto::ProviderReadinessReason::CredentialsWithheld; continue; } if is_non_injectable_provider_credential(provider, &key) @@ -1300,6 +1317,8 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin key = %key, "withholding static provider credential handle from endpointless profile" ); + readiness_reason = + openshell_core::proto::ProviderReadinessReason::CredentialsWithheld; continue; } if let Some(expires_at_ms) = resolved_refs @@ -1349,6 +1368,7 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin } Ok(ProviderEnvironment { + readiness_reason, environment: env, credential_expires_at_ms: expires, dynamic_credentials: resolve_dynamic_credentials_from_records(catalog, records), @@ -2495,6 +2515,7 @@ pub(super) async fn handle_create_provider( ); Ok(Response::new(ProviderResponse { provider: Some(provider), + ..Default::default() })) } Err(err) => { @@ -2529,6 +2550,7 @@ pub(super) async fn handle_get_provider( Ok(Response::new(ProviderResponse { provider: Some(provider), + ..Default::default() })) } @@ -3739,6 +3761,13 @@ pub(super) async fn handle_update_provider( if state.credentials.stores_provider_credentials() && !provider.credentials.is_empty() { state.compute.ensure_workspace(&workspace).await?; } + // Freeze this operation's target identities before updating authority. + // Attachments made later are separate operations; frozen attachment epochs + // prevent an intervening detach/reattach from satisfying an older receipt. + let targets = + sandboxes_using_provider_records(state.store.as_ref(), &workspace, provider.object_name()) + .await?; + let mutation_id = uuid::Uuid::new_v4().to_string(); let catalog = state .provider_profile_sources .snapshot_catalog(state.store.as_ref(), &workspace) @@ -3753,6 +3782,24 @@ pub(super) async fn handle_update_provider( .await; match result { Ok(provider) => { + let provider_version = provider + .metadata + .as_ref() + .map_or(0, |metadata| metadata.resource_version); + let mut target_receipts = Vec::with_capacity(targets.len()); + for sandbox in &targets { + target_receipts.push( + super::provider_readiness::record_provider_mutation( + state, + sandbox, + provider.object_name(), + openshell_core::proto::ProviderMutationKind::Update, + Some((provider.object_id(), provider_version)), + &mutation_id, + ) + .await?, + ); + } emit_provider_lifecycle( &provider.r#type, LifecycleOperation::Update, @@ -3760,6 +3807,8 @@ pub(super) async fn handle_update_provider( ); Ok(Response::new(ProviderResponse { provider: Some(provider), + target_receipts, + mutation_id, })) } Err(err) => { @@ -8306,7 +8355,7 @@ mod tests { } #[tokio::test] - async fn update_provider_record_overwrites_credentials_with_runtime() { + async fn update_provider_credential_publication_waits_for_staged_storage() { let store = test_store().await; let config = openshell_core::Config::new(None).with_credential_drivers(["test-static"]); let credentials = crate::credentials::CredentialRuntime::from_config(&config).unwrap(); @@ -8336,15 +8385,39 @@ mod tests { .handle .clone(); - let updated = update_provider_record_validating( + let (store_hit, release_store) = credentials.gate_next_store(); + let update = update_provider_record_validating( &store, "default", &catalog, provider_with_credential_value("openai-local", "openai", "OPENAI_API_KEY", "sk-second"), Some(&credentials), - ) - .await - .unwrap(); + ); + let inspect_while_storage_pending = async { + store_hit.await.unwrap(); + let published = store + .get_message_by_name::("default", "openai-local") + .await + .unwrap() + .unwrap(); + assert_eq!(published, stored_first); + let resolved = resolve_provider_environment_with_credentials( + &store, + &catalog, + "default", + &["openai-local".to_string()], + &credentials, + ) + .await + .unwrap(); + assert_eq!( + resolved.get("OPENAI_API_KEY").map(String::as_str), + Some("sk-first") + ); + release_store.send(()).unwrap(); + }; + let (updated, ()) = tokio::join!(update, inspect_while_storage_pending); + let updated = updated.unwrap(); assert_eq!( updated .credentials @@ -8360,13 +8433,18 @@ mod tests { .unwrap() .unwrap(); assert!(stored_second.credentials.is_empty()); - assert_eq!( + assert_ne!( stored_second .credential_handles .get("OPENAI_API_KEY") .map(|handle| handle.handle.as_str()), Some(first_handle.as_str()) ); + assert_eq!( + stored_second.metadata.as_ref().unwrap().resource_version, + stored_first.metadata.as_ref().unwrap().resource_version + 1 + ); + assert_eq!(credentials.stored_credential_count(), Some(1)); let result = resolve_provider_environment_with_credentials( &store, @@ -8380,6 +8458,367 @@ mod tests { assert_eq!(result.get("OPENAI_API_KEY"), Some(&"sk-second".to_string())); } + #[tokio::test] + async fn update_provider_credential_store_failure_preserves_published_revision() { + let store = test_store().await; + let config = openshell_core::Config::new(None).with_credential_drivers(["test-static"]); + let credentials = crate::credentials::CredentialRuntime::from_config(&config).unwrap(); + let catalog = ProviderProfileSources::with_default_sources() + .snapshot_catalog(&store, "default") + .await + .unwrap(); + create_provider_record_validating( + &store, + "default", + &catalog, + provider_with_credential_value("openai-local", "openai", "OPENAI_API_KEY", "sk-first"), + Some(&credentials), + ) + .await + .unwrap(); + let before = store + .get_message_by_name::("default", "openai-local") + .await + .unwrap() + .unwrap(); + credentials.fail_next_store(); + + let error = update_provider_record_validating( + &store, + "default", + &catalog, + provider_with_credential_value("openai-local", "openai", "OPENAI_API_KEY", "sk-failed"), + Some(&credentials), + ) + .await + .unwrap_err(); + + assert_eq!(error.code(), Code::Unavailable); + assert_eq!( + error.message(), + "credential storage failed before provider publication" + ); + let after = store + .get_message_by_name::("default", "openai-local") + .await + .unwrap() + .unwrap(); + assert_eq!(after, before); + assert_eq!(credentials.stored_credential_count(), Some(1)); + let resolved = resolve_provider_environment_with_credentials( + &store, + &catalog, + "default", + &["openai-local".to_string()], + &credentials, + ) + .await + .unwrap(); + assert_eq!( + resolved.get("OPENAI_API_KEY").map(String::as_str), + Some("sk-first") + ); + } + + #[tokio::test] + async fn update_provider_credential_publication_cas_loser_preserves_winner() { + let store = test_store().await; + let config = openshell_core::Config::new(None).with_credential_drivers(["test-static"]); + let credentials = crate::credentials::CredentialRuntime::from_config(&config).unwrap(); + let catalog = ProviderProfileSources::with_default_sources() + .snapshot_catalog(&store, "default") + .await + .unwrap(); + create_provider_record_validating( + &store, + "default", + &catalog, + provider_with_credential_value("openai-local", "openai", "OPENAI_API_KEY", "sk-first"), + Some(&credentials), + ) + .await + .unwrap(); + let initial = store + .get_message_by_name::("default", "openai-local") + .await + .unwrap() + .unwrap(); + let (store_hit, release_store) = credentials.gate_next_store(); + + // Pause one writer after it reads the provider version. A second writer + // publishes while it waits, so only a database CAS can reject the loser. + let loser = update_provider_record_validating( + &store, + "default", + &catalog, + provider_with_credential_value("openai-local", "openai", "OPENAI_API_KEY", "sk-loser"), + Some(&credentials), + ); + let winner = async { + store_hit.await.unwrap(); + let result = update_provider_record_validating( + &store, + "default", + &catalog, + provider_with_credential_value( + "openai-local", + "openai", + "OPENAI_API_KEY", + "sk-winner", + ), + Some(&credentials), + ) + .await; + release_store.send(()).unwrap(); + result + }; + let (loser_result, winner_result) = tokio::join!(loser, winner); + + assert_eq!(loser_result.unwrap_err().code(), Code::Aborted); + winner_result.unwrap(); + let published = store + .get_message_by_name::("default", "openai-local") + .await + .unwrap() + .unwrap(); + assert_eq!( + published.metadata.as_ref().unwrap().resource_version, + initial.metadata.as_ref().unwrap().resource_version + 1 + ); + assert_eq!(credentials.stored_credential_count(), Some(1)); + let resolved = resolve_provider_environment_with_credentials( + &store, + &catalog, + "default", + &["openai-local".to_string()], + &credentials, + ) + .await + .unwrap(); + assert_eq!( + resolved.get("OPENAI_API_KEY").map(String::as_str), + Some("sk-winner") + ); + } + + #[tokio::test] + async fn update_provider_receipts_freeze_targets_before_credential_publication() { + let state = test_server_state().await; + handle_create_provider( + &state, + authed_request(CreateProviderRequest { + provider: Some(provider_with_credential_value( + "openai-local", + "openai", + "OPENAI_API_KEY", + "sk-first", + )), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }), + ) + .await + .unwrap(); + for name in ["first", "second", "late"] { + state + .store + .put_message(&Sandbox { + metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { + id: name.to_string(), + name: name.to_string(), + workspace: "default".to_string(), + ..Default::default() + }), + spec: Some(SandboxSpec { + providers: if name == "late" { + Vec::new() + } else { + vec!["openai-local".to_string()] + }, + provider_attachment_epoch: format!("epoch-{name}"), + ..Default::default() + }), + ..Default::default() + }) + .await + .unwrap(); + } + let (store_hit, release_store) = state.credentials.gate_next_store(); + let update = handle_update_provider( + &state, + authed_request(UpdateProviderRequest { + provider: Some(provider_with_credential_value( + "openai-local", + "openai", + "OPENAI_API_KEY", + "sk-second", + )), + credential_expires_at_ms: HashMap::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }), + ); + let attach_from_another_replica = async { + store_hit.await.unwrap(); + // Another gateway's attachment is outside the captured target set, + // even when its write reaches the database before provider publish. + state + .store + .update_message_cas::("late", 0, |sandbox| { + let spec = sandbox.spec.as_mut().unwrap(); + spec.providers.push("openai-local".to_string()); + spec.provider_attachment_epoch = "epoch-late-attached".to_string(); + }) + .await + .unwrap(); + release_store.send(()).unwrap(); + }; + let (response, ()) = tokio::join!(update, attach_from_another_replica); + let response = response.unwrap().into_inner(); + let provider = response.provider.as_ref().unwrap(); + let targets: HashSet<_> = response + .target_receipts + .iter() + .map(|receipt| receipt.desired.as_ref().unwrap().sandbox_name.as_str()) + .collect(); + assert_eq!(targets, HashSet::from(["first", "second"])); + assert!(!response.mutation_id.is_empty()); + for receipt in &response.target_receipts { + assert_eq!(receipt.mutation_id, response.mutation_id); + assert_eq!( + receipt.kind, + openshell_core::proto::ProviderMutationKind::Update as i32 + ); + let desired = receipt.desired.as_ref().unwrap(); + assert_eq!(desired.provider_id, provider.object_id()); + assert_eq!( + desired.provider_resource_version, + provider.metadata.as_ref().unwrap().resource_version + ); + assert_eq!( + desired.attachment_epoch, + format!("epoch-{}", desired.sandbox_name) + ); + } + } + + #[tokio::test] + async fn update_provider_retirement_failure_preserves_publication_and_receipts() { + let state = test_server_state().await; + handle_create_provider( + &state, + authed_request(CreateProviderRequest { + provider: Some(provider_with_credential_value( + "openai-local", + "openai", + "OPENAI_API_KEY", + "sk-first", + )), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }), + ) + .await + .unwrap(); + let initial = state + .store + .get_message_by_name::("default", "openai-local") + .await + .unwrap() + .unwrap(); + let initial_version = initial.metadata.as_ref().unwrap().resource_version; + let mut target_epochs = HashMap::new(); + for name in ["s1", "s2"] { + let attachment_epoch = uuid::Uuid::new_v4().to_string(); + state + .store + .put_message(&Sandbox { + metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { + id: uuid::Uuid::new_v4().to_string(), + name: name.to_string(), + workspace: "default".to_string(), + ..Default::default() + }), + spec: Some(SandboxSpec { + providers: vec!["openai-local".to_string()], + provider_attachment_epoch: attachment_epoch.clone(), + policy: Some(openshell_policy::restrictive_default_policy()), + ..Default::default() + }), + ..Default::default() + }) + .await + .unwrap(); + target_epochs.insert(name, attachment_epoch); + } + + // Only old-object retirement fails: staging and the provider CAS must + // still publish the replacement and preserve every selected receipt. + state.credentials.fail_next_delete(); + let result = handle_update_provider( + &state, + authed_request(UpdateProviderRequest { + provider: Some(provider_with_credential_value( + "openai-local", + "openai", + "OPENAI_API_KEY", + "sk-second", + )), + credential_expires_at_ms: HashMap::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }), + ) + .await; + + let published = state + .store + .get_message_by_name::("default", "openai-local") + .await + .unwrap() + .unwrap(); + let published_version = published.metadata.as_ref().unwrap().resource_version; + assert_eq!(published_version, initial_version + 1); + assert_ne!(published.credential_handles, initial.credential_handles); + let resolved = state + .credentials + .resolve_provider_handles(&published, crate::persistence::current_time_ms()) + .await + .unwrap(); + assert_eq!(resolved.values["OPENAI_API_KEY"], "sk-second"); + assert_eq!(state.credentials.stored_credential_count(), Some(2)); + + let response = result + .expect("published credential rotation must return its mutation receipts") + .into_inner(); + let provider = response.provider.as_ref().unwrap(); + assert_eq!(provider.object_id(), published.object_id()); + assert_eq!( + provider.metadata.as_ref().unwrap().resource_version, + published_version + ); + assert!(!response.mutation_id.is_empty()); + let targets: HashSet<_> = response + .target_receipts + .iter() + .map(|receipt| receipt.desired.as_ref().unwrap().sandbox_name.as_str()) + .collect(); + assert_eq!(targets, HashSet::from(["s1", "s2"])); + assert_eq!(response.target_receipts.len(), target_epochs.len()); + for receipt in &response.target_receipts { + assert!(!receipt.receipt_id.is_empty()); + assert_eq!(receipt.mutation_id, response.mutation_id); + assert_eq!( + receipt.kind, + openshell_core::proto::ProviderMutationKind::Update as i32 + ); + let desired = receipt.desired.as_ref().unwrap(); + assert_eq!(desired.provider_id, published.object_id()); + assert_eq!(desired.provider_resource_version, published_version); + assert_eq!( + desired.attachment_epoch, + target_epochs[desired.sandbox_name.as_str()] + ); + assert!(!desired.policy_hash.is_empty()); + } + } + #[tokio::test] async fn update_provider_record_with_runtime_preserves_legacy_inline_credentials_on_noop() { let store = test_store().await; diff --git a/crates/openshell-server/src/grpc/provider_readiness.rs b/crates/openshell-server/src/grpc/provider_readiness.rs new file mode 100644 index 0000000000..0342878d08 --- /dev/null +++ b/crates/openshell-server/src/grpc/provider_readiness.rs @@ -0,0 +1,612 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Provider views of common configuration operations and session-bound evidence. + +#![allow(clippy::result_large_err)] // The RPC boundary returns tonic status values. + +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use openshell_core::proto::{ + GetSandboxProviderStatusRequest, GetSandboxProviderStatusResponse, Provider, + ProviderDesiredIdentity, ProviderMutationKind, ProviderMutationReceipt, + ProviderReadinessObservation, ProviderReadinessReason, ProviderReadinessState, + ProviderReadinessStatus, ReportProviderReadinessRequest, ReportProviderReadinessResponse, + Sandbox, SandboxPhase, SupervisorHello, +}; +use openshell_core::{ObjectId, ObjectName, ObjectWorkspace}; +use tonic::{Request, Response, Status}; +use uuid::Uuid; + +use crate::ServerState; +use crate::auth::guard::{enforce_sandbox_scope, ensure_sandbox_principal_scope}; +use crate::auth::workspace_authz::{MinWorkspaceRole, authorize_workspace_selector}; +use crate::config_update_operation; +use crate::persistence::{ObjectType, current_time_ms}; + +const REPORT_INTERVAL_SECONDS: u32 = 5; +const OBSERVATION_TTL_SECONDS: u32 = 15; + +/// Installation evidence owned by one live `ConnectSupervisor` session. +/// Replacing or removing that session discards this evidence with it. +#[derive(Clone, Debug)] +pub struct ProviderReadinessEvidence { + network_instance_id: String, + supported: bool, + process_instance_id: Option, + last_seen: Instant, + observation: Option, + observed_at_ms: i64, +} + +impl ProviderReadinessEvidence { + /// Require the instance last accepted into the persisted sandbox lifecycle. + /// A local session alone cannot prove ownership across gateway replicas. + pub(crate) fn belongs_to_instance(&self, active_instance_id: &str) -> bool { + !active_instance_id.is_empty() && self.network_instance_id == active_instance_id + } + + /// Capture capability and instance identity from the authenticated hello. + pub(crate) fn from_hello(hello: &SupervisorHello) -> Result { + if hello.supports_provider_readiness { + canonical_uuid(&hello.sandbox_id)?; + canonical_uuid(&hello.instance_id)?; + } + Ok(Self { + network_instance_id: hello.instance_id.clone(), + supported: hello.supports_provider_readiness, + process_instance_id: None, + last_seen: Instant::now(), + observation: None, + observed_at_ms: 0, + }) + } + + /// Accept an ordered report after the registry verifies session ownership. + /// Retrying an identical report never extends its original acceptance time. + pub(crate) fn accept( + &mut self, + observation: ProviderReadinessObservation, + ) -> Result<(), Status> { + if !self.supported { + return Err(Status::failed_precondition( + "supervisor did not advertise provider readiness support", + )); + } + if observation.sequence == 0 { + return Err(Status::invalid_argument("report sequence is required")); + } + if let Some(last) = self.observation.as_ref() { + if observation.sequence == last.sequence { + return if &observation == last { + Ok(()) + } else { + Err(Status::invalid_argument( + "a provider report sequence must reuse the identical observation", + )) + }; + } + if observation.sequence < last.sequence { + return Err(Status::failed_precondition( + "provider readiness observation is out of order", + )); + } + } + if observation.session_id.is_empty() { + return Err(Status::failed_precondition( + "supervisor session identity is required", + )); + } + ProviderReadinessReason::try_from(observation.reason) + .map_err(|_| Status::invalid_argument("unknown readiness reason"))?; + if !observation.attachment_epoch.is_empty() { + canonical_uuid(&observation.attachment_epoch)?; + } + if observation.policy_hash.len() > 128 + || !observation + .policy_hash + .bytes() + .all(|byte| byte.is_ascii_hexdigit()) + { + return Err(Status::invalid_argument("invalid policy fingerprint")); + } + if observation.launch_environment_installed && observation.process_instance_id.is_empty() { + return Err(Status::invalid_argument( + "process instance identity is required", + )); + } + if !observation.process_instance_id.is_empty() { + canonical_uuid(&observation.process_instance_id)?; + if self + .process_instance_id + .as_ref() + .is_some_and(|process_id| process_id != &observation.process_instance_id) + { + return Err(Status::failed_precondition("process instance changed")); + } + // The supervisor reports its authenticated boundary's process + // identity. A boundary replacement requires a new control session. + self.process_instance_id = Some(observation.process_instance_id.clone()); + } + self.last_seen = Instant::now(); + self.observed_at_ms = current_time_ms(); + self.observation = Some(observation); + Ok(()) + } +} + +fn registry_unavailable() -> Status { + Status::unavailable("provider readiness state is unavailable") +} + +fn canonical_uuid(value: &str) -> Result<(), Status> { + let parsed = Uuid::parse_str(value) + .map_err(|_| Status::invalid_argument("invalid readiness instance identity"))?; + if parsed.is_nil() || parsed.to_string() != value { + return Err(Status::invalid_argument( + "invalid readiness instance identity", + )); + } + Ok(()) +} + +/// Persist an immutable receipt for the target frozen before a source mutation. +/// Expected provider identity prevents a competing update from acquiring this +/// receipt even when snapshot reads happen after the source CAS. +pub(super) async fn record_provider_mutation( + state: &Arc, + sandbox: &Sandbox, + provider_name: &str, + kind: ProviderMutationKind, + expected_provider: Option<(&str, u64)>, + mutation_id: &str, +) -> Result { + let mut desired = target_identity(sandbox, expected_provider); + let snapshot_reason = match load_current_snapshot(state, sandbox, provider_name).await { + Ok((snapshot, _)) if same_authority(&desired, &snapshot) => { + desired = snapshot; + ProviderReadinessReason::Unspecified + } + Ok(_) => ProviderReadinessReason::SnapshotMismatch, + Err(_) => ProviderReadinessReason::CredentialsWithheld, + }; + let receipt = ProviderMutationReceipt { + receipt_id: Uuid::new_v4().to_string(), + mutation_id: mutation_id.to_string(), + provider_name: provider_name.to_string(), + workspace: sandbox.object_workspace().to_string(), + kind: kind.into(), + desired: Some(desired), + persisted_at_ms: current_time_ms(), + }; + config_update_operation::record_provider_operation( + state.store.as_ref(), + receipt, + snapshot_reason, + ) + .await +} + +fn target_identity(sandbox: &Sandbox, provider: Option<(&str, u64)>) -> ProviderDesiredIdentity { + // The empty initial epoch remains an opaque identity paired with the + // sandbox UUID. Every provider-set mutation atomically replaces the epoch. + ProviderDesiredIdentity { + sandbox_id: sandbox.object_id().to_string(), + sandbox_name: sandbox.object_name().to_string(), + attachment_epoch: sandbox + .spec + .as_ref() + .map(|spec| spec.provider_attachment_epoch.clone()) + .unwrap_or_default(), + provider_id: provider.map_or_else(String::new, |(id, _)| id.to_string()), + provider_resource_version: provider.map_or(0, |(_, version)| version), + ..Default::default() + } +} + +fn same_authority(left: &ProviderDesiredIdentity, right: &ProviderDesiredIdentity) -> bool { + left.sandbox_id == right.sandbox_id + && left.sandbox_name == right.sandbox_name + && left.attachment_epoch == right.attachment_epoch + && left.provider_id == right.provider_id + && left.provider_resource_version == right.provider_resource_version +} + +async fn current_target_identity( + state: &Arc, + sandbox: &Sandbox, + provider_name: &str, +) -> Result { + let attached = sandbox + .spec + .as_ref() + .is_some_and(|spec| spec.providers.iter().any(|name| name == provider_name)); + let provider = if attached { + state + .store + .get_by_name( + Provider::object_type(), + sandbox.object_workspace(), + provider_name, + ) + .await + .map_err(|_| registry_unavailable())? + } else { + None + }; + Ok(target_identity( + sandbox, + provider + .as_ref() + .map(|provider| (provider.id.as_str(), provider.resource_version)), + )) +} + +/// Bracket independently stored config and credential records with exact +/// identity reads. Inconsistent reads remain pending; revisions are never +/// treated as counters or compared with greater-than ordering. +async fn load_current_snapshot( + state: &Arc, + sandbox: &Sandbox, + provider_name: &str, +) -> Result<(ProviderDesiredIdentity, ProviderReadinessReason), Status> { + let mut desired = current_target_identity(state, sandbox, provider_name).await?; + let config = super::policy::load_sandbox_config(state, sandbox).await?; + let environment = + super::policy::load_sandbox_provider_environment(state, sandbox, true).await?; + let current = state + .store + .get_message::(sandbox.object_id()) + .await + .map_err(|_| registry_unavailable())? + .ok_or_else(|| Status::not_found("sandbox not found"))?; + let after = current_target_identity(state, ¤t, provider_name).await?; + let final_config = super::policy::load_sandbox_config(state, ¤t).await?; + if !same_authority(&desired, &after) + || config.provider_attachment_epoch != environment.provider_attachment_epoch + || config.provider_env_revision != environment.provider_env_revision + || (config.policy.is_some() && config.policy_hash != environment.policy_hash) + || config.config_revision != final_config.config_revision + || config.provider_env_revision != final_config.provider_env_revision + || config.policy_hash != final_config.policy_hash + { + return Err(Status::aborted("provider readiness snapshot changed")); + } + desired.provider_env_revision = config.provider_env_revision; + desired.config_revision = config.config_revision; + desired.policy_hash = config.policy_hash; + let reason = if config.policy.is_none() { + ProviderReadinessReason::LocalPolicy + } else { + ProviderReadinessReason::try_from(environment.readiness_reason) + .unwrap_or(ProviderReadinessReason::CredentialsWithheld) + }; + Ok((desired, reason)) +} + +/// Return a redacted operator view. Receipt-less lookup reconstructs current +/// intent only; it never claims that a mutation lost before persistence finished. +pub(super) async fn handle_get_sandbox_provider_status( + state: &Arc, + request: Request, +) -> Result, Status> { + let principal = super::extract_principal(&request)?; + let request = request.into_inner(); + let authz = authorize_workspace_selector( + &state.store, + &state.admin_role, + &principal, + request.workspace_scope.as_ref(), + MinWorkspaceRole::User, + ) + .await?; + let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) + .await? + .name; + // Validate the selector before it can contribute to a durable observation. + if request.provider_name.len() > super::MAX_NAME_LEN { + return Err(Status::invalid_argument( + "provider_name exceeds maximum length", + )); + } + let sandbox = state + .store + .get_message_by_name::(&workspace, &request.sandbox_name) + .await + .map_err(|_| registry_unavailable())? + .ok_or_else(|| Status::not_found("sandbox not found"))?; + let receipt_id = if request.receipt_id.is_empty() { + if request.provider_name.is_empty() { + return Err(Status::invalid_argument( + "provider_name or receipt_id is required", + )); + } + // Only existing workspace providers can create observation operations. + // A detached provider still has a useful absent-reference target; + // historical receipts remain queryable after the provider is deleted. + state + .store + .get_by_name(Provider::object_type(), &workspace, &request.provider_name) + .await + .map_err(|_| registry_unavailable())? + .ok_or_else(|| Status::not_found("provider not found"))?; + let current = current_target_identity(state, &sandbox, &request.provider_name).await?; + let provider = (!current.provider_id.is_empty()).then_some(( + current.provider_id.as_str(), + current.provider_resource_version, + )); + record_provider_mutation( + state, + &sandbox, + &request.provider_name, + ProviderMutationKind::Observe, + provider, + &Uuid::new_v4().to_string(), + ) + .await? + .receipt_id + } else { + canonical_uuid(&request.receipt_id)?; + request.receipt_id + }; + let stored = config_update_operation::get_provider_operation( + state.store.as_ref(), + &receipt_id, + &workspace, + ) + .await?; + let receipt = stored.receipt; + if receipt.workspace != workspace + || receipt + .desired + .as_ref() + .is_none_or(|desired| desired.sandbox_id != sandbox.object_id()) + || (!request.provider_name.is_empty() && receipt.provider_name != request.provider_name) + { + return Err(Status::not_found("provider receipt not found")); + } + let current_authority = + current_target_identity(state, &sandbox, &receipt.provider_name).await?; + let (current, current_reason) = if let Ok(snapshot) = + load_current_snapshot(state, &sandbox, &receipt.provider_name).await + { + snapshot + } else { + // A temporarily unreadable config cannot prove supersession or + // completion. Metadata changes remain comparable independently. + let mut fallback = current_authority; + if let Some(desired) = receipt.desired.as_ref() { + fallback.provider_env_revision = desired.provider_env_revision; + fallback.config_revision = desired.config_revision; + fallback.policy_hash.clone_from(&desired.policy_hash); + } + (fallback, ProviderReadinessReason::CredentialsWithheld) + }; + // Refresh lifecycle identity after configuration reads. Another gateway + // replica may have accepted a new supervisor while this replica retained + // the predecessor's local connection and installation evidence. + let active_sandbox = state + .store + .get_message::(sandbox.object_id()) + .await + .map_err(|_| registry_unavailable())? + .ok_or_else(|| Status::not_found("sandbox not found"))?; + let active_instance_id = active_sandbox + .status + .as_ref() + .map_or("", |status| status.main_process_instance_id.as_str()); + let session = state + .supervisor_sessions + .provider_readiness(sandbox.object_id())?; + let mut status = evaluate_status( + receipt, + stored.snapshot_reason, + ¤t, + current_reason, + active_sandbox.phase() == SandboxPhase::Ready as i32, + active_instance_id, + session.as_ref(), + ); + config_update_operation::observe_provider_status(state.store.as_ref(), &mut status).await?; + Ok(Response::new(GetSandboxProviderStatusResponse { + status: Some(status), + })) +} + +fn observation_matches( + observation: &ProviderReadinessObservation, + desired: &ProviderDesiredIdentity, +) -> bool { + observation.attachment_epoch == desired.attachment_epoch + && observation.provider_env_revision == desired.provider_env_revision + && observation.config_revision == desired.config_revision + && observation.policy_hash == desired.policy_hash +} + +fn failure_state(reason: ProviderReadinessReason) -> ProviderReadinessState { + match reason { + ProviderReadinessReason::CredentialsWithheld + | ProviderReadinessReason::CredentialExpired => ProviderReadinessState::Withheld, + ProviderReadinessReason::CredentialInstallFailed + | ProviderReadinessReason::PolicyActivationFailed + | ProviderReadinessReason::ProcessInstallFailed + | ProviderReadinessReason::UnsupportedSupervisor + | ProviderReadinessReason::LocalPolicy => ProviderReadinessState::Failed, + _ => ProviderReadinessState::Pending, + } +} + +fn evaluate_status( + receipt: ProviderMutationReceipt, + snapshot_reason: ProviderReadinessReason, + current: &ProviderDesiredIdentity, + current_reason: ProviderReadinessReason, + running: bool, + active_instance_id: &str, + session: Option<&ProviderReadinessEvidence>, +) -> ProviderReadinessStatus { + let mut status = ProviderReadinessStatus { + receipt: Some(receipt.clone()), + state: ProviderReadinessState::Persisted.into(), + reason: ProviderReadinessReason::WaitingForSupervisor.into(), + observed: session.and_then(|session| session.observation.clone()), + network_instance_id: session + .map(|session| session.network_instance_id.clone()) + .unwrap_or_default(), + observed_at_ms: session.map_or(0, |session| session.observed_at_ms), + evaluated_at_ms: current_time_ms(), + operation: None, + }; + let set = |status: &mut ProviderReadinessStatus, + state: ProviderReadinessState, + reason: ProviderReadinessReason| { + status.state = state.into(); + status.reason = reason.into(); + }; + let Some(desired) = receipt.desired.as_ref() else { + set( + &mut status, + ProviderReadinessState::Failed, + ProviderReadinessReason::SnapshotMismatch, + ); + return status; + }; + if !same_authority(desired, current) + || (snapshot_reason == ProviderReadinessReason::Unspecified && desired != current) + { + set( + &mut status, + ProviderReadinessState::Superseded, + ProviderReadinessReason::DesiredStateChanged, + ); + } else if snapshot_reason != ProviderReadinessReason::Unspecified { + set(&mut status, ProviderReadinessState::Failed, snapshot_reason); + } else if current_reason != ProviderReadinessReason::Unspecified { + set(&mut status, failure_state(current_reason), current_reason); + } else if !running { + set( + &mut status, + ProviderReadinessState::Pending, + ProviderReadinessReason::SupervisorDisconnected, + ); + } else if let Some(session) = session { + if !session.belongs_to_instance(active_instance_id) { + set( + &mut status, + ProviderReadinessState::Pending, + ProviderReadinessReason::SupervisorDisconnected, + ); + } else if session.last_seen.elapsed() + >= Duration::from_secs(u64::from(OBSERVATION_TTL_SECONDS)) + { + set( + &mut status, + ProviderReadinessState::Pending, + ProviderReadinessReason::SupervisorLeaseExpired, + ); + } else if !session.supported { + set( + &mut status, + ProviderReadinessState::Failed, + ProviderReadinessReason::UnsupportedSupervisor, + ); + } else if let Some(observation) = session.observation.as_ref() { + let reason = ProviderReadinessReason::try_from(observation.reason) + .unwrap_or(ProviderReadinessReason::SnapshotMismatch); + // Installation failures describe one snapshot. A retained failure + // from an older poll must not fail a newly persisted authority. + if !observation_matches(observation, desired) { + set( + &mut status, + ProviderReadinessState::Pending, + ProviderReadinessReason::SnapshotMismatch, + ); + } else if reason != ProviderReadinessReason::Unspecified { + set(&mut status, failure_state(reason), reason); + } else if !observation.credentials_installed { + set( + &mut status, + ProviderReadinessState::Pending, + ProviderReadinessReason::WaitingForCredentials, + ); + } else if !observation.policy_active { + set( + &mut status, + ProviderReadinessState::Pending, + ProviderReadinessReason::WaitingForPolicy, + ); + } else if !observation.launch_environment_installed + || observation.process_instance_id.is_empty() + { + set( + &mut status, + ProviderReadinessState::Pending, + ProviderReadinessReason::WaitingForProcess, + ); + } else { + let state = if desired.provider_id.is_empty() { + ProviderReadinessState::Revoked + } else { + ProviderReadinessState::Ready + }; + set(&mut status, state, ProviderReadinessReason::Unspecified); + } + } + } + status +} + +fn authorize_provider_readiness( + request: &Request, + sandbox_id: &str, +) -> Result { + let principal = enforce_sandbox_scope(request, sandbox_id)?; + ensure_sandbox_principal_scope(&principal, sandbox_id)?; + Ok(principal) +} + +/// Accept installation evidence only from the sandbox's current supervisor. +/// Authorization is independent from operator permission to inspect progress. +pub(super) async fn handle_report_provider_readiness( + state: &Arc, + request: Request, +) -> Result, Status> { + let sandbox_id = request.get_ref().sandbox_id.clone(); + let principal = authorize_provider_readiness(&request, &sandbox_id)?; + canonical_uuid(&sandbox_id)?; + let observation = request + .into_inner() + .observation + .ok_or_else(|| Status::invalid_argument("provider readiness observation is required"))?; + canonical_uuid(&observation.session_id)?; + // A valid session must still belong to an existing sandbox. The registry + // performs the final current-session check atomically with accepting evidence, + // so a reconnect during this read cannot publish into its replacement. + let sandbox = + super::sandbox::fetch_and_authorize_sandbox(state, &principal, &sandbox_id).await?; + if sandbox.phase() != SandboxPhase::Ready as i32 { + return Err(Status::failed_precondition( + "sandbox supervisor is not ready", + )); + } + let active_instance_id = sandbox + .status + .as_ref() + .map_or("", |status| status.main_process_instance_id.as_str()); + let accepted_sequence = observation.sequence; + state.supervisor_sessions.accept_provider_readiness( + &sandbox_id, + active_instance_id, + observation, + )?; + Ok(Response::new(ReportProviderReadinessResponse { + accepted_sequence, + report_interval_seconds: REPORT_INTERVAL_SECONDS, + observation_ttl_seconds: OBSERVATION_TTL_SECONDS, + })) +} + +#[cfg(test)] +#[path = "provider_readiness_tests.rs"] +mod tests; diff --git a/crates/openshell-server/src/grpc/provider_readiness_tests.rs b/crates/openshell-server/src/grpc/provider_readiness_tests.rs new file mode 100644 index 0000000000..3b4de1f11f --- /dev/null +++ b/crates/openshell-server/src/grpc/provider_readiness_tests.rs @@ -0,0 +1,1308 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use crate::auth::principal::{Principal, SandboxIdentitySource, SandboxPrincipal}; +use crate::config_update_operation::CONFIG_UPDATE_OPERATION_OBJECT_TYPE; +use crate::grpc::test_support::{authed_request, test_server_state}; +use crate::persistence::WriteCondition; +use crate::storage_proto::StoredConfigUpdateOperation; +use openshell_core::proto::SandboxSpec; +use openshell_core::proto::datamodel::v1::ObjectMeta; +use prost::Message; +use std::collections::HashMap; + +// Exercise the production session registry while keeping unrelated relay +// channel setup out of each installation-state test. +#[derive(Default)] +struct TestRegistry(crate::supervisor_session::SupervisorSessionRegistry); + +fn register_session( + registry: &crate::supervisor_session::SupervisorSessionRegistry, + hello: &SupervisorHello, +) -> Result { + let evidence = ProviderReadinessEvidence::from_hello(hello)?; + let session_id = Uuid::new_v4().to_string(); + let (sender, _receiver) = tokio::sync::mpsc::channel(1); + let (shutdown, _shutdown_receiver) = tokio::sync::oneshot::channel(); + registry.register( + hello.sandbox_id.clone(), + session_id.clone(), + sender, + shutdown, + ); + assert!(registry.initialize_endpoint_status_authority(&hello.sandbox_id, &session_id)); + registry.initialize_provider_readiness(&hello.sandbox_id, &session_id, evidence)?; + Ok(session_id) +} + +impl TestRegistry { + fn register(&self, hello: &SupervisorHello) -> Result { + register_session(&self.0, hello) + } + + fn accept( + &self, + sandbox_id: &str, + session_id: &str, + observation: ProviderReadinessObservation, + ) -> Result<(), Status> { + assert_eq!(session_id, observation.session_id); + let active_instance = self + .0 + .provider_readiness(sandbox_id)? + .map(|evidence| evidence.network_instance_id) + .unwrap_or_default(); + self.0 + .accept_provider_readiness(sandbox_id, &active_instance, observation) + } + + fn snapshot(&self, sandbox_id: &str) -> Result, Status> { + self.0.provider_readiness(sandbox_id) + } + + fn disconnect(&self, sandbox_id: &str, session_id: &str) { + self.0.remove_if_current(sandbox_id, session_id); + } +} + +fn hello() -> SupervisorHello { + SupervisorHello { + sandbox_id: Uuid::new_v4().to_string(), + instance_id: Uuid::new_v4().to_string(), + supports_provider_readiness: true, + } +} + +fn receipt(hello: &SupervisorHello) -> ProviderMutationReceipt { + ProviderMutationReceipt { + receipt_id: Uuid::new_v4().to_string(), + mutation_id: Uuid::new_v4().to_string(), + provider_name: "synthetic-provider".to_string(), + workspace: "default".to_string(), + kind: ProviderMutationKind::Attach.into(), + desired: Some(ProviderDesiredIdentity { + sandbox_id: hello.sandbox_id.clone(), + sandbox_name: "synthetic".to_string(), + attachment_epoch: Uuid::new_v4().to_string(), + provider_id: Uuid::new_v4().to_string(), + provider_resource_version: 2, + provider_env_revision: u64::MAX - 1, + config_revision: u64::MAX, + policy_hash: "abcd".to_string(), + }), + persisted_at_ms: 1, + } +} + +fn installed( + hello: &SupervisorHello, + session_id: &str, + receipt: &ProviderMutationReceipt, +) -> ProviderReadinessObservation { + let desired = receipt.desired.as_ref().unwrap(); + ProviderReadinessObservation { + session_id: session_id.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: hello.instance_id.clone(), + reason: ProviderReadinessReason::Unspecified.into(), + } +} + +fn evaluate( + receipt: &ProviderMutationReceipt, + session: Option<&ProviderReadinessEvidence>, +) -> ProviderReadinessStatus { + evaluate_status( + receipt.clone(), + ProviderReadinessReason::Unspecified, + receipt.desired.as_ref().unwrap(), + ProviderReadinessReason::Unspecified, + true, + session.map_or("", |evidence| evidence.network_instance_id.as_str()), + session, + ) +} + +#[test] +fn reporting_requires_own_authenticated_sandbox() { + let hello = hello(); + assert_eq!( + authorize_provider_readiness(&authed_request(()), &hello.sandbox_id) + .unwrap_err() + .code(), + tonic::Code::PermissionDenied + ); + assert_eq!( + authorize_provider_readiness(&Request::new(()), &hello.sandbox_id) + .unwrap_err() + .code(), + tonic::Code::Unauthenticated + ); + let mut request = Request::new(()); + request + .extensions_mut() + .insert(Principal::Sandbox(SandboxPrincipal { + sandbox_id: hello.sandbox_id.clone(), + source: SandboxIdentitySource::BootstrapJwt { + issuer: "test".to_string(), + }, + trust_domain: Some("test".to_string()), + })); + assert!(authorize_provider_readiness(&request, &hello.sandbox_id).is_ok()); + let mut other = hello; + other.sandbox_id = Uuid::new_v4().to_string(); + assert_eq!( + authorize_provider_readiness(&request, &other.sandbox_id) + .unwrap_err() + .code(), + tonic::Code::PermissionDenied + ); +} + +#[tokio::test] +async fn report_rpc_requires_the_authenticated_sandboxes_current_session() { + let state = test_server_state().await; + let hello = hello(); + let receipt = receipt(&hello); + state + .store + .put_message(&Sandbox { + metadata: Some(ObjectMeta { + id: hello.sandbox_id.clone(), + name: "report-owner".to_string(), + workspace: "default".to_string(), + ..Default::default() + }), + status: Some(openshell_core::proto::SandboxStatus { + phase: SandboxPhase::Ready as i32, + main_process_instance_id: hello.instance_id.clone(), + ..Default::default() + }), + ..Default::default() + }) + .await + .unwrap(); + let session_id = register_session(&state.supervisor_sessions, &hello).unwrap(); + let report = ReportProviderReadinessRequest { + sandbox_id: hello.sandbox_id.clone(), + observation: Some(installed(&hello, &session_id, &receipt)), + }; + let request = |report: ReportProviderReadinessRequest, sandbox_id: &str| { + let mut request = Request::new(report); + request + .extensions_mut() + .insert(Principal::Sandbox(SandboxPrincipal { + sandbox_id: sandbox_id.to_string(), + source: SandboxIdentitySource::BootstrapJwt { + issuer: "test".to_string(), + }, + trust_domain: Some("test".to_string()), + })); + request + }; + for (unauthorized, code) in [ + (Request::new(report.clone()), tonic::Code::Unauthenticated), + ( + authed_request(report.clone()), + tonic::Code::PermissionDenied, + ), + ( + request(report.clone(), &Uuid::new_v4().to_string()), + tonic::Code::PermissionDenied, + ), + ] { + assert_eq!( + handle_report_provider_readiness(&state, unauthorized) + .await + .unwrap_err() + .code(), + code + ); + } + let response = + handle_report_provider_readiness(&state, request(report.clone(), &hello.sandbox_id)) + .await + .unwrap() + .into_inner(); + assert_eq!(response.accepted_sequence, 1); + assert_eq!(response.observation_ttl_seconds, OBSERVATION_TTL_SECONDS); + // Another gateway replica can replace the persisted instance without + // touching this process's session registry. The old report must fail. + let mut sandbox = state + .store + .get_message::(&hello.sandbox_id) + .await + .unwrap() + .unwrap(); + sandbox.status.as_mut().unwrap().main_process_instance_id = Uuid::new_v4().to_string(); + state.store.put_message(&sandbox).await.unwrap(); + assert_eq!( + handle_report_provider_readiness(&state, request(report.clone(), &hello.sandbox_id)) + .await + .unwrap_err() + .code(), + tonic::Code::FailedPrecondition + ); + register_session(&state.supervisor_sessions, &hello).unwrap(); + assert_eq!( + handle_report_provider_readiness(&state, request(report, &hello.sandbox_id)) + .await + .unwrap_err() + .code(), + tonic::Code::PermissionDenied + ); +} + +#[test] +fn report_retries_preserve_evidence_and_reject_reordering_or_changed_content() { + let registry = TestRegistry::default(); + let hello = hello(); + let receipt = receipt(&hello); + let session = registry.register(&hello).unwrap(); + let mut observation = installed(&hello, &session, &receipt); + observation.sequence = 2; + registry + .accept(&hello.sandbox_id, &session, observation.clone()) + .unwrap(); + let accepted = registry.snapshot(&hello.sandbox_id).unwrap().unwrap(); + let mut changed = observation.clone(); + changed.credentials_installed = false; + assert_eq!( + registry + .accept(&hello.sandbox_id, &session, changed) + .unwrap_err() + .code(), + tonic::Code::InvalidArgument + ); + let mut older = observation.clone(); + older.sequence = 1; + assert_eq!( + registry + .accept(&hello.sandbox_id, &session, older) + .unwrap_err() + .code(), + tonic::Code::FailedPrecondition + ); + registry + .accept(&hello.sandbox_id, &session, observation.clone()) + .unwrap(); + let retried = registry.snapshot(&hello.sandbox_id).unwrap().unwrap(); + assert_eq!(retried.observation, Some(observation)); + assert_eq!(retried.last_seen, accepted.last_seen); + assert_eq!(retried.observed_at_ms, accepted.observed_at_ms); +} + +#[test] +fn expired_evidence_requires_a_new_report_sequence() { + let hello = hello(); + let receipt = receipt(&hello); + let mut evidence = ProviderReadinessEvidence::from_hello(&hello).unwrap(); + let mut observation = installed(&hello, &Uuid::new_v4().to_string(), &receipt); + evidence.accept(observation.clone()).unwrap(); + evidence.last_seen -= Duration::from_secs(u64::from(OBSERVATION_TTL_SECONDS) + 1); + let expired_at = evidence.last_seen; + evidence.accept(observation.clone()).unwrap(); + assert_eq!(evidence.last_seen, expired_at); + assert_eq!( + evaluate(&receipt, Some(&evidence)).reason, + ProviderReadinessReason::SupervisorLeaseExpired as i32 + ); + observation.sequence += 1; + evidence.accept(observation).unwrap(); + assert_eq!( + evaluate(&receipt, Some(&evidence)).state, + ProviderReadinessState::Ready as i32 + ); +} + +#[test] +fn unsupported_and_uninitialized_sessions_cannot_publish_evidence() { + let registry = TestRegistry::default(); + let mut hello = hello(); + hello.supports_provider_readiness = false; + let receipt = receipt(&hello); + let session = registry.register(&hello).unwrap(); + assert_eq!( + registry + .accept( + &hello.sandbox_id, + &session, + installed(&hello, &session, &receipt) + ) + .unwrap_err() + .code(), + tonic::Code::FailedPrecondition + ); + assert_eq!( + evaluate( + &receipt, + registry.snapshot(&hello.sandbox_id).unwrap().as_ref() + ) + .reason, + ProviderReadinessReason::UnsupportedSupervisor as i32 + ); + let other = Uuid::new_v4().to_string(); + let (tx, _rx) = tokio::sync::mpsc::channel(1); + let (shutdown, _shutdown_rx) = tokio::sync::oneshot::channel(); + registry + .0 + .register(hello.sandbox_id.clone(), other.clone(), tx, shutdown); + assert_eq!( + registry + .accept( + &hello.sandbox_id, + &other, + installed(&hello, &other, &receipt) + ) + .unwrap_err() + .code(), + tonic::Code::PermissionDenied + ); + assert!(registry.snapshot(&hello.sandbox_id).unwrap().is_none()); +} + +#[test] +fn malformed_reports_do_not_replace_accepted_evidence() { + let registry = TestRegistry::default(); + let hello = hello(); + let receipt = receipt(&hello); + let session = registry.register(&hello).unwrap(); + let initial = installed(&hello, &session, &receipt); + registry + .accept(&hello.sandbox_id, &session, initial.clone()) + .unwrap(); + for case in 0..6 { + let mut invalid = initial.clone(); + invalid.sequence = 2; + match case { + 0 => invalid.sequence = 0, + 1 => invalid.reason = i32::MAX, + 2 => invalid.attachment_epoch = "untrusted-identity".to_string(), + 3 => invalid.policy_hash = "a".repeat(129), + 4 => invalid.policy_hash = "not-a-fingerprint".to_string(), + _ => invalid.process_instance_id.clear(), + } + assert!( + registry + .accept(&hello.sandbox_id, &session, invalid) + .is_err() + ); + assert_eq!( + registry + .snapshot(&hello.sandbox_id) + .unwrap() + .unwrap() + .observation, + Some(initial.clone()) + ); + } +} + +#[test] +fn replacement_disconnect_and_replayed_reports_cannot_restore_readiness() { + let registry = TestRegistry::default(); + let hello = hello(); + let receipt = receipt(&hello); + let first = registry.register(&hello).unwrap(); + let observation = installed(&hello, &first, &receipt); + registry + .accept(&hello.sandbox_id, &first, observation.clone()) + .unwrap(); + assert_eq!( + evaluate( + &receipt, + registry.snapshot(&hello.sandbox_id).unwrap().as_ref() + ) + .state, + ProviderReadinessState::Ready as i32 + ); + let before_retry = registry.snapshot(&hello.sandbox_id).unwrap().unwrap(); + registry + .accept(&hello.sandbox_id, &first, observation.clone()) + .unwrap(); + let after_retry = registry.snapshot(&hello.sandbox_id).unwrap().unwrap(); + assert_eq!(before_retry.last_seen, after_retry.last_seen); + assert_eq!(before_retry.observed_at_ms, after_retry.observed_at_ms); + + let second = registry.register(&hello).unwrap(); + assert_ne!(first, second); + assert!( + registry + .accept(&hello.sandbox_id, &first, observation) + .is_err() + ); + registry.disconnect(&hello.sandbox_id, &first); + let current = registry.snapshot(&hello.sandbox_id).unwrap().unwrap(); + assert_eq!( + registry.0.current_session_id(&hello.sandbox_id), + Some(second.clone()) + ); + assert_eq!( + evaluate(&receipt, Some(¤t)).state, + ProviderReadinessState::Persisted as i32 + ); + registry + .accept( + &hello.sandbox_id, + &second, + installed(&hello, &second, &receipt), + ) + .unwrap(); + registry.disconnect(&hello.sandbox_id, &second); + assert!(registry.snapshot(&hello.sandbox_id).unwrap().is_none()); + assert_ne!( + evaluate(&receipt, None).state, + ProviderReadinessState::Ready as i32 + ); +} + +#[test] +fn sequence_order_does_not_order_revision_fingerprints() { + let registry = TestRegistry::default(); + let hello = hello(); + let receipt = receipt(&hello); + let session = registry.register(&hello).unwrap(); + let mut observation = installed(&hello, &session, &receipt); + registry + .accept(&hello.sandbox_id, &session, observation.clone()) + .unwrap(); + observation.sequence = 2; + observation.config_revision = 1; + observation.provider_env_revision = 1; + registry + .accept(&hello.sandbox_id, &session, observation) + .unwrap(); + assert_eq!( + evaluate( + &receipt, + registry.snapshot(&hello.sandbox_id).unwrap().as_ref() + ) + .reason, + ProviderReadinessReason::SnapshotMismatch as i32 + ); +} + +#[test] +fn partial_failed_incompatible_stopped_and_expired_installations_are_not_ready() { + let registry = TestRegistry::default(); + let hello = hello(); + let receipt = receipt(&hello); + let session_id = registry.register(&hello).unwrap(); + let observation = installed(&hello, &session_id, &receipt); + registry + .accept(&hello.sandbox_id, &session_id, observation) + .unwrap(); + let complete = registry.snapshot(&hello.sandbox_id).unwrap().unwrap(); + for reason in [ + ProviderReadinessReason::CredentialInstallFailed, + ProviderReadinessReason::PolicyActivationFailed, + ProviderReadinessReason::ProcessInstallFailed, + ProviderReadinessReason::CredentialsWithheld, + ProviderReadinessReason::LocalPolicy, + ] { + let mut failed = complete.clone(); + failed.observation.as_mut().unwrap().reason = reason.into(); + assert_ne!( + evaluate(&receipt, Some(&failed)).state, + ProviderReadinessState::Ready as i32 + ); + } + for (credentials, policy, process, reason) in [ + ( + false, + true, + true, + ProviderReadinessReason::WaitingForCredentials, + ), + (true, false, true, ProviderReadinessReason::WaitingForPolicy), + ( + true, + true, + false, + ProviderReadinessReason::WaitingForProcess, + ), + ] { + let mut partial = complete.clone(); + let observation = partial.observation.as_mut().unwrap(); + observation.credentials_installed = credentials; + observation.policy_active = policy; + observation.launch_environment_installed = process; + assert_eq!(evaluate(&receipt, Some(&partial)).reason, reason as i32); + } + let mut incompatible = complete.clone(); + incompatible.supported = false; + assert_eq!( + evaluate(&receipt, Some(&incompatible)).reason, + ProviderReadinessReason::UnsupportedSupervisor as i32 + ); + let mut expired = complete.clone(); + expired.last_seen -= Duration::from_secs(u64::from(OBSERVATION_TTL_SECONDS) + 1); + assert_eq!( + evaluate(&receipt, Some(&expired)).reason, + ProviderReadinessReason::SupervisorLeaseExpired as i32 + ); + let stopped = evaluate_status( + receipt.clone(), + ProviderReadinessReason::Unspecified, + receipt.desired.as_ref().unwrap(), + ProviderReadinessReason::Unspecified, + false, + &hello.instance_id, + Some(&complete), + ); + assert_eq!( + stopped.reason, + ProviderReadinessReason::SupervisorDisconnected as i32 + ); + let mut missing = complete; + missing + .observation + .as_mut() + .unwrap() + .process_instance_id + .clear(); + assert_eq!( + evaluate(&receipt, Some(&missing)).reason, + ProviderReadinessReason::WaitingForProcess as i32 + ); +} + +#[test] +fn boundary_ack_is_bound_to_one_supervisor_session() { + let registry = TestRegistry::default(); + let hello = hello(); + let receipt = receipt(&hello); + let session = registry.register(&hello).unwrap(); + let mut observation = installed(&hello, &session, &receipt); + observation.process_instance_id.clear(); + assert!( + registry + .accept(&hello.sandbox_id, &session, observation.clone()) + .is_err() + ); + observation.launch_environment_installed = false; + registry + .accept(&hello.sandbox_id, &session, observation.clone()) + .unwrap(); + assert_eq!( + evaluate( + &receipt, + registry.snapshot(&hello.sandbox_id).unwrap().as_ref() + ) + .reason, + ProviderReadinessReason::WaitingForProcess as i32 + ); + observation.sequence = 2; + observation.launch_environment_installed = true; + observation.process_instance_id = Uuid::new_v4().to_string(); + registry + .accept(&hello.sandbox_id, &session, observation.clone()) + .unwrap(); + assert_eq!( + evaluate( + &receipt, + registry.snapshot(&hello.sandbox_id).unwrap().as_ref() + ) + .state, + ProviderReadinessState::Ready as i32 + ); + observation.sequence = 3; + observation.process_instance_id = Uuid::new_v4().to_string(); + assert!( + registry + .accept(&hello.sandbox_id, &session, observation) + .is_err() + ); +} + +#[test] +fn superseded_authority_cannot_complete_an_older_receipt() { + let registry = TestRegistry::default(); + let hello = hello(); + let receipt = receipt(&hello); + let session_id = registry.register(&hello).unwrap(); + registry + .accept( + &hello.sandbox_id, + &session_id, + installed(&hello, &session_id, &receipt), + ) + .unwrap(); + let session = registry.snapshot(&hello.sandbox_id).unwrap(); + for field in 0..5 { + let mut current = receipt.desired.clone().unwrap(); + match field { + 0 => current.attachment_epoch = Uuid::new_v4().to_string(), + 1 => current.provider_resource_version += 1, + 2 => current.provider_env_revision = 1, + 3 => current.config_revision = 1, + _ => current.policy_hash = "dcba".to_string(), + } + let status = evaluate_status( + receipt.clone(), + ProviderReadinessReason::Unspecified, + ¤t, + ProviderReadinessReason::Unspecified, + true, + &hello.instance_id, + session.as_ref(), + ); + assert_eq!(status.state, ProviderReadinessState::Superseded as i32); + } + let failed_snapshot = evaluate_status( + receipt.clone(), + ProviderReadinessReason::SnapshotMismatch, + receipt.desired.as_ref().unwrap(), + ProviderReadinessReason::Unspecified, + true, + &hello.instance_id, + session.as_ref(), + ); + assert_eq!(failed_snapshot.state, ProviderReadinessState::Failed as i32); +} + +#[test] +fn older_failed_observation_keeps_new_receipt_pending_until_matching_report() { + let registry = TestRegistry::default(); + let hello = hello(); + let original = receipt(&hello); + let session_id = registry.register(&hello).unwrap(); + let mut failed = installed(&hello, &session_id, &original); + failed.reason = ProviderReadinessReason::CredentialInstallFailed.into(); + registry + .accept(&hello.sandbox_id, &session_id, failed) + .unwrap(); + + let mut replacement = receipt(&hello); + replacement.desired = original.desired; + let desired = replacement.desired.as_mut().unwrap(); + desired.provider_resource_version += 1; + desired.provider_env_revision = 1; + let pending = evaluate( + &replacement, + registry.snapshot(&hello.sandbox_id).unwrap().as_ref(), + ); + assert_eq!(pending.state, ProviderReadinessState::Pending as i32); + assert_eq!( + pending.reason, + ProviderReadinessReason::SnapshotMismatch as i32 + ); + + let mut matching = installed(&hello, &session_id, &replacement); + matching.sequence = 2; + matching.reason = ProviderReadinessReason::CredentialInstallFailed.into(); + registry + .accept(&hello.sandbox_id, &session_id, matching.clone()) + .unwrap(); + let failed = evaluate( + &replacement, + registry.snapshot(&hello.sandbox_id).unwrap().as_ref(), + ); + assert_eq!(failed.state, ProviderReadinessState::Failed as i32); + assert_eq!( + failed.reason, + ProviderReadinessReason::CredentialInstallFailed as i32 + ); + + matching.sequence = 3; + matching.reason = ProviderReadinessReason::Unspecified.into(); + registry + .accept(&hello.sandbox_id, &session_id, matching) + .unwrap(); + assert_eq!( + evaluate( + &replacement, + registry.snapshot(&hello.sandbox_id).unwrap().as_ref() + ) + .state, + ProviderReadinessState::Ready as i32 + ); +} + +#[tokio::test] +async fn attach_waiting_for_update_captures_published_revision_and_becomes_ready() { + use openshell_core::proto::{ + AttachSandboxProviderRequest, CreateProviderRequest, UpdateProviderRequest, + }; + + let state = test_server_state().await; + let hello = hello(); + let mut sandbox = Sandbox { + metadata: Some(ObjectMeta { + id: hello.sandbox_id.clone(), + name: "attach-race".to_string(), + workspace: "default".to_string(), + ..Default::default() + }), + spec: Some(SandboxSpec { + policy: Some(openshell_policy::restrictive_default_policy()), + ..Default::default() + }), + ..Default::default() + }; + sandbox.set_phase(SandboxPhase::Ready as i32); + sandbox + .status + .as_mut() + .unwrap() + .main_process_instance_id + .clone_from(&hello.instance_id); + state.store.put_message(&sandbox).await.unwrap(); + let provider = |value: &str| Provider { + metadata: Some(ObjectMeta { + name: "work-github".to_string(), + workspace: "default".to_string(), + ..Default::default() + }), + r#type: "github".to_string(), + credentials: HashMap::from([("GITHUB_TOKEN".to_string(), value.to_string())]), + ..Default::default() + }; + let initial = super::super::provider::handle_create_provider( + &state, + authed_request(CreateProviderRequest { + provider: Some(provider("synthetic-first")), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }), + ) + .await + .unwrap() + .into_inner() + .provider + .unwrap(); + + // The credential driver's gate holds UpdateProvider inside the shared + // mutation guard while the attach request reaches that same guard. + let (store_hit, release_store) = state.credentials.gate_next_store(); + let update_state = Arc::clone(&state); + let replacement = provider("synthetic-second"); + let update = tokio::spawn(async move { + super::super::provider::handle_update_provider( + &update_state, + authed_request(UpdateProviderRequest { + provider: Some(replacement), + credential_expires_at_ms: HashMap::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }), + ) + .await + }); + tokio::time::timeout(Duration::from_secs(5), store_hit) + .await + .unwrap() + .unwrap(); + let attach_wait_probe = Arc::new(tokio::sync::Notify::new()); + let mut attach_request = authed_request(AttachSandboxProviderRequest { + sandbox_name: "attach-race".to_string(), + provider_name: "work-github".to_string(), + expected_resource_version: 0, + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }); + attach_request + .extensions_mut() + .insert(Arc::clone(&attach_wait_probe)); + let attach_state = Arc::clone(&state); + let attach = tokio::spawn(async move { + super::super::sandbox::handle_attach_sandbox_provider(&attach_state, attach_request).await + }); + tokio::time::timeout(Duration::from_secs(5), attach_wait_probe.notified()) + .await + .unwrap(); + release_store.send(()).unwrap(); + let published = tokio::time::timeout(Duration::from_secs(5), update) + .await + .unwrap() + .unwrap() + .unwrap() + .into_inner() + .provider + .unwrap(); + let attached = tokio::time::timeout(Duration::from_secs(5), attach) + .await + .unwrap() + .unwrap() + .unwrap() + .into_inner(); + assert!(attached.attached); + let receipt = attached.receipt.unwrap(); + let desired = receipt.desired.as_ref().unwrap(); + let published_version = published.metadata.as_ref().unwrap().resource_version; + assert_ne!( + published_version, + initial.metadata.as_ref().unwrap().resource_version + ); + assert_eq!(desired.provider_resource_version, published_version); + assert_eq!(desired.provider_id, published.object_id()); + + let session_id = register_session(&state.supervisor_sessions, &hello).unwrap(); + state + .supervisor_sessions + .accept_provider_readiness( + &hello.sandbox_id, + &hello.instance_id, + installed(&hello, &session_id, &receipt), + ) + .unwrap(); + let status = handle_get_sandbox_provider_status( + &state, + authed_request(GetSandboxProviderStatusRequest { + sandbox_name: "attach-race".to_string(), + provider_name: "work-github".to_string(), + receipt_id: receipt.receipt_id, + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }), + ) + .await + .unwrap() + .into_inner() + .status + .unwrap(); + assert_eq!(status.state, ProviderReadinessState::Ready as i32); +} + +#[tokio::test] +async fn status_rejects_oversized_provider_name_before_persisting_receipt() { + let state = test_server_state().await; + let sandbox = Sandbox { + metadata: Some(ObjectMeta { + id: Uuid::new_v4().to_string(), + name: "s1".to_string(), + workspace: "default".to_string(), + ..Default::default() + }), + spec: Some(SandboxSpec { + policy: Some(openshell_policy::restrictive_default_policy()), + ..Default::default() + }), + ..Default::default() + }; + state.store.put_message(&sandbox).await.unwrap(); + let response = handle_get_sandbox_provider_status( + &state, + authed_request(GetSandboxProviderStatusRequest { + sandbox_name: "s1".to_string(), + provider_name: "x".repeat(super::super::MAX_NAME_LEN + 1), + receipt_id: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }), + ) + .await; + let receipt_count = state + .store + .count_in_workspace(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, "default") + .await + .unwrap(); + assert_eq!(receipt_count, 0, "invalid status query persisted a receipt"); + let error = response.unwrap_err(); + assert_eq!(error.code(), tonic::Code::InvalidArgument); + assert_eq!(error.message(), "provider_name exceeds maximum length"); +} + +#[tokio::test] +async fn status_accepts_maximum_provider_name_and_receipt_only_lookup() { + let state = test_server_state().await; + let sandbox = Sandbox { + metadata: Some(ObjectMeta { + id: Uuid::new_v4().to_string(), + name: "s1".to_string(), + workspace: "default".to_string(), + ..Default::default() + }), + spec: Some(SandboxSpec { + policy: Some(openshell_policy::restrictive_default_policy()), + ..Default::default() + }), + ..Default::default() + }; + state.store.put_message(&sandbox).await.unwrap(); + let provider_name = "x".repeat(super::super::MAX_NAME_LEN); + let provider = Provider { + metadata: Some(ObjectMeta { + id: Uuid::new_v4().to_string(), + name: provider_name.clone(), + workspace: "default".to_string(), + ..Default::default() + }), + ..Default::default() + }; + state.store.put_message(&provider).await.unwrap(); + let response = handle_get_sandbox_provider_status( + &state, + authed_request(GetSandboxProviderStatusRequest { + sandbox_name: "s1".to_string(), + provider_name: provider_name.clone(), + receipt_id: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }), + ) + .await + .unwrap() + .into_inner(); + let receipt = response.status.unwrap().receipt.unwrap(); + assert_eq!(receipt.provider_name, provider_name); + assert_eq!(receipt.kind, ProviderMutationKind::Observe as i32); + assert!(receipt.desired.as_ref().unwrap().provider_id.is_empty()); + + // Receipt-only requery retains the persisted detached-provider intent even + // after its record is deleted, without creating another observation. + state + .store + .delete(Provider::object_type(), provider.object_id()) + .await + .unwrap(); + let repeated = handle_get_sandbox_provider_status( + &state, + authed_request(GetSandboxProviderStatusRequest { + sandbox_name: "s1".to_string(), + provider_name: String::new(), + receipt_id: receipt.receipt_id.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }), + ) + .await + .unwrap() + .into_inner(); + assert_eq!(repeated.status.unwrap().receipt, Some(receipt)); + assert_eq!( + state + .store + .count_in_workspace(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, "default") + .await + .unwrap(), + 1 + ); +} + +async fn observation_fixture() -> ( + Arc, + Sandbox, + Provider, + GetSandboxProviderStatusRequest, +) { + let state = test_server_state().await; + let sandbox = Sandbox { + metadata: Some(ObjectMeta { + id: Uuid::new_v4().to_string(), + name: "observe-sandbox".to_string(), + workspace: "default".to_string(), + ..Default::default() + }), + spec: Some(SandboxSpec { + policy: Some(openshell_policy::restrictive_default_policy()), + provider_attachment_epoch: Uuid::new_v4().to_string(), + ..Default::default() + }), + ..Default::default() + }; + let provider = Provider { + metadata: Some(ObjectMeta { + id: Uuid::new_v4().to_string(), + name: "observe-provider".to_string(), + workspace: "default".to_string(), + ..Default::default() + }), + ..Default::default() + }; + state.store.put_message(&sandbox).await.unwrap(); + state.store.put_message(&provider).await.unwrap(); + let query = GetSandboxProviderStatusRequest { + sandbox_name: "observe-sandbox".to_string(), + provider_name: "observe-provider".to_string(), + receipt_id: String::new(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }; + (state, sandbox, provider, query) +} + +#[tokio::test] +async fn repeated_and_concurrent_receiptless_status_reuses_one_operation() { + let (state, _, _, query) = observation_fixture().await; + let results = futures::future::join_all( + (0..8).map(|_| handle_get_sandbox_provider_status(&state, authed_request(query.clone()))), + ) + .await; + let receipts: Vec<_> = results + .into_iter() + .map(|result| { + result + .unwrap() + .into_inner() + .status + .unwrap() + .receipt + .unwrap() + }) + .collect(); + let first = receipts.first().unwrap(); + assert!(receipts.iter().all(|receipt| receipt == first)); + let repeated = handle_get_sandbox_provider_status(&state, authed_request(query)) + .await + .unwrap() + .into_inner() + .status + .unwrap() + .receipt + .unwrap(); + assert_eq!(&repeated, first); + assert_eq!( + state + .store + .count_in_workspace(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, "default") + .await + .unwrap(), + 1 + ); +} + +#[tokio::test] +async fn receiptless_status_changed_target_creates_a_distinct_operation() { + let (state, mut sandbox, _, query) = observation_fixture().await; + let original = handle_get_sandbox_provider_status(&state, authed_request(query.clone())) + .await + .unwrap() + .into_inner() + .status + .unwrap() + .receipt + .unwrap(); + sandbox.spec.as_mut().unwrap().provider_attachment_epoch = Uuid::new_v4().to_string(); + state.store.put_message(&sandbox).await.unwrap(); + let replacement = handle_get_sandbox_provider_status(&state, authed_request(query)) + .await + .unwrap() + .into_inner() + .status + .unwrap() + .receipt + .unwrap(); + assert_ne!(replacement.receipt_id, original.receipt_id); + assert_ne!(replacement.desired, original.desired); + assert_eq!( + state + .store + .count_in_workspace(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, "default") + .await + .unwrap(), + 2 + ); +} + +#[tokio::test] +async fn receiptless_status_unknown_provider_does_not_persist() { + let (state, _, provider, query) = observation_fixture().await; + state + .store + .delete(Provider::object_type(), provider.object_id()) + .await + .unwrap(); + let error = handle_get_sandbox_provider_status(&state, authed_request(query)) + .await + .unwrap_err(); + assert_eq!(error.code(), tonic::Code::NotFound); + assert_eq!(error.message(), "provider not found"); + assert_eq!( + state + .store + .count_in_workspace(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, "default") + .await + .unwrap(), + 0 + ); +} + +#[tokio::test] +async fn stored_change_is_bound_to_its_sandbox_and_provider() { + let state = test_server_state().await; + let mut owner = Sandbox { + metadata: Some(ObjectMeta { + id: Uuid::new_v4().to_string(), + name: "owner".to_string(), + workspace: "default".to_string(), + ..Default::default() + }), + spec: Some(SandboxSpec { + provider_attachment_epoch: Uuid::new_v4().to_string(), + policy: Some(openshell_policy::restrictive_default_policy()), + ..Default::default() + }), + ..Default::default() + }; + state.store.put_message(&owner).await.unwrap(); + let receipt = record_provider_mutation( + &state, + &owner, + "provider-owner", + ProviderMutationKind::Detach, + None, + &Uuid::new_v4().to_string(), + ) + .await + .unwrap(); + owner.metadata.as_mut().unwrap().id = Uuid::new_v4().to_string(); + owner.metadata.as_mut().unwrap().name = "other".to_string(); + state.store.put_message(&owner).await.unwrap(); + for (sandbox_name, provider_name) in [("other", "provider-owner"), ("owner", "other-provider")] + { + let request = GetSandboxProviderStatusRequest { + sandbox_name: sandbox_name.to_string(), + provider_name: provider_name.to_string(), + receipt_id: receipt.receipt_id.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }; + assert_eq!( + handle_get_sandbox_provider_status(&state, authed_request(request)) + .await + .unwrap_err() + .code(), + tonic::Code::NotFound + ); + } +} + +#[tokio::test] +async fn detach_receipt_persists_but_gateway_restart_requires_fresh_installation() { + let state = test_server_state().await; + let hello = hello(); + let mut sandbox = Sandbox { + metadata: Some(ObjectMeta { + id: hello.sandbox_id.clone(), + name: "readiness".to_string(), + workspace: "default".to_string(), + ..Default::default() + }), + spec: Some(SandboxSpec { + provider_attachment_epoch: Uuid::new_v4().to_string(), + policy: Some(openshell_policy::restrictive_default_policy()), + ..Default::default() + }), + ..Default::default() + }; + sandbox.set_phase(SandboxPhase::Ready as i32); + sandbox + .status + .as_mut() + .unwrap() + .main_process_instance_id + .clone_from(&hello.instance_id); + state.store.put_message(&sandbox).await.unwrap(); + let receipt = record_provider_mutation( + &state, + &sandbox, + "detached", + ProviderMutationKind::Detach, + None, + &Uuid::new_v4().to_string(), + ) + .await + .unwrap(); + let record = state + .store + .get(CONFIG_UPDATE_OPERATION_OBJECT_TYPE, &receipt.receipt_id) + .await + .unwrap() + .unwrap(); + let stored = StoredConfigUpdateOperation::decode(record.payload.as_slice()).unwrap(); + assert_eq!( + stored.provider_snapshot_reason, + ProviderReadinessReason::Unspecified as i32 + ); + assert!( + state + .store + .put_if( + CONFIG_UPDATE_OPERATION_OBJECT_TYPE, + &receipt.receipt_id, + &receipt.receipt_id, + "default", + &record.payload, + None, + WriteCondition::MustCreate + ) + .await + .is_err() + ); + let query = GetSandboxProviderStatusRequest { + sandbox_name: "readiness".to_string(), + provider_name: "detached".to_string(), + receipt_id: receipt.receipt_id.clone(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }; + let initial = handle_get_sandbox_provider_status(&state, authed_request(query.clone())) + .await + .unwrap() + .into_inner() + .status + .unwrap(); + assert_eq!(initial.state, ProviderReadinessState::Persisted as i32); + let session_id = register_session(&state.supervisor_sessions, &hello).unwrap(); + state + .supervisor_sessions + .accept_provider_readiness( + &hello.sandbox_id, + &hello.instance_id, + installed(&hello, &session_id, &receipt), + ) + .unwrap(); + let ready = handle_get_sandbox_provider_status(&state, authed_request(query.clone())) + .await + .unwrap() + .into_inner() + .status + .unwrap(); + assert_eq!(ready.state, ProviderReadinessState::Revoked as i32); + + // A different replica may accept a replacement supervisor while this + // gateway still has the old local session and matching config hashes. + sandbox.status.as_mut().unwrap().main_process_instance_id = Uuid::new_v4().to_string(); + state.store.put_message(&sandbox).await.unwrap(); + let replaced = handle_get_sandbox_provider_status(&state, authed_request(query.clone())) + .await + .unwrap() + .into_inner() + .status + .unwrap(); + assert_eq!(replaced.state, ProviderReadinessState::Pending as i32); + assert_eq!( + replaced.reason, + ProviderReadinessReason::SupervisorDisconnected as i32 + ); + sandbox + .status + .as_mut() + .unwrap() + .main_process_instance_id + .clone_from(&hello.instance_id); + state.store.put_message(&sandbox).await.unwrap(); + + let mut restarted = test_server_state().await; + Arc::get_mut(&mut restarted).unwrap().store = Arc::clone(&state.store); + let after_restart = + handle_get_sandbox_provider_status(&restarted, authed_request(query.clone())) + .await + .unwrap() + .into_inner() + .status + .unwrap(); + assert_eq!( + after_restart.state, + ProviderReadinessState::Persisted as i32 + ); + assert_eq!(after_restart.receipt, Some(receipt)); + sandbox.spec.as_mut().unwrap().provider_attachment_epoch = Uuid::new_v4().to_string(); + state.store.put_message(&sandbox).await.unwrap(); + let changed = handle_get_sandbox_provider_status(&state, authed_request(query)) + .await + .unwrap() + .into_inner() + .status + .unwrap(); + assert_eq!(changed.state, ProviderReadinessState::Superseded as i32); +} diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 75b07f8c9b..9c5b3b9cd3 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -29,12 +29,12 @@ use openshell_core::proto::{ ExecSandboxEvent, ExecSandboxExit, ExecSandboxInput, ExecSandboxRequest, ExecSandboxStderr, ExecSandboxStdout, GetSandboxRequest, GetSandboxTemplateRequest, ListSandboxProvidersRequest, ListSandboxProvidersResponse, ListSandboxTemplatesRequest, ListSandboxTemplatesResponse, - ListSandboxesRequest, ListSandboxesResponse, Provider, ResourceRequirements, - RevokeSshSessionRequest, RevokeSshSessionResponse, SandboxResources, SandboxResponse, - SandboxSpec, SandboxStreamEvent, SandboxTemplateResponse, SandboxWorkloadTemplate, - SandboxWorkloadTemplateProvenance, SshRelayTarget, StartSandboxRequest, StopSandboxRequest, - TcpForwardFrame, TcpForwardInit, TcpRelayTarget, WatchSandboxRequest, relay_open, - tcp_forward_init, + ListSandboxesRequest, ListSandboxesResponse, Provider, ProviderMutationKind, + ResourceRequirements, RevokeSshSessionRequest, RevokeSshSessionResponse, SandboxResources, + SandboxResponse, SandboxSpec, SandboxStreamEvent, SandboxTemplateResponse, + SandboxWorkloadTemplate, SandboxWorkloadTemplateProvenance, SshRelayTarget, + StartSandboxRequest, StopSandboxRequest, TcpForwardFrame, TcpForwardInit, TcpRelayTarget, + WatchSandboxRequest, relay_open, tcp_forward_init, }; use openshell_core::proto::{ BeginRootfsTarStagingRequest, BeginRootfsTarStagingResponse, Sandbox, SandboxPhase, @@ -172,7 +172,9 @@ pub(super) async fn handle_create_sandbox( request: Request, ) -> Result, Status> { let create_request = request.get_ref().clone(); - let result = handle_create_sandbox_inner(state, request).await; + // Sandbox creation retains large configuration values across awaits. + // Box the inner future to keep this wrapper small for every caller. + let result = Box::pin(handle_create_sandbox_inner(state, request)).await; let created_sandbox = result .as_ref() .ok() @@ -371,6 +373,10 @@ async fn handle_create_sandbox_inner( (resolved, Some(provenance)) }; + // Attachment identity belongs to the gateway. Accepting an epoch from a + // create request or workload template could revive stale installation proof. + spec.provider_attachment_epoch = uuid::Uuid::new_v4().to_string(); + // Leave an omitted command empty rather than persisting a concrete shell: // the sandbox boundary resolves the default login shell against the agent image // (bash when present, otherwise /bin/sh on minimal images like Alpine), @@ -1063,6 +1069,11 @@ pub(super) async fn handle_attach_sandbox_provider( request: Request, ) -> Result, Status> { let principal = super::extract_principal(&request)?; + #[cfg(test)] + let attach_wait_probe = request + .extensions() + .get::>() + .cloned(); let request = request.into_inner(); let authz = authorize_workspace_selector( &state.store, @@ -1089,20 +1100,27 @@ pub(super) async fn handle_attach_sandbox_provider( ))); } - get_provider_record(state.store.as_ref(), &workspace, &request.provider_name) - .await - .map_err(|err| { - if err.code() == tonic::Code::NotFound { - Status::failed_precondition(format!( - "provider '{}' not found", - request.provider_name - )) - } else { - err - } - })?; - + // The receipt must capture the provider revision selected by this + // serialized mutation, after any preceding credential update has finished. + #[cfg(test)] + if let Some(probe) = attach_wait_probe { + probe.notify_one(); + } let _sandbox_sync_guard = state.compute.sandbox_sync_guard().await; + let provider_record = + get_provider_record(state.store.as_ref(), &workspace, &request.provider_name) + .await + .map_err(|err| { + if err.code() == tonic::Code::NotFound { + Status::failed_precondition(format!( + "provider '{}' not found", + request.provider_name + )) + } else { + err + } + })?; + let sandbox = sandbox_by_name(state, &workspace, &request.sandbox_name).await?; let sandbox_id = sandbox .metadata @@ -1168,6 +1186,7 @@ pub(super) async fn handle_attach_sandbox_provider( let provider_name = request.provider_name.clone(); let attached = Arc::new(AtomicBool::new(false)); let attached_clone = attached.clone(); + let mutation_id = uuid::Uuid::new_v4().to_string(); let sandbox = state .store @@ -1175,16 +1194,22 @@ pub(super) async fn handle_attach_sandbox_provider( &sandbox_id, request.expected_resource_version, |sandbox| { + attached_clone.store(false, Ordering::Relaxed); let Some(ref mut spec) = sandbox.spec else { // Spec should always exist post-creation; if missing, fail CAS to surface error return; }; + if spec.provider_attachment_epoch.is_empty() { + spec.provider_attachment_epoch.clone_from(&mutation_id); + } + dedupe_provider_names(&mut spec.providers); if !spec.providers.iter().any(|name| name == &provider_name) && spec.providers.len() < MAX_PROVIDERS { spec.providers.push(provider_name.clone()); + spec.provider_attachment_epoch.clone_from(&mutation_id); attached_clone.store(true, Ordering::Relaxed); } }, @@ -1193,6 +1218,18 @@ pub(super) async fn handle_attach_sandbox_provider( .map_err(|e| super::persistence_error_to_status(e, "attach sandbox provider"))?; let attached = attached.load(Ordering::Relaxed); + let receipt = super::provider_readiness::record_provider_mutation( + state, + &sandbox, + &request.provider_name, + ProviderMutationKind::Attach, + Some(( + provider_record.object_id(), + provider_record.get_resource_version(), + )), + &mutation_id, + ) + .await?; info!( sandbox_name = %request.sandbox_name, @@ -1204,6 +1241,7 @@ pub(super) async fn handle_attach_sandbox_provider( Ok(Response::new(AttachSandboxProviderResponse { sandbox: Some(sandbox), attached, + receipt: Some(receipt), })) } @@ -1267,6 +1305,7 @@ pub(super) async fn handle_detach_sandbox_provider( let provider_name = request.provider_name.clone(); let detached = Arc::new(AtomicBool::new(false)); let detached_clone = detached.clone(); + let mutation_id = uuid::Uuid::new_v4().to_string(); let sandbox = state .store @@ -1274,14 +1313,20 @@ pub(super) async fn handle_detach_sandbox_provider( &sandbox_id, request.expected_resource_version, |sandbox| { + detached_clone.store(false, Ordering::Relaxed); let Some(ref mut spec) = sandbox.spec else { // Spec should always exist post-creation; if missing, fail CAS to surface error return; }; + if spec.provider_attachment_epoch.is_empty() { + spec.provider_attachment_epoch.clone_from(&mutation_id); + } + let before_len = spec.providers.len(); spec.providers.retain(|name| name != &provider_name); if spec.providers.len() != before_len { + spec.provider_attachment_epoch.clone_from(&mutation_id); detached_clone.store(true, Ordering::Relaxed); // Only dedupe after making a change dedupe_provider_names(&mut spec.providers); @@ -1292,6 +1337,15 @@ pub(super) async fn handle_detach_sandbox_provider( .map_err(|e| super::persistence_error_to_status(e, "detach sandbox provider"))?; let detached = detached.load(Ordering::Relaxed); + let receipt = super::provider_readiness::record_provider_mutation( + state, + &sandbox, + &request.provider_name, + ProviderMutationKind::Detach, + None, + &mutation_id, + ) + .await?; info!( sandbox_name = %request.sandbox_name, @@ -1303,6 +1357,7 @@ pub(super) async fn handle_detach_sandbox_provider( Ok(Response::new(DetachSandboxProviderResponse { sandbox: Some(sandbox), detached, + receipt: Some(receipt), })) } @@ -5330,6 +5385,7 @@ mod tests { "template", "resource_requirements", ], + &["provider_attachment_epoch"], ); } @@ -5337,6 +5393,7 @@ mod tests { message_name: &str, copied_from_create_request: &[&str], rejected_template_workload_overrides: &[&str], + generated_by_gateway: &[&str], ) { let pool = prost_reflect::DescriptorPool::decode(openshell_core::FILE_DESCRIPTOR_SET) .expect("decode descriptor set"); @@ -5346,8 +5403,16 @@ mod tests { let classified: std::collections::HashSet<&str> = copied_from_create_request .iter() .chain(rejected_template_workload_overrides.iter()) + .chain(generated_by_gateway.iter()) .copied() .collect(); + assert_eq!( + classified.len(), + copied_from_create_request.len() + + rejected_template_workload_overrides.len() + + generated_by_gateway.len(), + "every field must have exactly one create-time owner" + ); let actual: std::collections::HashSet = message .fields() .map(|field| field.name().to_string()) @@ -5358,7 +5423,8 @@ mod tests { classified.contains(field.as_str()), "{message_name}.{field} is not classified for template-backed sandbox creates. \ Add it to copied_from_create_request when callers own the create-time value, \ - or to rejected_template_workload_overrides when the workload template owns it." + to rejected_template_workload_overrides when the workload template owns it, \ + or to generated_by_gateway when the gateway replaces the caller's value." ); } @@ -5371,6 +5437,55 @@ mod tests { } } + #[tokio::test] + async fn create_sandbox_ignores_caller_provider_attachment_epoch() { + let state = test_server_state().await; + handle_create_sandbox_template( + &state, + authed_request(CreateSandboxTemplateRequest { + template: Some(test_workload_template("epoch-template")), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + }), + ) + .await + .unwrap(); + let supplied_epoch = uuid::Uuid::new_v4().to_string(); + let mut generated_epochs = std::collections::HashSet::new(); + for (name, workload_template_name) in + [("direct-epoch", ""), ("template-epoch", "epoch-template")] + { + let created = handle_create_sandbox( + &state, + authed_request(CreateSandboxRequest { + name: name.to_string(), + spec: Some(SandboxSpec { + provider_attachment_epoch: supplied_epoch.clone(), + ..Default::default() + }), + workload_template_name: workload_template_name.to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + ..Default::default() + }), + ) + .await + .unwrap() + .into_inner() + .sandbox + .unwrap(); + let epoch = &created.spec.as_ref().unwrap().provider_attachment_epoch; + assert_ne!(epoch, &supplied_epoch); + assert!(uuid::Uuid::parse_str(epoch).is_ok()); + assert!(generated_epochs.insert(epoch.clone())); + let stored = state + .store + .get_message::(created.object_id()) + .await + .unwrap() + .unwrap(); + assert_eq!(&stored.spec.unwrap().provider_attachment_epoch, epoch); + } + } + #[tokio::test] async fn create_sandbox_from_workload_template_resolves_workload_and_preserves_governance() { let state = test_server_state().await; diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index b086177530..58d58e6e67 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -18,6 +18,7 @@ pub mod certgen; pub mod cli; mod compute; pub mod config_file; +mod config_update_operation; mod credentials; mod defaults; mod gateway_listener; diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index f0ec02107a..f326fc5683 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -117,13 +117,13 @@ mod tests { use std::collections::{BTreeMap, BTreeSet, VecDeque}; const STORAGE_V1_SCHEMA_SHA256: &str = - "79c72615d957fc0653c672f61998bf7d8d21b757bc05d07b3fff92bd70fc8f52"; + "1148159c1894e89648ff49fcfff8087ca3d41c7e930365c08ceebe450f2e0e58"; const PUBLIC_RPC_SCHEMA_SHA256: &str = - "91025c34fadd69f2d96d5ad571f0e6e031490ff1f0ff3060b16021ae6633a81a"; + "1f9ff0c3269949f29842b0b597eb3f6234c76ec2787666fe2b8f008306fa6f03"; const DURABLE_SCHEMA_SHA256: &str = - "568ec5637c504726b40a616d286457f41b5be2f4761872c749313e1ee16b5c85"; + "9886c99fb2aac4f7ed8435895244b17472ef4895b85b7069ca1e60e6f08b1e63"; const PUBLIC_DURABLE_OVERLAP_SHA256: &str = - "f96d841e67da5c3443fa0aca15936dd14ac30d2e150ddf0439b0d195c4a0cfd9"; + "c2b98bd95d798abf2be680dbd795e39ddddbef274161173537a41d66214ee5d2"; // A persisted Sandbox without endpoint status retains its lifecycle fields; // the absent repeated field decodes empty and needs no database rewrite. const SANDBOX_WITHOUT_ENDPOINT_STATUS: &str = "0a1e0a0a73616e64626f782d6964120773616e64626f783a0764656661756c741a2b0a0773616e64626f782a0d0a05526561647912045472756530023807420d73757065727669736f722d6964"; @@ -139,21 +139,31 @@ mod tests { "0a0472756c651a07666978747572652d0000403f3a0b6578616d706c652e636f6d40bb035002"; const V0_0_116_POLICY_RECORD: &str = "0a09706f6c6963792d6964120a73616e64626f782d6964180222030102032a0673686132353632066c6f616465643a046e6f6e6540fa0148ac0252110a06736f75726365120766697874757265"; const V0_0_116_DRAFT_RECORD: &str = "0a086368756e6b2d6964120a73616e64626f782d69641802220770656e64696e672a0472756c65320204053a076669787475726549000000000000e83f50de02589003620b6578616d706c652e636f6d68bb037801"; - const STORAGE_MESSAGE_NAMES: [&str; 7] = [ + const STORAGE_MESSAGE_NAMES: [&str; 8] = [ "DraftChunkPayload", "PolicyRevisionPayload", + "StoredConfigUpdateOperation", "StoredDraftChunk", "StoredPolicyRevision", "StoredProviderCredentialRefreshState", "StoredProviderProfile", "StoredRefreshMaterialDeletion", ]; - const DURABLE_ROOTS: [&str; 12] = [ + const PROVIDER_READINESS_RPC_SIGNATURES: [&str; 2] = [ + "openshell.v1.OpenShell/GetSandboxProviderStatus|.openshell.v1.GetSandboxProviderStatusRequest|.openshell.v1.GetSandboxProviderStatusResponse|false|false", + "openshell.v1.OpenShell/ReportProviderReadiness|.openshell.v1.ReportProviderReadinessRequest|.openshell.v1.ReportProviderReadinessResponse|false|false", + ]; + // Synthetic SandboxSpec bytes with log level, provider, and command fields, + // emitted before the gateway-owned attachment epoch field was introduced. + const PRE_READINESS_SANDBOX_SPEC: &str = + "0a04696e666f421273796e7468657469632d70726f766964657262046563686f"; + const DURABLE_ROOTS: &[&str] = &[ ".openshell.datamodel.v1.Provider", ".openshell.datamodel.v1.Workspace", ".openshell.sandbox.v1.SandboxPolicy", ".openshell.storage.v1.DraftChunkPayload", ".openshell.storage.v1.PolicyRevisionPayload", + ".openshell.storage.v1.StoredConfigUpdateOperation", ".openshell.storage.v1.StoredProviderCredentialRefreshState", ".openshell.storage.v1.StoredProviderProfile", ".openshell.v1.Sandbox", @@ -450,19 +460,34 @@ mod tests { } } methods.sort(); - assert_eq!(compiled_method_count, 101, "classify every compiled RPC"); - assert_eq!(methods.len(), 75, "inventory every public gateway RPC"); + for signature in PROVIDER_READINESS_RPC_SIGNATURES { + assert!( + methods.iter().any(|method| method == signature), + "provider readiness RPC is missing or changed: {signature}" + ); + } + assert_eq!( + compiled_method_count, + 101 + PROVIDER_READINESS_RPC_SIGNATURES.len(), + "classify every compiled RPC" + ); + assert_eq!( + methods.len(), + 75 + PROVIDER_READINESS_RPC_SIGNATURES.len(), + "inventory every public gateway RPC" + ); assert_eq!( methods .iter() .filter(|method| method.starts_with("openshell.v1.OpenShell/")) .count(), - 75 + 75 + PROVIDER_READINESS_RPC_SIGNATURES.len() ); assert!(methods.iter().all(|method| !method.contains(".storage."))); let public_closure = schema_closure(&index, public_roots); - let durable_closure = schema_closure(&index, DURABLE_ROOTS.into_iter().map(str::to_string)); + let durable_closure = + schema_closure(&index, DURABLE_ROOTS.iter().copied().map(str::to_string)); let overlap_messages = public_closure .messages .intersection(&durable_closure.messages) @@ -488,28 +513,39 @@ mod tests { let durable_inventory_hash = schema_fingerprint(&index, &durable_closure); let overlap_hash = format!("{:x}", Sha256::digest(overlap_inventory.as_bytes())); + // Report the complete measured inventory on failure so one schema + // change exposes every affected boundary in the same focused run. assert_eq!( - (public_closure.messages.len(), public_closure.enums.len()), - (282, 13) - ); - assert_eq!( - (durable_closure.messages.len(), durable_closure.enums.len()), - (82, 9) + ( + (public_closure.messages.len(), public_closure.enums.len()), + (durable_closure.messages.len(), durable_closure.enums.len()), + (overlap_messages.len(), overlap_enums.len()), + public_inventory_hash.as_str(), + durable_inventory_hash.as_str(), + overlap_hash.as_str(), + ), + ( + (293, 19), + (89, 15), + (77, 15), + PUBLIC_RPC_SCHEMA_SHA256, + DURABLE_SCHEMA_SHA256, + PUBLIC_DURABLE_OVERLAP_SHA256 + ), + "the public/durable schema inventory changed; review API and storage ownership, preserve prior-payload decoding, and update the reviewed fingerprints" ); - assert_eq!((overlap_messages.len(), overlap_enums.len()), (72, 9)); + } - assert_eq!( - public_inventory_hash, PUBLIC_RPC_SCHEMA_SHA256, - "the public RPC schema closure changed; review API compatibility and update the inventory and architecture/gateway.md" - ); - assert_eq!( - durable_inventory_hash, DURABLE_SCHEMA_SHA256, - "a durable protobuf root or transitive dependency changed; record migration handling and a prior-version fixture before updating this fingerprint" - ); - assert_eq!( - overlap_hash, PUBLIC_DURABLE_OVERLAP_SHA256, - "the public/durable protobuf overlap changed; review both API and storage compatibility before updating this inventory" - ); + #[test] + fn pre_readiness_sandbox_spec_decodes_with_initial_attachment_epoch() { + let spec = openshell_core::proto::SandboxSpec::decode( + legacy_bytes(PRE_READINESS_SANDBOX_SPEC).as_slice(), + ) + .expect("prior sandbox spec must decode"); + assert_eq!(spec.log_level, "info"); + assert_eq!(spec.providers, ["synthetic-provider"]); + assert_eq!(spec.command, ["echo"]); + assert!(spec.provider_attachment_epoch.is_empty()); } fn legacy_bytes(encoded: &str) -> Vec { diff --git a/crates/openshell-server/src/supervisor_session.rs b/crates/openshell-server/src/supervisor_session.rs index 91a2151037..cf931e7a3c 100644 --- a/crates/openshell-server/src/supervisor_session.rs +++ b/crates/openshell-server/src/supervisor_session.rs @@ -14,14 +14,16 @@ use tracing::{debug, info, warn}; use uuid::Uuid; use openshell_core::proto::{ - GatewayMessage, RelayFrame, RelayInit, RelayOpen, ReportMainProcessExitRequest, - ReportMainProcessExitResponse, Sandbox, SandboxPhase, SessionAccepted, SshRelayTarget, - SupervisorMessage, gateway_message, relay_open, supervisor_message, + GatewayMessage, ProviderReadinessObservation, RelayFrame, RelayInit, RelayOpen, + ReportMainProcessExitRequest, ReportMainProcessExitResponse, Sandbox, SandboxPhase, + SessionAccepted, SshRelayTarget, SupervisorMessage, gateway_message, relay_open, + supervisor_message, }; use openshell_core::transport_errors::is_expected_transport_close_status; use crate::ServerState; use crate::auth::principal::Principal; +use crate::grpc::provider_readiness::ProviderReadinessEvidence; use crate::persistence::ObjectId; const HEARTBEAT_INTERVAL_SECS: u32 = 15; @@ -71,6 +73,9 @@ struct LiveSession { /// gateway restart invalidates every session and startup reconciliation /// resets any persisted endpoint result before requests are served. endpoint_report_cursor: Option, + /// Installation evidence belongs to this connection and is never restored + /// from persistence or inherited by a replacement supervisor session. + provider_readiness: Option, #[allow(dead_code)] connected_at: Instant, } @@ -155,6 +160,7 @@ impl SupervisorSessionRegistry { terminal_delivery_finalized: false, endpoint_status_initialized: false, endpoint_report_cursor: None, + provider_readiness: None, connected_at: Instant::now(), }, ); @@ -267,6 +273,79 @@ impl SupervisorSessionRegistry { .is_some_and(|session| session.session_id == session_id) } + /// Bind the authenticated hello's installation capability to its session. + /// Initialization is single-use and cannot erase accepted observations. + pub(crate) fn initialize_provider_readiness( + &self, + sandbox_id: &str, + session_id: &str, + evidence: ProviderReadinessEvidence, + ) -> Result<(), Status> { + let mut sessions = self + .sessions + .lock() + .map_err(|_| Status::unavailable("supervisor session state is unavailable"))?; + let session = sessions + .get_mut(sandbox_id) + .filter(|session| session.session_id == session_id) + .ok_or_else(|| Status::failed_precondition("supervisor session was replaced"))?; + if session.provider_readiness.is_some() { + return Err(Status::failed_precondition( + "provider readiness is already initialized", + )); + } + session.provider_readiness = Some(evidence); + Ok(()) + } + + /// Accept installation evidence only while its session owns this sandbox. + /// Session comparison and publication share one lock so reconnects cannot + /// transfer a predecessor's evidence into the replacement session. + pub(crate) fn accept_provider_readiness( + &self, + sandbox_id: &str, + active_instance_id: &str, + observation: ProviderReadinessObservation, + ) -> Result<(), Status> { + let mut sessions = self + .sessions + .lock() + .map_err(|_| Status::unavailable("supervisor session state is unavailable"))?; + let evidence = sessions + .get_mut(sandbox_id) + .filter(|session| { + session.session_id == observation.session_id && session.endpoint_status_initialized + }) + .and_then(|session| session.provider_readiness.as_mut()) + .ok_or_else(|| { + Status::permission_denied( + "provider readiness requires the active supervisor session", + ) + })?; + if !evidence.belongs_to_instance(active_instance_id) { + return Err(Status::failed_precondition( + "provider readiness requires the current sandbox instance", + )); + } + evidence.accept(observation) + } + + /// Snapshot installation evidence from the current initialized session. + /// Disconnect and replacement discard the previous connection's state. + pub(crate) fn provider_readiness( + &self, + sandbox_id: &str, + ) -> Result, Status> { + let sessions = self + .sessions + .lock() + .map_err(|_| Status::unavailable("supervisor session state is unavailable"))?; + Ok(sessions + .get(sandbox_id) + .filter(|session| session.endpoint_status_initialized) + .and_then(|session| session.provider_readiness.clone())) + } + /// Mark the current session as the observation authority after its /// public endpoint results have been durably reset. pub(crate) fn initialize_endpoint_status_authority( @@ -870,6 +949,9 @@ pub async fn handle_connect_supervisor( crate::auth::guard::ensure_sandbox_principal_scope(principal, &sandbox_id)?; } 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. + let provider_readiness = ProviderReadinessEvidence::from_hello(&hello)?; let session_id = Uuid::new_v4().to_string(); info!( @@ -919,6 +1001,16 @@ pub async fn handle_connect_supervisor( "supervisor session was replaced during endpoint status initialization", )); } + if let Err(error) = state.supervisor_sessions.initialize_provider_readiness( + &sandbox_id, + &session_id, + provider_readiness, + ) { + state + .supervisor_sessions + .remove_if_current(&sandbox_id, &session_id); + return Err(error); + } // Step 3: Send SessionAccepted. let accepted = GatewayMessage { diff --git a/crates/openshell-server/tests/common/mod.rs b/crates/openshell-server/tests/common/mod.rs index fe159dcf64..02b51d603a 100644 --- a/crates/openshell-server/tests/common/mod.rs +++ b/crates/openshell-server/tests/common/mod.rs @@ -218,6 +218,22 @@ impl OpenShell for TestOpenShell { Ok(Response::new(GetGatewayConfigResponse::default())) } + async fn get_sandbox_provider_status( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "provider readiness is not exercised by this mock", + )) + } + + async fn report_provider_readiness( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("provider readiness")) + } + async fn get_sandbox_provider_environment( &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 448f3e4765..f3ae883f78 100644 --- a/crates/openshell-server/tests/supervisor_relay_integration.rs +++ b/crates/openshell-server/tests/supervisor_relay_integration.rs @@ -248,6 +248,22 @@ impl OpenShell for RelayGateway { ) -> Result, Status> { Err(Status::unimplemented("unused")) } + async fn get_sandbox_provider_status( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "provider readiness is not exercised by this mock", + )) + } + + async fn report_provider_readiness( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("provider readiness")) + } + async fn get_sandbox_provider_environment( &self, _: tonic::Request, diff --git a/crates/openshell-supervisor-network/src/opa.rs b/crates/openshell-supervisor-network/src/opa.rs index 34fc81a6e2..f161b8c73d 100644 --- a/crates/openshell-supervisor-network/src/opa.rs +++ b/crates/openshell-supervisor-network/src/opa.rs @@ -170,7 +170,7 @@ pub(crate) fn test_opa_query_count() -> u64 { } /// Generation guard captured when an HTTP tunnel or request path starts. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct PolicyGenerationGuard { captured_generation: u64, current_generation: Arc, @@ -708,7 +708,7 @@ impl OpaEngine { /// validation guarantees as initial load. Atomically replaces the inner /// engine on success; on failure the previous engine is untouched (LKG). pub fn reload_from_proto(&self, proto: &ProtoSandboxPolicy) -> Result<()> { - self.reload_from_proto_with_pid(proto, 0) + self.reload_from_proto_with_pid(proto, 0).map(|_| ()) } /// Reload policy from a proto with symlink resolution. @@ -716,11 +716,12 @@ impl OpaEngine { /// When `entrypoint_pid` is non-zero, binary paths that are symlinks /// inside the container filesystem are resolved and added as additional /// match entries. See [`from_proto_with_pid`] for details. + /// Returns evidence tied to the generation installed by this call. pub fn reload_from_proto_with_pid( &self, proto: &ProtoSandboxPolicy, entrypoint_pid: u32, - ) -> Result<()> { + ) -> Result { // Build a complete new engine through the same validated pipeline. let new = Self::from_proto_with_pid(proto, entrypoint_pid)?; let new_engine = new @@ -736,8 +737,10 @@ impl OpaEngine { .fail_closed_reason .write() .map_err(|_| miette::miette!("OPA fail-closed state lock poisoned"))? = None; - self.advance_generation(); - Ok(()) + let generation = self.advance_generation(); + // Capture evidence while the installation lock still excludes a + // competing reload; reading the generation later can bind the wrong policy. + self.generation_guard(generation) } /// Reload the policy and middleware registry as one runtime generation. @@ -746,12 +749,13 @@ impl OpaEngine { /// engine and runner are then swapped while holding both locks, followed by /// a single generation increment. A preparation or lock failure leaves the /// live pair and generation untouched. + /// Returns evidence tied to that combined installation. pub fn reload_policy_and_middleware_from_proto_with_pid( &self, proto: &ProtoSandboxPolicy, entrypoint_pid: u32, registry: MiddlewareRegistry, - ) -> Result<()> { + ) -> Result { let new = Self::from_proto_with_pid(proto, entrypoint_pid)?; let new_engine = new .engine @@ -774,8 +778,8 @@ impl OpaEngine { .fail_closed_reason .write() .map_err(|_| miette::miette!("OPA fail-closed state lock poisoned"))? = None; - self.advance_generation(); - Ok(()) + let generation = self.advance_generation(); + self.generation_guard(generation) } /// Publish a deny-all quarantine generation without activating any part diff --git a/crates/openshell-supervisor-network/src/proxy.rs b/crates/openshell-supervisor-network/src/proxy.rs index dbb8c98fd9..6344617621 100644 --- a/crates/openshell-supervisor-network/src/proxy.rs +++ b/crates/openshell-supervisor-network/src/proxy.rs @@ -7990,6 +7990,7 @@ network_policies: }, ); let snapshot = ProviderCredentialSnapshot { + installation_id: String::new(), revision: 42, child_env: std::collections::HashMap::new(), dynamic_credentials, diff --git a/crates/openshell-supervisor-network/src/run.rs b/crates/openshell-supervisor-network/src/run.rs index 024185d1ac..533ee2b2eb 100644 --- a/crates/openshell-supervisor-network/src/run.rs +++ b/crates/openshell-supervisor-network/src/run.rs @@ -274,7 +274,7 @@ pub async fn run_networking( "Container filesystem accessible, resolving policy binary symlinks" ); match resolve_engine.reload_from_proto_with_pid(&resolve_proto, pid) { - Ok(()) => { + Ok(_) => { info!( pid = pid, "Policy binary symlink resolution complete \ diff --git a/crates/openshell-supervisor-process/src/supervisor_session.rs b/crates/openshell-supervisor-process/src/supervisor_session.rs index bc5189468d..aa629102ee 100644 --- a/crates/openshell-supervisor-process/src/supervisor_session.rs +++ b/crates/openshell-supervisor-process/src/supervisor_session.rs @@ -394,6 +394,7 @@ async fn run_single_session( payload: Some(supervisor_message::Payload::Hello(SupervisorHello { sandbox_id: config.sandbox_id.clone(), instance_id: config.instance_id.clone(), + supports_provider_readiness: true, })), }) .await diff --git a/crates/openshell-supervisor/src/lib.rs b/crates/openshell-supervisor/src/lib.rs index 827c131ef2..bdeeed0a34 100644 --- a/crates/openshell-supervisor/src/lib.rs +++ b/crates/openshell-supervisor/src/lib.rs @@ -19,6 +19,7 @@ mod activity_aggregator; mod denial_aggregator; mod endpoint_status; mod mechanistic_mapper; +mod provider_readiness; use miette::{IntoDiagnostic, Result, WrapErr}; use std::future::Future; @@ -268,10 +269,12 @@ async fn wait_for_control_shutdown_signal() { use openshell_core::denial::DenialEvent; use openshell_core::policy::{NetworkMode, NetworkPolicy, ProxyPolicy, SandboxPolicy}; use openshell_core::proposals::AgentProposals; +use openshell_core::proto::ProviderReadinessReason; use openshell_core::provider_credentials::ProviderCredentialState; -use openshell_supervisor_network::opa::OpaEngine; +use openshell_supervisor_network::opa::{OpaEngine, PolicyGenerationGuard}; use openshell_supervisor_network::proxy::ProxyHandle; use openshell_supervisor_process::skills; +use provider_readiness::{EnvironmentIdentity, Tracker as ProviderReadinessTracker}; use tokio::sync::mpsc::UnboundedSender; use tokio::time::timeout; @@ -638,18 +641,12 @@ pub async fn run_sandbox( // guest account data against the host's /etc/passwd and /etc/group. let workspace = workdir; + let provider_readiness = ProviderReadinessTracker::new(); let provider_credentials = { // Fetch provider environment variables from the server. // This is done after loading the policy so the sandbox can still start // even if provider env fetch fails (graceful degradation). - let ( - provider_env_revision, - provider_env, - provider_credential_expires_at_ms, - dynamic_credentials, - static_credential_bindings, - non_secret_environment_keys, - ) = if let (Some(id), Some(endpoint)) = (&sandbox_id, &openshell_endpoint) { + let environment = if let (Some(id), Some(endpoint)) = (&sandbox_id, &openshell_endpoint) { match openshell_core::grpc_client::fetch_provider_environment(endpoint, id).await { Ok(result) => { ocsf_emit!( @@ -663,14 +660,7 @@ pub async fn run_sandbox( )) .build() ); - ( - result.provider_env_revision, - result.environment, - result.credential_expires_at_ms, - result.dynamic_credentials, - result.static_credential_bindings, - result.non_secret_environment_keys, - ) + Some(result) } Err(e) => { ocsf_emit!( @@ -683,56 +673,24 @@ pub async fn run_sandbox( )) .build() ); - ( - 0, - std::collections::HashMap::new(), - std::collections::HashMap::new(), - std::collections::HashMap::new(), - std::collections::HashMap::new(), - Vec::new(), - ) + None } } } else { - ( - 0, - std::collections::HashMap::new(), - std::collections::HashMap::new(), - std::collections::HashMap::new(), - std::collections::HashMap::new(), - Vec::new(), - ) + None }; - let dynamic_credentials_fallback = dynamic_credentials.clone(); - match ProviderCredentialState::from_bound_environment( - provider_env_revision, - provider_env, - provider_credential_expires_at_ms, - dynamic_credentials, - static_credential_bindings, - non_secret_environment_keys, - ) { - Ok(credentials) => credentials, - Err(error) => { - ocsf_emit!( - ConfigStateChangeBuilder::new(ocsf_ctx()) - .severity(SeverityId::High) - .status(StatusId::Failure) - .state(StateId::Disabled, "fail_closed") - .message(format!( - "Rejected provider environment bindings; static provider credentials were revoked; fetched dynamic token grants remain active: {error}" - )) - .build() - ); + environment.map_or_else( + || { ProviderCredentialState::from_environment( - provider_env_revision, - std::collections::HashMap::new(), - std::collections::HashMap::new(), - dynamic_credentials_fallback, + 0, + std::collections::HashMap::default(), + std::collections::HashMap::default(), + std::collections::HashMap::default(), ) - } - } + }, + |result| initial_provider_credentials(result, &provider_readiness), + ) }; if credential_gating_unavailable( @@ -1043,6 +1001,7 @@ pub async fn run_sandbox( interval_secs: poll_interval_secs, ocsf_enabled: poll_ocsf_enabled, provider_credentials: poll_provider_credentials, + provider_readiness: provider_readiness.clone(), policy_local_ctx: poll_policy_local, agent_proposals: agent_proposals.clone(), middleware_registry_status, @@ -1057,7 +1016,7 @@ pub async fn run_sandbox( endpoint_observation_tx, endpoint_status_rx, endpoint_policy: poll_endpoint_policy, - supervisor_session_id, + supervisor_session_id: supervisor_session_id.clone(), }; tokio::spawn(async move { @@ -1111,6 +1070,19 @@ pub async fn run_sandbox( ) .await?; info!(backend = %backend_name, "Control-mode access plane started"); + let _provider_reporter = + sandbox_id + .as_ref() + .zip(openshell_endpoint.as_ref()) + .map(|(id, endpoint)| { + provider_readiness.start_reporter( + endpoint.clone(), + id.clone(), + provider_credentials.clone(), + supervisor_session_id.clone(), + running.exec(), + ) + }); let mut control_readiness = if let Some(path) = health_socket_path { Some(ControlReadiness::start( path, @@ -2579,7 +2551,7 @@ async fn reload_gateway_policy_runtime( entrypoint_pid: u32, middleware: MiddlewareReloadContext<'_>, transparent_tcp: TransparentTcpReloadState, -) -> std::result::Result<(), GatewayRuntimeReloadError> { +) -> std::result::Result { if let Some(policy) = policy && policy_contains_explicit_tcp(policy) { @@ -2815,7 +2787,7 @@ fn initial_policy_ack_candidate( } if loaded.version != canonical.version || loaded.policy_hash != canonical.policy_hash - || canonical.config_revision < loaded.config_revision + || canonical.config_revision != loaded.config_revision { return None; } @@ -2922,6 +2894,64 @@ fn report_credential_gating_unavailable() { ); } +/// Install the first gateway snapshot before passing placeholders to the workload. +/// Invalid static bindings leave only independently authorized dynamic grants active. +fn initial_provider_credentials( + result: openshell_core::grpc_client::ProviderEnvironmentResult, + readiness: &ProviderReadinessTracker, +) -> ProviderCredentialState { + let identity = EnvironmentIdentity::from_environment(&result); + let expires_at_ms = result + .credential_expires_at_ms + .values() + .copied() + .filter(|expiry| *expiry > 0) + .min(); + if result.readiness_reason != ProviderReadinessReason::Unspecified { + readiness.credentials_failed(identity, result.readiness_reason); + return ProviderCredentialState::from_environment( + result.provider_env_revision, + std::collections::HashMap::default(), + std::collections::HashMap::default(), + result.dynamic_credentials, + ); + } + let dynamic_credentials_fallback = result.dynamic_credentials.clone(); + match ProviderCredentialState::from_bound_environment( + result.provider_env_revision, + result.environment, + result.credential_expires_at_ms, + result.dynamic_credentials, + result.static_credential_bindings, + result.non_secret_environment_keys, + ) { + Ok(credentials) => { + readiness.credentials_installed(identity, &credentials, expires_at_ms); + credentials + } + Err(error) => { + readiness + .credentials_failed(identity, ProviderReadinessReason::CredentialInstallFailed); + ocsf_emit!( + ConfigStateChangeBuilder::new(ocsf_ctx()) + .severity(SeverityId::High) + .status(StatusId::Failure) + .state(StateId::Disabled, "fail_closed") + .message(format!( + "Rejected provider environment bindings; static provider credentials were revoked; fetched dynamic token grants remain active: {error}" + )) + .build() + ); + ProviderCredentialState::from_environment( + result.provider_env_revision, + std::collections::HashMap::new(), + std::collections::HashMap::new(), + dynamic_credentials_fallback, + ) + } + } +} + /// Deliver policy status updates independently from policy reconciliation. /// /// The channel is FIFO, so a delayed older status can never arrive after a @@ -2950,6 +2980,15 @@ trait PolicyGatewayClient: Clone + Send + Sync + 'static { Ok(()) } + /// Fetch the complete provider snapshot through the ordinary static-binding API. + async fn fetch_provider_environment( + &self, + endpoint: &str, + sandbox_id: &str, + ) -> Result { + openshell_core::grpc_client::fetch_provider_environment(endpoint, sandbox_id).await + } + async fn refresh_installed_extension_credentials(&self) -> Result<()> { Ok(()) } @@ -3160,6 +3199,7 @@ struct PolicyPollLoopContext { interval_secs: u64, ocsf_enabled: Arc, provider_credentials: ProviderCredentialState, + provider_readiness: ProviderReadinessTracker, policy_local_ctx: Option>, agent_proposals: AgentProposals, middleware_registry_status: MiddlewareRegistryStatus, @@ -3590,6 +3630,7 @@ async fn run_policy_poll_loop_with_client( let mut current_provider_env_revision: u64 = ctx.provider_credentials.snapshot().revision; let mut current_policy_version: u32 = 0; let mut current_policy_hash = String::new(); + let mut current_policy_generation = None; let mut current_endpoint_policy = ctx.endpoint_policy.clone(); let mut current_middleware_services = Vec::new(); let mut current_extension_authentication_enabled = ctx.extension_authentication_enabled; @@ -3607,6 +3648,12 @@ async fn run_policy_poll_loop_with_client( // pass through the normal reconciliation path immediately. It must never // seed the applied-state trackers before OPA actually loads it. let mut pending_result = None; + // Bind startup evidence before awaiting the gateway. A generation changed + // during that request no longer proves which policy startup installed. + let initial_generation = ctx + .opa_engine + .generation_guard(ctx.opa_engine.current_generation()) + .ok(); // Initialize revision from the first poll and acknowledge the initial // policy revision the supervisor actually loaded. A mismatched result is @@ -3614,8 +3661,20 @@ async fn run_policy_poll_loop_with_client( match client.poll_settings(&ctx.sandbox_id).await { Ok(result) => { let _ = ctx.workspace_tx.send(client.workspace()); - match initial_poll_disposition(&ctx.loaded_policy_origin, &result) { - InitialPollDisposition::Acknowledge(candidate) => { + match ( + initial_poll_disposition(&ctx.loaded_policy_origin, &result), + initial_generation.as_ref(), + ) { + (InitialPollDisposition::Acknowledge(candidate), Some(generation)) + if middleware_registry_status == MiddlewareRegistryStatus::Synchronized + && !generation.is_stale() => + { + ctx.provider_readiness.policy_activated( + &EnvironmentIdentity::from_settings(&result), + result.config_revision, + generation.clone(), + ); + current_policy_generation = Some(generation.clone()); apply_ocsf_json_setting(&ctx.ocsf_enabled, &result.settings); apply_agent_proposals_enabled( &ctx.agent_proposals, @@ -3648,8 +3707,12 @@ async fn run_policy_poll_loop_with_client( "Settings poll: initial policy matches loaded revision" ); } - InitialPollDisposition::Reconcile => pending_result = Some(result), - InitialPollDisposition::TrackOnly => { + (InitialPollDisposition::Acknowledge(_) | InitialPollDisposition::Reconcile, _) => { + // Matching policy bytes cannot prove an unavailable registry + // or a replaced generation. Install this snapshot immediately. + pending_result = Some(result); + } + (InitialPollDisposition::TrackOnly, _) => { apply_ocsf_json_setting(&ctx.ocsf_enabled, &result.settings); apply_agent_proposals_enabled( &ctx.agent_proposals, @@ -3723,7 +3786,9 @@ async fn run_policy_poll_loop_with_client( }; let config_changed = result.config_revision != current_config_revision; - let provider_env_changed = result.provider_env_revision != current_provider_env_revision; + let desired_identity = EnvironmentIdentity::from_settings(&result); + let provider_env_changed = result.provider_env_revision != current_provider_env_revision + || ctx.provider_readiness.needs_environment(&desired_identity); let policy_changed = result.policy_hash != current_policy_hash; let extension_authentication_changed = current_extension_authentication_enabled != result.extension_authentication_enabled; @@ -3742,6 +3807,10 @@ async fn run_policy_poll_loop_with_client( .as_ref() .is_some_and(|rejected| rejected.policy_hash != result.policy_hash); let policy_runtime_changed = recovering_rejected_policy + || (reloads_gateway_policy + && current_policy_generation + .as_ref() + .is_some_and(PolicyGenerationGuard::is_stale)) || extension_authentication_changed || gateway_policy_runtime_needs_reconciliation( reloads_gateway_policy, @@ -3841,24 +3910,38 @@ async fn run_policy_poll_loop_with_client( } if provider_env_changed { - match openshell_core::grpc_client::fetch_provider_environment( - &ctx.endpoint, - &ctx.sandbox_id, - ) - .await + ctx.provider_readiness.credentials_failed( + desired_identity.clone(), + ProviderReadinessReason::WaitingForCredentials, + ); + match client + .fetch_provider_environment(&ctx.endpoint, &ctx.sandbox_id) + .await { Ok(env_result) => { - let provider_env_revision = env_result.provider_env_revision; - let install_result = ctx.provider_credentials.install_bound_environment( - provider_env_revision, - env_result.environment, - env_result.credential_expires_at_ms, - env_result.dynamic_credentials, - env_result.static_credential_bindings, - env_result.non_secret_environment_keys, - ); - if let Err(error) = install_result { - ocsf_emit!( + let identity = EnvironmentIdentity::from_environment(&env_result); + let expires_at_ms = env_result + .credential_expires_at_ms + .values() + .copied() + .filter(|expiry| *expiry > 0) + .min(); + if env_result.readiness_reason == ProviderReadinessReason::Unspecified { + let provider_env_revision = env_result.provider_env_revision; + let install_result = ctx.provider_credentials.install_bound_environment( + provider_env_revision, + env_result.environment, + env_result.credential_expires_at_ms, + env_result.dynamic_credentials, + env_result.static_credential_bindings, + env_result.non_secret_environment_keys, + ); + if let Err(error) = install_result { + ctx.provider_readiness.credentials_failed( + identity, + ProviderReadinessReason::CredentialInstallFailed, + ); + ocsf_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::High) .status(StatusId::Failure) @@ -3868,11 +3951,16 @@ async fn run_policy_poll_loop_with_client( )) .build() ); - } else { - let env_count = - ctx.provider_credentials.child_env_with_gcp_resolved().len(); - current_provider_env_revision = provider_env_revision; - ocsf_emit!( + } else { + ctx.provider_readiness.credentials_installed( + identity, + &ctx.provider_credentials, + expires_at_ms, + ); + let env_count = + ctx.provider_credentials.child_env_with_gcp_resolved().len(); + current_provider_env_revision = provider_env_revision; + ocsf_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::Informational) .status(StatusId::Success) @@ -3886,9 +3974,19 @@ async fn run_policy_poll_loop_with_client( )) .build() ); + } + } else { + ctx.provider_credentials + .revoke_static_provider_environment(env_result.provider_env_revision); + ctx.provider_readiness + .credentials_failed(identity, env_result.readiness_reason); } } Err(e) => { + ctx.provider_readiness.credentials_failed( + desired_identity.clone(), + ProviderReadinessReason::CredentialInstallFailed, + ); ctx.provider_credentials .revoke_static_provider_environment(result.provider_env_revision); warn!( @@ -3930,8 +4028,14 @@ async fn run_policy_poll_loop_with_client( .await; match runtime_result { - Ok(()) => { + Ok(generation) => { policy_runtime_reconciled = true; + ctx.provider_readiness.policy_activated( + &desired_identity, + result.config_revision, + generation.clone(), + ); + current_policy_generation = Some(generation); let policy = result .policy .as_ref() @@ -4029,6 +4133,8 @@ async fn run_policy_poll_loop_with_client( last_failed_runtime_revision = None; } Err(failure) => { + ctx.provider_readiness + .policy_install_failed(desired_identity.clone(), result.config_revision); let failed_revision = FailedRuntimeRevision::new( result.config_revision, &result.policy_hash, @@ -4126,6 +4232,21 @@ async fn run_policy_poll_loop_with_client( current_policy_version = version; } + if reloads_gateway_policy + && !policy_runtime_changed + && let Some(generation) = current_policy_generation + .as_ref() + .filter(|generation| !generation.is_stale()) + { + // The same installed policy may serve a new attachment/configuration + // identity. Its generation is retained, never inferred from a cursor. + ctx.provider_readiness.policy_activated( + &desired_identity, + result.config_revision, + generation.clone(), + ); + } + if policy_runtime_reconciled || provider_env_changed { endpoint_status::reset( ctx.endpoint_observation_tx.as_ref(), @@ -4663,6 +4784,7 @@ network_policies: settings: std::collections::HashMap::new(), global_policy_version: 0, provider_env_revision: 0, + provider_attachment_epoch: String::new(), supervisor_middleware_services: Vec::new(), workspace: String::new(), policy_validation_failure_mode: PolicyValidationFailureMode::default(), @@ -4773,6 +4895,505 @@ network_policies: ) } + fn static_provider_environment( + revision: u64, + value: Option<&str>, + ) -> openshell_core::grpc_client::ProviderEnvironmentResult { + use openshell_core::proto::{StaticCredentialBinding, StaticCredentialEndpointBinding}; + use std::collections::HashMap; + + let mut result = openshell_core::grpc_client::ProviderEnvironmentResult { + environment: HashMap::new(), + provider_env_revision: revision, + provider_attachment_epoch: String::new(), + policy_hash: "hash-v1".to_string(), + readiness_reason: ProviderReadinessReason::Unspecified, + credential_expires_at_ms: HashMap::new(), + dynamic_credentials: HashMap::new(), + static_credential_bindings: HashMap::new(), + non_secret_environment_keys: Vec::new(), + }; + if let Some(value) = value { + result + .environment + .insert("EXTERNAL_TOKEN".into(), value.into()); + result.static_credential_bindings.insert( + "EXTERNAL_TOKEN".into(), + StaticCredentialBinding { + endpoints: vec![StaticCredentialEndpointBinding { + host: "tools.example.com".into(), + port: 443, + path: "/v1/**".into(), + }], + credential_identity: "provider-a:EXTERNAL_TOKEN".into(), + workload_credential_handle: String::new(), + }, + ); + } + result + } + + #[test] + fn initial_provider_credentials_preserve_revision_scoped_delivery() { + let readiness = ProviderReadinessTracker::new(); + let state = initial_provider_credentials( + static_provider_environment(1, Some("initial")), + &readiness, + ); + let (revision, child_env) = state.child_env_snapshot_with_gcp_resolved().unwrap(); + let reference = &child_env["EXTERNAL_TOKEN"]; + assert_eq!(revision, 1); + assert_eq!(reference, "openshell:resolve:env:v1_EXTERNAL_TOKEN"); + assert_eq!( + state + .resolver_for_endpoint("tools.example.com", 443, "/v1/chat") + .unwrap() + .resolve_placeholder(reference), + Some("initial"), + ); + assert!(readiness.observation(&state).credentials_installed); + assert!(!readiness.observation(&state).launch_environment_installed); + // The boundary receives the prepared snapshot without gaining resolver authority. + let boundary = + ProviderCredentialState::from_child_env_snapshot(revision, child_env.clone()); + assert_eq!(boundary.snapshot().child_env, child_env); + assert!(boundary.resolver().is_none()); + + let mut invalid = static_provider_environment(2, Some("invalid")); + invalid.static_credential_bindings.clear(); + let rejected = initial_provider_credentials(invalid, &readiness); + assert!(rejected.snapshot().child_env.is_empty()); + assert!(rejected.resolver().is_none()); + assert_eq!( + readiness.observation(&rejected).reason, + i32::from(ProviderReadinessReason::CredentialInstallFailed) + ); + } + + type ProviderFetchRequest = tokio::sync::oneshot::Sender< + Result, + >; + + #[derive(Clone)] + struct ScriptedProviderGateway { + policy: ScriptedPolicyGateway, + requests: UnboundedSender, + } + + #[tonic::async_trait] + impl PolicyGatewayClient for ScriptedProviderGateway { + async fn poll_settings( + &self, + sandbox_id: &str, + ) -> Result { + self.policy.poll_settings(sandbox_id).await + } + + async fn report_policy_status( + &self, + sandbox_id: &str, + version: u32, + loaded: bool, + error: &str, + ) -> Result<()> { + self.policy + .report_policy_status(sandbox_id, version, loaded, error) + .await + } + + async fn fetch_provider_environment( + &self, + _endpoint: &str, + sandbox_id: &str, + ) -> Result { + assert_eq!(sandbox_id, "sandbox-test"); + let (response, received) = tokio::sync::oneshot::channel(); + self.requests + .send(response) + .map_err(|_| miette::miette!("provider request channel closed"))?; + received + .await + .map_err(|_| miette::miette!("provider response channel closed"))? + } + + fn workspace(&self) -> String { + self.policy.workspace() + } + } + + #[tokio::test] + async fn provider_readiness_initial_poll_waits_for_middleware_reconciliation() { + let policy = proto_policy_fixture(); + let mut settings = settings_poll_result( + Some(policy.clone()), + 1, + openshell_core::proto::PolicySource::Sandbox, + ); + settings.provider_attachment_epoch = "epoch".to_string(); + settings.provider_env_revision = 9; + settings.supervisor_middleware_services = + vec![openshell_core::proto::SupervisorMiddlewareService { + name: "scripted-guard".to_string(), + grpc_endpoint: "http://scripted.invalid".to_string(), + ..Default::default() + }]; + let (attempt_tx, mut attempts) = tokio::sync::mpsc::unbounded_channel(); + let (complete, completions) = tokio::sync::mpsc::unbounded_channel::(); + let completions = Arc::new(tokio::sync::Mutex::new(completions)); + let connector: MiddlewareConnector = Arc::new(move |services, _authentication| { + assert_eq!(services.len(), 1); + assert_eq!(services[0].name, "scripted-guard"); + attempt_tx.send(()).unwrap(); + let completions = completions.clone(); + Box::pin(async move { + if completions.lock().await.recv().await.unwrap() { + connect_middleware_registry(&[], &MiddlewareAuthentication::default()).await + } else { + Err(miette::miette!("scripted middleware connection failure")) + } + }) + }); + let engine = Arc::new(OpaEngine::from_proto(&policy).unwrap()); + let mut context = policy_poll_test_context( + engine.clone(), + LoadedPolicyOrigin::Gateway { + revision: Some(LoadedPolicyRevision::from_snapshot(&settings)), + has_last_valid_policy: true, + }, + connector, + ); + context.middleware_registry_status = MiddlewareRegistryStatus::NeedsReconciliation; + context.provider_credentials = ProviderCredentialState::from_child_env_snapshot( + 9, + std::collections::HashMap::default(), + ); + let credentials = context.provider_credentials.clone(); + let tracker = context.provider_readiness.clone(); + tracker.credentials_installed( + EnvironmentIdentity::from_settings(&settings), + &credentials, + None, + ); + let (gateway, polls, mut reports) = scripted_policy_gateway(); + let task = tokio::spawn(run_policy_poll_loop_with_client(context, gateway)); + polls.send(settings.clone()).unwrap(); + timeout(Duration::from_secs(1), attempts.recv()) + .await + .expect("initial snapshot must reconcile without a second poll") + .unwrap(); + let observed = tracker.observation(&credentials); + assert!(observed.credentials_installed); + assert!(!observed.policy_active); + assert!( + !observed.launch_environment_installed, + "process evidence requires its own boundary acknowledgment" + ); + expect_no_policy_report(&mut reports).await; + + complete.send(false).unwrap(); + timeout(Duration::from_secs(1), async { + while tracker.observation(&credentials).reason + != i32::from(ProviderReadinessReason::PolicyActivationFailed) + { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + assert!(!tracker.observation(&credentials).policy_active); + assert_eq!(engine.current_generation(), 0); + expect_no_policy_report(&mut reports).await; + + polls.send(settings).unwrap(); + timeout(Duration::from_secs(1), attempts.recv()) + .await + .unwrap() + .unwrap(); + assert!(!tracker.observation(&credentials).policy_active); + complete.send(true).unwrap(); + expect_policy_report(&mut reports, 1).await; + assert!(tracker.observation(&credentials).policy_active); + assert_eq!(engine.current_generation(), 1); + task.abort(); + let _ = task.await; + } + + #[derive(Clone)] + struct GenerationChangingPolicyGateway { + inner: ScriptedPolicyGateway, + engine: Arc, + first_poll: Arc, + } + + #[tonic::async_trait] + impl PolicyGatewayClient for GenerationChangingPolicyGateway { + async fn poll_settings( + &self, + sandbox_id: &str, + ) -> Result { + let result = self.inner.poll_settings(sandbox_id).await?; + if self.first_poll.swap(false, Ordering::SeqCst) { + self.engine + .enter_fail_closed("generation replaced while first poll was pending")?; + } + Ok(result) + } + + async fn report_policy_status( + &self, + sandbox_id: &str, + version: u32, + loaded: bool, + error: &str, + ) -> Result<()> { + self.inner + .report_policy_status(sandbox_id, version, loaded, error) + .await + } + + fn workspace(&self) -> String { + self.inner.workspace() + } + } + + #[tokio::test] + async fn provider_readiness_initial_poll_reconciles_a_replaced_policy_generation() { + let policy = proto_policy_fixture(); + let settings = settings_poll_result( + Some(policy.clone()), + 1, + openshell_core::proto::PolicySource::Sandbox, + ); + let engine = Arc::new(OpaEngine::from_proto(&policy).unwrap()); + let context = policy_poll_test_context( + engine.clone(), + LoadedPolicyOrigin::Gateway { + revision: Some(LoadedPolicyRevision::from_snapshot(&settings)), + has_last_valid_policy: true, + }, + default_middleware_connector(), + ); + let credentials = context.provider_credentials.clone(); + let tracker = context.provider_readiness.clone(); + tracker.credentials_installed( + EnvironmentIdentity::from_settings(&settings), + &credentials, + None, + ); + let (inner, polls, mut reports) = scripted_policy_gateway(); + let gateway = GenerationChangingPolicyGateway { + inner, + engine: engine.clone(), + first_poll: Arc::new(AtomicBool::new(true)), + }; + let task = tokio::spawn(run_policy_poll_loop_with_client(context, gateway)); + polls.send(settings).unwrap(); + expect_policy_report(&mut reports, 1).await; + assert!( + engine.fail_closed_reason().is_none(), + "the delivered policy must replace the intervening quarantine before acknowledgment" + ); + assert_eq!( + engine.current_generation(), + 2, + "generation 1 was not the policy startup installed" + ); + assert!(tracker.observation(&credentials).policy_active); + task.abort(); + let _ = task.await; + } + + #[tokio::test] + async fn provider_readiness_poll_waits_for_installation_and_retries_same_fingerprint() { + let engine = Arc::new(OpaEngine::from_proto(&proto_policy_fixture()).unwrap()); + let mut initial = settings_poll_result( + Some(proto_policy_fixture()), + 1, + openshell_core::proto::PolicySource::Sandbox, + ); + initial.provider_env_revision = 6; + let ctx = policy_poll_test_context( + engine.clone(), + LoadedPolicyOrigin::Gateway { + revision: Some(LoadedPolicyRevision::from_snapshot(&initial)), + has_last_valid_policy: true, + }, + default_middleware_connector(), + ); + let credentials = ctx.provider_credentials.clone(); + let tracker = ctx.provider_readiness.clone(); + let (policy, polls, mut reports) = scripted_policy_gateway(); + let (requests, mut received) = tokio::sync::mpsc::unbounded_channel(); + polls.send(initial.clone()).unwrap(); + let task = tokio::spawn(run_policy_poll_loop_with_client( + ctx, + ScriptedProviderGateway { policy, requests }, + )); + expect_policy_report(&mut reports, 1).await; + + polls.send(initial.clone()).unwrap(); + let response = timeout(Duration::from_secs(1), received.recv()) + .await + .unwrap() + .unwrap(); + assert!( + !tracker.observation(&credentials).credentials_installed, + "fetching desired credentials does not install them" + ); + assert!(response.send(Err(miette::miette!("unavailable"))).is_ok()); + timeout(Duration::from_secs(1), async { + while tracker.observation(&credentials).reason + != i32::from(ProviderReadinessReason::CredentialInstallFailed) + { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + let failed_id = credentials.snapshot().installation_id.clone(); + + polls.send(initial.clone()).unwrap(); + let response = timeout(Duration::from_secs(1), received.recv()) + .await + .unwrap() + .unwrap(); + assert!( + response + .send(Ok(static_provider_environment(6, Some("repaired")))) + .is_ok() + ); + timeout(Duration::from_secs(1), async { + while !tracker.observation(&credentials).credentials_installed { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + let observed = tracker.observation(&credentials); + assert_ne!(failed_id, credentials.snapshot().installation_id); + assert!(observed.policy_active); + assert!( + !observed.launch_environment_installed, + "a separate boundary acknowledgment is still required" + ); + + // A policy change can preserve the provider content fingerprint while + // changing the credential authority. Fetch and install that identity too. + let mut changed = initial; + changed.version = 2; + changed.config_revision = 200; + changed.policy_hash = "hash-v2".to_string(); + polls.send(changed).unwrap(); + let response = timeout(Duration::from_secs(1), received.recv()) + .await + .unwrap() + .unwrap(); + assert!(!tracker.observation(&credentials).policy_active); + let mut environment = static_provider_environment(6, Some("repaired")); + environment.policy_hash = "hash-v2".to_string(); + assert!(response.send(Ok(environment)).is_ok()); + expect_policy_report(&mut reports, 2).await; + let observed = tracker.observation(&credentials); + assert!(observed.credentials_installed && observed.policy_active); + assert_eq!(observed.provider_env_revision, 6); + assert_eq!(observed.config_revision, 200); + assert_eq!(observed.policy_hash, "hash-v2"); + task.abort(); + let _ = task.await; + } + + #[tokio::test] + async fn provider_poll_preserves_static_references_across_rotation_failure_and_detach() { + let engine = Arc::new(OpaEngine::from_proto(&proto_policy_fixture()).unwrap()); + let initial = settings_poll_result( + Some(proto_policy_fixture()), + 1, + openshell_core::proto::PolicySource::Sandbox, + ); + let ctx = policy_poll_test_context( + engine, + LoadedPolicyOrigin::Gateway { + revision: Some(LoadedPolicyRevision::from_snapshot(&initial)), + has_last_valid_policy: true, + }, + default_middleware_connector(), + ); + let state = ctx.provider_credentials.clone(); + let tracker = ctx.provider_readiness.clone(); + let (policy, polls, mut reports) = scripted_policy_gateway(); + let (requests, mut received) = tokio::sync::mpsc::unbounded_channel(); + polls.send(initial.clone()).unwrap(); + let task = tokio::spawn(run_policy_poll_loop_with_client( + ctx, + ScriptedProviderGateway { policy, requests }, + )); + expect_policy_report(&mut reports, 1).await; + + // Rotation gives future processes a new reference; the retained old + // reference continues to resolve its original value until revocation. + let old_reference = "openshell:resolve:env:v1_EXTERNAL_TOKEN"; + for (revision, value, fail) in [ + (1, Some("initial"), false), + (2, Some("rotated"), false), + (3, None, true), + (3, Some("recovered"), false), + (4, None, false), + (5, None, false), + ] { + let mut poll = initial.clone(); + poll.provider_env_revision = revision; + polls.send(poll).unwrap(); + let response = timeout(Duration::from_secs(5), received.recv()) + .await + .expect("provider refresh requested") + .expect("poll loop active"); + let result = if fail { + Err(miette::miette!("provider snapshot unavailable")) + } else { + Ok(static_provider_environment(revision, value)) + }; + assert!(response.send(result).is_ok()); + let reference = format!("openshell:resolve:env:v{revision}_EXTERNAL_TOKEN"); + timeout(Duration::from_secs(5), async { + loop { + let resolved = state + .resolver_for_endpoint("tools.example.com", 443, "/v1/chat") + .and_then(|resolver| { + resolver.resolve_placeholder(&reference).map(str::to_owned) + }); + let observed = tracker.observation(&state); + if state.revision() == revision + && resolved.as_deref() == value + && (fail || observed.credentials_installed) + { + break; + } + tokio::task::yield_now().await; + } + }) + .await + .expect("provider snapshot installed or revoked"); + if value.is_some() { + assert_eq!(state.snapshot().child_env["EXTERNAL_TOKEN"], reference); + if revision == 2 { + assert_ne!(reference, old_reference); + assert_eq!( + state + .resolver_for_endpoint("tools.example.com", 443, "/v1/chat") + .unwrap() + .resolve_placeholder(old_reference), + Some("initial") + ); + } + } else { + assert!(state.snapshot().child_env.is_empty()); + assert!(state.resolver().is_none()); + } + } + task.abort(); + let _ = task.await; + } + fn policy_poll_test_context( opa_engine: Arc, loaded_policy_origin: LoadedPolicyOrigin, @@ -4791,6 +5412,7 @@ network_policies: 0, std::collections::HashMap::new(), ), + provider_readiness: ProviderReadinessTracker::new(), policy_local_ctx: None, agent_proposals: AgentProposals::default(), middleware_registry_status: MiddlewareRegistryStatus::Synchronized, @@ -5555,6 +6177,23 @@ network_policies: )); } + #[test] + fn provider_readiness_initial_policy_requires_exact_config_identity() { + let mut canonical = settings_poll_result( + Some(proto_policy_fixture()), + 2, + openshell_core::proto::PolicySource::Sandbox, + ); + let loaded = LoadedPolicyRevision::from_snapshot(&canonical); + for revision in [1, u64::MAX] { + canonical.config_revision = revision; + assert!( + initial_policy_ack_candidate(Some(&loaded), &canonical).is_none(), + "matching policy bytes cannot acknowledge a different installed configuration" + ); + } + } + #[test] fn initial_ack_candidate_matches_sandbox_revision() { let canonical = settings_poll_result( diff --git a/crates/openshell-supervisor/src/provider_readiness.rs b/crates/openshell-supervisor/src/provider_readiness.rs new file mode 100644 index 0000000000..6f7479489a --- /dev/null +++ b/crates/openshell-supervisor/src/provider_readiness.rs @@ -0,0 +1,792 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Installation evidence reported under the accepted supervisor session. +//! +//! Desired-state cursors never establish success. Credentials, policy, and +//! the authenticated workload boundary must acknowledge the same authority. + +use std::sync::Arc; +use std::time::Duration; + +use openshell_core::proto::{ + ProviderReadinessObservation, ProviderReadinessReason as Reason, + ReportProviderReadinessResponse, +}; +use openshell_core::provider_credentials::ProviderCredentialState; +use openshell_isolation_interface::contract::{BoundaryExec, ProviderEnvironmentInstallation}; +use openshell_supervisor_network::opa::PolicyGenerationGuard; +use tokio::sync::watch; + +/// Authority captured before consuming a provider response's credential fields. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct EnvironmentIdentity { + pub(crate) attachment_epoch: String, + pub(crate) revision: u64, + pub(crate) policy_hash: String, +} + +impl EnvironmentIdentity { + /// Preserve the authority delivered with a credential snapshot before consuming it. + pub(crate) fn from_environment( + result: &openshell_core::grpc_client::ProviderEnvironmentResult, + ) -> Self { + Self { + attachment_epoch: result.provider_attachment_epoch.clone(), + revision: result.provider_env_revision, + policy_hash: result.policy_hash.clone(), + } + } + + /// Capture the requested authority without claiming that it is installed. + pub(crate) fn from_settings(result: &openshell_core::grpc_client::SettingsPollResult) -> Self { + Self { + attachment_epoch: result.provider_attachment_epoch.clone(), + revision: result.provider_env_revision, + policy_hash: result.policy_hash.clone(), + } + } +} + +#[derive(Clone, Debug)] +struct InstalledPolicy { + epoch: String, + hash: String, + config_revision: u64, + generation: PolicyGenerationGuard, +} + +#[derive(Clone, Debug)] +struct FailedPolicy { + identity: EnvironmentIdentity, + config_revision: u64, +} + +#[derive(Clone, Debug)] +struct State { + identity: EnvironmentIdentity, + installation_id: String, + credential_reason: Reason, + expires_at_ms: Option, + policy: Option, + policy_failure: Option, + process: Option, + process_reason: Reason, +} + +/// Shared installation tracker; each transition wakes the independent reporter. +#[derive(Clone, Debug)] +pub struct Tracker { + state: watch::Sender, +} + +impl Tracker { + /// Start without credential, policy, or process installation evidence. + pub(crate) fn new() -> Self { + let (state, _) = watch::channel(State { + identity: EnvironmentIdentity::default(), + installation_id: String::new(), + credential_reason: Reason::WaitingForCredentials, + expires_at_ms: None, + policy: None, + policy_failure: None, + process: None, + process_reason: Reason::WaitingForProcess, + }); + Self { state } + } + + /// Invalidate old success before a replacement or fail-closed clear begins. + pub(crate) fn credentials_failed(&self, identity: EnvironmentIdentity, reason: Reason) { + self.state.send_modify(|state| { + state.identity = identity; + state.installation_id.clear(); + state.credential_reason = reason; + state.expires_at_ms = None; + state.process = None; + state.process_reason = Reason::WaitingForProcess; + }); + } + + /// Record a completed credential installation and require a fresh process acknowledgment. + pub(crate) fn credentials_installed( + &self, + identity: EnvironmentIdentity, + credentials: &ProviderCredentialState, + expires_at_ms: Option, + ) { + let snapshot = credentials.snapshot(); + self.state.send_modify(|state| { + state.credential_reason = + if !identity.policy_hash.is_empty() && snapshot.revision == identity.revision { + Reason::Unspecified + } else { + Reason::SnapshotMismatch + }; + state.identity = identity; + state.installation_id.clone_from(&snapshot.installation_id); + state.expires_at_ms = expires_at_ms; + state.process = None; + state.process_reason = Reason::WaitingForProcess; + }); + } + + /// Retry failed installations even when the requested fingerprint is unchanged. + pub(crate) fn needs_environment(&self, identity: &EnvironmentIdentity) -> bool { + let state = self.state.borrow(); + state.credential_reason != Reason::Unspecified || state.identity != *identity + } + + /// Bind policy evidence to the exact generation returned by runtime installation. + pub(crate) fn policy_activated( + &self, + identity: &EnvironmentIdentity, + config_revision: u64, + generation: PolicyGenerationGuard, + ) { + self.state.send_modify(|state| { + state.policy = Some(InstalledPolicy { + epoch: identity.attachment_epoch.clone(), + hash: identity.policy_hash.clone(), + config_revision, + generation, + }); + state.policy_failure = None; + }); + } + + /// Report the rejected desired configuration without acknowledging its policy. + pub(crate) fn policy_install_failed( + &self, + identity: EnvironmentIdentity, + config_revision: u64, + ) { + self.state.send_modify(|state| { + // Failed desired identity must not masquerade as evidence for the + // last installed policy, even when the failure retains that policy. + state.policy_failure = Some(FailedPolicy { + identity, + config_revision, + }); + }); + } + + fn process_installed( + &self, + installed: ProviderEnvironmentInstallation, + credentials: &ProviderCredentialState, + ) { + let current = credentials.snapshot(); + self.state.send_if_modified(|state| { + if installed.installation_id != current.installation_id + || installed.installation_id != state.installation_id + || installed.revision != state.identity.revision + { + return false; + } + let changed = state.process.as_ref() != Some(&installed) + || state.process_reason != Reason::Unspecified; + state.process = Some(installed); + state.process_reason = Reason::Unspecified; + changed + }); + } + + fn process_failed(&self) { + self.state.send_if_modified(|state| { + let changed = + state.process.is_some() || state.process_reason != Reason::ProcessInstallFailed; + state.process = None; + state.process_reason = Reason::ProcessInstallFailed; + changed + }); + } + + /// Read current evidence, rechecking credential expiry and policy generation. + pub(crate) fn observation( + &self, + credentials: &ProviderCredentialState, + ) -> ProviderReadinessObservation { + let state = self.state.borrow(); + let current = credentials.snapshot(); + let identity = state + .policy_failure + .as_ref() + .map_or(&state.identity, |failure| &failure.identity); + let expired = state + .expires_at_ms + .is_some_and(|expiry| expiry <= openshell_core::time::now_ms()); + let credentials_installed = state.credential_reason == Reason::Unspecified + && !expired + && state.identity == *identity + && current.installation_id == state.installation_id; + let policy_active = state.policy_failure.is_none() + && state.policy.as_ref().is_some_and(|policy| { + !policy.hash.is_empty() + && policy.hash == identity.policy_hash + && policy.epoch == identity.attachment_epoch + && !policy.generation.is_stale() + }); + let launch_environment_installed = credentials_installed + && state.process.as_ref().is_some_and(|process| { + process.installation_id == state.installation_id + && process.revision == identity.revision + }); + let reason = if state.policy_failure.is_some() { + Reason::PolicyActivationFailed + } else if expired { + Reason::CredentialExpired + } else if state.credential_reason != Reason::Unspecified { + state.credential_reason + } else if !credentials_installed { + Reason::WaitingForCredentials + } else if !policy_active { + Reason::WaitingForPolicy + } else if !launch_environment_installed { + if state.process_reason == Reason::Unspecified { + Reason::WaitingForProcess + } else { + state.process_reason + } + } else { + Reason::Unspecified + }; + ProviderReadinessObservation { + attachment_epoch: identity.attachment_epoch.clone(), + provider_env_revision: identity.revision, + config_revision: state.policy_failure.as_ref().map_or_else( + || { + state + .policy + .as_ref() + .map_or(0, |policy| policy.config_revision) + }, + |failure| failure.config_revision, + ), + policy_hash: identity.policy_hash.clone(), + credentials_installed, + policy_active, + launch_environment_installed, + process_instance_id: state + .process + .as_ref() + .map_or_else(String::new, |process| process.session_id.to_string()), + reason: reason.into(), + ..Default::default() + } + } + + /// Report while the owner holds this handle; reconnects use the accepted session. + pub(crate) fn start_reporter( + &self, + endpoint: String, + sandbox_id: String, + credentials: ProviderCredentialState, + sessions: watch::Receiver>, + boundary: Arc, + ) -> Reporter { + let tracker = self.clone(); + Reporter(tokio::spawn(async move { + tracker + .run_reporter( + credentials, + sessions, + boundary, + GatewayReporter { + endpoint, + sandbox_id, + }, + ) + .await; + })) + } + + async fn run_reporter( + &self, + credentials: ProviderCredentialState, + mut sessions: watch::Receiver>, + boundary: Arc, + client: C, + ) { + let tracker = self; + let mut updates = tracker.state.subscribe(); + let mut active_session = None; + let mut sequence = 0_u64; + let mut interval = tokio::time::interval(Duration::from_secs(5)); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + loop { + let guard = tracker + .state + .borrow() + .policy + .as_ref() + .map(|policy| policy.generation.clone()) + .filter(|guard| !guard.is_stale()); + tokio::select! { + changed = sessions.changed() => { if changed.is_err() { return; } } + changed = updates.changed() => { if changed.is_err() { return; } } + _ = interval.tick() => {} + () = async { + if let Some(guard) = guard { guard.wait_until_stale().await; } + else { std::future::pending::<()>().await; } + } => {} + } + let session = sessions.borrow_and_update().clone(); + if session != active_session { + sequence = 0; + active_session = session.clone(); + } + let Some(session) = session else { + continue; + }; + // Synchronization is also a boundary liveness check. A stopped + // boundary cannot keep renewing successful launch evidence. + let synchronization = tokio::time::timeout( + Duration::from_secs(10), + boundary.synchronize_provider_environment(), + ); + let installed = tokio::select! { + changed = sessions.changed() => { if changed.is_err() { return; } interval.reset_immediately(); continue; } + installed = synchronization => installed, + }; + match installed { + Ok(Ok(installed)) => tracker.process_installed(installed, &credentials), + _ => tracker.process_failed(), + } + let Some(next_sequence) = sequence.checked_add(1) else { + return; + }; + sequence = next_sequence; + let mut observation = tracker.observation(&credentials); + observation.session_id.clone_from(&session); + observation.sequence = sequence; + let report = tokio::time::timeout(Duration::from_secs(5), client.report(observation)); + let response = tokio::select! { + changed = sessions.changed() => { if changed.is_err() { return; } interval.reset_immediately(); continue; } + response = report => response, + }; + if let Ok(Ok(response)) = response + && (response.accepted_sequence != sequence || response.observation_ttl_seconds == 0) + { + // A gateway rejection does not invalidate the boundary's + // installation. Retry on the normal cadence or a real state + // change without creating a self-triggered failure/retry loop. + tracing::warn!("Provider readiness report was not acknowledged"); + } + } + } +} + +#[tonic::async_trait] +trait ReportClient: Send + Sync { + async fn report( + &self, + observation: ProviderReadinessObservation, + ) -> miette::Result; +} + +struct GatewayReporter { + endpoint: String, + sandbox_id: String, +} + +#[tonic::async_trait] +impl ReportClient for GatewayReporter { + async fn report( + &self, + observation: ProviderReadinessObservation, + ) -> miette::Result { + openshell_core::grpc_client::report_provider_readiness( + &self.endpoint, + &self.sandbox_id, + observation, + ) + .await + } +} + +/// Cancels reporting before supervisor teardown can renew stale evidence. +pub struct Reporter(tokio::task::JoinHandle<()>); + +impl Drop for Reporter { + fn drop(&mut self) { + self.0.abort(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + struct CapturingReporter(tokio::sync::mpsc::UnboundedSender); + + #[tonic::async_trait] + impl ReportClient for CapturingReporter { + async fn report( + &self, + observation: ProviderReadinessObservation, + ) -> miette::Result { + let sequence = observation.sequence; + self.0.send(observation).unwrap(); + Ok(ReportProviderReadinessResponse { + accepted_sequence: sequence, + report_interval_seconds: 5, + observation_ttl_seconds: 15, + }) + } + } + + struct DelayedReporter( + tokio::sync::mpsc::UnboundedSender<( + ProviderReadinessObservation, + tokio::sync::oneshot::Sender, + )>, + ); + + #[tonic::async_trait] + impl ReportClient for DelayedReporter { + async fn report( + &self, + observation: ProviderReadinessObservation, + ) -> miette::Result { + let (response, received) = tokio::sync::oneshot::channel(); + self.0.send((observation, response)).unwrap(); + received + .await + .map_err(|_| miette::miette!("report response channel closed")) + } + } + + struct DelayedBoundary( + tokio::sync::mpsc::UnboundedSender< + tokio::sync::oneshot::Sender, + >, + ); + + #[tonic::async_trait] + impl BoundaryExec for DelayedBoundary { + async fn exec( + &self, + _spec: openshell_isolation_interface::contract::ExecSpec, + ) -> Result< + openshell_isolation_interface::contract::ExecSession, + openshell_isolation_interface::contract::BackendError, + > { + unreachable!("reporting must never launch a workload process") + } + + async fn synchronize_provider_environment( + &self, + ) -> Result< + ProviderEnvironmentInstallation, + openshell_isolation_interface::contract::BackendError, + > { + let (sender, receiver) = tokio::sync::oneshot::channel(); + self.0.send(sender).unwrap(); + receiver.await.map_err(|_| { + openshell_isolation_interface::contract::BackendError::Unavailable( + "boundary disconnected".into(), + ) + }) + } + } + + async fn next_installation_report( + installations: &mut tokio::sync::mpsc::UnboundedReceiver< + tokio::sync::oneshot::Sender, + >, + reports: &mut tokio::sync::mpsc::UnboundedReceiver<( + ProviderReadinessObservation, + tokio::sync::oneshot::Sender, + )>, + installed: &ProviderEnvironmentInstallation, + wait: Duration, + ) -> ( + ProviderReadinessObservation, + tokio::sync::oneshot::Sender, + ) { + tokio::time::timeout(wait, async { + installations + .recv() + .await + .unwrap() + .send(installed.clone()) + .unwrap(); + reports.recv().await.unwrap() + }) + .await + .expect("reporter did not synchronize and report before the deadline") + } + + #[tokio::test] + async fn invalid_report_acknowledgments_retry_without_installation_feedback() { + let tracker = Tracker::new(); + let credentials = ProviderCredentialState::from_child_env_snapshot(6, HashMap::new()); + tracker.credentials_installed(identity(), &credentials, None); + let policy = openshell_policy::restrictive_default_policy(); + let engine = openshell_supervisor_network::opa::OpaEngine::from_proto(&policy).unwrap(); + tracker.policy_activated(&identity(), 12, engine.generation_guard(0).unwrap()); + let mut installed = installation(&credentials); + let (sessions, receiver) = watch::channel(Some("current-session".to_string())); + let (boundary_tx, mut boundary_rx) = tokio::sync::mpsc::unbounded_channel(); + let (report_tx, mut report_rx) = tokio::sync::mpsc::unbounded_channel(); + let reporting_tracker = tracker.clone(); + let reporting_credentials = credentials.clone(); + let task = tokio::spawn(async move { + reporting_tracker + .run_reporter( + reporting_credentials, + receiver, + Arc::new(DelayedBoundary(boundary_tx)), + DelayedReporter(report_tx), + ) + .await; + }); + // The first installation changes local evidence once. Its watch update + // may cause one additional report, but an invalid gateway reply must + // not keep toggling that evidence and scheduling more installations. + for sequence in 1..=2 { + let (report, response) = next_installation_report( + &mut boundary_rx, + &mut report_rx, + &installed, + Duration::from_secs(1), + ) + .await; + assert_eq!(report.sequence, sequence); + assert!(report.launch_environment_installed); + response + .send(ReportProviderReadinessResponse::default()) + .unwrap(); + } + assert!( + tokio::time::timeout(Duration::from_millis(100), boundary_rx.recv()) + .await + .is_err(), + "invalid report replies must not repeatedly reinstall an unchanged environment" + ); + + // A real credential installation still wakes the reporter immediately. + credentials.install_child_env_snapshot(6, HashMap::new()); + tracker.credentials_installed(identity(), &credentials, None); + installed.installation_id = credentials.snapshot().installation_id.clone(); + let (report, response) = next_installation_report( + &mut boundary_rx, + &mut report_rx, + &installed, + Duration::from_secs(1), + ) + .await; + assert_eq!(report.sequence, 3); + assert!(report.launch_environment_installed); + // A policy update received while the report is pending must survive + // processing its reply and cause a report of the new configuration. + tracker.policy_activated(&identity(), 13, engine.generation_guard(0).unwrap()); + response + .send(ReportProviderReadinessResponse::default()) + .unwrap(); + let (report, response) = next_installation_report( + &mut boundary_rx, + &mut report_rx, + &installed, + Duration::from_secs(1), + ) + .await; + assert_eq!(report.sequence, 4); + assert_eq!(report.config_revision, 13); + response + .send(ReportProviderReadinessResponse::default()) + .unwrap(); + assert!( + tokio::time::timeout(Duration::from_millis(100), boundary_rx.recv()) + .await + .is_err() + ); + + sessions.send_replace(Some("replacement-session".to_string())); + let (report, response) = next_installation_report( + &mut boundary_rx, + &mut report_rx, + &installed, + Duration::from_secs(1), + ) + .await; + assert_eq!(report.session_id, "replacement-session"); + assert_eq!(report.sequence, 1); + response + .send(ReportProviderReadinessResponse::default()) + .unwrap(); + assert!( + tokio::time::timeout(Duration::from_millis(100), boundary_rx.recv()) + .await + .is_err() + ); + + // Without further state changes, the periodic liveness check retries. + let (report, response) = next_installation_report( + &mut boundary_rx, + &mut report_rx, + &installed, + Duration::from_secs(6), + ) + .await; + assert_eq!(report.session_id, "replacement-session"); + assert_eq!(report.sequence, 2); + assert!(report.launch_environment_installed); + response + .send(ReportProviderReadinessResponse::default()) + .unwrap(); + task.abort(); + let _ = task.await; + } + + #[tokio::test] + async fn reconnect_cancels_delayed_boundary_ack_and_starts_a_new_report_sequence() { + let tracker = Tracker::new(); + let credentials = ProviderCredentialState::from_child_env_snapshot(6, HashMap::new()); + tracker.credentials_installed(identity(), &credentials, None); + let installed = installation(&credentials); + let (sessions, receiver) = watch::channel(Some("old-session".to_string())); + let (boundary_tx, mut boundary_rx) = tokio::sync::mpsc::unbounded_channel(); + let (report_tx, mut report_rx) = tokio::sync::mpsc::unbounded_channel(); + let task = tokio::spawn(async move { + tracker + .run_reporter( + credentials, + receiver, + Arc::new(DelayedBoundary(boundary_tx)), + CapturingReporter(report_tx), + ) + .await; + }); + let old_ack = tokio::time::timeout(Duration::from_secs(1), boundary_rx.recv()) + .await + .unwrap() + .unwrap(); + sessions.send_replace(Some("current-session".to_string())); + let current_ack = tokio::time::timeout(Duration::from_secs(1), boundary_rx.recv()) + .await + .unwrap() + .unwrap(); + assert!( + old_ack.send(installed.clone()).is_err(), + "replaced session must cancel its pending installation" + ); + current_ack.send(installed.clone()).unwrap(); + let report = tokio::time::timeout(Duration::from_secs(1), report_rx.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(report.session_id, "current-session"); + assert_eq!(report.sequence, 1); + assert!(report.launch_environment_installed); + // The installation transition wakes a second report, whose sequence + // must advance even though its desired authority is unchanged. + let next_ack = tokio::time::timeout(Duration::from_secs(1), boundary_rx.recv()) + .await + .unwrap() + .unwrap(); + next_ack.send(installed).unwrap(); + let report = tokio::time::timeout(Duration::from_secs(1), report_rx.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(report.sequence, 2); + assert!( + tokio::time::timeout(Duration::from_millis(30), boundary_rx.recv()) + .await + .is_err(), + "an unchanged installation acknowledgment must not trigger another report" + ); + sessions.send_replace(None); + assert!( + tokio::time::timeout(Duration::from_millis(30), report_rx.recv()) + .await + .is_err() + ); + task.abort(); + let _ = task.await; + } + + fn identity() -> EnvironmentIdentity { + EnvironmentIdentity { + attachment_epoch: "epoch".into(), + revision: 6, + policy_hash: "policy".into(), + } + } + + fn installation(credentials: &ProviderCredentialState) -> ProviderEnvironmentInstallation { + ProviderEnvironmentInstallation { + installation_id: credentials.snapshot().installation_id.clone(), + revision: 6, + session_id: uuid::Uuid::new_v4().to_string().parse().unwrap(), + } + } + + #[test] + fn same_revision_repair_rejects_the_previous_boundary_acknowledgment() { + let tracker = Tracker::new(); + let credentials = ProviderCredentialState::from_child_env_snapshot(6, HashMap::new()); + tracker.credentials_installed(identity(), &credentials, None); + let old = installation(&credentials); + credentials + .install_child_env_snapshot(6, HashMap::from([("TOKEN".into(), "restored".into())])); + tracker.credentials_installed(identity(), &credentials, None); + tracker.process_installed(old, &credentials); + assert!( + !tracker + .observation(&credentials) + .launch_environment_installed + ); + tracker.process_installed(installation(&credentials), &credentials); + assert!( + tracker + .observation(&credentials) + .launch_environment_installed + ); + } + + #[test] + fn policy_failure_and_generation_change_invalidate_installed_evidence() { + let tracker = Tracker::new(); + let credentials = ProviderCredentialState::from_child_env_snapshot(6, HashMap::new()); + let policy = openshell_policy::restrictive_default_policy(); + let engine = openshell_supervisor_network::opa::OpaEngine::from_proto(&policy).unwrap(); + tracker.credentials_installed(identity(), &credentials, None); + tracker.policy_activated(&identity(), 12, engine.generation_guard(0).unwrap()); + tracker.process_installed(installation(&credentials), &credentials); + assert!(tracker.observation(&credentials).policy_active); + engine.enter_fail_closed("test quarantine").unwrap(); + assert!(!tracker.observation(&credentials).policy_active); + let mut rejected = identity(); + rejected.policy_hash = "rejected".into(); + tracker.policy_install_failed(rejected, 13); + let observed = tracker.observation(&credentials); + assert_eq!(observed.config_revision, 13); + assert_eq!(observed.policy_hash, "rejected"); + assert!(!observed.credentials_installed); + assert!(!observed.launch_environment_installed); + } + + #[test] + fn expired_credentials_and_disconnected_boundary_never_complete() { + let tracker = Tracker::new(); + let credentials = ProviderCredentialState::from_child_env_snapshot(6, HashMap::new()); + tracker.credentials_installed(identity(), &credentials, Some(1)); + tracker.process_installed(installation(&credentials), &credentials); + assert_eq!( + tracker.observation(&credentials).reason, + i32::from(Reason::CredentialExpired) + ); + tracker.credentials_installed(identity(), &credentials, None); + tracker.process_installed(installation(&credentials), &credentials); + tracker.process_failed(); + assert!( + !tracker + .observation(&credentials) + .launch_environment_installed + ); + } +} diff --git a/docs/providers/profiles.mdx b/docs/providers/profiles.mdx index 79ccd78bd7..a7ac5a6a32 100644 --- a/docs/providers/profiles.mdx +++ b/docs/providers/profiles.mdx @@ -40,7 +40,7 @@ Provider profiles include these user-facing features: - Provider instances whose submitted credentials can be stored by a configured gateway credential driver. - Profile-backed credential discovery for explicit `openshell provider create --from-existing` and `openshell provider update --from-existing` flows. The built-in `google-vertex-ai` profile also supplements discovery with Vertex config env vars such as `VERTEX_AI_PROJECT_ID` and `VERTEX_AI_REGION`. - Just-in-time effective policy composition from sandbox policy plus attached provider profiles. -- Runtime sandbox provider lifecycle commands under `openshell sandbox provider list|attach|detach`. +- Runtime sandbox provider commands under `openshell sandbox provider list|attach|detach|status`, with an option to wait until the sandbox applies a change. - Credential refresh configuration with `openshell provider refresh status|configure|rotate|delete`. - Credential expiry metadata with `openshell provider update --credential-expires-at`; values accept Unix epoch milliseconds or ISO/RFC3339 timestamps. - Dynamic token grants that use the sandbox's SPIFFE JWT-SVID as an OAuth2 client assertion and inject short-lived tokens into supported headers for matching profile endpoints. @@ -962,29 +962,62 @@ Composition follows these rules: ## Attach and Detach Providers -Attach an existing provider to a running sandbox: +Attach an existing provider and wait until the sandbox can use it: ```shell -openshell sandbox provider attach provider-demo work-github +openshell sandbox provider attach provider-demo work-github --wait --timeout 30 ``` -Detach a provider: +Detach a provider and wait until the sandbox stops resolving its credentials: ```shell -openshell sandbox provider detach provider-demo work-github +openshell sandbox provider detach provider-demo work-github --wait --timeout 30 ``` Attach and detach are idempotent. Attach validates that the provider exists before mutating the sandbox, and provider deletion fails while the provider is attached to any sandbox. +### Inspect Provider Readiness + +Without `--wait`, attach, detach, and update confirm that the gateway saved the change. Add `--wait` when the next step depends on that change taking effect. The command succeeds after the sandbox applies the matching credentials and policy, and updates the environment used by new processes. + +Each result includes a change record called a `receipt` in the API. Its ID lets you check the same change later, including after a timeout: + +```shell +openshell sandbox provider status provider-demo work-github --output json +openshell sandbox provider status provider-demo work-github --receipt RECEIPT_ID --wait --timeout 30 +``` + +Replace `RECEIPT_ID` with the `receipt_id` returned by attach, detach, or update. The ID always refers to the original change. If a later change replaces it, the status is `superseded` and the wait ends without reporting success. + +Readiness states have these meanings: + +- `persisted`: the gateway saved the change; the command has not checked the sandbox yet. +- `pending`: the sandbox has not confirmed all parts of the change. +- `ready`: the sandbox applied the requested provider credentials, policy, and environment for new processes. +- `revoked`: the detached provider's credentials no longer resolve, and new processes do not receive its credential references. +- `withheld`: a credential or supervisor configuration prevents the sandbox from applying the change. +- `failed`: the sandbox could not install the credentials, policy, or process environment. +- `superseded`: a later change replaced the original request. + +The default wait is 30 seconds and the maximum is 3600 seconds. The timeout starts after the gateway saves the change and includes time spent requesting status. If time runs out, the command exits with an error and reports `wait_outcome: timed_out` with the last known state. The change remains saved and may finish applying later. Check its ID again to learn the result. + +An unavailable supervisor, an expired report, a failed installation, or a missing process-environment acknowledgment cannot produce `ready` or `revoked`. After a reconnect or restart, the current authenticated supervisor must report its installed state again. + +JSON and YAML output include change IDs, requested and installed revisions, timestamps, reason categories, and a result for each sandbox. Revision strings identify configurations; compare them for equality rather than numerical order. Status output excludes credential values, credential references, authorization headers, and raw installation errors. + +The API's `operation` field records the common operation's historical outcome; its `operation_id` equals the receipt ID. Use the provider `state` and the wait result for current readiness. A disconnected supervisor can make the live state pending even after the operation previously applied, and a newer change can supersede the live result. Historical completion does not override those checks. + ### Runtime Limitations -Provider attach and detach update the persisted sandbox provider list. Running sandboxes poll for provider environment revisions and effective policy changes. +Running sandboxes periodically check for provider and policy changes. 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. -Already-running processes keep the placeholder environment they started with. OpenShell does not mutate a live process environment after provider attach, detach, or credential update. The proxy resolves existing placeholders against current credentials and bindings, so rotation, expiry, endpoint changes, and detach take effect without restarting the process. If a long-running process needs a newly attached provider credential placeholder, restart that process or launch a new process after the sandbox has observed the provider update. +Already-running processes keep the placeholder environment they started with. For ordinary static credentials, an existing reference retains its selected revision after a value update; readiness does not retarget that reference to the replacement value. After an update wait succeeds, launch a new process to receive the updated credential reference. Gateway-managed refresh follows its own reference lifecycle. Expiry, endpoint authorization, and acknowledged detach continue to apply when the proxy resolves retained references. + +For a static provider, the sequence is attach, wait, then launch client A; update, wait, then launch client B. B's first request uses the newly installed credential. Readiness does not establish that A has stopped using its retained revision or that an old upstream key can be retired. -Detaching a provider removes its provider policy layer from future effective policy reads, revokes resolution for its existing placeholders, and removes its credential placeholders from future process environments. It does not remove the placeholder strings from already-running process environments. +An acknowledged detachment removes its provider policy layer from the active effective policy, revokes future resolution for its existing placeholders, and removes its credential placeholders from future process environments. It does not remove strings from already-running process environments or undo requests already forwarded upstream. OpenShell rejects provider updates and refresh configuration when they would make two providers attached to the same sandbox expose the same active credential environment key. It also rejects attached provider sets with ambiguous dynamic token grants at equal host/path specificity. Use provider-specific credential names and make one dynamic grant selector more specific when one sandbox needs multiple providers with overlapping upstream concepts. diff --git a/docs/sandboxes/manage-providers.mdx b/docs/sandboxes/manage-providers.mdx index 93f5b004b6..1b20185ce9 100644 --- a/docs/sandboxes/manage-providers.mdx +++ b/docs/sandboxes/manage-providers.mdx @@ -178,6 +178,18 @@ Update a provider's credentials: openshell provider update my-claude --from-existing ``` +To wait until attached sandboxes apply the updated credentials, add `--wait`: + +```shell +openshell provider update my-claude --from-existing --wait --timeout 30 --output json +``` + +The update records which sandboxes are attached before saving the new credentials. Its result includes a change ID and outcome for each of those sandboxes. Sandboxes attached later are outside this wait. The timeout applies to the whole group; if any selected sandbox fails or times out, the command exits with an error and still reports each sandbox's outcome. When no sandboxes are attached, it reports the saved change and an empty target list. + +Credential refresh status tells you whether OpenShell obtained credentials. [Provider readiness](/providers/profiles#inspect-provider-readiness) tells you whether the sandbox applied the credentials, policy, and environment for new processes. It does not test access to a backend model or cancel requests already sent upstream. + +After a static credential update completes, launch a new client process to use the updated reference. An existing process keeps its revision-scoped reference; readiness does not make that old reference resolve the new value. Acknowledged detach revokes retained references and removes them from environments for future launches. + Set or clear a credential expiry timestamp: ```shell diff --git a/e2e/rust/Cargo.toml b/e2e/rust/Cargo.toml index d81a5865fe..51f30c37fc 100644 --- a/e2e/rust/Cargo.toml +++ b/e2e/rust/Cargo.toml @@ -108,6 +108,11 @@ name = "provider_refresh_handles" path = "tests/provider_refresh_handles.rs" required-features = ["e2e-podman"] +[[test]] +name = "provider_readiness" +path = "tests/provider_readiness.rs" +required-features = ["e2e-docker"] + [[test]] name = "vm_gateway_start" path = "tests/vm_gateway_start.rs" diff --git a/e2e/rust/tests/provider_readiness.rs b/e2e/rust/tests/provider_readiness.rs new file mode 100644 index 0000000000..1cfb5dde5a --- /dev/null +++ b/e2e/rust/tests/provider_readiness.rs @@ -0,0 +1,1655 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(feature = "e2e-docker")] + +//! Provider changes acknowledged through a real sandbox HTTPS request path. +//! +//! Attach, update, and detach wait for confirmation of their exact saved changes. +//! Clients launched after attach and update keep their own static references. +//! The first request from the updated launch uses the acknowledged credential; +//! detach revokes both retained references and removes them from future launches. +//! Only the synthetic backends and privileged provider CLI receive keys; +//! workload files, responses, and diagnostics contain status information only. + +use std::net::Ipv4Addr; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::time::Duration; + +use openshell_e2e::harness::binary::openshell_cmd; +use openshell_e2e::harness::container::{ContainerEngine, e2e_network_name}; +use openshell_e2e::harness::gateway::ManagedGateway; +use openshell_e2e::harness::sandbox::SandboxGuard; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use tempfile::TempDir; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines}; +use tokio::process::{Child, ChildStdin, ChildStdout, Command}; +use tokio::time::{Instant, sleep, timeout}; + +const TOKEN_ENV: &str = "PROVIDER_READINESS_E2E_TOKEN"; +const BACKEND_PORT: u16 = 8443; +const OTHER_BACKEND_PORT: u16 = 8444; +const READY: &str = "provider-readiness-client-ready"; +const CONTROL: &str = "/sandbox/provider-readiness-probe"; +const RESULT: &str = "/sandbox/provider-readiness-result"; +const COMMAND_TIMEOUT: Duration = Duration::from_secs(60); +const IMAGE_BUILD_TIMEOUT: Duration = Duration::from_secs(300); +const READINESS_TIMEOUT_SECONDS: &str = "90"; +const READINESS_COMMAND_TIMEOUT: Duration = Duration::from_secs(105); +const GATEWAY_READY_TIMEOUT: Duration = Duration::from_secs(120); + +// Keys arrive on a private stdin pipe after process creation. Neither the +// command line nor any file contains them, and HTTP/server errors are silent. +const BACKEND: &str = r" +import http.server, json, ssl, sys, threading + +config = json.loads(sys.stdin.readline()) +keys = config.pop('keys') +lock = threading.Lock() +state = {'phase': 0, 'total': 0, 'accepted': [0, 0], 'rejected': 0} + +class Server(http.server.ThreadingHTTPServer): + daemon_threads = True + def handle_error(self, request, client_address): + pass + +class Handler(http.server.BaseHTTPRequestHandler): + def log_message(self, *args): + pass + def do_POST(self): + with lock: + phase = state['phase'] + valid = self.headers.get('Authorization') == 'Bearer ' + keys[phase] + state['total'] += 1 + if valid: + state['accepted'][phase] += 1 + else: + state['rejected'] += 1 + body = json.dumps({'authorized': valid, 'phase': phase}).encode() + self.send_response(200 if valid else 401) + self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', str(len(body))) + self.send_header('Connection', 'close') + self.end_headers() + self.wfile.write(body) + +context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) +context.minimum_version = ssl.TLSVersion.TLSv1_2 +context.load_cert_chain(config['certificate'], config['private_key']) +servers = [] +for port in config['ports']: + server = Server(('0.0.0.0', port), Handler) + server.socket = context.wrap_socket(server.socket, server_side=True) + threading.Thread(target=server.serve_forever, daemon=True).start() + servers.append(server) +print(json.dumps({'ready': True}), flush=True) + +for line in sys.stdin: + request = json.loads(line) + with lock: + if request['command'] == 'rotate': + state['phase'] = 1 + print(json.dumps(state), flush=True) +for server in servers: + server.shutdown() + server.server_close() +"; + +// This source is safe to place in the workload image: it contains environment +// key names and endpoint coordinates, but never a key value or issued handle. +const CLIENT: &str = r" +import json, os, pathlib, re, ssl, sys, time, urllib.error, urllib.request + +config = json.loads(sys.argv[1]) +key = 'PROVIDER_READINESS_E2E_TOKEN' +token = os.environ.get(key, '') +if not re.fullmatch(r'openshell:resolve:env:v[1-9][0-9]*_' + key, token): + print('client did not receive a revision-scoped reference', flush=True) + sys.exit(64) +client = sys.argv[2] +if client not in ('a', 'b'): + sys.exit(64) +pid = os.getpid() +control = pathlib.Path('/sandbox/provider-readiness-probe-' + client) +result = pathlib.Path('/sandbox/provider-readiness-result-' + client) + +def probe(phase): + host, port, path = config['host'], config['port'], '/v1/chat/completions' + target = phase[:-8] if phase.endswith('_control') else phase + if target == 'wrong_host': + host = config['other_host'] + elif target == 'wrong_port': + port = config['other_port'] + elif target == 'wrong_path': + path = '/outside' + url = 'https://%s:%s%s' % (host, port, path) + response = {'phase': phase, 'pid': pid, 'same_reference': os.environ.get(key) == token, + 'ok': False, 'status': 0, 'backend_phase': -1} + try: + context = ssl.create_default_context() + if phase == 'untrusted_ca': + # An empty trust store proves this client does verify TLS. + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + headers = {'Content-Type': 'application/json'} + if not phase.endswith('_control'): + headers['Authorization'] = 'Bearer ' + token + request = urllib.request.Request(url, data=b'{}', headers=headers) + try: + reply = urllib.request.urlopen(request, context=context, timeout=5) + except urllib.error.HTTPError as error: + # The backend's safe 401 body proves the negative endpoint is + # reachable before a credential-bearing request is denied. + reply = error + with reply: + body = json.loads(reply.read(1024)) + response['ok'] = body.get('authorized') is True + response['status'] = reply.status + response['backend_phase'] = body.get('phase', -1) + except Exception as error: + # HTTP/TLS exceptions can embed headers; expose only fixed type names. + response['error_kind'] = type(error).__name__ + response['reason_kind'] = type(getattr(error, 'reason', None)).__name__ + return response + +print('provider-readiness-client-ready', flush=True) +deadline = time.monotonic() + 600 +while time.monotonic() < deadline: + if control.exists(): + phase = control.read_text().strip() + control.unlink() + temporary = result.with_suffix('.tmp') + temporary.write_text(json.dumps(probe(phase))) + temporary.replace(result) + time.sleep(0.05) +"; + +struct FixtureImage { + engine: ContainerEngine, + tag: String, +} + +impl FixtureImage { + fn new() -> Result { + Ok(Self { + engine: ContainerEngine::from_env()?, + tag: format!( + "localhost/openshell-e2e-readiness-{}-{:016x}:latest", + std::process::id(), + rand::random::(), + ), + }) + } + + fn tag(&self) -> &str { + &self.tag + } + + async fn build(&self, dockerfile: &Path, context: &Path) -> Result<(), String> { + let mut command = Command::from(self.engine.command()); + command + .args(["build", "--file"]) + .arg(dockerfile) + .args(["--tag", &self.tag]) + .arg(context); + checked_command_with_timeout(&mut command, "build fixture image", IMAGE_BUILD_TIMEOUT) + .await + .map(|_| ()) + } + + async fn remove(&self) -> Result<(), String> { + let mut command = Command::from(self.engine.command()); + command.args(["image", "rm", "--force", &self.tag]); + // Teardown is explicit and bounded. This type has no Drop subprocess + // that could block the test runtime after the removal deadline expires. + checked_command(&mut command, "remove fixture image") + .await + .map(|_| ()) + } +} + +// This fixture owns the only test in its binary. The wrapper's gateway is +// private to this run, so replacing its supervisor image cannot affect another +// test while the public fixture CA is installed in the supervisor trust store. +struct GatewayTrustConfig { + path: PathBuf, + original: String, + image_range: std::ops::Range, + supervisor_image: String, + health_port: u16, + restore_required: bool, +} + +impl GatewayTrustConfig { + fn load() -> Result { + if std::env::var_os("OPENSHELL_GATEWAY_ENDPOINT").is_some() + || std::env::var_os("OPENSHELL_E2E_GATEWAY_BIN").is_none() + || std::env::var("OPENSHELL_E2E_DRIVER").as_deref() != Ok("docker") + || std::env::var("OPENSHELL_E2E_EXTERNAL_COMPUTE_DRIVER") + .is_ok_and(|value| value != "0") + { + return Err("provider readiness fixture requires a wrapper-owned gateway with the bundled Docker driver".to_string()); + } + let args_file = std::env::var_os("OPENSHELL_E2E_GATEWAY_ARGS_FILE") + .ok_or("managed gateway argument metadata is missing")?; + let raw = + std::fs::read(args_file).map_err(|_| "could not read managed gateway arguments")?; + let args = raw + .split(|byte| *byte == 0) + .filter(|arg| !arg.is_empty()) + .map(std::str::from_utf8) + .collect::, _>>() + .map_err(|_| "managed gateway arguments were not UTF-8")?; + let argument = |name| { + let mut values = args.windows(2).filter(|pair| pair[0] == name); + let value = values.next().ok_or("managed gateway argument is missing")?[1]; + if values.next().is_some() { + return Err("managed gateway argument is duplicated"); + } + Ok(value) + }; + let path = PathBuf::from(argument("--config")?); + let health_port = argument("--health-port")? + .parse::() + .map_err(|_| "managed gateway health port is invalid")?; + let original = std::fs::read_to_string(&path) + .map_err(|_| "could not read managed gateway configuration")?; + let (image_range, supervisor_image) = docker_supervisor_image(&original)?; + Ok(Self { + path, + original, + image_range, + supervisor_image, + health_port, + restore_required: false, + }) + } + + async fn apply(&mut self, image: &str) -> Result<(), String> { + let mut updated = self.original.clone(); + updated.replace_range(self.image_range.clone(), image); + // Set the guard before the write: a failed write or restart must still + // flow through explicit restoration of the exact original bytes. + self.restore_required = true; + std::fs::write(&self.path, updated) + .map_err(|_| "could not install fixture supervisor configuration")?; + restart_fixture_gateway(self.health_port).await + } + + async fn restore(&mut self) -> Result<(), String> { + if !self.restore_required { + return Ok(()); + } + std::fs::write(&self.path, &self.original) + .map_err(|_| "could not restore original gateway configuration")?; + restart_fixture_gateway(self.health_port) + .await + .map_err(|_| "original gateway configuration was restored but restart failed")?; + self.restore_required = false; + Ok(()) + } +} + +impl Drop for GatewayTrustConfig { + fn drop(&mut self) { + if self.restore_required { + // Cancellation/panic fallback restores disk state only. Normal + // Result paths explicitly restart and verify health; Drop never + // launches a subprocess or hides a failed restart as success. + let _ = std::fs::write(&self.path, &self.original); + } + } +} + +fn docker_supervisor_image(config: &str) -> Result<(std::ops::Range, String), String> { + let mut in_docker = false; + let mut offset = 0; + let mut found = None; + for line in config.split_inclusive('\n') { + let trimmed = line.trim(); + if trimmed.starts_with('[') { + in_docker = trimmed == "[openshell.drivers.docker]"; + } else if in_docker && let Some((key, value)) = trimmed.split_once('=') { + if key.trim() == "socket_path" { + return Err( + "fixture cannot replace an external Docker driver configuration".to_string(), + ); + } + if key.trim() == "supervisor_image" { + // Accept only the wrapper's single-line quoted OCI reference. + // Reject escapes/comments instead of treating general TOML as + // text and accidentally changing a different configuration key. + let image = value + .trim() + .strip_prefix('"') + .and_then(|value| value.strip_suffix('"')) + .filter(|image| { + !image.is_empty() + && image.bytes().all(|byte| { + byte.is_ascii_alphanumeric() || b"/:@._-".contains(&byte) + }) + }) + .ok_or("managed supervisor image is not a simple quoted OCI reference")?; + let start = offset + + line + .find('"') + .ok_or("managed supervisor image is not quoted")? + + 1; + if found + .replace((start..start + image.len(), image.to_string())) + .is_some() + { + return Err("managed Docker supervisor image is duplicated".to_string()); + } + } + } + offset += line.len(); + } + found.ok_or_else(|| "managed Docker supervisor image is missing".to_string()) +} + +async fn restart_fixture_gateway(health_port: u16) -> Result<(), String> { + let gateway = ManagedGateway::from_env() + .map_err(|_| "could not load managed gateway restart metadata")? + .ok_or("managed gateway restart metadata disappeared")?; + // ManagedGateway bounds graceful shutdown before force-kill. Keep it local: + // its Drop can start a stopped gateway, but never owns configuration restore. + gateway + .stop() + .map_err(|_| "could not stop fixture gateway")?; + gateway + .start() + .map_err(|_| "could not restart fixture gateway")?; + let url = format!("http://127.0.0.1:{health_port}/healthz"); + let deadline = Instant::now() + GATEWAY_READY_TIMEOUT; + loop { + if checked_command( + Command::new("curl").args(["--silent", "--fail", "--max-time", "2", &url]), + "check fixture gateway health", + ) + .await + .is_ok() + { + return Ok(()); + } + if Instant::now() >= deadline { + return Err("fixture gateway did not become healthy".to_string()); + } + sleep(Duration::from_millis(250)).await; + } +} + +struct Backend { + engine: ContainerEngine, + name: String, + network: String, + namespace: String, + child: Option, + input: Option, + output: Option>>, + launch_attempted: bool, +} + +struct PersistentClient { + child: Child, + // Keep the stream open after reading the marker so the CLI can continue + // relaying the same remote process until explicit sandbox teardown. + _output: Lines>, +} + +impl PersistentClient { + async fn start( + sandbox: &SandboxGuard, + python: &str, + config: &str, + client: &str, + ) -> Result { + let mut command = sandbox_command( + sandbox, + &[ + python, + "-u", + "/opt/provider-readiness-client.py", + config, + client, + ], + ); + let mut child = command + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .kill_on_drop(true) + .spawn() + .map_err(|_| "persistent workload exec could not start")?; + let output = child + .stdout + .take() + .ok_or("workload stdout was unavailable")?; + let mut output = BufReader::new(output).lines(); + timeout(COMMAND_TIMEOUT, async { + while let Some(line) = output + .next_line() + .await + .map_err(|_| "workload marker could not be read")? + { + if line.trim() == READY { + return Ok(()); + } + // A CLI or workload error can contain environment material. + // Ignore every unrecognized line without including it in errors. + } + Err("persistent workload closed before its ready marker") + }) + .await + .map_err(|_| "persistent workload ready marker timed out")??; + Ok(Self { + child, + _output: output, + }) + } + + async fn stop(&mut self) -> Result<(), String> { + // Sandbox deletion ends the remote workload; reap the retained relay + // even if the remote deletion or normal stream shutdown fails. + timeout(COMMAND_TIMEOUT, self.child.kill()) + .await + .map_err(|_| "persistent client cleanup timed out")? + .map_err(|_| "persistent client cleanup failed".to_string()) + } +} + +impl Backend { + fn new(name: String) -> Result { + Ok(Self { + engine: ContainerEngine::from_env()?, + name, + network: e2e_network_name().ok_or("fixture requires the managed Docker network")?, + namespace: std::env::var("OPENSHELL_E2E_SANDBOX_NAMESPACE") + .map_err(|_| "fixture requires the managed Docker namespace")?, + child: None, + input: None, + output: None, + launch_attempted: false, + }) + } + + async fn spawn(&mut self, base: &str, tls_directory: &Path) -> Result { + let tls_directory = tls_directory + .to_str() + .filter(|path| !path.contains([',', '\n', '\r'])) + .ok_or("fixture TLS mount path is invalid")?; + let mount = format!("type=bind,src={tls_directory},dst=/fixture-tls,readonly"); + let namespace_label = format!("openshell.ai/sandbox-namespace={}", self.namespace); + let mut command = Command::from(self.engine.command()); + command + .args([ + "run", + "--rm", + "--interactive", + "--pull=never", + "--name", + &self.name, + "--network", + &self.network, + "--label", + "openshell.ai/managed-by=openshell", + "--label", + &namespace_label, + "--label", + "openshell.ai/isolation-role=fixture", + "--read-only", + "--cap-drop=ALL", + "--security-opt=no-new-privileges:true", + "--mount", + &mount, + "--entrypoint", + "/usr/bin/python3", + base, + "-u", + "-c", + BACKEND, + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .kill_on_drop(true); + // Record the unique container name before creation. Every Result exit + // removes it, including a failed readiness exchange after Docker starts. + // Wrapper-scoped labels also let wrapper teardown reap an interrupted run. + self.launch_attempted = true; + let child = command + .spawn() + .map_err(|_| "could not start synthetic HTTPS backend".to_string())?; + self.child = Some(child); + let child = self.child.as_mut().ok_or("backend child was absent")?; + self.input = Some( + child + .stdin + .take() + .ok_or("backend stdin was not available")?, + ); + let output = child + .stdout + .take() + .ok_or("backend stdout was not available")?; + self.output = Some(BufReader::new(output).lines()); + // Python waits for its private stdin configuration before reading TLS + // files. Discover the actual address first so its certificate can name + // that exact endpoint without DNS or an unverified TLS connection. + self.address().await + } + + async fn address(&mut self) -> Result { + let deadline = Instant::now() + COMMAND_TIMEOUT; + loop { + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Err("backend bridge address did not become available".to_string()); + } + let mut inspect = Command::from(self.engine.command()); + inspect.args([ + "inspect", + "--format", + "{{json .NetworkSettings.Networks}}", + &self.name, + ]); + if let Ok(output) = checked_command_with_timeout( + &mut inspect, + "inspect backend bridge address", + remaining.min(Duration::from_secs(2)), + ) + .await + { + let networks: Value = serde_json::from_str(&output) + .map_err(|_| "backend network metadata was invalid")?; + if let Some(address) = networks[&self.network]["IPAddress"] + .as_str() + .filter(|address| !address.is_empty()) + { + let address = address + .parse::() + .map_err(|_| "backend network address was not IPv4")?; + if !address.is_private() { + return Err("fixture backend requires a private bridge address".to_string()); + } + return Ok(address.to_string()); + } + } + if self + .child + .as_mut() + .ok_or("backend child was absent")? + .try_wait() + .map_err(|_| "could not inspect backend client status")? + .is_some() + { + return Err("backend exited before its bridge address was available".to_string()); + } + sleep(Duration::from_millis(100)).await; + } + } + + async fn initialize(&mut self, config: &Value) -> Result<(), String> { + if self.exchange(config).await?["ready"] != true { + return Err("synthetic HTTPS backend did not become ready".to_string()); + } + Ok(()) + } + + async fn exchange(&mut self, request: &Value) -> Result { + let mut bytes = + serde_json::to_vec(request).map_err(|_| "backend request encoding failed")?; + bytes.push(b'\n'); + let input = self.input.as_mut().ok_or("backend stdin was absent")?; + let output = self.output.as_mut().ok_or("backend stdout was absent")?; + timeout(COMMAND_TIMEOUT, async { + input + .write_all(&bytes) + .await + .map_err(|_| "backend control write failed")?; + let line = output + .next_line() + .await + .map_err(|_| "backend control read failed")? + .ok_or("backend closed its control stream")?; + serde_json::from_str(&line).map_err(|_| "backend status was not valid JSON") + }) + .await + .map_err(|_| "backend control operation timed out".to_string())? + .map_err(str::to_string) + } + + async fn stop(&mut self) -> Result<(), String> { + // Closing stdin lets Python shut down normally. Removing the named + // container also covers a stalled startup or disconnected Docker client; + // killing the attached client alone cannot prove the container stopped. + drop(self.input.take()); + let mut reap_result = Ok(()); + if let Some(child) = self.child.as_mut() { + reap_result = timeout(COMMAND_TIMEOUT, child.kill()) + .await + .map_err(|_| "backend client teardown timed out".to_string()) + .and_then(|result| { + result.map_err(|_| "backend client teardown failed".to_string()) + }); + } + if !self.launch_attempted { + return reap_result; + } + remove_fixture_container(&self.engine, &self.name).await?; + reap_result + } +} + +/// Counters deliberately contain no authorization headers or secret values. +#[derive(Deserialize, Serialize)] +struct BackendCounts { + phase: u8, + total: u64, + accepted: [u64; 2], + rejected: u64, +} + +// A second endpoint supplies a reachable wrong-host control. Each container +// retains its own counters; their sum proves denied traffic reaches neither. +struct BackendPair { + backends: [Backend; 2], +} + +impl BackendPair { + fn new(name: &str) -> Result { + Ok(Self { + backends: [ + Backend::new(format!("{name}-backend"))?, + Backend::new(format!("{name}-other-backend"))?, + ], + }) + } + + async fn spawn(&mut self, base: &str, tls_directory: &Path) -> Result<[String; 2], String> { + let host = self.backends[0].spawn(base, tls_directory).await?; + let other_host = self.backends[1].spawn(base, tls_directory).await?; + if host == other_host { + return Err("wrong-host control requires a distinct backend address".to_string()); + } + Ok([host, other_host]) + } + + async fn initialize(&mut self, config: &Value) -> Result<(), String> { + for backend in &mut self.backends { + backend.initialize(config).await?; + } + Ok(()) + } + + async fn rotate(&mut self) -> Result<(), String> { + for backend in &mut self.backends { + if backend.exchange(&json!({"command": "rotate"})).await?["phase"] != 1 { + return Err("backend did not switch to the replacement key".to_string()); + } + } + Ok(()) + } + + async fn counts(&mut self) -> Result { + let mut combined: Option = None; + for backend in &mut self.backends { + let response = backend.exchange(&json!({"command": "snapshot"})).await?; + let counters: BackendCounts = + serde_json::from_value(response).map_err(|_| "backend counters were invalid")?; + if let Some(total) = combined.as_mut() { + if total.phase != counters.phase { + return Err("fixture backends disagree on the active key".to_string()); + } + let add = |left: u64, right: u64| { + left.checked_add(right).ok_or("backend counters overflowed") + }; + total.total = add(total.total, counters.total)?; + total.rejected = add(total.rejected, counters.rejected)?; + for (left, right) in total.accepted.iter_mut().zip(counters.accepted) { + *left = add(*left, right)?; + } + } else { + combined = Some(counters); + } + } + serde_json::to_value(combined.ok_or("backend counters were absent")?) + .map_err(|_| "backend counters could not be encoded".to_string()) + } + + async fn stop(&mut self) -> Result<(), String> { + let mut failures = Vec::new(); + for backend in &mut self.backends { + if let Err(error) = backend.stop().await { + failures.push(error); + } + } + if failures.is_empty() { + Ok(()) + } else { + Err(failures.join("; ")) + } + } +} + +async fn checked_command(command: &mut Command, label: &str) -> Result { + checked_command_with_timeout(command, label, COMMAND_TIMEOUT).await +} + +async fn checked_command_with_timeout( + command: &mut Command, + label: &str, + max_wait: Duration, +) -> Result { + command + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .kill_on_drop(true); + let output = timeout(max_wait, command.output()) + .await + .map_err(|_| format!("{label} timed out"))? + .map_err(|_| format!("{label} could not start"))?; + if !output.status.success() { + // CLI arguments, captured output, and subprocess errors can contain + // credential material. Error messages expose only the safe operation. + return Err(format!("{label} failed; subprocess output withheld")); + } + String::from_utf8(output.stdout).map_err(|_| format!("{label} returned invalid UTF-8")) +} + +async fn cli(label: &str, args: &[&str], credential: Option<&str>) -> Result { + let mut command = openshell_cmd(); + command.args(args); + if let Some(credential) = credential { + command.env(TOKEN_ENV, credential); + } + checked_command(&mut command, label).await +} + +fn sandbox_command(sandbox: &SandboxGuard, argv: &[&str]) -> Command { + let mut command = openshell_cmd(); + command + .args(["sandbox", "exec", "--name", &sandbox.name, "--no-tty", "--"]) + .args(argv); + command +} + +async fn generate_certificates( + directory: &Path, + host: &str, + other_host: &str, +) -> Result<(PathBuf, PathBuf), String> { + let ca_key = directory.join("ca.key.fixture"); + let ca = directory.join("ca.crt"); + let key = directory.join("backend.key.fixture"); + let csr = directory.join("backend.csr"); + let certificate = directory.join("backend.crt"); + let extensions = directory.join("backend.ext"); + std::fs::write( + &extensions, + format!( + "basicConstraints=critical,CA:FALSE\nsubjectAltName=IP:{host},IP:{other_host}\nextendedKeyUsage=serverAuth\n" + ), + ) + .map_err(|_| "could not write public TLS certificate extensions")?; + checked_command( + Command::new("openssl") + .args([ + "req", + "-x509", + "-newkey", + "rsa:2048", + "-nodes", + "-days", + "1", + "-subj", + "/CN=provider-readiness-e2e-ca", + "-keyout", + ]) + .arg(&ca_key) + .arg("-out") + .arg(&ca), + "generate fixture CA", + ) + .await?; + checked_command( + Command::new("openssl") + .args([ + "req", + "-newkey", + "rsa:2048", + "-nodes", + "-subj", + &format!("/CN={host}"), + "-keyout", + ]) + .arg(&key) + .arg("-out") + .arg(&csr), + "generate fixture TLS key", + ) + .await?; + checked_command( + Command::new("openssl") + .args(["x509", "-req", "-days", "1", "-in"]) + .arg(&csr) + .arg("-CA") + .arg(&ca) + .arg("-CAkey") + .arg(&ca_key) + .arg("-CAcreateserial") + .arg("-extfile") + .arg(&extensions) + .arg("-out") + .arg(&certificate), + "sign fixture TLS certificate", + ) + .await?; + Ok((certificate, key)) +} + +async fn base_binaries(base: &str) -> Result { + let engine = ContainerEngine::from_env()?; + let name = format!( + "e2e-readiness-binaries-{}-{:016x}", + std::process::id(), + rand::random::() + ); + let namespace = std::env::var("OPENSHELL_E2E_SANDBOX_NAMESPACE") + .map_err(|_| "binary probe requires the managed Docker namespace")?; + let namespace_label = format!("openshell.ai/sandbox-namespace={namespace}"); + let mut command = Command::from(engine.command()); + command.args([ + "run", "--rm", "--name", &name, "--network", "none", + "--label", "openshell.ai/managed-by=openshell", + "--label", &namespace_label, + "--label", "openshell.ai/isolation-role=fixture", + "--entrypoint", "/usr/bin/python3", base, + "-c", "import json,os,shutil,sys; print(json.dumps({'python':os.path.realpath(sys.executable),'curl':shutil.which('curl')}))", + ]); + // A timeout terminates the attached CLI, which does not prove its container + // stopped. Always remove the known name; labels also let wrapper teardown + // recover it if the entire test is cancelled before explicit cleanup. + let result = async { + let output = checked_command(&mut command, "inspect fixture image binaries").await?; + let binaries: Value = + serde_json::from_str(&output).map_err(|_| "image binary probe was invalid")?; + for field in ["python", "curl"] { + if !binaries[field] + .as_str() + .is_some_and(|path| Path::new(path).is_absolute()) + { + return Err(format!("fixture image has no absolute {field} executable")); + } + } + Ok(binaries) + } + .await; + let cleanup = remove_fixture_container(&engine, &name).await; + match (result, cleanup) { + (Ok(binaries), Ok(())) => Ok(binaries), + (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error), + (Err(error), Err(cleanup)) => Err(format!("{error}; {cleanup}")), + } +} + +async fn remove_fixture_container(engine: &ContainerEngine, name: &str) -> Result<(), String> { + let mut remove = Command::from(engine.command()); + remove.args(["rm", "--force", name]); + let removed = checked_command(&mut remove, "remove fixture container").await; + // --rm may have already removed a normally exited fixture. Only a + // successful exact-name listing can establish absence after rm fails. + if removed.is_err() { + let mut list = Command::from(engine.command()); + let filter = format!("name=^/{name}$"); + list.args(["ps", "--all", "--quiet", "--filter", &filter]); + if !checked_command(&mut list, "verify fixture container removal") + .await? + .trim() + .is_empty() + { + return Err("fixture container remained after teardown".to_string()); + } + } + Ok(()) +} + +fn write_profile( + path: &Path, + name: &str, + host: &str, + port: u16, + python: &str, +) -> Result<(), String> { + let document = json!({ + "id": name, "display_name": "Provider readiness E2E", "category": "other", + "credentials": [{"name": "synthetic", "env_vars": [TOKEN_ENV], "required": true, + "auth_style": "bearer", "header_name": "authorization"}], + "endpoints": [{"host": host, "port": port, "path": "/v1/**", "protocol": "rest", + "access": "full", "enforcement": "enforce", + "allowed_ips": [host]}], + "binaries": [python], + }); + std::fs::write(path, document.to_string()) + .map_err(|_| "could not write synthetic profile".to_string()) +} + +fn write_policy( + path: &Path, + host: &str, + other_host: &str, + port: u16, + other_port: u16, + python: &str, +) -> Result<(), String> { + // Permit the negative endpoint probes at the network layer so the + // credential binding itself must prevent them from reaching the backend. + // The exact binary allowlist independently denies curl at the valid endpoint. + let endpoints = [(host, port), (host, other_port), (other_host, port)] + .into_iter() + .map(|(host, port)| { + json!({"host": host, "port": port, "path": "/**", "protocol": "rest", + "access": "full", "enforcement": "enforce", + "allowed_ips": [host]}) + }) + .collect::>(); + let document = json!({ + "version": 1, + "filesystem_policy": {"include_workdir": false, + "read_only": ["/usr", "/lib", "/proc", "/dev/urandom", "/etc", "/opt", "/var/log"], + "read_write": ["/sandbox", "/tmp", "/dev/null"]}, + "landlock": {"compatibility": "best_effort"}, + "process": {"run_as_user": "sandbox", "run_as_group": "sandbox"}, + "network_policies": {"synthetic_backend": {"name": "synthetic_backend", + "endpoints": endpoints, "binaries": [{"path": python}]}}, + }); + std::fs::write(path, document.to_string()) + .map_err(|_| "could not write synthetic policy".to_string()) +} + +struct MutationReceipt { + mutation_id: String, + receipt: Value, +} + +fn readiness_output(output: &str, keys: &[String; 2]) -> Result { + // Never echo a malformed response: both a key leak and a leaked issued + // reference must fail with the same fixed diagnostic. + if keys.iter().any(|key| output.contains(key)) + || output.contains("openshell:resolve:") + || output.contains("Bearer ") + { + return Err("provider readiness output contained credential material".to_string()); + } + serde_json::from_str(output) + .map_err(|_| "provider readiness output was invalid JSON".to_string()) +} + +fn single_target(body: &Value) -> Result<&Value, String> { + let targets = body["targets"] + .as_array() + .ok_or("provider readiness targets were absent")?; + // This provider is unique and has exactly one attachment. A mutation that + // selects a different sandbox or omits this sandbox cannot satisfy the test. + if targets.len() != 1 { + return Err("provider readiness did not identify exactly one sandbox".to_string()); + } + Ok(&targets[0]) +} + +fn nonempty_string<'a>(object: &'a Value, field: &str) -> Result<&'a str, String> { + object[field] + .as_str() + .filter(|value| !value.is_empty()) + .ok_or_else(|| format!("provider readiness identity field {field} was absent")) +} + +impl MutationReceipt { + fn capture( + output: &str, + sandbox: &SandboxGuard, + provider: &str, + kind: &str, + keys: &[String; 2], + ) -> Result { + let body = readiness_output(output, keys)?; + let status = single_target(&body)?; + let receipt = &status["receipt"]; + let desired = &receipt["desired"]; + let mutation_id = nonempty_string(&body, "mutation_id")?; + if receipt["mutation_id"] != mutation_id + || receipt["provider_name"] != provider + || receipt["kind"] != kind + || desired["sandbox_name"] != sandbox.name + || status["state"] != "persisted" + || status["wait_outcome"] != "not_requested" + { + return Err( + "provider mutation receipt did not match persisted target intent".to_string(), + ); + } + nonempty_string(receipt, "receipt_id")?; + for field in [ + "sandbox_id", + "attachment_epoch", + "provider_resource_version", + "provider_env_revision", + "config_revision", + "policy_hash", + ] { + nonempty_string(desired, field)?; + } + if kind == "detach" { + if desired["provider_id"] != "" || desired["provider_resource_version"] != "0" { + return Err("detach receipt retained provider authority".to_string()); + } + } else { + nonempty_string(desired, "provider_id")?; + } + if receipt["persisted_at_ms"] + .as_i64() + .is_none_or(|time| time <= 0) + { + return Err("provider mutation receipt persistence time was absent".to_string()); + } + Ok(Self { + mutation_id: mutation_id.to_string(), + receipt: receipt.clone(), + }) + } + + fn follows(&self, previous: &Self) -> Result<(), String> { + let desired = &self.receipt["desired"]; + let preceding = &previous.receipt["desired"]; + if self.mutation_id == previous.mutation_id + || self.receipt["receipt_id"] == previous.receipt["receipt_id"] + || self.receipt["provider_name"] != previous.receipt["provider_name"] + || self.receipt["workspace"] != previous.receipt["workspace"] + || desired["sandbox_id"] != preceding["sandbox_id"] + || desired["sandbox_name"] != preceding["sandbox_name"] + || desired["provider_env_revision"] == preceding["provider_env_revision"] + { + return Err( + "provider mutation did not identify distinct authority for the same target" + .to_string(), + ); + } + // Revisions are opaque strings. Only identity equality is meaningful; + // a numerically lower fingerprint is still a different installation. + if self.receipt["kind"] == "update" + && (desired["provider_id"] != preceding["provider_id"] + || desired["provider_resource_version"] == preceding["provider_resource_version"] + || desired["attachment_epoch"] != preceding["attachment_epoch"]) + { + return Err("provider update did not preserve its attachment identity".to_string()); + } + if self.receipt["kind"] == "detach" + && desired["attachment_epoch"] == preceding["attachment_epoch"] + { + return Err("provider detach did not replace its attachment epoch".to_string()); + } + Ok(()) + } + + async fn wait( + &self, + sandbox: &SandboxGuard, + provider: &str, + expected_state: &str, + keys: &[String; 2], + ) -> Result { + let mut command = openshell_cmd(); + command.args([ + "sandbox", + "provider", + "status", + &sandbox.name, + provider, + "--receipt", + nonempty_string(&self.receipt, "receipt_id")?, + "--wait", + "--timeout", + READINESS_TIMEOUT_SECONDS, + "-o", + "json", + ]); + let output = checked_command_with_timeout( + &mut command, + "wait for original provider mutation receipt", + READINESS_COMMAND_TIMEOUT, + ) + .await?; + let body = readiness_output(&output, keys)?; + let status = single_target(&body)?; + if body["mutation_id"] != self.mutation_id + || status["receipt"] != self.receipt + || status["state"] != expected_state + || status["reason"] != "unspecified" + || status["wait_outcome"] != "complete" + { + return Err( + "original provider receipt did not complete against its exact desired authority" + .to_string(), + ); + } + let desired = &self.receipt["desired"]; + let observed = &status["observed"]; + for field in [ + "attachment_epoch", + "provider_env_revision", + "config_revision", + "policy_hash", + ] { + if observed[field] != desired[field] { + return Err(format!( + "provider installation did not match desired {field}" + )); + } + } + for field in [ + "credentials_installed", + "policy_active", + "launch_environment_installed", + ] { + if observed[field] != true { + return Err(format!("provider receipt completed without {field}")); + } + } + for field in ["session_id", "sequence", "process_instance_id"] { + nonempty_string(observed, field)?; + } + nonempty_string(status, "network_instance_id")?; + if observed["reason"] != "unspecified" + || status["observed_at_ms"] + .as_i64() + .is_none_or(|time| time <= 0) + || status["evaluated_at_ms"] + .as_i64() + .is_none_or(|time| time <= 0) + { + return Err( + "provider installation observation lacked a successful timestamped acknowledgment" + .to_string(), + ); + } + Ok(status.clone()) + } +} + +async fn future_environment( + sandbox: &SandboxGuard, + python: &str, + expected_present: bool, +) -> Result<(), String> { + // A new exec consumes the process supervisor's current launch snapshot. + // Only booleans leave the process, never the reference or its length. + let script = "import json,os,re; key='PROVIDER_READINESS_E2E_TOKEN'; value=os.environ.get(key,''); print(json.dumps({'present':key in os.environ,'reference':bool(re.fullmatch(r'openshell:resolve:env:v[1-9][0-9]*_'+key,value))}))"; + let output = checked_command( + &mut sandbox_command(sandbox, &[python, "-c", script]), + "probe future process provider environment", + ) + .await?; + let result: Value = + serde_json::from_str(output.trim()).map_err(|_| "future environment probe was invalid")?; + if result["present"] != expected_present || result["reference"] != expected_present { + return Err( + "future process environment did not match acknowledged provider authority".to_string(), + ); + } + Ok(()) +} + +async fn probe(sandbox: &SandboxGuard, client: &str, phase: &str) -> Result { + // Each retained process owns a separate mailbox so both old and current + // references can be checked after detach without a client consuming the + // other client's command. Labels and phases are fixed fixture constants. + let control = format!("{CONTROL}-{client}"); + let result = format!("{RESULT}-{client}"); + // The client consumes the control file as soon as it appears. Publish a + // complete phase atomically so polling cannot observe an empty write. + let command = format!( + "rm -f {result} && printf '%s' '{phase}' > {control}.tmp && mv {control}.tmp {control}" + ); + checked_command( + &mut sandbox_command(sandbox, &["sh", "-c", &command]), + "trigger persistent client phase", + ) + .await?; + let deadline = Instant::now() + Duration::from_secs(20); + loop { + if let Ok(output) = checked_command_with_timeout( + &mut sandbox_command(sandbox, &["cat", &result]), + "read persistent client phase", + Duration::from_secs(10), + ) + .await + { + let response: Value = + serde_json::from_str(output.trim()).map_err(|_| "client result was invalid")?; + if response["phase"] != phase || response["same_reference"] != true { + return Err( + "persistent client changed its retained environment reference".to_string(), + ); + } + return Ok(response); + } + if Instant::now() >= deadline { + return Err(format!("client phase {phase} did not finish")); + } + sleep(Duration::from_millis(100)).await; + } +} + +async fn probe_disallowed_binary( + sandbox: &SandboxGuard, + curl: &str, + host: &str, + port: u16, +) -> Result<(), String> { + // Binary authorization includes allowed ancestors. Launch curl as a + // sibling of the retained Python client, so Python cannot authorize it. + // The shell builtin sends the reference only through curl's stdin pipe. + let script = r#" +test -n "$PROVIDER_READINESS_E2E_TOKEN" || exit 64 +"$1" --version >/dev/null 2>&1 || exit 65 +if printf 'Authorization: Bearer %s\n' "$PROVIDER_READINESS_E2E_TOKEN" | \ + "$1" --silent --fail --max-time 5 --output /dev/null --header @- --data '{}' "$2" 2>/dev/null +then + printf 'unexpected-success' +else + printf 'denied' +fi +"#; + let url = format!("https://{host}:{port}/v1/chat/completions"); + let output = checked_command( + &mut sandbox_command(sandbox, &["sh", "-c", script, "binary-probe", curl, &url]), + "probe independent disallowed binary", + ) + .await?; + if output.trim() != "denied" { + return Err("independent disallowed binary reached the endpoint".to_string()); + } + Ok(()) +} + +fn check( + response: &Value, + pid: u64, + success: bool, + backend_phase: Option, +) -> Result<(), String> { + if response["pid"].as_u64() != Some(pid) || response["ok"].as_bool() != Some(success) { + // Report only typed status fields; never include HTTP error text or + // a serialized response that might later grow a credential field. + return Err(format!( + "client probe failed: pid_matches={}, expected_ok={success}, actual_ok={:?}, status={:?}, error_kind={:?}, reason_kind={:?}", + response["pid"].as_u64() == Some(pid), + response["ok"].as_bool(), + response["status"].as_u64(), + response["error_kind"].as_str(), + response["reason_kind"].as_str(), + )); + } + if let Some(phase) = backend_phase + && (response["backend_phase"].as_u64() != Some(phase) || response["status"] != 200) + { + return Err("backend did not attest the expected credential generation".to_string()); + } + Ok(()) +} + +#[tokio::test] +// Both clients remain alive through detach so their own retained references +// prove revocation across the attach and update launch boundaries. +#[allow(clippy::too_many_lines)] +async fn acknowledged_provider_changes_apply_to_fresh_clients_and_revoke_retained_references() +-> Result<(), String> { + let mut gateway_config = GatewayTrustConfig::load()?; + // Sandbox names are limited to 19 characters. Retain all 64 random bits + // within that limit so concurrent fixtures still own distinct resources. + let name = format!("e2e{:016x}", rand::random::()); + let mut backend = BackendPair::new(&name)?; + // The wrapper's directory is shared with the host Docker daemon in CI; + // a job-container-local temporary path cannot back the TLS bind mount. + let fixture_parent = gateway_config + .path + .parent() + .ok_or("managed gateway configuration has no parent directory")?; + let directory = + TempDir::new_in(fixture_parent).map_err(|_| "could not allocate fixture directory")?; + let context = directory.path().join("image"); + std::fs::create_dir(&context).map_err(|_| "could not allocate public image context")?; + let backend_tls = directory.path().join("backend-tls"); + std::fs::create_dir(&backend_tls).map_err(|_| "could not allocate backend TLS directory")?; + let base = std::env::var("OPENSHELL_E2E_DOCKER_SANDBOX_IMAGE") + .unwrap_or_else(|_| "ghcr.io/nvidia/openshell-community/sandboxes/base:latest".to_string()); + if base.chars().any(char::is_whitespace) { + return Err("fixture image reference contains whitespace".to_string()); + } + let binaries = base_binaries(&base).await?; + let python = binaries["python"] + .as_str() + .ok_or("Python executable was absent")?; + let curl = binaries["curl"] + .as_str() + .ok_or("curl executable was absent")?; + let image = FixtureImage::new()?; + let supervisor_image = FixtureImage::new()?; + // Each backend has its own network namespace, so fixed internal ports need + // no host reservation or publication and remain independent across runs. + let port = BACKEND_PORT; + let other_port = OTHER_BACKEND_PORT; + let keys = [ + format!("e2e-{:032x}", rand::random::()), + format!("e2e-{:032x}", rand::random::()), + ]; + let mut sandbox = None; + let mut sandbox_attempted = false; + let mut clients = Vec::with_capacity(2); + let result = async { + // Begin container mutation inside this scope so certificate, image, + // and enrollment failures still reach explicit bounded teardown. + let [host, other_host] = backend.spawn(&base, &backend_tls).await?; + let (certificate, private_key) = + generate_certificates(directory.path(), &host, &other_host).await?; + std::fs::copy(&certificate, backend_tls.join("backend.crt")) + .map_err(|_| "could not stage backend certificate")?; + let backend_tls_key = backend_tls.join("backend.key.fixture"); + std::fs::copy(&private_key, &backend_tls_key) + .map_err(|_| "could not stage backend TLS key")?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + // The backends inherit the image's unprivileged user. Only their + // mounted leaf key is readable there; the host TempDir is private + // and the CA signing key never enters a container or image context. + std::fs::set_permissions(&backend_tls_key, std::fs::Permissions::from_mode(0o444)) + .map_err(|_| "could not set backend TLS key permissions")?; + } + backend.initialize(&json!({"keys": keys, "ports": [port, other_port], + "certificate": "/fixture-tls/backend.crt", "private_key": "/fixture-tls/backend.key.fixture"})) + .await?; + std::fs::copy(directory.path().join("ca.crt"), context.join("fixture-ca.crt")) + .map_err(|_| "could not copy public fixture CA")?; + std::fs::write(context.join("client.py"), CLIENT) + .map_err(|_| "could not write client source")?; + let dockerfile = context.join("Dockerfile"); + std::fs::write(&dockerfile, format!( + "FROM {base}\nUSER root\nCOPY client.py /opt/provider-readiness-client.py\nUSER sandbox\n" + )).map_err(|_| "could not write fixture Dockerfile")?; + let supervisor_dockerfile = context.join("Dockerfile.supervisor"); + // Outbound TLS belongs to the separate supervisor. Its combined public + // trust bundle is delivered to the workload through the sandbox protocol. + // Preserve the base image's user setting: Docker's archive upload applies + // an explicit image user to the supervisor's private bootstrap files. + std::fs::write(&supervisor_dockerfile, format!( + "FROM {}\nCOPY fixture-ca.crt /tmp/readiness-fixture-ca.crt\nRUN cat /tmp/readiness-fixture-ca.crt >> /etc/ssl/certs/ca-certificates.crt && rm /tmp/readiness-fixture-ca.crt\n", + gateway_config.supervisor_image + )).map_err(|_| "could not write fixture supervisor Dockerfile")?; + image.build(&dockerfile, &context).await?; + supervisor_image.build(&supervisor_dockerfile, &context).await?; + gateway_config.apply(supervisor_image.tag()).await?; + let profile = directory.path().join("profile.json"); + let policy = directory.path().join("policy.json"); + write_profile(&profile, &name, &host, port, python)?; + write_policy(&policy, &host, &other_host, port, other_port, python)?; + let profile_path = profile.to_str().ok_or("profile path was not UTF-8")?; + let policy_path = policy.to_str().ok_or("policy path was not UTF-8")?; + let configuration = json!({"host": host, "other_host": other_host, "port": port, + "other_port": other_port}).to_string(); + cli( + "import synthetic provider profile", + &["provider", "profile", "import", "--file", profile_path], + None, + ) + .await?; + cli( + "create synthetic provider", + &[ + "provider", + "create", + "--name", + &name, + "--type", + &name, + "--credential", + TOKEN_ENV, + ], + Some(&keys[0]), + ) + .await?; + // Retain a known name even when creation times out after persistence, + // so teardown can still remove the partially created sandbox. + sandbox_attempted = true; + sandbox = Some( + SandboxGuard::create(&[ + "--name", + &name, + "--from", + image.tag(), + "--policy", + policy_path, + "--no-auto-providers", + ]) + .await + .map_err(|_| "scratch sandbox could not start")?, + ); + let running = sandbox + .as_ref() + .ok_or("sandbox was absent after creation")?; + future_environment(running, python, false).await?; + let output = cli( + "attach synthetic provider", + &[ + "sandbox", + "provider", + "attach", + &running.name, + &name, + "-o", + "json", + ], + None, + ) + .await?; + let attach = MutationReceipt::capture(&output, running, &name, "attach", &keys)?; + let attached = attach.wait(running, &name, "ready", &keys).await?; + future_environment(running, python, true).await?; + // Start A only after the actual launch environment acknowledgment. + // It retains this static reference until teardown; updates need not + // change the credential selected by an already-running process. + clients.push(PersistentClient::start(running, python, &configuration, "a").await?); + let initial = probe(running, "a", "initial").await?; + let pid_a = initial["pid"].as_u64().ok_or("client A PID was absent")?; + check(&initial, pid_a, true, Some(0))?; + let before = backend.counts().await?; + if before["total"] != 1 || before["accepted"][0] != 1 { + return Err("initial backend request count was incorrect".to_string()); + } + for phase in [ + "wrong_host_control", + "wrong_port_control", + "wrong_path_control", + ] { + let control = probe(running, "a", phase).await?; + check(&control, pid_a, false, None)?; + if control["status"] != 401 || control["backend_phase"] != 0 { + return Err(format!( + "negative endpoint {phase} was not reachable without a credential" + )); + } + } + let before = backend.counts().await?; + if before["total"] != 4 || before["rejected"] != 3 || before["accepted"] != json!([1, 0]) { + return Err("uncredentialed endpoint control assertions failed".to_string()); + } + for phase in [ + "wrong_host", + "wrong_port", + "wrong_path", + "untrusted_ca", + ] { + check(&probe(running, "a", phase).await?, pid_a, false, None)?; + if backend.counts().await? != before { + return Err(format!("denied phase {phase} reached the backend")); + } + } + probe_disallowed_binary(running, curl, &host, port).await?; + if backend.counts().await? != before { + return Err("disallowed binary reached the backend".to_string()); + } + + backend.rotate().await?; + let output = cli( + "update synthetic provider once", + &[ + "provider", + "update", + &name, + "--credential", + TOKEN_ENV, + "-o", + "json", + ], + Some(&keys[1]), + ) + .await?; + let update = MutationReceipt::capture(&output, running, &name, "update", &keys)?; + update.follows(&attach)?; + // No workload requests occur while waiting. Launch B through the + // acknowledged environment boundary; its first request must use the + // new credential without warming requests or success retries. + let updated = update.wait(running, &name, "ready", &keys).await?; + future_environment(running, python, true).await?; + clients.push(PersistentClient::start(running, python, &configuration, "b").await?); + let rotated_request = probe(running, "b", "rotated").await?; + let pid_b = rotated_request["pid"].as_u64().ok_or("client B PID was absent")?; + if pid_b == pid_a { + return Err("provider update reused the original client process".to_string()); + } + check(&rotated_request, pid_b, true, Some(1))?; + let rotated = backend.counts().await?; + if rotated["total"] != 5 || rotated["accepted"] != json!([1, 1]) || rotated["rejected"] != 3 + { + return Err("single-update backend assertions failed".to_string()); + } + + let output = cli( + "detach synthetic provider", + &[ + "sandbox", + "provider", + "detach", + &running.name, + &name, + "-o", + "json", + ], + None, + ) + .await?; + let detach = MutationReceipt::capture(&output, running, &name, "detach", &keys)?; + detach.follows(&update)?; + let revoked = detach.wait(running, &name, "revoked", &keys).await?; + future_environment(running, python, false).await?; + // Detach must revoke the reference while leaving the independently + // authorized route usable; a network outage cannot satisfy this proof. + let control = probe(running, "b", "detached_control").await?; + check(&control, pid_b, false, None)?; + if control["status"] != 401 || control["backend_phase"] != 1 { + return Err("detached endpoint was not reachable without a credential".to_string()); + } + let detached = backend.counts().await?; + if detached["total"] != 6 + || detached["accepted"] != json!([1, 1]) + || detached["rejected"] != 4 + || detached["phase"] != 1 + { + return Err("detached endpoint control assertions failed".to_string()); + } + for (client, pid) in [("a", pid_a), ("b", pid_b)] { + check(&probe(running, client, "detached").await?, pid, false, None)?; + if backend.counts().await? != detached { + return Err(format!("detached client {client} credential reached the backend")); + } + } + println!( + "{}", + json!({"phase": "complete", "client_a_pid": pid_a, "client_b_pid": pid_b, + "clients_retained_own_references": true, "fresh_client_first_request_rotated": true, + "endpoint_denials": true, "binary_denial": true, + "tls_verified": true, "both_client_references_revoked": true, + "future_environment_installed": true, "future_environment_removed": true, + "attach": attached, "update": updated, "detach": revoked}) + ); + Ok(()) + } + .await; + + // Resource names are unique to this run. Always attempt cleanup, including + // failures during enrollment, and keep failures visible without exposing + // captured provider output. The generic guard suppresses delete errors, so + // first issue an explicit checked delete before disarming its fallback. + let mut cleanup_results = Vec::new(); + if sandbox_attempted { + cleanup_results.push( + cli( + "delete fixture sandbox", + &["sandbox", "delete", &name], + None, + ) + .await + .map(|_| ()), + ); + } + if let Some(mut sandbox) = sandbox { + cleanup_results.push( + timeout(COMMAND_TIMEOUT, sandbox.cleanup()) + .await + .map_err(|_| "sandbox guard cleanup timed out".to_string()), + ); + } + for mut client in clients { + cleanup_results.push(client.stop().await); + } + cleanup_results.push( + cli( + "delete synthetic provider", + &["provider", "delete", &name], + None, + ) + .await + .map(|_| ()), + ); + cleanup_results.push( + cli( + "delete synthetic profile", + &["provider", "profile", "delete", &name], + None, + ) + .await + .map(|_| ()), + ); + cleanup_results.push(backend.stop().await); + // Restore the original runtime before removing its replacement. Retain the + // derived supervisor image if restoration fails, and report that failure + // even when a lifecycle assertion already failed. + let gateway_restore = gateway_config.restore().await; + let supervisor_cleanup = if gateway_restore.is_ok() { + supervisor_image.remove().await + } else { + Ok(()) + }; + let image_cleanup = image.remove().await; + let failures = [result, gateway_restore, supervisor_cleanup, image_cleanup] + .into_iter() + .chain(cleanup_results) + .filter_map(Result::err) + .collect::>(); + if failures.is_empty() { + Ok(()) + } else { + Err(failures.join("; ")) + } +} diff --git a/proto/openshell.proto b/proto/openshell.proto index 2462799d6d..0ca055b50b 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -157,6 +157,16 @@ service OpenShell { }; } + // Inspect the installed authority for one sandbox provider mutation. + rpc GetSandboxProviderStatus(GetSandboxProviderStatusRequest) + returns (GetSandboxProviderStatusResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + // Delete a sandbox by name. rpc DeleteSandbox(DeleteSandboxRequest) returns (DeleteSandboxResponse) { option (openshell.options.v1.authorization) = { @@ -483,6 +493,15 @@ service OpenShell { }; } + // Report installed provider state for the current ConnectSupervisor session. + // Replacing or losing that session invalidates its observations. + rpc ReportProviderReadiness(ReportProviderReadinessRequest) + returns (ReportProviderReadinessResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + // Get provider environment for a sandbox (called by sandbox supervisor at startup). rpc GetSandboxProviderEnvironment(GetSandboxProviderEnvironmentRequest) returns (GetSandboxProviderEnvironmentResponse) { @@ -950,6 +969,9 @@ message SandboxSpec { repeated string command = 12; // Allocate a retained pseudo-terminal for the main process. bool tty = 13; + // Gateway-owned attachment identity, changed atomically with the provider set. + // Equality only: detach and reattach must not revive an older receipt. + string provider_attachment_epoch = 14; } message ResourceRequirements { @@ -1360,6 +1382,8 @@ message AttachSandboxProviderResponse { Sandbox sandbox = 1; // True when the provider was newly attached. False means it was already attached. bool attached = 2; + // Persisted intent; readiness requires current supervisor observations. + ProviderMutationReceipt receipt = 3; } // Detach provider from sandbox response. @@ -1367,6 +1391,205 @@ message DetachSandboxProviderResponse { Sandbox sandbox = 1; // True when the provider was removed. False means it was not attached. bool detached = 2; + // Revocation is complete only when this receipt reports REVOKED. + ProviderMutationReceipt receipt = 3; +} + +// Operation whose installed authority is tracked by a receipt. +enum ProviderMutationKind { + PROVIDER_MUTATION_KIND_UNSPECIFIED = 0; + PROVIDER_MUTATION_KIND_ATTACH = 1; + PROVIDER_MUTATION_KIND_DETACH = 2; + PROVIDER_MUTATION_KIND_UPDATE = 3; + // Reconstructed status for existing desired state without a mutation receipt. + PROVIDER_MUTATION_KIND_OBSERVE = 4; +} + +// Readiness states describe persisted intent separately from installed state. +enum ProviderReadinessState { + PROVIDER_READINESS_STATE_UNSPECIFIED = 0; + PROVIDER_READINESS_STATE_PERSISTED = 1; + PROVIDER_READINESS_STATE_PENDING = 2; + PROVIDER_READINESS_STATE_READY = 3; + PROVIDER_READINESS_STATE_WITHHELD = 4; + PROVIDER_READINESS_STATE_REVOKED = 5; + PROVIDER_READINESS_STATE_FAILED = 6; + PROVIDER_READINESS_STATE_SUPERSEDED = 7; +} + +// Closed reason categories are safe to display. Raw installation errors are +// never part of the readiness protocol. +enum ProviderReadinessReason { + PROVIDER_READINESS_REASON_UNSPECIFIED = 0; + PROVIDER_READINESS_REASON_WAITING_FOR_SUPERVISOR = 1; + PROVIDER_READINESS_REASON_WAITING_FOR_CREDENTIALS = 2; + PROVIDER_READINESS_REASON_WAITING_FOR_POLICY = 3; + PROVIDER_READINESS_REASON_WAITING_FOR_PROCESS = 4; + PROVIDER_READINESS_REASON_UNSUPPORTED_SUPERVISOR = 5; + PROVIDER_READINESS_REASON_CREDENTIALS_WITHHELD = 6; + PROVIDER_READINESS_REASON_CREDENTIAL_INSTALL_FAILED = 7; + PROVIDER_READINESS_REASON_POLICY_ACTIVATION_FAILED = 8; + PROVIDER_READINESS_REASON_PROCESS_INSTALL_FAILED = 9; + PROVIDER_READINESS_REASON_SUPERVISOR_DISCONNECTED = 10; + PROVIDER_READINESS_REASON_SUPERVISOR_LEASE_EXPIRED = 11; + PROVIDER_READINESS_REASON_DESIRED_STATE_CHANGED = 12; + PROVIDER_READINESS_REASON_CREDENTIAL_EXPIRED = 13; + PROVIDER_READINESS_REASON_LOCAL_POLICY = 14; + PROVIDER_READINESS_REASON_SNAPSHOT_MISMATCH = 15; +} + +// Exact desired authority. Revisions are opaque identities, never ordered. +message ProviderDesiredIdentity { + string sandbox_id = 1; + string sandbox_name = 2; + string attachment_epoch = 3; + // Empty for a detached provider. + string provider_id = 4; + uint64 provider_resource_version = 5; + uint64 provider_env_revision = 6; + uint64 config_revision = 7; + string policy_hash = 8; +} + +// Component whose desired state is tracked by a durable update operation. +enum ConfigComponent { + CONFIG_COMPONENT_UNSPECIFIED = 0; + CONFIG_COMPONENT_SANDBOX_CONFIG = 1; + CONFIG_COMPONENT_PROVIDER_ENVIRONMENT = 2; +} + +// Identifies one component snapshot revision. Revisions are equality tokens, +// not members of one shared ordering domain. +message ConfigSnapshotRevision { + oneof component { + SandboxConfigRevision sandbox_config = 1; + uint64 provider_environment = 2; + // Complete desired authority for one sandbox-scoped provider mutation. + ProviderDesiredIdentity provider_target = 3; + } +} + +// Identity needed to correlate effective sandbox configuration with the +// policy-history row whose apply status the gateway records. +message SandboxConfigRevision { + uint64 config_revision = 1; + uint32 policy_version = 2; + openshell.sandbox.v1.PolicySource policy_source = 3; + uint32 global_policy_version = 4; + // Monotonic revision of the sandbox-scoped settings row. This disambiguates + // setting operations whose effective config fingerprint is equality-only. + uint64 settings_revision = 5; +} + +// Result of applying a component revision at its owning runtime boundary. +enum ConfigApplyOutcome { + CONFIG_APPLY_OUTCOME_UNSPECIFIED = 0; + CONFIG_APPLY_OUTCOME_APPLIED = 1; + CONFIG_APPLY_OUTCOME_IGNORED_DUPLICATE = 2; + CONFIG_APPLY_OUTCOME_IGNORED_STALE = 3; + CONFIG_APPLY_OUTCOME_RETAINED_LOCAL_OVERRIDE = 4; + CONFIG_APPLY_OUTCOME_DEGRADED = 5; + CONFIG_APPLY_OUTCOME_FAILED_RETAINED_LAST_KNOWN_GOOD = 6; + CONFIG_APPLY_OUTCOME_FAILED_CLOSED = 7; + CONFIG_APPLY_OUTCOME_UNSUPPORTED = 8; +} + +// Durable lifecycle of one desired-state update operation. +enum ConfigUpdateOperationState { + CONFIG_UPDATE_OPERATION_STATE_UNSPECIFIED = 0; + CONFIG_UPDATE_OPERATION_STATE_PENDING = 1; + CONFIG_UPDATE_OPERATION_STATE_APPLIED = 2; + CONFIG_UPDATE_OPERATION_STATE_INACTIVE = 3; + CONFIG_UPDATE_OPERATION_STATE_FAILED = 4; + CONFIG_UPDATE_OPERATION_STATE_SUPERSEDED = 5; + CONFIG_UPDATE_OPERATION_STATE_CANCELLED = 6; +} + +// Durable progress for one sandbox-scoped desired-state mutation. Snapshot +// contents and credentials are never stored in this resource. +message ConfigUpdateOperation { + string operation_id = 1; + string sandbox_id = 2; + ConfigComponent component = 3; + ConfigSnapshotRevision target_revision = 4; + ConfigUpdateOperationState state = 5; + ConfigApplyOutcome outcome = 6; + string sanitized_error = 7; + int64 created_at_ms = 8; + int64 updated_at_ms = 9; + int64 completed_at_ms = 10; +} + +// Immutable, secret-free record of one sandbox's intended provider mutation. +message ProviderMutationReceipt { + string receipt_id = 1; + // Shared by all sandbox receipts from one provider update. + string mutation_id = 2; + string provider_name = 3; + string workspace = 4; + ProviderMutationKind kind = 5; + ProviderDesiredIdentity desired = 6; + int64 persisted_at_ms = 7; +} + +// Installed state reported by the current supervisor. Process installation is +// acknowledged by the authenticated sandbox boundary after replacing its +// environment for future process launches. +message ProviderReadinessObservation { + // Gateway-issued identifier from the current ConnectSupervisor response. + string session_id = 1; + // Monotonic only within this connection; unrelated to revision fingerprints. + uint64 sequence = 2; + string attachment_epoch = 3; + uint64 provider_env_revision = 4; + uint64 config_revision = 5; + string policy_hash = 6; + bool credentials_installed = 7; + bool policy_active = 8; + bool launch_environment_installed = 9; + string process_instance_id = 10; + ProviderReadinessReason reason = 11; +} + +// Operator view of desired and observed state; contains no credential material. +message ProviderReadinessStatus { + ProviderMutationReceipt receipt = 1; + ProviderReadinessState state = 2; + ProviderReadinessReason reason = 3; + ProviderReadinessObservation observed = 4; + string network_instance_id = 5; + int64 observed_at_ms = 6; + int64 evaluated_at_ms = 7; + // Durable operation for the receipt, including its terminal apply outcome. + ConfigUpdateOperation operation = 8; +} + +// Query an immutable receipt, or reconstruct the current desired state when +// receipt_id is empty. The sandbox identity must match the receipt. +message GetSandboxProviderStatusRequest { + string sandbox_name = 1; + string provider_name = 2; + string receipt_id = 3; + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; +} + +message GetSandboxProviderStatusResponse { + ProviderReadinessStatus status = 1; +} + +// Installation evidence from the authenticated supervisor for this sandbox. +// A caller-provided instance identifier alone never establishes authority. +message ReportProviderReadinessRequest { + string sandbox_id = 1; + ProviderReadinessObservation observation = 2; +} + +// Acknowledges accepted evidence without granting a separate session authority. +// Identical retries do not extend the evidence's original acceptance time. +message ReportProviderReadinessResponse { + uint64 accepted_sequence = 1; + uint32 report_interval_seconds = 2; + uint32 observation_ttl_seconds = 3; } // Delete sandbox response. @@ -1765,6 +1988,11 @@ message DeleteProviderRequest { // Provider response. message ProviderResponse { openshell.datamodel.v1.Provider provider = 1; + // Selection-time sandbox target set for an update, with one receipt per target. + // Sandboxes attached later are outside this operation's readiness result. + repeated ProviderMutationReceipt target_receipts = 2; + // Identifies the update even when its target set is empty. + string mutation_id = 3; } // List providers response. @@ -2198,6 +2426,12 @@ message GetSandboxProviderEnvironmentResponse { // Environment variables that contain provider configuration rather than // credentials and therefore do not require endpoint-scoped resolution. repeated string non_secret_environment_keys = 6; + // Attachment identity captured with the returned provider records. + string provider_attachment_epoch = 7; + // Effective policy identity used to derive this snapshot's endpoint bindings. + string policy_hash = 8; + // Nonzero when material was withheld; installing an empty map is not readiness. + ProviderReadinessReason readiness_reason = 9; } message ExchangeProviderSubjectTokenRequest { @@ -2505,6 +2739,8 @@ message SupervisorHello { string sandbox_id = 1; // Supervisor instance ID (e.g. boot id or process epoch). string instance_id = 2; + // The supervisor can report credential, policy, and launch-environment installation. + bool supports_provider_readiness = 4; } // Gateway accepts the supervisor session. diff --git a/proto/sandbox.proto b/proto/sandbox.proto index 894e3b2754..180f1e3488 100644 --- a/proto/sandbox.proto +++ b/proto/sandbox.proto @@ -398,6 +398,9 @@ message GetSandboxConfigResponse { // False also covers older gateways that do not advertise this capability; // supervisors preserve their legacy unauthenticated connection behavior. bool extension_authentication_enabled = 12; + // Gateway-owned attachment identity captured with this desired configuration. + // Compare for equality; reattachment invalidates previous installation evidence. + string provider_attachment_epoch = 14; } // Connection details for one operator-registered supervisor middleware service. diff --git a/sdk/go/openshell/v1/internal/converter/coverage_test.go b/sdk/go/openshell/v1/internal/converter/coverage_test.go index 5103d4b1fc..ff2c99ff74 100644 --- a/sdk/go/openshell/v1/internal/converter/coverage_test.go +++ b/sdk/go/openshell/v1/internal/converter/coverage_test.go @@ -33,7 +33,12 @@ func TestConverterCoversAllProtoFields_SandboxSpec(t *testing.T) { "tty": true, } - assertAllFieldsCovered(t, (&pb.SandboxSpec{}).ProtoReflect().Descriptor(), handled, nil) + // The gateway owns this identity. Provider status exposes it through the + // raw API; callers must not supply it when constructing a sandbox spec. + skipped := fieldSet{ + "provider_attachment_epoch": true, + } + assertAllFieldsCovered(t, (&pb.SandboxSpec{}).ProtoReflect().Descriptor(), handled, skipped) } func TestConverterCoversAllProtoFields_SandboxTemplate(t *testing.T) { diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index e3200acbc8..a359292204 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -104,6 +104,398 @@ func (SandboxPhase) EnumDescriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{0} } +// Operation whose installed authority is tracked by a receipt. +type ProviderMutationKind int32 + +const ( + ProviderMutationKind_PROVIDER_MUTATION_KIND_UNSPECIFIED ProviderMutationKind = 0 + ProviderMutationKind_PROVIDER_MUTATION_KIND_ATTACH ProviderMutationKind = 1 + ProviderMutationKind_PROVIDER_MUTATION_KIND_DETACH ProviderMutationKind = 2 + ProviderMutationKind_PROVIDER_MUTATION_KIND_UPDATE ProviderMutationKind = 3 + // Reconstructed status for existing desired state without a mutation receipt. + ProviderMutationKind_PROVIDER_MUTATION_KIND_OBSERVE ProviderMutationKind = 4 +) + +// Enum value maps for ProviderMutationKind. +var ( + ProviderMutationKind_name = map[int32]string{ + 0: "PROVIDER_MUTATION_KIND_UNSPECIFIED", + 1: "PROVIDER_MUTATION_KIND_ATTACH", + 2: "PROVIDER_MUTATION_KIND_DETACH", + 3: "PROVIDER_MUTATION_KIND_UPDATE", + 4: "PROVIDER_MUTATION_KIND_OBSERVE", + } + ProviderMutationKind_value = map[string]int32{ + "PROVIDER_MUTATION_KIND_UNSPECIFIED": 0, + "PROVIDER_MUTATION_KIND_ATTACH": 1, + "PROVIDER_MUTATION_KIND_DETACH": 2, + "PROVIDER_MUTATION_KIND_UPDATE": 3, + "PROVIDER_MUTATION_KIND_OBSERVE": 4, + } +) + +func (x ProviderMutationKind) Enum() *ProviderMutationKind { + p := new(ProviderMutationKind) + *p = x + return p +} + +func (x ProviderMutationKind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderMutationKind) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[1].Descriptor() +} + +func (ProviderMutationKind) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[1] +} + +func (x ProviderMutationKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderMutationKind.Descriptor instead. +func (ProviderMutationKind) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{1} +} + +// Readiness states describe persisted intent separately from installed state. +type ProviderReadinessState int32 + +const ( + ProviderReadinessState_PROVIDER_READINESS_STATE_UNSPECIFIED ProviderReadinessState = 0 + ProviderReadinessState_PROVIDER_READINESS_STATE_PERSISTED ProviderReadinessState = 1 + ProviderReadinessState_PROVIDER_READINESS_STATE_PENDING ProviderReadinessState = 2 + ProviderReadinessState_PROVIDER_READINESS_STATE_READY ProviderReadinessState = 3 + ProviderReadinessState_PROVIDER_READINESS_STATE_WITHHELD ProviderReadinessState = 4 + ProviderReadinessState_PROVIDER_READINESS_STATE_REVOKED ProviderReadinessState = 5 + ProviderReadinessState_PROVIDER_READINESS_STATE_FAILED ProviderReadinessState = 6 + ProviderReadinessState_PROVIDER_READINESS_STATE_SUPERSEDED ProviderReadinessState = 7 +) + +// Enum value maps for ProviderReadinessState. +var ( + ProviderReadinessState_name = map[int32]string{ + 0: "PROVIDER_READINESS_STATE_UNSPECIFIED", + 1: "PROVIDER_READINESS_STATE_PERSISTED", + 2: "PROVIDER_READINESS_STATE_PENDING", + 3: "PROVIDER_READINESS_STATE_READY", + 4: "PROVIDER_READINESS_STATE_WITHHELD", + 5: "PROVIDER_READINESS_STATE_REVOKED", + 6: "PROVIDER_READINESS_STATE_FAILED", + 7: "PROVIDER_READINESS_STATE_SUPERSEDED", + } + ProviderReadinessState_value = map[string]int32{ + "PROVIDER_READINESS_STATE_UNSPECIFIED": 0, + "PROVIDER_READINESS_STATE_PERSISTED": 1, + "PROVIDER_READINESS_STATE_PENDING": 2, + "PROVIDER_READINESS_STATE_READY": 3, + "PROVIDER_READINESS_STATE_WITHHELD": 4, + "PROVIDER_READINESS_STATE_REVOKED": 5, + "PROVIDER_READINESS_STATE_FAILED": 6, + "PROVIDER_READINESS_STATE_SUPERSEDED": 7, + } +) + +func (x ProviderReadinessState) Enum() *ProviderReadinessState { + p := new(ProviderReadinessState) + *p = x + return p +} + +func (x ProviderReadinessState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderReadinessState) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[2].Descriptor() +} + +func (ProviderReadinessState) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[2] +} + +func (x ProviderReadinessState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderReadinessState.Descriptor instead. +func (ProviderReadinessState) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{2} +} + +// Closed reason categories are safe to display. Raw installation errors are +// never part of the readiness protocol. +type ProviderReadinessReason int32 + +const ( + ProviderReadinessReason_PROVIDER_READINESS_REASON_UNSPECIFIED ProviderReadinessReason = 0 + ProviderReadinessReason_PROVIDER_READINESS_REASON_WAITING_FOR_SUPERVISOR ProviderReadinessReason = 1 + ProviderReadinessReason_PROVIDER_READINESS_REASON_WAITING_FOR_CREDENTIALS ProviderReadinessReason = 2 + ProviderReadinessReason_PROVIDER_READINESS_REASON_WAITING_FOR_POLICY ProviderReadinessReason = 3 + ProviderReadinessReason_PROVIDER_READINESS_REASON_WAITING_FOR_PROCESS ProviderReadinessReason = 4 + ProviderReadinessReason_PROVIDER_READINESS_REASON_UNSUPPORTED_SUPERVISOR ProviderReadinessReason = 5 + ProviderReadinessReason_PROVIDER_READINESS_REASON_CREDENTIALS_WITHHELD ProviderReadinessReason = 6 + ProviderReadinessReason_PROVIDER_READINESS_REASON_CREDENTIAL_INSTALL_FAILED ProviderReadinessReason = 7 + ProviderReadinessReason_PROVIDER_READINESS_REASON_POLICY_ACTIVATION_FAILED ProviderReadinessReason = 8 + ProviderReadinessReason_PROVIDER_READINESS_REASON_PROCESS_INSTALL_FAILED ProviderReadinessReason = 9 + ProviderReadinessReason_PROVIDER_READINESS_REASON_SUPERVISOR_DISCONNECTED ProviderReadinessReason = 10 + ProviderReadinessReason_PROVIDER_READINESS_REASON_SUPERVISOR_LEASE_EXPIRED ProviderReadinessReason = 11 + ProviderReadinessReason_PROVIDER_READINESS_REASON_DESIRED_STATE_CHANGED ProviderReadinessReason = 12 + ProviderReadinessReason_PROVIDER_READINESS_REASON_CREDENTIAL_EXPIRED ProviderReadinessReason = 13 + ProviderReadinessReason_PROVIDER_READINESS_REASON_LOCAL_POLICY ProviderReadinessReason = 14 + ProviderReadinessReason_PROVIDER_READINESS_REASON_SNAPSHOT_MISMATCH ProviderReadinessReason = 15 +) + +// Enum value maps for ProviderReadinessReason. +var ( + ProviderReadinessReason_name = map[int32]string{ + 0: "PROVIDER_READINESS_REASON_UNSPECIFIED", + 1: "PROVIDER_READINESS_REASON_WAITING_FOR_SUPERVISOR", + 2: "PROVIDER_READINESS_REASON_WAITING_FOR_CREDENTIALS", + 3: "PROVIDER_READINESS_REASON_WAITING_FOR_POLICY", + 4: "PROVIDER_READINESS_REASON_WAITING_FOR_PROCESS", + 5: "PROVIDER_READINESS_REASON_UNSUPPORTED_SUPERVISOR", + 6: "PROVIDER_READINESS_REASON_CREDENTIALS_WITHHELD", + 7: "PROVIDER_READINESS_REASON_CREDENTIAL_INSTALL_FAILED", + 8: "PROVIDER_READINESS_REASON_POLICY_ACTIVATION_FAILED", + 9: "PROVIDER_READINESS_REASON_PROCESS_INSTALL_FAILED", + 10: "PROVIDER_READINESS_REASON_SUPERVISOR_DISCONNECTED", + 11: "PROVIDER_READINESS_REASON_SUPERVISOR_LEASE_EXPIRED", + 12: "PROVIDER_READINESS_REASON_DESIRED_STATE_CHANGED", + 13: "PROVIDER_READINESS_REASON_CREDENTIAL_EXPIRED", + 14: "PROVIDER_READINESS_REASON_LOCAL_POLICY", + 15: "PROVIDER_READINESS_REASON_SNAPSHOT_MISMATCH", + } + ProviderReadinessReason_value = map[string]int32{ + "PROVIDER_READINESS_REASON_UNSPECIFIED": 0, + "PROVIDER_READINESS_REASON_WAITING_FOR_SUPERVISOR": 1, + "PROVIDER_READINESS_REASON_WAITING_FOR_CREDENTIALS": 2, + "PROVIDER_READINESS_REASON_WAITING_FOR_POLICY": 3, + "PROVIDER_READINESS_REASON_WAITING_FOR_PROCESS": 4, + "PROVIDER_READINESS_REASON_UNSUPPORTED_SUPERVISOR": 5, + "PROVIDER_READINESS_REASON_CREDENTIALS_WITHHELD": 6, + "PROVIDER_READINESS_REASON_CREDENTIAL_INSTALL_FAILED": 7, + "PROVIDER_READINESS_REASON_POLICY_ACTIVATION_FAILED": 8, + "PROVIDER_READINESS_REASON_PROCESS_INSTALL_FAILED": 9, + "PROVIDER_READINESS_REASON_SUPERVISOR_DISCONNECTED": 10, + "PROVIDER_READINESS_REASON_SUPERVISOR_LEASE_EXPIRED": 11, + "PROVIDER_READINESS_REASON_DESIRED_STATE_CHANGED": 12, + "PROVIDER_READINESS_REASON_CREDENTIAL_EXPIRED": 13, + "PROVIDER_READINESS_REASON_LOCAL_POLICY": 14, + "PROVIDER_READINESS_REASON_SNAPSHOT_MISMATCH": 15, + } +) + +func (x ProviderReadinessReason) Enum() *ProviderReadinessReason { + p := new(ProviderReadinessReason) + *p = x + return p +} + +func (x ProviderReadinessReason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderReadinessReason) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[3].Descriptor() +} + +func (ProviderReadinessReason) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[3] +} + +func (x ProviderReadinessReason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderReadinessReason.Descriptor instead. +func (ProviderReadinessReason) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{3} +} + +// Component whose desired state is tracked by a durable update operation. +type ConfigComponent int32 + +const ( + ConfigComponent_CONFIG_COMPONENT_UNSPECIFIED ConfigComponent = 0 + ConfigComponent_CONFIG_COMPONENT_SANDBOX_CONFIG ConfigComponent = 1 + ConfigComponent_CONFIG_COMPONENT_PROVIDER_ENVIRONMENT ConfigComponent = 2 +) + +// Enum value maps for ConfigComponent. +var ( + ConfigComponent_name = map[int32]string{ + 0: "CONFIG_COMPONENT_UNSPECIFIED", + 1: "CONFIG_COMPONENT_SANDBOX_CONFIG", + 2: "CONFIG_COMPONENT_PROVIDER_ENVIRONMENT", + } + ConfigComponent_value = map[string]int32{ + "CONFIG_COMPONENT_UNSPECIFIED": 0, + "CONFIG_COMPONENT_SANDBOX_CONFIG": 1, + "CONFIG_COMPONENT_PROVIDER_ENVIRONMENT": 2, + } +) + +func (x ConfigComponent) Enum() *ConfigComponent { + p := new(ConfigComponent) + *p = x + return p +} + +func (x ConfigComponent) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConfigComponent) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[4].Descriptor() +} + +func (ConfigComponent) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[4] +} + +func (x ConfigComponent) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConfigComponent.Descriptor instead. +func (ConfigComponent) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{4} +} + +// Result of applying a component revision at its owning runtime boundary. +type ConfigApplyOutcome int32 + +const ( + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_UNSPECIFIED ConfigApplyOutcome = 0 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_APPLIED ConfigApplyOutcome = 1 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_IGNORED_DUPLICATE ConfigApplyOutcome = 2 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_IGNORED_STALE ConfigApplyOutcome = 3 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_RETAINED_LOCAL_OVERRIDE ConfigApplyOutcome = 4 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_DEGRADED ConfigApplyOutcome = 5 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_FAILED_RETAINED_LAST_KNOWN_GOOD ConfigApplyOutcome = 6 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_FAILED_CLOSED ConfigApplyOutcome = 7 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_UNSUPPORTED ConfigApplyOutcome = 8 +) + +// Enum value maps for ConfigApplyOutcome. +var ( + ConfigApplyOutcome_name = map[int32]string{ + 0: "CONFIG_APPLY_OUTCOME_UNSPECIFIED", + 1: "CONFIG_APPLY_OUTCOME_APPLIED", + 2: "CONFIG_APPLY_OUTCOME_IGNORED_DUPLICATE", + 3: "CONFIG_APPLY_OUTCOME_IGNORED_STALE", + 4: "CONFIG_APPLY_OUTCOME_RETAINED_LOCAL_OVERRIDE", + 5: "CONFIG_APPLY_OUTCOME_DEGRADED", + 6: "CONFIG_APPLY_OUTCOME_FAILED_RETAINED_LAST_KNOWN_GOOD", + 7: "CONFIG_APPLY_OUTCOME_FAILED_CLOSED", + 8: "CONFIG_APPLY_OUTCOME_UNSUPPORTED", + } + ConfigApplyOutcome_value = map[string]int32{ + "CONFIG_APPLY_OUTCOME_UNSPECIFIED": 0, + "CONFIG_APPLY_OUTCOME_APPLIED": 1, + "CONFIG_APPLY_OUTCOME_IGNORED_DUPLICATE": 2, + "CONFIG_APPLY_OUTCOME_IGNORED_STALE": 3, + "CONFIG_APPLY_OUTCOME_RETAINED_LOCAL_OVERRIDE": 4, + "CONFIG_APPLY_OUTCOME_DEGRADED": 5, + "CONFIG_APPLY_OUTCOME_FAILED_RETAINED_LAST_KNOWN_GOOD": 6, + "CONFIG_APPLY_OUTCOME_FAILED_CLOSED": 7, + "CONFIG_APPLY_OUTCOME_UNSUPPORTED": 8, + } +) + +func (x ConfigApplyOutcome) Enum() *ConfigApplyOutcome { + p := new(ConfigApplyOutcome) + *p = x + return p +} + +func (x ConfigApplyOutcome) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConfigApplyOutcome) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[5].Descriptor() +} + +func (ConfigApplyOutcome) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[5] +} + +func (x ConfigApplyOutcome) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConfigApplyOutcome.Descriptor instead. +func (ConfigApplyOutcome) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{5} +} + +// Durable lifecycle of one desired-state update operation. +type ConfigUpdateOperationState int32 + +const ( + ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_UNSPECIFIED ConfigUpdateOperationState = 0 + ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_PENDING ConfigUpdateOperationState = 1 + ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_APPLIED ConfigUpdateOperationState = 2 + ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_INACTIVE ConfigUpdateOperationState = 3 + ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_FAILED ConfigUpdateOperationState = 4 + ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_SUPERSEDED ConfigUpdateOperationState = 5 + ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_CANCELLED ConfigUpdateOperationState = 6 +) + +// Enum value maps for ConfigUpdateOperationState. +var ( + ConfigUpdateOperationState_name = map[int32]string{ + 0: "CONFIG_UPDATE_OPERATION_STATE_UNSPECIFIED", + 1: "CONFIG_UPDATE_OPERATION_STATE_PENDING", + 2: "CONFIG_UPDATE_OPERATION_STATE_APPLIED", + 3: "CONFIG_UPDATE_OPERATION_STATE_INACTIVE", + 4: "CONFIG_UPDATE_OPERATION_STATE_FAILED", + 5: "CONFIG_UPDATE_OPERATION_STATE_SUPERSEDED", + 6: "CONFIG_UPDATE_OPERATION_STATE_CANCELLED", + } + ConfigUpdateOperationState_value = map[string]int32{ + "CONFIG_UPDATE_OPERATION_STATE_UNSPECIFIED": 0, + "CONFIG_UPDATE_OPERATION_STATE_PENDING": 1, + "CONFIG_UPDATE_OPERATION_STATE_APPLIED": 2, + "CONFIG_UPDATE_OPERATION_STATE_INACTIVE": 3, + "CONFIG_UPDATE_OPERATION_STATE_FAILED": 4, + "CONFIG_UPDATE_OPERATION_STATE_SUPERSEDED": 5, + "CONFIG_UPDATE_OPERATION_STATE_CANCELLED": 6, + } +) + +func (x ConfigUpdateOperationState) Enum() *ConfigUpdateOperationState { + p := new(ConfigUpdateOperationState) + *p = x + return p +} + +func (x ConfigUpdateOperationState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConfigUpdateOperationState) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[6].Descriptor() +} + +func (ConfigUpdateOperationState) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[6] +} + +func (x ConfigUpdateOperationState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConfigUpdateOperationState.Descriptor instead. +func (ConfigUpdateOperationState) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{6} +} + // Provider credential token grant configuration. // When present, the credential is obtained dynamically via OAuth2 grant when needed. type ProviderCredentialTokenGrantType int32 @@ -139,11 +531,11 @@ func (x ProviderCredentialTokenGrantType) String() string { } func (ProviderCredentialTokenGrantType) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[1].Descriptor() + return file_openshell_proto_enumTypes[7].Descriptor() } func (ProviderCredentialTokenGrantType) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[1] + return &file_openshell_proto_enumTypes[7] } func (x ProviderCredentialTokenGrantType) Number() protoreflect.EnumNumber { @@ -152,7 +544,7 @@ func (x ProviderCredentialTokenGrantType) Number() protoreflect.EnumNumber { // Deprecated: Use ProviderCredentialTokenGrantType.Descriptor instead. func (ProviderCredentialTokenGrantType) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{1} + return file_openshell_proto_rawDescGZIP(), []int{7} } type ProviderCredentialRefreshStrategy int32 @@ -200,11 +592,11 @@ func (x ProviderCredentialRefreshStrategy) String() string { } func (ProviderCredentialRefreshStrategy) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[2].Descriptor() + return file_openshell_proto_enumTypes[8].Descriptor() } func (ProviderCredentialRefreshStrategy) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[2] + return &file_openshell_proto_enumTypes[8] } func (x ProviderCredentialRefreshStrategy) Number() protoreflect.EnumNumber { @@ -213,7 +605,7 @@ func (x ProviderCredentialRefreshStrategy) Number() protoreflect.EnumNumber { // Deprecated: Use ProviderCredentialRefreshStrategy.Descriptor instead. func (ProviderCredentialRefreshStrategy) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{2} + return file_openshell_proto_rawDescGZIP(), []int{8} } // Stable provider profile categories used by clients for grouping and filtering. @@ -265,11 +657,11 @@ func (x ProviderProfileCategory) String() string { } func (ProviderProfileCategory) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[3].Descriptor() + return file_openshell_proto_enumTypes[9].Descriptor() } func (ProviderProfileCategory) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[3] + return &file_openshell_proto_enumTypes[9] } func (x ProviderProfileCategory) Number() protoreflect.EnumNumber { @@ -278,7 +670,7 @@ func (x ProviderProfileCategory) Number() protoreflect.EnumNumber { // Deprecated: Use ProviderProfileCategory.Descriptor instead. func (ProviderProfileCategory) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{3} + return file_openshell_proto_rawDescGZIP(), []int{9} } // Policy load status. @@ -327,11 +719,11 @@ func (x PolicyStatus) String() string { } func (PolicyStatus) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[4].Descriptor() + return file_openshell_proto_enumTypes[10].Descriptor() } func (PolicyStatus) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[4] + return &file_openshell_proto_enumTypes[10] } func (x PolicyStatus) Number() protoreflect.EnumNumber { @@ -340,7 +732,7 @@ func (x PolicyStatus) Number() protoreflect.EnumNumber { // Deprecated: Use PolicyStatus.Descriptor instead. func (PolicyStatus) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{4} + return file_openshell_proto_rawDescGZIP(), []int{10} } // Service status enum. @@ -380,11 +772,11 @@ func (x ServiceStatus) String() string { } func (ServiceStatus) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[5].Descriptor() + return file_openshell_proto_enumTypes[11].Descriptor() } func (ServiceStatus) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[5] + return &file_openshell_proto_enumTypes[11] } func (x ServiceStatus) Number() protoreflect.EnumNumber { @@ -393,7 +785,7 @@ func (x ServiceStatus) Number() protoreflect.EnumNumber { // Deprecated: Use ServiceStatus.Descriptor instead. func (ServiceStatus) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{5} + return file_openshell_proto_rawDescGZIP(), []int{11} } // Workspace-scoped role for members. @@ -430,11 +822,11 @@ func (x WorkspaceRole) String() string { } func (WorkspaceRole) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[6].Descriptor() + return file_openshell_proto_enumTypes[12].Descriptor() } func (WorkspaceRole) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[6] + return &file_openshell_proto_enumTypes[12] } func (x WorkspaceRole) Number() protoreflect.EnumNumber { @@ -443,7 +835,7 @@ func (x WorkspaceRole) Number() protoreflect.EnumNumber { // Deprecated: Use WorkspaceRole.Descriptor instead. func (WorkspaceRole) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{6} + return file_openshell_proto_rawDescGZIP(), []int{12} } // Stable recovery action for the most recent provider credential refresh @@ -489,11 +881,11 @@ func (x ProviderCredentialRefreshRecoveryAction) String() string { } func (ProviderCredentialRefreshRecoveryAction) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[7].Descriptor() + return file_openshell_proto_enumTypes[13].Descriptor() } func (ProviderCredentialRefreshRecoveryAction) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[7] + return &file_openshell_proto_enumTypes[13] } func (x ProviderCredentialRefreshRecoveryAction) Number() protoreflect.EnumNumber { @@ -502,7 +894,7 @@ func (x ProviderCredentialRefreshRecoveryAction) Number() protoreflect.EnumNumbe // Deprecated: Use ProviderCredentialRefreshRecoveryAction.Descriptor instead. func (ProviderCredentialRefreshRecoveryAction) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{7} + return file_openshell_proto_rawDescGZIP(), []int{13} } // Last observed network result for a configured external tool endpoint. @@ -563,11 +955,11 @@ func (x EndpointResult) String() string { } func (EndpointResult) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[8].Descriptor() + return file_openshell_proto_enumTypes[14].Descriptor() } func (EndpointResult) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[8] + return &file_openshell_proto_enumTypes[14] } func (x EndpointResult) Number() protoreflect.EnumNumber { @@ -576,7 +968,7 @@ func (x EndpointResult) Number() protoreflect.EnumNumber { // Deprecated: Use EndpointResult.Descriptor instead. func (EndpointResult) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{8} + return file_openshell_proto_rawDescGZIP(), []int{14} } // IssueSandboxToken request. Empty body; identity is established by the @@ -1569,9 +1961,12 @@ type SandboxSpec struct { // portable scratch login shell before persistence. Command []string `protobuf:"bytes,12,rep,name=command,proto3" json:"command,omitempty"` // Allocate a retained pseudo-terminal for the main process. - Tty bool `protobuf:"varint,13,opt,name=tty,proto3" json:"tty,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Tty bool `protobuf:"varint,13,opt,name=tty,proto3" json:"tty,omitempty"` + // Gateway-owned attachment identity, changed atomically with the provider set. + // Equality only: detach and reattach must not revive an older receipt. + ProviderAttachmentEpoch string `protobuf:"bytes,14,opt,name=provider_attachment_epoch,json=providerAttachmentEpoch,proto3" json:"provider_attachment_epoch,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SandboxSpec) Reset() { @@ -1660,6 +2055,13 @@ func (x *SandboxSpec) GetTty() bool { return false } +func (x *SandboxSpec) GetProviderAttachmentEpoch() string { + if x != nil { + return x.ProviderAttachmentEpoch + } + return "" +} + type ResourceRequirements struct { state protoimpl.MessageState `protogen:"open.v1"` // GPU requirements for the sandbox. Presence indicates a GPU request. @@ -3856,7 +4258,9 @@ type AttachSandboxProviderResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` // True when the provider was newly attached. False means it was already attached. - Attached bool `protobuf:"varint,2,opt,name=attached,proto3" json:"attached,omitempty"` + Attached bool `protobuf:"varint,2,opt,name=attached,proto3" json:"attached,omitempty"` + // Persisted intent; readiness requires current supervisor observations. + Receipt *ProviderMutationReceipt `protobuf:"bytes,3,opt,name=receipt,proto3" json:"receipt,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3905,12 +4309,21 @@ func (x *AttachSandboxProviderResponse) GetAttached() bool { return false } +func (x *AttachSandboxProviderResponse) GetReceipt() *ProviderMutationReceipt { + if x != nil { + return x.Receipt + } + return nil +} + // Detach provider from sandbox response. type DetachSandboxProviderResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` // True when the provider was removed. False means it was not attached. - Detached bool `protobuf:"varint,2,opt,name=detached,proto3" json:"detached,omitempty"` + Detached bool `protobuf:"varint,2,opt,name=detached,proto3" json:"detached,omitempty"` + // Revocation is complete only when this receipt reports REVOKED. + Receipt *ProviderMutationReceipt `protobuf:"bytes,3,opt,name=receipt,proto3" json:"receipt,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3959,28 +4372,43 @@ func (x *DetachSandboxProviderResponse) GetDetached() bool { return false } -// Delete sandbox response. -type DeleteSandboxResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *DetachSandboxProviderResponse) GetReceipt() *ProviderMutationReceipt { + if x != nil { + return x.Receipt + } + return nil } -func (x *DeleteSandboxResponse) Reset() { - *x = DeleteSandboxResponse{} +// Exact desired authority. Revisions are opaque identities, never ordered. +type ProviderDesiredIdentity struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + SandboxName string `protobuf:"bytes,2,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` + AttachmentEpoch string `protobuf:"bytes,3,opt,name=attachment_epoch,json=attachmentEpoch,proto3" json:"attachment_epoch,omitempty"` + // Empty for a detached provider. + ProviderId string `protobuf:"bytes,4,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` + ProviderResourceVersion uint64 `protobuf:"varint,5,opt,name=provider_resource_version,json=providerResourceVersion,proto3" json:"provider_resource_version,omitempty"` + ProviderEnvRevision uint64 `protobuf:"varint,6,opt,name=provider_env_revision,json=providerEnvRevision,proto3" json:"provider_env_revision,omitempty"` + ConfigRevision uint64 `protobuf:"varint,7,opt,name=config_revision,json=configRevision,proto3" json:"config_revision,omitempty"` + PolicyHash string `protobuf:"bytes,8,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderDesiredIdentity) Reset() { + *x = ProviderDesiredIdentity{} mi := &file_openshell_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DeleteSandboxResponse) String() string { +func (x *ProviderDesiredIdentity) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DeleteSandboxResponse) ProtoMessage() {} +func (*ProviderDesiredIdentity) ProtoMessage() {} -func (x *DeleteSandboxResponse) ProtoReflect() protoreflect.Message { +func (x *ProviderDesiredIdentity) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3992,33 +4420,981 @@ func (x *DeleteSandboxResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DeleteSandboxResponse.ProtoReflect.Descriptor instead. -func (*DeleteSandboxResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use ProviderDesiredIdentity.ProtoReflect.Descriptor instead. +func (*ProviderDesiredIdentity) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{54} } -func (x *DeleteSandboxResponse) GetDeleted() bool { +func (x *ProviderDesiredIdentity) GetSandboxId() string { if x != nil { - return x.Deleted + return x.SandboxId } - return false + return "" } -// Create SSH session request. -type CreateSshSessionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *ProviderDesiredIdentity) GetSandboxName() string { + if x != nil { + return x.SandboxName + } + return "" } -func (x *CreateSshSessionRequest) Reset() { - *x = CreateSshSessionRequest{} - mi := &file_openshell_proto_msgTypes[55] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} +func (x *ProviderDesiredIdentity) GetAttachmentEpoch() string { + if x != nil { + return x.AttachmentEpoch + } + return "" +} + +func (x *ProviderDesiredIdentity) GetProviderId() string { + if x != nil { + return x.ProviderId + } + return "" +} + +func (x *ProviderDesiredIdentity) GetProviderResourceVersion() uint64 { + if x != nil { + return x.ProviderResourceVersion + } + return 0 +} + +func (x *ProviderDesiredIdentity) GetProviderEnvRevision() uint64 { + if x != nil { + return x.ProviderEnvRevision + } + return 0 +} + +func (x *ProviderDesiredIdentity) GetConfigRevision() uint64 { + if x != nil { + return x.ConfigRevision + } + return 0 +} + +func (x *ProviderDesiredIdentity) GetPolicyHash() string { + if x != nil { + return x.PolicyHash + } + return "" +} + +// Identifies one component snapshot revision. Revisions are equality tokens, +// not members of one shared ordering domain. +type ConfigSnapshotRevision struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Component: + // + // *ConfigSnapshotRevision_SandboxConfig + // *ConfigSnapshotRevision_ProviderEnvironment + // *ConfigSnapshotRevision_ProviderTarget + Component isConfigSnapshotRevision_Component `protobuf_oneof:"component"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigSnapshotRevision) Reset() { + *x = ConfigSnapshotRevision{} + mi := &file_openshell_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigSnapshotRevision) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigSnapshotRevision) ProtoMessage() {} + +func (x *ConfigSnapshotRevision) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[55] + 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 ConfigSnapshotRevision.ProtoReflect.Descriptor instead. +func (*ConfigSnapshotRevision) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{55} +} + +func (x *ConfigSnapshotRevision) GetComponent() isConfigSnapshotRevision_Component { + if x != nil { + return x.Component + } + return nil +} + +func (x *ConfigSnapshotRevision) GetSandboxConfig() *SandboxConfigRevision { + if x != nil { + if x, ok := x.Component.(*ConfigSnapshotRevision_SandboxConfig); ok { + return x.SandboxConfig + } + } + return nil +} + +func (x *ConfigSnapshotRevision) GetProviderEnvironment() uint64 { + if x != nil { + if x, ok := x.Component.(*ConfigSnapshotRevision_ProviderEnvironment); ok { + return x.ProviderEnvironment + } + } + return 0 +} + +func (x *ConfigSnapshotRevision) GetProviderTarget() *ProviderDesiredIdentity { + if x != nil { + if x, ok := x.Component.(*ConfigSnapshotRevision_ProviderTarget); ok { + return x.ProviderTarget + } + } + return nil +} + +type isConfigSnapshotRevision_Component interface { + isConfigSnapshotRevision_Component() +} + +type ConfigSnapshotRevision_SandboxConfig struct { + SandboxConfig *SandboxConfigRevision `protobuf:"bytes,1,opt,name=sandbox_config,json=sandboxConfig,proto3,oneof"` +} + +type ConfigSnapshotRevision_ProviderEnvironment struct { + ProviderEnvironment uint64 `protobuf:"varint,2,opt,name=provider_environment,json=providerEnvironment,proto3,oneof"` +} + +type ConfigSnapshotRevision_ProviderTarget struct { + // Complete desired authority for one sandbox-scoped provider mutation. + ProviderTarget *ProviderDesiredIdentity `protobuf:"bytes,3,opt,name=provider_target,json=providerTarget,proto3,oneof"` +} + +func (*ConfigSnapshotRevision_SandboxConfig) isConfigSnapshotRevision_Component() {} + +func (*ConfigSnapshotRevision_ProviderEnvironment) isConfigSnapshotRevision_Component() {} + +func (*ConfigSnapshotRevision_ProviderTarget) isConfigSnapshotRevision_Component() {} + +// Identity needed to correlate effective sandbox configuration with the +// policy-history row whose apply status the gateway records. +type SandboxConfigRevision struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConfigRevision uint64 `protobuf:"varint,1,opt,name=config_revision,json=configRevision,proto3" json:"config_revision,omitempty"` + PolicyVersion uint32 `protobuf:"varint,2,opt,name=policy_version,json=policyVersion,proto3" json:"policy_version,omitempty"` + PolicySource sandboxv1.PolicySource `protobuf:"varint,3,opt,name=policy_source,json=policySource,proto3,enum=openshell.sandbox.v1.PolicySource" json:"policy_source,omitempty"` + GlobalPolicyVersion uint32 `protobuf:"varint,4,opt,name=global_policy_version,json=globalPolicyVersion,proto3" json:"global_policy_version,omitempty"` + // Monotonic revision of the sandbox-scoped settings row. This disambiguates + // setting operations whose effective config fingerprint is equality-only. + SettingsRevision uint64 `protobuf:"varint,5,opt,name=settings_revision,json=settingsRevision,proto3" json:"settings_revision,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxConfigRevision) Reset() { + *x = SandboxConfigRevision{} + mi := &file_openshell_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxConfigRevision) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxConfigRevision) ProtoMessage() {} + +func (x *SandboxConfigRevision) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[56] + 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 SandboxConfigRevision.ProtoReflect.Descriptor instead. +func (*SandboxConfigRevision) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{56} +} + +func (x *SandboxConfigRevision) GetConfigRevision() uint64 { + if x != nil { + return x.ConfigRevision + } + return 0 +} + +func (x *SandboxConfigRevision) GetPolicyVersion() uint32 { + if x != nil { + return x.PolicyVersion + } + return 0 +} + +func (x *SandboxConfigRevision) GetPolicySource() sandboxv1.PolicySource { + if x != nil { + return x.PolicySource + } + return sandboxv1.PolicySource(0) +} + +func (x *SandboxConfigRevision) GetGlobalPolicyVersion() uint32 { + if x != nil { + return x.GlobalPolicyVersion + } + return 0 +} + +func (x *SandboxConfigRevision) GetSettingsRevision() uint64 { + if x != nil { + return x.SettingsRevision + } + return 0 +} + +// Durable progress for one sandbox-scoped desired-state mutation. Snapshot +// contents and credentials are never stored in this resource. +type ConfigUpdateOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + OperationId string `protobuf:"bytes,1,opt,name=operation_id,json=operationId,proto3" json:"operation_id,omitempty"` + SandboxId string `protobuf:"bytes,2,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + Component ConfigComponent `protobuf:"varint,3,opt,name=component,proto3,enum=openshell.v1.ConfigComponent" json:"component,omitempty"` + TargetRevision *ConfigSnapshotRevision `protobuf:"bytes,4,opt,name=target_revision,json=targetRevision,proto3" json:"target_revision,omitempty"` + State ConfigUpdateOperationState `protobuf:"varint,5,opt,name=state,proto3,enum=openshell.v1.ConfigUpdateOperationState" json:"state,omitempty"` + Outcome ConfigApplyOutcome `protobuf:"varint,6,opt,name=outcome,proto3,enum=openshell.v1.ConfigApplyOutcome" json:"outcome,omitempty"` + SanitizedError string `protobuf:"bytes,7,opt,name=sanitized_error,json=sanitizedError,proto3" json:"sanitized_error,omitempty"` + CreatedAtMs int64 `protobuf:"varint,8,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` + UpdatedAtMs int64 `protobuf:"varint,9,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` + CompletedAtMs int64 `protobuf:"varint,10,opt,name=completed_at_ms,json=completedAtMs,proto3" json:"completed_at_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigUpdateOperation) Reset() { + *x = ConfigUpdateOperation{} + mi := &file_openshell_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigUpdateOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigUpdateOperation) ProtoMessage() {} + +func (x *ConfigUpdateOperation) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[57] + 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 ConfigUpdateOperation.ProtoReflect.Descriptor instead. +func (*ConfigUpdateOperation) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{57} +} + +func (x *ConfigUpdateOperation) GetOperationId() string { + if x != nil { + return x.OperationId + } + return "" +} + +func (x *ConfigUpdateOperation) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ConfigUpdateOperation) GetComponent() ConfigComponent { + if x != nil { + return x.Component + } + return ConfigComponent_CONFIG_COMPONENT_UNSPECIFIED +} + +func (x *ConfigUpdateOperation) GetTargetRevision() *ConfigSnapshotRevision { + if x != nil { + return x.TargetRevision + } + return nil +} + +func (x *ConfigUpdateOperation) GetState() ConfigUpdateOperationState { + if x != nil { + return x.State + } + return ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_UNSPECIFIED +} + +func (x *ConfigUpdateOperation) GetOutcome() ConfigApplyOutcome { + if x != nil { + return x.Outcome + } + return ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_UNSPECIFIED +} + +func (x *ConfigUpdateOperation) GetSanitizedError() string { + if x != nil { + return x.SanitizedError + } + return "" +} + +func (x *ConfigUpdateOperation) GetCreatedAtMs() int64 { + if x != nil { + return x.CreatedAtMs + } + return 0 +} + +func (x *ConfigUpdateOperation) GetUpdatedAtMs() int64 { + if x != nil { + return x.UpdatedAtMs + } + return 0 +} + +func (x *ConfigUpdateOperation) GetCompletedAtMs() int64 { + if x != nil { + return x.CompletedAtMs + } + return 0 +} + +// Immutable, secret-free record of one sandbox's intended provider mutation. +type ProviderMutationReceipt struct { + state protoimpl.MessageState `protogen:"open.v1"` + ReceiptId string `protobuf:"bytes,1,opt,name=receipt_id,json=receiptId,proto3" json:"receipt_id,omitempty"` + // Shared by all sandbox receipts from one provider update. + MutationId string `protobuf:"bytes,2,opt,name=mutation_id,json=mutationId,proto3" json:"mutation_id,omitempty"` + ProviderName string `protobuf:"bytes,3,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` + Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` + Kind ProviderMutationKind `protobuf:"varint,5,opt,name=kind,proto3,enum=openshell.v1.ProviderMutationKind" json:"kind,omitempty"` + Desired *ProviderDesiredIdentity `protobuf:"bytes,6,opt,name=desired,proto3" json:"desired,omitempty"` + PersistedAtMs int64 `protobuf:"varint,7,opt,name=persisted_at_ms,json=persistedAtMs,proto3" json:"persisted_at_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderMutationReceipt) Reset() { + *x = ProviderMutationReceipt{} + mi := &file_openshell_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderMutationReceipt) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderMutationReceipt) ProtoMessage() {} + +func (x *ProviderMutationReceipt) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[58] + 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 ProviderMutationReceipt.ProtoReflect.Descriptor instead. +func (*ProviderMutationReceipt) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{58} +} + +func (x *ProviderMutationReceipt) GetReceiptId() string { + if x != nil { + return x.ReceiptId + } + return "" +} + +func (x *ProviderMutationReceipt) GetMutationId() string { + if x != nil { + return x.MutationId + } + return "" +} + +func (x *ProviderMutationReceipt) GetProviderName() string { + if x != nil { + return x.ProviderName + } + return "" +} + +func (x *ProviderMutationReceipt) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +func (x *ProviderMutationReceipt) GetKind() ProviderMutationKind { + if x != nil { + return x.Kind + } + return ProviderMutationKind_PROVIDER_MUTATION_KIND_UNSPECIFIED +} + +func (x *ProviderMutationReceipt) GetDesired() *ProviderDesiredIdentity { + if x != nil { + return x.Desired + } + return nil +} + +func (x *ProviderMutationReceipt) GetPersistedAtMs() int64 { + if x != nil { + return x.PersistedAtMs + } + return 0 +} + +// Installed state reported by the current supervisor. Process installation is +// acknowledged by the authenticated sandbox boundary after replacing its +// environment for future process launches. +type ProviderReadinessObservation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Gateway-issued identifier from the current ConnectSupervisor response. + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + // Monotonic only within this connection; unrelated to revision fingerprints. + Sequence uint64 `protobuf:"varint,2,opt,name=sequence,proto3" json:"sequence,omitempty"` + AttachmentEpoch string `protobuf:"bytes,3,opt,name=attachment_epoch,json=attachmentEpoch,proto3" json:"attachment_epoch,omitempty"` + ProviderEnvRevision uint64 `protobuf:"varint,4,opt,name=provider_env_revision,json=providerEnvRevision,proto3" json:"provider_env_revision,omitempty"` + ConfigRevision uint64 `protobuf:"varint,5,opt,name=config_revision,json=configRevision,proto3" json:"config_revision,omitempty"` + PolicyHash string `protobuf:"bytes,6,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + CredentialsInstalled bool `protobuf:"varint,7,opt,name=credentials_installed,json=credentialsInstalled,proto3" json:"credentials_installed,omitempty"` + PolicyActive bool `protobuf:"varint,8,opt,name=policy_active,json=policyActive,proto3" json:"policy_active,omitempty"` + LaunchEnvironmentInstalled bool `protobuf:"varint,9,opt,name=launch_environment_installed,json=launchEnvironmentInstalled,proto3" json:"launch_environment_installed,omitempty"` + ProcessInstanceId string `protobuf:"bytes,10,opt,name=process_instance_id,json=processInstanceId,proto3" json:"process_instance_id,omitempty"` + Reason ProviderReadinessReason `protobuf:"varint,11,opt,name=reason,proto3,enum=openshell.v1.ProviderReadinessReason" json:"reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderReadinessObservation) Reset() { + *x = ProviderReadinessObservation{} + mi := &file_openshell_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderReadinessObservation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderReadinessObservation) ProtoMessage() {} + +func (x *ProviderReadinessObservation) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[59] + 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 ProviderReadinessObservation.ProtoReflect.Descriptor instead. +func (*ProviderReadinessObservation) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{59} +} + +func (x *ProviderReadinessObservation) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *ProviderReadinessObservation) GetSequence() uint64 { + if x != nil { + return x.Sequence + } + return 0 +} + +func (x *ProviderReadinessObservation) GetAttachmentEpoch() string { + if x != nil { + return x.AttachmentEpoch + } + return "" +} + +func (x *ProviderReadinessObservation) GetProviderEnvRevision() uint64 { + if x != nil { + return x.ProviderEnvRevision + } + return 0 +} + +func (x *ProviderReadinessObservation) GetConfigRevision() uint64 { + if x != nil { + return x.ConfigRevision + } + return 0 +} + +func (x *ProviderReadinessObservation) GetPolicyHash() string { + if x != nil { + return x.PolicyHash + } + return "" +} + +func (x *ProviderReadinessObservation) GetCredentialsInstalled() bool { + if x != nil { + return x.CredentialsInstalled + } + return false +} + +func (x *ProviderReadinessObservation) GetPolicyActive() bool { + if x != nil { + return x.PolicyActive + } + return false +} + +func (x *ProviderReadinessObservation) GetLaunchEnvironmentInstalled() bool { + if x != nil { + return x.LaunchEnvironmentInstalled + } + return false +} + +func (x *ProviderReadinessObservation) GetProcessInstanceId() string { + if x != nil { + return x.ProcessInstanceId + } + return "" +} + +func (x *ProviderReadinessObservation) GetReason() ProviderReadinessReason { + if x != nil { + return x.Reason + } + return ProviderReadinessReason_PROVIDER_READINESS_REASON_UNSPECIFIED +} + +// Operator view of desired and observed state; contains no credential material. +type ProviderReadinessStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Receipt *ProviderMutationReceipt `protobuf:"bytes,1,opt,name=receipt,proto3" json:"receipt,omitempty"` + State ProviderReadinessState `protobuf:"varint,2,opt,name=state,proto3,enum=openshell.v1.ProviderReadinessState" json:"state,omitempty"` + Reason ProviderReadinessReason `protobuf:"varint,3,opt,name=reason,proto3,enum=openshell.v1.ProviderReadinessReason" json:"reason,omitempty"` + Observed *ProviderReadinessObservation `protobuf:"bytes,4,opt,name=observed,proto3" json:"observed,omitempty"` + NetworkInstanceId string `protobuf:"bytes,5,opt,name=network_instance_id,json=networkInstanceId,proto3" json:"network_instance_id,omitempty"` + ObservedAtMs int64 `protobuf:"varint,6,opt,name=observed_at_ms,json=observedAtMs,proto3" json:"observed_at_ms,omitempty"` + EvaluatedAtMs int64 `protobuf:"varint,7,opt,name=evaluated_at_ms,json=evaluatedAtMs,proto3" json:"evaluated_at_ms,omitempty"` + // Durable operation for the receipt, including its terminal apply outcome. + Operation *ConfigUpdateOperation `protobuf:"bytes,8,opt,name=operation,proto3" json:"operation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderReadinessStatus) Reset() { + *x = ProviderReadinessStatus{} + mi := &file_openshell_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderReadinessStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderReadinessStatus) ProtoMessage() {} + +func (x *ProviderReadinessStatus) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[60] + 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 ProviderReadinessStatus.ProtoReflect.Descriptor instead. +func (*ProviderReadinessStatus) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{60} +} + +func (x *ProviderReadinessStatus) GetReceipt() *ProviderMutationReceipt { + if x != nil { + return x.Receipt + } + return nil +} + +func (x *ProviderReadinessStatus) GetState() ProviderReadinessState { + if x != nil { + return x.State + } + return ProviderReadinessState_PROVIDER_READINESS_STATE_UNSPECIFIED +} + +func (x *ProviderReadinessStatus) GetReason() ProviderReadinessReason { + if x != nil { + return x.Reason + } + return ProviderReadinessReason_PROVIDER_READINESS_REASON_UNSPECIFIED +} + +func (x *ProviderReadinessStatus) GetObserved() *ProviderReadinessObservation { + if x != nil { + return x.Observed + } + return nil +} + +func (x *ProviderReadinessStatus) GetNetworkInstanceId() string { + if x != nil { + return x.NetworkInstanceId + } + return "" +} + +func (x *ProviderReadinessStatus) GetObservedAtMs() int64 { + if x != nil { + return x.ObservedAtMs + } + return 0 +} + +func (x *ProviderReadinessStatus) GetEvaluatedAtMs() int64 { + if x != nil { + return x.EvaluatedAtMs + } + return 0 +} + +func (x *ProviderReadinessStatus) GetOperation() *ConfigUpdateOperation { + if x != nil { + return x.Operation + } + return nil +} + +// Query an immutable receipt, or reconstruct the current desired state when +// receipt_id is empty. The sandbox identity must match the receipt. +type GetSandboxProviderStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` + ProviderName string `protobuf:"bytes,2,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` + ReceiptId string `protobuf:"bytes,3,opt,name=receipt_id,json=receiptId,proto3" json:"receipt_id,omitempty"` + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxProviderStatusRequest) Reset() { + *x = GetSandboxProviderStatusRequest{} + mi := &file_openshell_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxProviderStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxProviderStatusRequest) ProtoMessage() {} + +func (x *GetSandboxProviderStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[61] + 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 GetSandboxProviderStatusRequest.ProtoReflect.Descriptor instead. +func (*GetSandboxProviderStatusRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{61} +} + +func (x *GetSandboxProviderStatusRequest) GetSandboxName() string { + if x != nil { + return x.SandboxName + } + return "" +} + +func (x *GetSandboxProviderStatusRequest) GetProviderName() string { + if x != nil { + return x.ProviderName + } + return "" +} + +func (x *GetSandboxProviderStatusRequest) GetReceiptId() string { + if x != nil { + return x.ReceiptId + } + return "" +} + +func (x *GetSandboxProviderStatusRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +type GetSandboxProviderStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status *ProviderReadinessStatus `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxProviderStatusResponse) Reset() { + *x = GetSandboxProviderStatusResponse{} + mi := &file_openshell_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxProviderStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxProviderStatusResponse) ProtoMessage() {} + +func (x *GetSandboxProviderStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[62] + 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 GetSandboxProviderStatusResponse.ProtoReflect.Descriptor instead. +func (*GetSandboxProviderStatusResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{62} +} + +func (x *GetSandboxProviderStatusResponse) GetStatus() *ProviderReadinessStatus { + if x != nil { + return x.Status + } + return nil +} + +// Installation evidence from the authenticated supervisor for this sandbox. +// A caller-provided instance identifier alone never establishes authority. +type ReportProviderReadinessRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + Observation *ProviderReadinessObservation `protobuf:"bytes,2,opt,name=observation,proto3" json:"observation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportProviderReadinessRequest) Reset() { + *x = ReportProviderReadinessRequest{} + mi := &file_openshell_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportProviderReadinessRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportProviderReadinessRequest) ProtoMessage() {} + +func (x *ReportProviderReadinessRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[63] + 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 ReportProviderReadinessRequest.ProtoReflect.Descriptor instead. +func (*ReportProviderReadinessRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{63} +} + +func (x *ReportProviderReadinessRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ReportProviderReadinessRequest) GetObservation() *ProviderReadinessObservation { + if x != nil { + return x.Observation + } + return nil +} + +// Acknowledges accepted evidence without granting a separate session authority. +// Identical retries do not extend the evidence's original acceptance time. +type ReportProviderReadinessResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AcceptedSequence uint64 `protobuf:"varint,1,opt,name=accepted_sequence,json=acceptedSequence,proto3" json:"accepted_sequence,omitempty"` + ReportIntervalSeconds uint32 `protobuf:"varint,2,opt,name=report_interval_seconds,json=reportIntervalSeconds,proto3" json:"report_interval_seconds,omitempty"` + ObservationTtlSeconds uint32 `protobuf:"varint,3,opt,name=observation_ttl_seconds,json=observationTtlSeconds,proto3" json:"observation_ttl_seconds,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportProviderReadinessResponse) Reset() { + *x = ReportProviderReadinessResponse{} + mi := &file_openshell_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportProviderReadinessResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportProviderReadinessResponse) ProtoMessage() {} + +func (x *ReportProviderReadinessResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[64] + 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 ReportProviderReadinessResponse.ProtoReflect.Descriptor instead. +func (*ReportProviderReadinessResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{64} +} + +func (x *ReportProviderReadinessResponse) GetAcceptedSequence() uint64 { + if x != nil { + return x.AcceptedSequence + } + return 0 +} + +func (x *ReportProviderReadinessResponse) GetReportIntervalSeconds() uint32 { + if x != nil { + return x.ReportIntervalSeconds + } + return 0 +} + +func (x *ReportProviderReadinessResponse) GetObservationTtlSeconds() uint32 { + if x != nil { + return x.ObservationTtlSeconds + } + return 0 +} + +// Delete sandbox response. +type DeleteSandboxResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteSandboxResponse) Reset() { + *x = DeleteSandboxResponse{} + mi := &file_openshell_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteSandboxResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteSandboxResponse) ProtoMessage() {} + +func (x *DeleteSandboxResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[65] + 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 DeleteSandboxResponse.ProtoReflect.Descriptor instead. +func (*DeleteSandboxResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{65} +} + +func (x *DeleteSandboxResponse) GetDeleted() bool { + if x != nil { + return x.Deleted + } + return false +} + +// Create SSH session request. +type CreateSshSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox id. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSshSessionRequest) Reset() { + *x = CreateSshSessionRequest{} + mi := &file_openshell_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} func (x *CreateSshSessionRequest) String() string { return protoimpl.X.MessageStringOf(x) @@ -4027,7 +5403,7 @@ func (x *CreateSshSessionRequest) String() string { func (*CreateSshSessionRequest) ProtoMessage() {} func (x *CreateSshSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[55] + mi := &file_openshell_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4040,7 +5416,7 @@ func (x *CreateSshSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateSshSessionRequest.ProtoReflect.Descriptor instead. func (*CreateSshSessionRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{55} + return file_openshell_proto_rawDescGZIP(), []int{66} } func (x *CreateSshSessionRequest) GetSandboxId() string { @@ -4083,7 +5459,7 @@ type CreateSshSessionResponse struct { func (x *CreateSshSessionResponse) Reset() { *x = CreateSshSessionResponse{} - mi := &file_openshell_proto_msgTypes[56] + mi := &file_openshell_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4095,7 +5471,7 @@ func (x *CreateSshSessionResponse) String() string { func (*CreateSshSessionResponse) ProtoMessage() {} func (x *CreateSshSessionResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[56] + mi := &file_openshell_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4108,7 +5484,7 @@ func (x *CreateSshSessionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateSshSessionResponse.ProtoReflect.Descriptor instead. func (*CreateSshSessionResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{56} + return file_openshell_proto_rawDescGZIP(), []int{67} } func (x *CreateSshSessionResponse) GetSandboxId() string { @@ -4179,7 +5555,7 @@ type ExposeServiceRequest struct { func (x *ExposeServiceRequest) Reset() { *x = ExposeServiceRequest{} - mi := &file_openshell_proto_msgTypes[57] + mi := &file_openshell_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4191,7 +5567,7 @@ func (x *ExposeServiceRequest) String() string { func (*ExposeServiceRequest) ProtoMessage() {} func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[57] + mi := &file_openshell_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4204,7 +5580,7 @@ func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead. func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{57} + return file_openshell_proto_rawDescGZIP(), []int{68} } func (x *ExposeServiceRequest) GetSandbox() string { @@ -4257,7 +5633,7 @@ type GetServiceRequest struct { func (x *GetServiceRequest) Reset() { *x = GetServiceRequest{} - mi := &file_openshell_proto_msgTypes[58] + mi := &file_openshell_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4269,7 +5645,7 @@ func (x *GetServiceRequest) String() string { func (*GetServiceRequest) ProtoMessage() {} func (x *GetServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[58] + mi := &file_openshell_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4282,7 +5658,7 @@ func (x *GetServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetServiceRequest.ProtoReflect.Descriptor instead. func (*GetServiceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{58} + return file_openshell_proto_rawDescGZIP(), []int{69} } func (x *GetServiceRequest) GetSandbox() string { @@ -4325,7 +5701,7 @@ type ListServicesRequest struct { func (x *ListServicesRequest) Reset() { *x = ListServicesRequest{} - mi := &file_openshell_proto_msgTypes[59] + mi := &file_openshell_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4337,7 +5713,7 @@ func (x *ListServicesRequest) String() string { func (*ListServicesRequest) ProtoMessage() {} func (x *ListServicesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[59] + mi := &file_openshell_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4350,7 +5726,7 @@ func (x *ListServicesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListServicesRequest.ProtoReflect.Descriptor instead. func (*ListServicesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{59} + return file_openshell_proto_rawDescGZIP(), []int{70} } func (x *ListServicesRequest) GetSandbox() string { @@ -4393,7 +5769,7 @@ type ListServicesResponse struct { func (x *ListServicesResponse) Reset() { *x = ListServicesResponse{} - mi := &file_openshell_proto_msgTypes[60] + mi := &file_openshell_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4405,7 +5781,7 @@ func (x *ListServicesResponse) String() string { func (*ListServicesResponse) ProtoMessage() {} func (x *ListServicesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[60] + mi := &file_openshell_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4418,7 +5794,7 @@ func (x *ListServicesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListServicesResponse.ProtoReflect.Descriptor instead. func (*ListServicesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{60} + return file_openshell_proto_rawDescGZIP(), []int{71} } func (x *ListServicesResponse) GetServices() []*ServiceEndpointResponse { @@ -4450,7 +5826,7 @@ type DeleteServiceRequest struct { func (x *DeleteServiceRequest) Reset() { *x = DeleteServiceRequest{} - mi := &file_openshell_proto_msgTypes[61] + mi := &file_openshell_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4462,7 +5838,7 @@ func (x *DeleteServiceRequest) String() string { func (*DeleteServiceRequest) ProtoMessage() {} func (x *DeleteServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[61] + mi := &file_openshell_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4475,7 +5851,7 @@ func (x *DeleteServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteServiceRequest.ProtoReflect.Descriptor instead. func (*DeleteServiceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{61} + return file_openshell_proto_rawDescGZIP(), []int{72} } func (x *DeleteServiceRequest) GetSandbox() string { @@ -4510,7 +5886,7 @@ type DeleteServiceResponse struct { func (x *DeleteServiceResponse) Reset() { *x = DeleteServiceResponse{} - mi := &file_openshell_proto_msgTypes[62] + mi := &file_openshell_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4522,7 +5898,7 @@ func (x *DeleteServiceResponse) String() string { func (*DeleteServiceResponse) ProtoMessage() {} func (x *DeleteServiceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[62] + mi := &file_openshell_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4535,7 +5911,7 @@ func (x *DeleteServiceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteServiceResponse.ProtoReflect.Descriptor instead. func (*DeleteServiceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{62} + return file_openshell_proto_rawDescGZIP(), []int{73} } func (x *DeleteServiceResponse) GetDeleted() bool { @@ -4566,7 +5942,7 @@ type ServiceEndpoint struct { func (x *ServiceEndpoint) Reset() { *x = ServiceEndpoint{} - mi := &file_openshell_proto_msgTypes[63] + mi := &file_openshell_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4578,7 +5954,7 @@ func (x *ServiceEndpoint) String() string { func (*ServiceEndpoint) ProtoMessage() {} func (x *ServiceEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[63] + mi := &file_openshell_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4591,7 +5967,7 @@ func (x *ServiceEndpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceEndpoint.ProtoReflect.Descriptor instead. func (*ServiceEndpoint) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{63} + return file_openshell_proto_rawDescGZIP(), []int{74} } func (x *ServiceEndpoint) GetMetadata() *datamodelv1.ObjectMeta { @@ -4647,7 +6023,7 @@ type ServiceEndpointResponse struct { func (x *ServiceEndpointResponse) Reset() { *x = ServiceEndpointResponse{} - mi := &file_openshell_proto_msgTypes[64] + mi := &file_openshell_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4659,7 +6035,7 @@ func (x *ServiceEndpointResponse) String() string { func (*ServiceEndpointResponse) ProtoMessage() {} func (x *ServiceEndpointResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[64] + mi := &file_openshell_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4672,7 +6048,7 @@ func (x *ServiceEndpointResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceEndpointResponse.ProtoReflect.Descriptor instead. func (*ServiceEndpointResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{64} + return file_openshell_proto_rawDescGZIP(), []int{75} } func (x *ServiceEndpointResponse) GetEndpoint() *ServiceEndpoint { @@ -4700,7 +6076,7 @@ type RevokeSshSessionRequest struct { func (x *RevokeSshSessionRequest) Reset() { *x = RevokeSshSessionRequest{} - mi := &file_openshell_proto_msgTypes[65] + mi := &file_openshell_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4712,7 +6088,7 @@ func (x *RevokeSshSessionRequest) String() string { func (*RevokeSshSessionRequest) ProtoMessage() {} func (x *RevokeSshSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[65] + mi := &file_openshell_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4725,7 +6101,7 @@ func (x *RevokeSshSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeSshSessionRequest.ProtoReflect.Descriptor instead. func (*RevokeSshSessionRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{65} + return file_openshell_proto_rawDescGZIP(), []int{76} } func (x *RevokeSshSessionRequest) GetToken() string { @@ -4746,7 +6122,7 @@ type RevokeSshSessionResponse struct { func (x *RevokeSshSessionResponse) Reset() { *x = RevokeSshSessionResponse{} - mi := &file_openshell_proto_msgTypes[66] + mi := &file_openshell_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4758,7 +6134,7 @@ func (x *RevokeSshSessionResponse) String() string { func (*RevokeSshSessionResponse) ProtoMessage() {} func (x *RevokeSshSessionResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[66] + mi := &file_openshell_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4771,7 +6147,7 @@ func (x *RevokeSshSessionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeSshSessionResponse.ProtoReflect.Descriptor instead. func (*RevokeSshSessionResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{66} + return file_openshell_proto_rawDescGZIP(), []int{77} } func (x *RevokeSshSessionResponse) GetRevoked() bool { @@ -4814,7 +6190,7 @@ type ExecSandboxRequest struct { func (x *ExecSandboxRequest) Reset() { *x = ExecSandboxRequest{} - mi := &file_openshell_proto_msgTypes[67] + mi := &file_openshell_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4826,7 +6202,7 @@ func (x *ExecSandboxRequest) String() string { func (*ExecSandboxRequest) ProtoMessage() {} func (x *ExecSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[67] + mi := &file_openshell_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4839,7 +6215,7 @@ func (x *ExecSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxRequest.ProtoReflect.Descriptor instead. func (*ExecSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{67} + return file_openshell_proto_rawDescGZIP(), []int{78} } func (x *ExecSandboxRequest) GetSandboxId() string { @@ -4922,7 +6298,7 @@ type ExecSandboxStdout struct { func (x *ExecSandboxStdout) Reset() { *x = ExecSandboxStdout{} - mi := &file_openshell_proto_msgTypes[68] + mi := &file_openshell_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4934,7 +6310,7 @@ func (x *ExecSandboxStdout) String() string { func (*ExecSandboxStdout) ProtoMessage() {} func (x *ExecSandboxStdout) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[68] + mi := &file_openshell_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4947,7 +6323,7 @@ func (x *ExecSandboxStdout) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxStdout.ProtoReflect.Descriptor instead. func (*ExecSandboxStdout) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{68} + return file_openshell_proto_rawDescGZIP(), []int{79} } func (x *ExecSandboxStdout) GetData() []byte { @@ -4967,7 +6343,7 @@ type ExecSandboxStderr struct { func (x *ExecSandboxStderr) Reset() { *x = ExecSandboxStderr{} - mi := &file_openshell_proto_msgTypes[69] + mi := &file_openshell_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4979,7 +6355,7 @@ func (x *ExecSandboxStderr) String() string { func (*ExecSandboxStderr) ProtoMessage() {} func (x *ExecSandboxStderr) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[69] + mi := &file_openshell_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4992,7 +6368,7 @@ func (x *ExecSandboxStderr) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxStderr.ProtoReflect.Descriptor instead. func (*ExecSandboxStderr) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{69} + return file_openshell_proto_rawDescGZIP(), []int{80} } func (x *ExecSandboxStderr) GetData() []byte { @@ -5012,7 +6388,7 @@ type ExecSandboxExit struct { func (x *ExecSandboxExit) Reset() { *x = ExecSandboxExit{} - mi := &file_openshell_proto_msgTypes[70] + mi := &file_openshell_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5024,7 +6400,7 @@ func (x *ExecSandboxExit) String() string { func (*ExecSandboxExit) ProtoMessage() {} func (x *ExecSandboxExit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[70] + mi := &file_openshell_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5037,7 +6413,7 @@ func (x *ExecSandboxExit) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxExit.ProtoReflect.Descriptor instead. func (*ExecSandboxExit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{70} + return file_openshell_proto_rawDescGZIP(), []int{81} } func (x *ExecSandboxExit) GetExitCode() int32 { @@ -5062,7 +6438,7 @@ type ExecSandboxEvent struct { func (x *ExecSandboxEvent) Reset() { *x = ExecSandboxEvent{} - mi := &file_openshell_proto_msgTypes[71] + mi := &file_openshell_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5074,7 +6450,7 @@ func (x *ExecSandboxEvent) String() string { func (*ExecSandboxEvent) ProtoMessage() {} func (x *ExecSandboxEvent) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[71] + mi := &file_openshell_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5087,7 +6463,7 @@ func (x *ExecSandboxEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxEvent.ProtoReflect.Descriptor instead. func (*ExecSandboxEvent) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{71} + return file_openshell_proto_rawDescGZIP(), []int{82} } func (x *ExecSandboxEvent) GetPayload() isExecSandboxEvent_Payload { @@ -5169,7 +6545,7 @@ type TcpForwardInit struct { func (x *TcpForwardInit) Reset() { *x = TcpForwardInit{} - mi := &file_openshell_proto_msgTypes[72] + mi := &file_openshell_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5181,7 +6557,7 @@ func (x *TcpForwardInit) String() string { func (*TcpForwardInit) ProtoMessage() {} func (x *TcpForwardInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[72] + mi := &file_openshell_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5194,7 +6570,7 @@ func (x *TcpForwardInit) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpForwardInit.ProtoReflect.Descriptor instead. func (*TcpForwardInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{72} + return file_openshell_proto_rawDescGZIP(), []int{83} } func (x *TcpForwardInit) GetSandboxId() string { @@ -5273,7 +6649,7 @@ type TcpForwardFrame struct { func (x *TcpForwardFrame) Reset() { *x = TcpForwardFrame{} - mi := &file_openshell_proto_msgTypes[73] + mi := &file_openshell_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5285,7 +6661,7 @@ func (x *TcpForwardFrame) String() string { func (*TcpForwardFrame) ProtoMessage() {} func (x *TcpForwardFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[73] + mi := &file_openshell_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5298,7 +6674,7 @@ func (x *TcpForwardFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpForwardFrame.ProtoReflect.Descriptor instead. func (*TcpForwardFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{73} + return file_openshell_proto_rawDescGZIP(), []int{84} } func (x *TcpForwardFrame) GetPayload() isTcpForwardFrame_Payload { @@ -5357,7 +6733,7 @@ type ExecSandboxInput struct { func (x *ExecSandboxInput) Reset() { *x = ExecSandboxInput{} - mi := &file_openshell_proto_msgTypes[74] + mi := &file_openshell_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5369,7 +6745,7 @@ func (x *ExecSandboxInput) String() string { func (*ExecSandboxInput) ProtoMessage() {} func (x *ExecSandboxInput) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[74] + mi := &file_openshell_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5382,7 +6758,7 @@ func (x *ExecSandboxInput) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxInput.ProtoReflect.Descriptor instead. func (*ExecSandboxInput) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{74} + return file_openshell_proto_rawDescGZIP(), []int{85} } func (x *ExecSandboxInput) GetPayload() isExecSandboxInput_Payload { @@ -5455,7 +6831,7 @@ type ExecSandboxWindowResize struct { func (x *ExecSandboxWindowResize) Reset() { *x = ExecSandboxWindowResize{} - mi := &file_openshell_proto_msgTypes[75] + mi := &file_openshell_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5467,7 +6843,7 @@ func (x *ExecSandboxWindowResize) String() string { func (*ExecSandboxWindowResize) ProtoMessage() {} func (x *ExecSandboxWindowResize) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[75] + mi := &file_openshell_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5480,7 +6856,7 @@ func (x *ExecSandboxWindowResize) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxWindowResize.ProtoReflect.Descriptor instead. func (*ExecSandboxWindowResize) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{75} + return file_openshell_proto_rawDescGZIP(), []int{86} } func (x *ExecSandboxWindowResize) GetCols() uint32 { @@ -5517,7 +6893,7 @@ type SshSession struct { func (x *SshSession) Reset() { *x = SshSession{} - mi := &file_openshell_proto_msgTypes[76] + mi := &file_openshell_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5529,7 +6905,7 @@ func (x *SshSession) String() string { func (*SshSession) ProtoMessage() {} func (x *SshSession) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[76] + mi := &file_openshell_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5542,7 +6918,7 @@ func (x *SshSession) ProtoReflect() protoreflect.Message { // Deprecated: Use SshSession.ProtoReflect.Descriptor instead. func (*SshSession) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{76} + return file_openshell_proto_rawDescGZIP(), []int{87} } func (x *SshSession) GetMetadata() *datamodelv1.ObjectMeta { @@ -5611,7 +6987,7 @@ type WatchSandboxRequest struct { func (x *WatchSandboxRequest) Reset() { *x = WatchSandboxRequest{} - mi := &file_openshell_proto_msgTypes[77] + mi := &file_openshell_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5623,7 +6999,7 @@ func (x *WatchSandboxRequest) String() string { func (*WatchSandboxRequest) ProtoMessage() {} func (x *WatchSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[77] + mi := &file_openshell_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5636,7 +7012,7 @@ func (x *WatchSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSandboxRequest.ProtoReflect.Descriptor instead. func (*WatchSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{77} + return file_openshell_proto_rawDescGZIP(), []int{88} } func (x *WatchSandboxRequest) GetId() string { @@ -5726,7 +7102,7 @@ type SandboxStreamEvent struct { func (x *SandboxStreamEvent) Reset() { *x = SandboxStreamEvent{} - mi := &file_openshell_proto_msgTypes[78] + mi := &file_openshell_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5738,7 +7114,7 @@ func (x *SandboxStreamEvent) String() string { func (*SandboxStreamEvent) ProtoMessage() {} func (x *SandboxStreamEvent) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[78] + mi := &file_openshell_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5751,7 +7127,7 @@ func (x *SandboxStreamEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxStreamEvent.ProtoReflect.Descriptor instead. func (*SandboxStreamEvent) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{78} + return file_openshell_proto_rawDescGZIP(), []int{89} } func (x *SandboxStreamEvent) GetPayload() isSandboxStreamEvent_Payload { @@ -5864,7 +7240,7 @@ type SandboxLogLine struct { func (x *SandboxLogLine) Reset() { *x = SandboxLogLine{} - mi := &file_openshell_proto_msgTypes[79] + mi := &file_openshell_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5876,7 +7252,7 @@ func (x *SandboxLogLine) String() string { func (*SandboxLogLine) ProtoMessage() {} func (x *SandboxLogLine) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[79] + mi := &file_openshell_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5889,7 +7265,7 @@ func (x *SandboxLogLine) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxLogLine.ProtoReflect.Descriptor instead. func (*SandboxLogLine) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{79} + return file_openshell_proto_rawDescGZIP(), []int{90} } func (x *SandboxLogLine) GetSandboxId() string { @@ -5950,7 +7326,7 @@ type SandboxStreamWarning struct { func (x *SandboxStreamWarning) Reset() { *x = SandboxStreamWarning{} - mi := &file_openshell_proto_msgTypes[80] + mi := &file_openshell_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5962,7 +7338,7 @@ func (x *SandboxStreamWarning) String() string { func (*SandboxStreamWarning) ProtoMessage() {} func (x *SandboxStreamWarning) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[80] + mi := &file_openshell_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5975,7 +7351,7 @@ func (x *SandboxStreamWarning) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxStreamWarning.ProtoReflect.Descriptor instead. func (*SandboxStreamWarning) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{80} + return file_openshell_proto_rawDescGZIP(), []int{91} } func (x *SandboxStreamWarning) GetMessage() string { @@ -5997,7 +7373,7 @@ type CreateProviderRequest struct { func (x *CreateProviderRequest) Reset() { *x = CreateProviderRequest{} - mi := &file_openshell_proto_msgTypes[81] + mi := &file_openshell_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6009,7 +7385,7 @@ func (x *CreateProviderRequest) String() string { func (*CreateProviderRequest) ProtoMessage() {} func (x *CreateProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[81] + mi := &file_openshell_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6022,7 +7398,7 @@ func (x *CreateProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateProviderRequest.ProtoReflect.Descriptor instead. func (*CreateProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{81} + return file_openshell_proto_rawDescGZIP(), []int{92} } func (x *CreateProviderRequest) GetProvider() *datamodelv1.Provider { @@ -6051,7 +7427,7 @@ type GetProviderRequest struct { func (x *GetProviderRequest) Reset() { *x = GetProviderRequest{} - mi := &file_openshell_proto_msgTypes[82] + mi := &file_openshell_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6063,7 +7439,7 @@ func (x *GetProviderRequest) String() string { func (*GetProviderRequest) ProtoMessage() {} func (x *GetProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[82] + mi := &file_openshell_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6076,7 +7452,7 @@ func (x *GetProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRequest.ProtoReflect.Descriptor instead. func (*GetProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{82} + return file_openshell_proto_rawDescGZIP(), []int{93} } func (x *GetProviderRequest) GetName() string { @@ -6110,7 +7486,7 @@ type ListProvidersRequest struct { func (x *ListProvidersRequest) Reset() { *x = ListProvidersRequest{} - mi := &file_openshell_proto_msgTypes[83] + mi := &file_openshell_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6122,7 +7498,7 @@ func (x *ListProvidersRequest) String() string { func (*ListProvidersRequest) ProtoMessage() {} func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[83] + mi := &file_openshell_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6135,7 +7511,7 @@ func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProvidersRequest.ProtoReflect.Descriptor instead. func (*ListProvidersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{83} + return file_openshell_proto_rawDescGZIP(), []int{94} } func (x *ListProvidersRequest) GetPageSize() int32 { @@ -6174,7 +7550,7 @@ type UpdateProviderRequest struct { func (x *UpdateProviderRequest) Reset() { *x = UpdateProviderRequest{} - mi := &file_openshell_proto_msgTypes[84] + mi := &file_openshell_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6186,7 +7562,7 @@ func (x *UpdateProviderRequest) String() string { func (*UpdateProviderRequest) ProtoMessage() {} func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[84] + mi := &file_openshell_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6199,7 +7575,7 @@ func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderRequest.ProtoReflect.Descriptor instead. func (*UpdateProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{84} + return file_openshell_proto_rawDescGZIP(), []int{95} } func (x *UpdateProviderRequest) GetProvider() *datamodelv1.Provider { @@ -6235,7 +7611,7 @@ type DeleteProviderRequest struct { func (x *DeleteProviderRequest) Reset() { *x = DeleteProviderRequest{} - mi := &file_openshell_proto_msgTypes[85] + mi := &file_openshell_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6247,7 +7623,7 @@ func (x *DeleteProviderRequest) String() string { func (*DeleteProviderRequest) ProtoMessage() {} func (x *DeleteProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[85] + mi := &file_openshell_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6260,7 +7636,7 @@ func (x *DeleteProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{85} + return file_openshell_proto_rawDescGZIP(), []int{96} } func (x *DeleteProviderRequest) GetName() string { @@ -6279,15 +7655,20 @@ func (x *DeleteProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelect // Provider response. type ProviderResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + // Selection-time sandbox target set for an update, with one receipt per target. + // Sandboxes attached later are outside this operation's readiness result. + TargetReceipts []*ProviderMutationReceipt `protobuf:"bytes,2,rep,name=target_receipts,json=targetReceipts,proto3" json:"target_receipts,omitempty"` + // Identifies the update even when its target set is empty. + MutationId string `protobuf:"bytes,3,opt,name=mutation_id,json=mutationId,proto3" json:"mutation_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ProviderResponse) Reset() { *x = ProviderResponse{} - mi := &file_openshell_proto_msgTypes[86] + mi := &file_openshell_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6299,7 +7680,7 @@ func (x *ProviderResponse) String() string { func (*ProviderResponse) ProtoMessage() {} func (x *ProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[86] + mi := &file_openshell_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6312,14 +7693,28 @@ func (x *ProviderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderResponse.ProtoReflect.Descriptor instead. func (*ProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{86} + return file_openshell_proto_rawDescGZIP(), []int{97} +} + +func (x *ProviderResponse) GetProvider() *datamodelv1.Provider { + if x != nil { + return x.Provider + } + return nil +} + +func (x *ProviderResponse) GetTargetReceipts() []*ProviderMutationReceipt { + if x != nil { + return x.TargetReceipts + } + return nil } -func (x *ProviderResponse) GetProvider() *datamodelv1.Provider { +func (x *ProviderResponse) GetMutationId() string { if x != nil { - return x.Provider + return x.MutationId } - return nil + return "" } // List providers response. @@ -6334,7 +7729,7 @@ type ListProvidersResponse struct { func (x *ListProvidersResponse) Reset() { *x = ListProvidersResponse{} - mi := &file_openshell_proto_msgTypes[87] + mi := &file_openshell_proto_msgTypes[98] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6346,7 +7741,7 @@ func (x *ListProvidersResponse) String() string { func (*ListProvidersResponse) ProtoMessage() {} func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[87] + mi := &file_openshell_proto_msgTypes[98] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6359,7 +7754,7 @@ func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProvidersResponse.ProtoReflect.Descriptor instead. func (*ListProvidersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{87} + return file_openshell_proto_rawDescGZIP(), []int{98} } func (x *ListProvidersResponse) GetProviders() []*datamodelv1.Provider { @@ -6394,7 +7789,7 @@ type ListProviderProfilesRequest struct { func (x *ListProviderProfilesRequest) Reset() { *x = ListProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[88] + mi := &file_openshell_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6406,7 +7801,7 @@ func (x *ListProviderProfilesRequest) String() string { func (*ListProviderProfilesRequest) ProtoMessage() {} func (x *ListProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[88] + mi := &file_openshell_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6419,7 +7814,7 @@ func (x *ListProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*ListProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{88} + return file_openshell_proto_rawDescGZIP(), []int{99} } func (x *ListProviderProfilesRequest) GetPageSize() int32 { @@ -6457,7 +7852,7 @@ type GetProviderProfileRequest struct { func (x *GetProviderProfileRequest) Reset() { *x = GetProviderProfileRequest{} - mi := &file_openshell_proto_msgTypes[89] + mi := &file_openshell_proto_msgTypes[100] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6469,7 +7864,7 @@ func (x *GetProviderProfileRequest) String() string { func (*GetProviderProfileRequest) ProtoMessage() {} func (x *GetProviderProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[89] + mi := &file_openshell_proto_msgTypes[100] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6482,7 +7877,7 @@ func (x *GetProviderProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderProfileRequest.ProtoReflect.Descriptor instead. func (*GetProviderProfileRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{89} + return file_openshell_proto_rawDescGZIP(), []int{100} } func (x *GetProviderProfileRequest) GetId() string { @@ -6510,7 +7905,7 @@ type ProviderProfileImportItem struct { func (x *ProviderProfileImportItem) Reset() { *x = ProviderProfileImportItem{} - mi := &file_openshell_proto_msgTypes[90] + mi := &file_openshell_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6522,7 +7917,7 @@ func (x *ProviderProfileImportItem) String() string { func (*ProviderProfileImportItem) ProtoMessage() {} func (x *ProviderProfileImportItem) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[90] + mi := &file_openshell_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6535,7 +7930,7 @@ func (x *ProviderProfileImportItem) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileImportItem.ProtoReflect.Descriptor instead. func (*ProviderProfileImportItem) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{90} + return file_openshell_proto_rawDescGZIP(), []int{101} } func (x *ProviderProfileImportItem) GetProfile() *ProviderProfile { @@ -6566,7 +7961,7 @@ type ProviderProfileDiagnostic struct { func (x *ProviderProfileDiagnostic) Reset() { *x = ProviderProfileDiagnostic{} - mi := &file_openshell_proto_msgTypes[91] + mi := &file_openshell_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6578,7 +7973,7 @@ func (x *ProviderProfileDiagnostic) String() string { func (*ProviderProfileDiagnostic) ProtoMessage() {} func (x *ProviderProfileDiagnostic) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[91] + mi := &file_openshell_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6591,7 +7986,7 @@ func (x *ProviderProfileDiagnostic) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileDiagnostic.ProtoReflect.Descriptor instead. func (*ProviderProfileDiagnostic) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{91} + return file_openshell_proto_rawDescGZIP(), []int{102} } func (x *ProviderProfileDiagnostic) GetSource() string { @@ -6648,7 +8043,7 @@ type ProviderCredentialTokenGrantAudienceOverride struct { func (x *ProviderCredentialTokenGrantAudienceOverride) Reset() { *x = ProviderCredentialTokenGrantAudienceOverride{} - mi := &file_openshell_proto_msgTypes[92] + mi := &file_openshell_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6660,7 +8055,7 @@ func (x *ProviderCredentialTokenGrantAudienceOverride) String() string { func (*ProviderCredentialTokenGrantAudienceOverride) ProtoMessage() {} func (x *ProviderCredentialTokenGrantAudienceOverride) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[92] + mi := &file_openshell_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6673,7 +8068,7 @@ func (x *ProviderCredentialTokenGrantAudienceOverride) ProtoReflect() protorefle // Deprecated: Use ProviderCredentialTokenGrantAudienceOverride.ProtoReflect.Descriptor instead. func (*ProviderCredentialTokenGrantAudienceOverride) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{92} + return file_openshell_proto_rawDescGZIP(), []int{103} } func (x *ProviderCredentialTokenGrantAudienceOverride) GetHost() string { @@ -6727,7 +8122,7 @@ type ProviderCredentialTokenGrantSubjectToken struct { func (x *ProviderCredentialTokenGrantSubjectToken) Reset() { *x = ProviderCredentialTokenGrantSubjectToken{} - mi := &file_openshell_proto_msgTypes[93] + mi := &file_openshell_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6739,7 +8134,7 @@ func (x *ProviderCredentialTokenGrantSubjectToken) String() string { func (*ProviderCredentialTokenGrantSubjectToken) ProtoMessage() {} func (x *ProviderCredentialTokenGrantSubjectToken) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[93] + mi := &file_openshell_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6752,7 +8147,7 @@ func (x *ProviderCredentialTokenGrantSubjectToken) ProtoReflect() protoreflect.M // Deprecated: Use ProviderCredentialTokenGrantSubjectToken.ProtoReflect.Descriptor instead. func (*ProviderCredentialTokenGrantSubjectToken) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{93} + return file_openshell_proto_rawDescGZIP(), []int{104} } func (x *ProviderCredentialTokenGrantSubjectToken) GetSource() string { @@ -6809,7 +8204,7 @@ type ProviderCredentialTokenGrant struct { func (x *ProviderCredentialTokenGrant) Reset() { *x = ProviderCredentialTokenGrant{} - mi := &file_openshell_proto_msgTypes[94] + mi := &file_openshell_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6821,7 +8216,7 @@ func (x *ProviderCredentialTokenGrant) String() string { func (*ProviderCredentialTokenGrant) ProtoMessage() {} func (x *ProviderCredentialTokenGrant) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[94] + mi := &file_openshell_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6834,7 +8229,7 @@ func (x *ProviderCredentialTokenGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialTokenGrant.ProtoReflect.Descriptor instead. func (*ProviderCredentialTokenGrant) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{94} + return file_openshell_proto_rawDescGZIP(), []int{105} } func (x *ProviderCredentialTokenGrant) GetTokenEndpoint() string { @@ -6926,7 +8321,7 @@ type ProviderProfileCredential struct { func (x *ProviderProfileCredential) Reset() { *x = ProviderProfileCredential{} - mi := &file_openshell_proto_msgTypes[95] + mi := &file_openshell_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6938,7 +8333,7 @@ func (x *ProviderProfileCredential) String() string { func (*ProviderProfileCredential) ProtoMessage() {} func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[95] + mi := &file_openshell_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6951,7 +8346,7 @@ func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileCredential.ProtoReflect.Descriptor instead. func (*ProviderProfileCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{95} + return file_openshell_proto_rawDescGZIP(), []int{106} } func (x *ProviderProfileCredential) GetName() string { @@ -7036,7 +8431,7 @@ type ProviderCredentialRefreshMaterial struct { func (x *ProviderCredentialRefreshMaterial) Reset() { *x = ProviderCredentialRefreshMaterial{} - mi := &file_openshell_proto_msgTypes[96] + mi := &file_openshell_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7048,7 +8443,7 @@ func (x *ProviderCredentialRefreshMaterial) String() string { func (*ProviderCredentialRefreshMaterial) ProtoMessage() {} func (x *ProviderCredentialRefreshMaterial) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[96] + mi := &file_openshell_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7061,7 +8456,7 @@ func (x *ProviderCredentialRefreshMaterial) ProtoReflect() protoreflect.Message // Deprecated: Use ProviderCredentialRefreshMaterial.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshMaterial) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{96} + return file_openshell_proto_rawDescGZIP(), []int{107} } func (x *ProviderCredentialRefreshMaterial) GetName() string { @@ -7106,7 +8501,7 @@ type ProviderCredentialRefreshOutput struct { func (x *ProviderCredentialRefreshOutput) Reset() { *x = ProviderCredentialRefreshOutput{} - mi := &file_openshell_proto_msgTypes[97] + mi := &file_openshell_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7118,7 +8513,7 @@ func (x *ProviderCredentialRefreshOutput) String() string { func (*ProviderCredentialRefreshOutput) ProtoMessage() {} func (x *ProviderCredentialRefreshOutput) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[97] + mi := &file_openshell_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7131,7 +8526,7 @@ func (x *ProviderCredentialRefreshOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefreshOutput.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshOutput) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{97} + return file_openshell_proto_rawDescGZIP(), []int{108} } func (x *ProviderCredentialRefreshOutput) GetOutput() string { @@ -7163,7 +8558,7 @@ type ProviderCredentialRefresh struct { func (x *ProviderCredentialRefresh) Reset() { *x = ProviderCredentialRefresh{} - mi := &file_openshell_proto_msgTypes[98] + mi := &file_openshell_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7175,7 +8570,7 @@ func (x *ProviderCredentialRefresh) String() string { func (*ProviderCredentialRefresh) ProtoMessage() {} func (x *ProviderCredentialRefresh) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[98] + mi := &file_openshell_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7188,7 +8583,7 @@ func (x *ProviderCredentialRefresh) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefresh.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefresh) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{98} + return file_openshell_proto_rawDescGZIP(), []int{109} } func (x *ProviderCredentialRefresh) GetStrategy() ProviderCredentialRefreshStrategy { @@ -7271,7 +8666,7 @@ type ProviderCredentialRefreshStatus struct { func (x *ProviderCredentialRefreshStatus) Reset() { *x = ProviderCredentialRefreshStatus{} - mi := &file_openshell_proto_msgTypes[99] + mi := &file_openshell_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7283,7 +8678,7 @@ func (x *ProviderCredentialRefreshStatus) String() string { func (*ProviderCredentialRefreshStatus) ProtoMessage() {} func (x *ProviderCredentialRefreshStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[99] + mi := &file_openshell_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7296,7 +8691,7 @@ func (x *ProviderCredentialRefreshStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefreshStatus.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{99} + return file_openshell_proto_rawDescGZIP(), []int{110} } func (x *ProviderCredentialRefreshStatus) GetProviderName() string { @@ -7401,7 +8796,7 @@ type ProviderProfileDiscovery struct { func (x *ProviderProfileDiscovery) Reset() { *x = ProviderProfileDiscovery{} - mi := &file_openshell_proto_msgTypes[100] + mi := &file_openshell_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7413,7 +8808,7 @@ func (x *ProviderProfileDiscovery) String() string { func (*ProviderProfileDiscovery) ProtoMessage() {} func (x *ProviderProfileDiscovery) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[100] + mi := &file_openshell_proto_msgTypes[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7426,7 +8821,7 @@ func (x *ProviderProfileDiscovery) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileDiscovery.ProtoReflect.Descriptor instead. func (*ProviderProfileDiscovery) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{100} + return file_openshell_proto_rawDescGZIP(), []int{111} } func (x *ProviderProfileDiscovery) GetCredentials() []string { @@ -7448,7 +8843,7 @@ type GetProviderRefreshStatusRequest struct { func (x *GetProviderRefreshStatusRequest) Reset() { *x = GetProviderRefreshStatusRequest{} - mi := &file_openshell_proto_msgTypes[101] + mi := &file_openshell_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7460,7 +8855,7 @@ func (x *GetProviderRefreshStatusRequest) String() string { func (*GetProviderRefreshStatusRequest) ProtoMessage() {} func (x *GetProviderRefreshStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[101] + mi := &file_openshell_proto_msgTypes[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7473,7 +8868,7 @@ func (x *GetProviderRefreshStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRefreshStatusRequest.ProtoReflect.Descriptor instead. func (*GetProviderRefreshStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{101} + return file_openshell_proto_rawDescGZIP(), []int{112} } func (x *GetProviderRefreshStatusRequest) GetProvider() string { @@ -7506,7 +8901,7 @@ type GetProviderRefreshStatusResponse struct { func (x *GetProviderRefreshStatusResponse) Reset() { *x = GetProviderRefreshStatusResponse{} - mi := &file_openshell_proto_msgTypes[102] + mi := &file_openshell_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7518,7 +8913,7 @@ func (x *GetProviderRefreshStatusResponse) String() string { func (*GetProviderRefreshStatusResponse) ProtoMessage() {} func (x *GetProviderRefreshStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[102] + mi := &file_openshell_proto_msgTypes[113] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7531,7 +8926,7 @@ func (x *GetProviderRefreshStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRefreshStatusResponse.ProtoReflect.Descriptor instead. func (*GetProviderRefreshStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{102} + return file_openshell_proto_rawDescGZIP(), []int{113} } func (x *GetProviderRefreshStatusResponse) GetCredentials() []*ProviderCredentialRefreshStatus { @@ -7560,7 +8955,7 @@ type ConfigureProviderRefreshRequest struct { func (x *ConfigureProviderRefreshRequest) Reset() { *x = ConfigureProviderRefreshRequest{} - mi := &file_openshell_proto_msgTypes[103] + mi := &file_openshell_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7572,7 +8967,7 @@ func (x *ConfigureProviderRefreshRequest) String() string { func (*ConfigureProviderRefreshRequest) ProtoMessage() {} func (x *ConfigureProviderRefreshRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[103] + mi := &file_openshell_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7585,7 +8980,7 @@ func (x *ConfigureProviderRefreshRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProviderRefreshRequest.ProtoReflect.Descriptor instead. func (*ConfigureProviderRefreshRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{103} + return file_openshell_proto_rawDescGZIP(), []int{114} } func (x *ConfigureProviderRefreshRequest) GetProvider() string { @@ -7646,7 +9041,7 @@ type ConfigureProviderRefreshResponse struct { func (x *ConfigureProviderRefreshResponse) Reset() { *x = ConfigureProviderRefreshResponse{} - mi := &file_openshell_proto_msgTypes[104] + mi := &file_openshell_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7658,7 +9053,7 @@ func (x *ConfigureProviderRefreshResponse) String() string { func (*ConfigureProviderRefreshResponse) ProtoMessage() {} func (x *ConfigureProviderRefreshResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[104] + mi := &file_openshell_proto_msgTypes[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7671,7 +9066,7 @@ func (x *ConfigureProviderRefreshResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProviderRefreshResponse.ProtoReflect.Descriptor instead. func (*ConfigureProviderRefreshResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{104} + return file_openshell_proto_rawDescGZIP(), []int{115} } func (x *ConfigureProviderRefreshResponse) GetStatus() *ProviderCredentialRefreshStatus { @@ -7693,7 +9088,7 @@ type RotateProviderCredentialRequest struct { func (x *RotateProviderCredentialRequest) Reset() { *x = RotateProviderCredentialRequest{} - mi := &file_openshell_proto_msgTypes[105] + mi := &file_openshell_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7705,7 +9100,7 @@ func (x *RotateProviderCredentialRequest) String() string { func (*RotateProviderCredentialRequest) ProtoMessage() {} func (x *RotateProviderCredentialRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[105] + mi := &file_openshell_proto_msgTypes[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7718,7 +9113,7 @@ func (x *RotateProviderCredentialRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateProviderCredentialRequest.ProtoReflect.Descriptor instead. func (*RotateProviderCredentialRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{105} + return file_openshell_proto_rawDescGZIP(), []int{116} } func (x *RotateProviderCredentialRequest) GetProvider() string { @@ -7751,7 +9146,7 @@ type RotateProviderCredentialResponse struct { func (x *RotateProviderCredentialResponse) Reset() { *x = RotateProviderCredentialResponse{} - mi := &file_openshell_proto_msgTypes[106] + mi := &file_openshell_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7763,7 +9158,7 @@ func (x *RotateProviderCredentialResponse) String() string { func (*RotateProviderCredentialResponse) ProtoMessage() {} func (x *RotateProviderCredentialResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[106] + mi := &file_openshell_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7776,7 +9171,7 @@ func (x *RotateProviderCredentialResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateProviderCredentialResponse.ProtoReflect.Descriptor instead. func (*RotateProviderCredentialResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{106} + return file_openshell_proto_rawDescGZIP(), []int{117} } func (x *RotateProviderCredentialResponse) GetStatus() *ProviderCredentialRefreshStatus { @@ -7798,7 +9193,7 @@ type DeleteProviderRefreshRequest struct { func (x *DeleteProviderRefreshRequest) Reset() { *x = DeleteProviderRefreshRequest{} - mi := &file_openshell_proto_msgTypes[107] + mi := &file_openshell_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7810,7 +9205,7 @@ func (x *DeleteProviderRefreshRequest) String() string { func (*DeleteProviderRefreshRequest) ProtoMessage() {} func (x *DeleteProviderRefreshRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[107] + mi := &file_openshell_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7823,7 +9218,7 @@ func (x *DeleteProviderRefreshRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRefreshRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderRefreshRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{107} + return file_openshell_proto_rawDescGZIP(), []int{118} } func (x *DeleteProviderRefreshRequest) GetProvider() string { @@ -7856,7 +9251,7 @@ type DeleteProviderRefreshResponse struct { func (x *DeleteProviderRefreshResponse) Reset() { *x = DeleteProviderRefreshResponse{} - mi := &file_openshell_proto_msgTypes[108] + mi := &file_openshell_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7868,7 +9263,7 @@ func (x *DeleteProviderRefreshResponse) String() string { func (*DeleteProviderRefreshResponse) ProtoMessage() {} func (x *DeleteProviderRefreshResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[108] + mi := &file_openshell_proto_msgTypes[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7881,7 +9276,7 @@ func (x *DeleteProviderRefreshResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRefreshResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderRefreshResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{108} + return file_openshell_proto_rawDescGZIP(), []int{119} } func (x *DeleteProviderRefreshResponse) GetDeleted() bool { @@ -7921,7 +9316,7 @@ type ProviderProfile struct { func (x *ProviderProfile) Reset() { *x = ProviderProfile{} - mi := &file_openshell_proto_msgTypes[109] + mi := &file_openshell_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7933,7 +9328,7 @@ func (x *ProviderProfile) String() string { func (*ProviderProfile) ProtoMessage() {} func (x *ProviderProfile) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[109] + mi := &file_openshell_proto_msgTypes[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7946,7 +9341,7 @@ func (x *ProviderProfile) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfile.ProtoReflect.Descriptor instead. func (*ProviderProfile) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{109} + return file_openshell_proto_rawDescGZIP(), []int{120} } func (x *ProviderProfile) GetId() string { @@ -8050,7 +9445,7 @@ type ProviderProfileResponse struct { func (x *ProviderProfileResponse) Reset() { *x = ProviderProfileResponse{} - mi := &file_openshell_proto_msgTypes[110] + mi := &file_openshell_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8062,7 +9457,7 @@ func (x *ProviderProfileResponse) String() string { func (*ProviderProfileResponse) ProtoMessage() {} func (x *ProviderProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[110] + mi := &file_openshell_proto_msgTypes[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8075,7 +9470,7 @@ func (x *ProviderProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileResponse.ProtoReflect.Descriptor instead. func (*ProviderProfileResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{110} + return file_openshell_proto_rawDescGZIP(), []int{121} } func (x *ProviderProfileResponse) GetProfile() *ProviderProfile { @@ -8097,7 +9492,7 @@ type ListProviderProfilesResponse struct { func (x *ListProviderProfilesResponse) Reset() { *x = ListProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[111] + mi := &file_openshell_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8109,7 +9504,7 @@ func (x *ListProviderProfilesResponse) String() string { func (*ListProviderProfilesResponse) ProtoMessage() {} func (x *ListProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[111] + mi := &file_openshell_proto_msgTypes[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8122,7 +9517,7 @@ func (x *ListProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*ListProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{111} + return file_openshell_proto_rawDescGZIP(), []int{122} } func (x *ListProviderProfilesResponse) GetProfiles() []*ProviderProfile { @@ -8152,7 +9547,7 @@ type ImportProviderProfilesRequest struct { func (x *ImportProviderProfilesRequest) Reset() { *x = ImportProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[112] + mi := &file_openshell_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8164,7 +9559,7 @@ func (x *ImportProviderProfilesRequest) String() string { func (*ImportProviderProfilesRequest) ProtoMessage() {} func (x *ImportProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[112] + mi := &file_openshell_proto_msgTypes[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8177,7 +9572,7 @@ func (x *ImportProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*ImportProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{112} + return file_openshell_proto_rawDescGZIP(), []int{123} } func (x *ImportProviderProfilesRequest) GetProfiles() []*ProviderProfileImportItem { @@ -8206,7 +9601,7 @@ type ImportProviderProfilesResponse struct { func (x *ImportProviderProfilesResponse) Reset() { *x = ImportProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[113] + mi := &file_openshell_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8218,7 +9613,7 @@ func (x *ImportProviderProfilesResponse) String() string { func (*ImportProviderProfilesResponse) ProtoMessage() {} func (x *ImportProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[113] + mi := &file_openshell_proto_msgTypes[124] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8231,7 +9626,7 @@ func (x *ImportProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*ImportProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{113} + return file_openshell_proto_rawDescGZIP(), []int{124} } func (x *ImportProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -8275,7 +9670,7 @@ type UpdateProviderProfilesRequest struct { func (x *UpdateProviderProfilesRequest) Reset() { *x = UpdateProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[114] + mi := &file_openshell_proto_msgTypes[125] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8287,7 +9682,7 @@ func (x *UpdateProviderProfilesRequest) String() string { func (*UpdateProviderProfilesRequest) ProtoMessage() {} func (x *UpdateProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[114] + mi := &file_openshell_proto_msgTypes[125] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8300,7 +9695,7 @@ func (x *UpdateProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*UpdateProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{114} + return file_openshell_proto_rawDescGZIP(), []int{125} } func (x *UpdateProviderProfilesRequest) GetProfile() *ProviderProfileImportItem { @@ -8343,7 +9738,7 @@ type UpdateProviderProfilesResponse struct { func (x *UpdateProviderProfilesResponse) Reset() { *x = UpdateProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[115] + mi := &file_openshell_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8355,7 +9750,7 @@ func (x *UpdateProviderProfilesResponse) String() string { func (*UpdateProviderProfilesResponse) ProtoMessage() {} func (x *UpdateProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[115] + mi := &file_openshell_proto_msgTypes[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8368,7 +9763,7 @@ func (x *UpdateProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*UpdateProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{115} + return file_openshell_proto_rawDescGZIP(), []int{126} } func (x *UpdateProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -8405,7 +9800,7 @@ type LintProviderProfilesRequest struct { func (x *LintProviderProfilesRequest) Reset() { *x = LintProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[116] + mi := &file_openshell_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8417,7 +9812,7 @@ func (x *LintProviderProfilesRequest) String() string { func (*LintProviderProfilesRequest) ProtoMessage() {} func (x *LintProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[116] + mi := &file_openshell_proto_msgTypes[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8430,7 +9825,7 @@ func (x *LintProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LintProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*LintProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{116} + return file_openshell_proto_rawDescGZIP(), []int{127} } func (x *LintProviderProfilesRequest) GetProfiles() []*ProviderProfileImportItem { @@ -8458,7 +9853,7 @@ type LintProviderProfilesResponse struct { func (x *LintProviderProfilesResponse) Reset() { *x = LintProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[117] + mi := &file_openshell_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8470,7 +9865,7 @@ func (x *LintProviderProfilesResponse) String() string { func (*LintProviderProfilesResponse) ProtoMessage() {} func (x *LintProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[117] + mi := &file_openshell_proto_msgTypes[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8483,7 +9878,7 @@ func (x *LintProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LintProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*LintProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{117} + return file_openshell_proto_rawDescGZIP(), []int{128} } func (x *LintProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -8510,7 +9905,7 @@ type DeleteProviderResponse struct { func (x *DeleteProviderResponse) Reset() { *x = DeleteProviderResponse{} - mi := &file_openshell_proto_msgTypes[118] + mi := &file_openshell_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8522,7 +9917,7 @@ func (x *DeleteProviderResponse) String() string { func (*DeleteProviderResponse) ProtoMessage() {} func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[118] + mi := &file_openshell_proto_msgTypes[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8535,7 +9930,7 @@ func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{118} + return file_openshell_proto_rawDescGZIP(), []int{129} } func (x *DeleteProviderResponse) GetDeleted() bool { @@ -8558,7 +9953,7 @@ type DeleteProviderProfileRequest struct { func (x *DeleteProviderProfileRequest) Reset() { *x = DeleteProviderProfileRequest{} - mi := &file_openshell_proto_msgTypes[119] + mi := &file_openshell_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8570,7 +9965,7 @@ func (x *DeleteProviderProfileRequest) String() string { func (*DeleteProviderProfileRequest) ProtoMessage() {} func (x *DeleteProviderProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[119] + mi := &file_openshell_proto_msgTypes[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8583,7 +9978,7 @@ func (x *DeleteProviderProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderProfileRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderProfileRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{119} + return file_openshell_proto_rawDescGZIP(), []int{130} } func (x *DeleteProviderProfileRequest) GetId() string { @@ -8610,7 +10005,7 @@ type DeleteProviderProfileResponse struct { func (x *DeleteProviderProfileResponse) Reset() { *x = DeleteProviderProfileResponse{} - mi := &file_openshell_proto_msgTypes[120] + mi := &file_openshell_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8622,7 +10017,7 @@ func (x *DeleteProviderProfileResponse) String() string { func (*DeleteProviderProfileResponse) ProtoMessage() {} func (x *DeleteProviderProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[120] + mi := &file_openshell_proto_msgTypes[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8635,7 +10030,7 @@ func (x *DeleteProviderProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderProfileResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderProfileResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{120} + return file_openshell_proto_rawDescGZIP(), []int{131} } func (x *DeleteProviderProfileResponse) GetDeleted() bool { @@ -8660,7 +10055,7 @@ type GetSandboxProviderEnvironmentRequest struct { func (x *GetSandboxProviderEnvironmentRequest) Reset() { *x = GetSandboxProviderEnvironmentRequest{} - mi := &file_openshell_proto_msgTypes[121] + mi := &file_openshell_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8672,7 +10067,7 @@ func (x *GetSandboxProviderEnvironmentRequest) String() string { func (*GetSandboxProviderEnvironmentRequest) ProtoMessage() {} func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[121] + mi := &file_openshell_proto_msgTypes[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8685,7 +10080,7 @@ func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use GetSandboxProviderEnvironmentRequest.ProtoReflect.Descriptor instead. func (*GetSandboxProviderEnvironmentRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{121} + return file_openshell_proto_rawDescGZIP(), []int{132} } func (x *GetSandboxProviderEnvironmentRequest) GetSandboxId() string { @@ -8714,7 +10109,7 @@ type StaticCredentialEndpointBinding struct { func (x *StaticCredentialEndpointBinding) Reset() { *x = StaticCredentialEndpointBinding{} - mi := &file_openshell_proto_msgTypes[122] + mi := &file_openshell_proto_msgTypes[133] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8726,7 +10121,7 @@ func (x *StaticCredentialEndpointBinding) String() string { func (*StaticCredentialEndpointBinding) ProtoMessage() {} func (x *StaticCredentialEndpointBinding) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[122] + mi := &file_openshell_proto_msgTypes[133] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8739,7 +10134,7 @@ func (x *StaticCredentialEndpointBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticCredentialEndpointBinding.ProtoReflect.Descriptor instead. func (*StaticCredentialEndpointBinding) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{122} + return file_openshell_proto_rawDescGZIP(), []int{133} } func (x *StaticCredentialEndpointBinding) GetHost() string { @@ -8783,7 +10178,7 @@ type StaticCredentialBinding struct { func (x *StaticCredentialBinding) Reset() { *x = StaticCredentialBinding{} - mi := &file_openshell_proto_msgTypes[123] + mi := &file_openshell_proto_msgTypes[134] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8795,7 +10190,7 @@ func (x *StaticCredentialBinding) String() string { func (*StaticCredentialBinding) ProtoMessage() {} func (x *StaticCredentialBinding) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[123] + mi := &file_openshell_proto_msgTypes[134] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8808,7 +10203,7 @@ func (x *StaticCredentialBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticCredentialBinding.ProtoReflect.Descriptor instead. func (*StaticCredentialBinding) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{123} + return file_openshell_proto_rawDescGZIP(), []int{134} } func (x *StaticCredentialBinding) GetEndpoints() []*StaticCredentialEndpointBinding { @@ -8853,13 +10248,19 @@ type GetSandboxProviderEnvironmentResponse struct { // Environment variables that contain provider configuration rather than // credentials and therefore do not require endpoint-scoped resolution. NonSecretEnvironmentKeys []string `protobuf:"bytes,6,rep,name=non_secret_environment_keys,json=nonSecretEnvironmentKeys,proto3" json:"non_secret_environment_keys,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Attachment identity captured with the returned provider records. + ProviderAttachmentEpoch string `protobuf:"bytes,7,opt,name=provider_attachment_epoch,json=providerAttachmentEpoch,proto3" json:"provider_attachment_epoch,omitempty"` + // Effective policy identity used to derive this snapshot's endpoint bindings. + PolicyHash string `protobuf:"bytes,8,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + // Nonzero when material was withheld; installing an empty map is not readiness. + ReadinessReason ProviderReadinessReason `protobuf:"varint,9,opt,name=readiness_reason,json=readinessReason,proto3,enum=openshell.v1.ProviderReadinessReason" json:"readiness_reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetSandboxProviderEnvironmentResponse) Reset() { *x = GetSandboxProviderEnvironmentResponse{} - mi := &file_openshell_proto_msgTypes[124] + mi := &file_openshell_proto_msgTypes[135] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8871,7 +10272,7 @@ func (x *GetSandboxProviderEnvironmentResponse) String() string { func (*GetSandboxProviderEnvironmentResponse) ProtoMessage() {} func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[124] + mi := &file_openshell_proto_msgTypes[135] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8884,7 +10285,7 @@ func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use GetSandboxProviderEnvironmentResponse.ProtoReflect.Descriptor instead. func (*GetSandboxProviderEnvironmentResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{124} + return file_openshell_proto_rawDescGZIP(), []int{135} } func (x *GetSandboxProviderEnvironmentResponse) GetEnvironment() map[string]string { @@ -8915,18 +10316,39 @@ func (x *GetSandboxProviderEnvironmentResponse) GetDynamicCredentials() map[stri return nil } -func (x *GetSandboxProviderEnvironmentResponse) GetStaticCredentialBindings() map[string]*StaticCredentialBinding { +func (x *GetSandboxProviderEnvironmentResponse) GetStaticCredentialBindings() map[string]*StaticCredentialBinding { + if x != nil { + return x.StaticCredentialBindings + } + return nil +} + +func (x *GetSandboxProviderEnvironmentResponse) GetNonSecretEnvironmentKeys() []string { + if x != nil { + return x.NonSecretEnvironmentKeys + } + return nil +} + +func (x *GetSandboxProviderEnvironmentResponse) GetProviderAttachmentEpoch() string { + if x != nil { + return x.ProviderAttachmentEpoch + } + return "" +} + +func (x *GetSandboxProviderEnvironmentResponse) GetPolicyHash() string { if x != nil { - return x.StaticCredentialBindings + return x.PolicyHash } - return nil + return "" } -func (x *GetSandboxProviderEnvironmentResponse) GetNonSecretEnvironmentKeys() []string { +func (x *GetSandboxProviderEnvironmentResponse) GetReadinessReason() ProviderReadinessReason { if x != nil { - return x.NonSecretEnvironmentKeys + return x.ReadinessReason } - return nil + return ProviderReadinessReason_PROVIDER_READINESS_REASON_UNSPECIFIED } type ExchangeProviderSubjectTokenRequest struct { @@ -8946,7 +10368,7 @@ type ExchangeProviderSubjectTokenRequest struct { func (x *ExchangeProviderSubjectTokenRequest) Reset() { *x = ExchangeProviderSubjectTokenRequest{} - mi := &file_openshell_proto_msgTypes[125] + mi := &file_openshell_proto_msgTypes[136] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8958,7 +10380,7 @@ func (x *ExchangeProviderSubjectTokenRequest) String() string { func (*ExchangeProviderSubjectTokenRequest) ProtoMessage() {} func (x *ExchangeProviderSubjectTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[125] + mi := &file_openshell_proto_msgTypes[136] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8971,7 +10393,7 @@ func (x *ExchangeProviderSubjectTokenRequest) ProtoReflect() protoreflect.Messag // Deprecated: Use ExchangeProviderSubjectTokenRequest.ProtoReflect.Descriptor instead. func (*ExchangeProviderSubjectTokenRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{125} + return file_openshell_proto_rawDescGZIP(), []int{136} } func (x *ExchangeProviderSubjectTokenRequest) GetSandboxId() string { @@ -9013,7 +10435,7 @@ type ExchangeProviderSubjectTokenResponse struct { func (x *ExchangeProviderSubjectTokenResponse) Reset() { *x = ExchangeProviderSubjectTokenResponse{} - mi := &file_openshell_proto_msgTypes[126] + mi := &file_openshell_proto_msgTypes[137] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9025,7 +10447,7 @@ func (x *ExchangeProviderSubjectTokenResponse) String() string { func (*ExchangeProviderSubjectTokenResponse) ProtoMessage() {} func (x *ExchangeProviderSubjectTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[126] + mi := &file_openshell_proto_msgTypes[137] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9038,7 +10460,7 @@ func (x *ExchangeProviderSubjectTokenResponse) ProtoReflect() protoreflect.Messa // Deprecated: Use ExchangeProviderSubjectTokenResponse.ProtoReflect.Descriptor instead. func (*ExchangeProviderSubjectTokenResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{126} + return file_openshell_proto_rawDescGZIP(), []int{137} } func (x *ExchangeProviderSubjectTokenResponse) GetAccessToken() string { @@ -9110,7 +10532,7 @@ type UpdateConfigRequest struct { func (x *UpdateConfigRequest) Reset() { *x = UpdateConfigRequest{} - mi := &file_openshell_proto_msgTypes[127] + mi := &file_openshell_proto_msgTypes[138] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9122,7 +10544,7 @@ func (x *UpdateConfigRequest) String() string { func (*UpdateConfigRequest) ProtoMessage() {} func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[127] + mi := &file_openshell_proto_msgTypes[138] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9135,7 +10557,7 @@ func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigRequest.ProtoReflect.Descriptor instead. func (*UpdateConfigRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{127} + return file_openshell_proto_rawDescGZIP(), []int{138} } func (x *UpdateConfigRequest) GetName() string { @@ -9225,7 +10647,7 @@ type PolicyMergeOperation struct { func (x *PolicyMergeOperation) Reset() { *x = PolicyMergeOperation{} - mi := &file_openshell_proto_msgTypes[128] + mi := &file_openshell_proto_msgTypes[139] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9237,7 +10659,7 @@ func (x *PolicyMergeOperation) String() string { func (*PolicyMergeOperation) ProtoMessage() {} func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[128] + mi := &file_openshell_proto_msgTypes[139] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9250,7 +10672,7 @@ func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyMergeOperation.ProtoReflect.Descriptor instead. func (*PolicyMergeOperation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{128} + return file_openshell_proto_rawDescGZIP(), []int{139} } func (x *PolicyMergeOperation) GetOperation() isPolicyMergeOperation_Operation { @@ -9364,7 +10786,7 @@ type AddNetworkRule struct { func (x *AddNetworkRule) Reset() { *x = AddNetworkRule{} - mi := &file_openshell_proto_msgTypes[129] + mi := &file_openshell_proto_msgTypes[140] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9376,7 +10798,7 @@ func (x *AddNetworkRule) String() string { func (*AddNetworkRule) ProtoMessage() {} func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[129] + mi := &file_openshell_proto_msgTypes[140] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9389,7 +10811,7 @@ func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use AddNetworkRule.ProtoReflect.Descriptor instead. func (*AddNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{129} + return file_openshell_proto_rawDescGZIP(), []int{140} } func (x *AddNetworkRule) GetRuleName() string { @@ -9417,7 +10839,7 @@ type RemoveNetworkEndpoint struct { func (x *RemoveNetworkEndpoint) Reset() { *x = RemoveNetworkEndpoint{} - mi := &file_openshell_proto_msgTypes[130] + mi := &file_openshell_proto_msgTypes[141] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9429,7 +10851,7 @@ func (x *RemoveNetworkEndpoint) String() string { func (*RemoveNetworkEndpoint) ProtoMessage() {} func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[130] + mi := &file_openshell_proto_msgTypes[141] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9442,7 +10864,7 @@ func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkEndpoint.ProtoReflect.Descriptor instead. func (*RemoveNetworkEndpoint) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{130} + return file_openshell_proto_rawDescGZIP(), []int{141} } func (x *RemoveNetworkEndpoint) GetRuleName() string { @@ -9475,7 +10897,7 @@ type RemoveNetworkRule struct { func (x *RemoveNetworkRule) Reset() { *x = RemoveNetworkRule{} - mi := &file_openshell_proto_msgTypes[131] + mi := &file_openshell_proto_msgTypes[142] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9487,7 +10909,7 @@ func (x *RemoveNetworkRule) String() string { func (*RemoveNetworkRule) ProtoMessage() {} func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[131] + mi := &file_openshell_proto_msgTypes[142] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9500,7 +10922,7 @@ func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkRule.ProtoReflect.Descriptor instead. func (*RemoveNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{131} + return file_openshell_proto_rawDescGZIP(), []int{142} } func (x *RemoveNetworkRule) GetRuleName() string { @@ -9521,7 +10943,7 @@ type AddDenyRules struct { func (x *AddDenyRules) Reset() { *x = AddDenyRules{} - mi := &file_openshell_proto_msgTypes[132] + mi := &file_openshell_proto_msgTypes[143] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9533,7 +10955,7 @@ func (x *AddDenyRules) String() string { func (*AddDenyRules) ProtoMessage() {} func (x *AddDenyRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[132] + mi := &file_openshell_proto_msgTypes[143] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9546,7 +10968,7 @@ func (x *AddDenyRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddDenyRules.ProtoReflect.Descriptor instead. func (*AddDenyRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{132} + return file_openshell_proto_rawDescGZIP(), []int{143} } func (x *AddDenyRules) GetHost() string { @@ -9581,7 +11003,7 @@ type AddAllowRules struct { func (x *AddAllowRules) Reset() { *x = AddAllowRules{} - mi := &file_openshell_proto_msgTypes[133] + mi := &file_openshell_proto_msgTypes[144] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9593,7 +11015,7 @@ func (x *AddAllowRules) String() string { func (*AddAllowRules) ProtoMessage() {} func (x *AddAllowRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[133] + mi := &file_openshell_proto_msgTypes[144] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9606,7 +11028,7 @@ func (x *AddAllowRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddAllowRules.ProtoReflect.Descriptor instead. func (*AddAllowRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{133} + return file_openshell_proto_rawDescGZIP(), []int{144} } func (x *AddAllowRules) GetHost() string { @@ -9640,7 +11062,7 @@ type RemoveNetworkBinary struct { func (x *RemoveNetworkBinary) Reset() { *x = RemoveNetworkBinary{} - mi := &file_openshell_proto_msgTypes[134] + mi := &file_openshell_proto_msgTypes[145] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9652,7 +11074,7 @@ func (x *RemoveNetworkBinary) String() string { func (*RemoveNetworkBinary) ProtoMessage() {} func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[134] + mi := &file_openshell_proto_msgTypes[145] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9665,7 +11087,7 @@ func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkBinary.ProtoReflect.Descriptor instead. func (*RemoveNetworkBinary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{134} + return file_openshell_proto_rawDescGZIP(), []int{145} } func (x *RemoveNetworkBinary) GetRuleName() string { @@ -9701,7 +11123,7 @@ type UpdateConfigResponse struct { func (x *UpdateConfigResponse) Reset() { *x = UpdateConfigResponse{} - mi := &file_openshell_proto_msgTypes[135] + mi := &file_openshell_proto_msgTypes[146] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9713,7 +11135,7 @@ func (x *UpdateConfigResponse) String() string { func (*UpdateConfigResponse) ProtoMessage() {} func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[135] + mi := &file_openshell_proto_msgTypes[146] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9726,7 +11148,7 @@ func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigResponse.ProtoReflect.Descriptor instead. func (*UpdateConfigResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{135} + return file_openshell_proto_rawDescGZIP(), []int{146} } func (x *UpdateConfigResponse) GetVersion() uint32 { @@ -9782,7 +11204,7 @@ type GetSandboxPolicyStatusRequest struct { func (x *GetSandboxPolicyStatusRequest) Reset() { *x = GetSandboxPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[136] + mi := &file_openshell_proto_msgTypes[147] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9794,7 +11216,7 @@ func (x *GetSandboxPolicyStatusRequest) String() string { func (*GetSandboxPolicyStatusRequest) ProtoMessage() {} func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[136] + mi := &file_openshell_proto_msgTypes[147] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9807,7 +11229,7 @@ func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{136} + return file_openshell_proto_rawDescGZIP(), []int{147} } func (x *GetSandboxPolicyStatusRequest) GetName() string { @@ -9851,7 +11273,7 @@ type GetSandboxPolicyStatusResponse struct { func (x *GetSandboxPolicyStatusResponse) Reset() { *x = GetSandboxPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[137] + mi := &file_openshell_proto_msgTypes[148] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9863,7 +11285,7 @@ func (x *GetSandboxPolicyStatusResponse) String() string { func (*GetSandboxPolicyStatusResponse) ProtoMessage() {} func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[137] + mi := &file_openshell_proto_msgTypes[148] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9876,7 +11298,7 @@ func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{137} + return file_openshell_proto_rawDescGZIP(), []int{148} } func (x *GetSandboxPolicyStatusResponse) GetRevision() *SandboxPolicyRevision { @@ -9915,7 +11337,7 @@ type ListSandboxPoliciesRequest struct { func (x *ListSandboxPoliciesRequest) Reset() { *x = ListSandboxPoliciesRequest{} - mi := &file_openshell_proto_msgTypes[138] + mi := &file_openshell_proto_msgTypes[149] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9927,7 +11349,7 @@ func (x *ListSandboxPoliciesRequest) String() string { func (*ListSandboxPoliciesRequest) ProtoMessage() {} func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[138] + mi := &file_openshell_proto_msgTypes[149] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9940,7 +11362,7 @@ func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesRequest.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{138} + return file_openshell_proto_rawDescGZIP(), []int{149} } func (x *ListSandboxPoliciesRequest) GetName() string { @@ -9992,7 +11414,7 @@ type ListSandboxPoliciesResponse struct { func (x *ListSandboxPoliciesResponse) Reset() { *x = ListSandboxPoliciesResponse{} - mi := &file_openshell_proto_msgTypes[139] + mi := &file_openshell_proto_msgTypes[150] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10004,7 +11426,7 @@ func (x *ListSandboxPoliciesResponse) String() string { func (*ListSandboxPoliciesResponse) ProtoMessage() {} func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[139] + mi := &file_openshell_proto_msgTypes[150] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10017,7 +11439,7 @@ func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesResponse.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{139} + return file_openshell_proto_rawDescGZIP(), []int{150} } func (x *ListSandboxPoliciesResponse) GetRevisions() []*SandboxPolicyRevision { @@ -10051,7 +11473,7 @@ type ReportPolicyStatusRequest struct { func (x *ReportPolicyStatusRequest) Reset() { *x = ReportPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[140] + mi := &file_openshell_proto_msgTypes[151] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10063,7 +11485,7 @@ func (x *ReportPolicyStatusRequest) String() string { func (*ReportPolicyStatusRequest) ProtoMessage() {} func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[140] + mi := &file_openshell_proto_msgTypes[151] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10076,7 +11498,7 @@ func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{140} + return file_openshell_proto_rawDescGZIP(), []int{151} } func (x *ReportPolicyStatusRequest) GetSandboxId() string { @@ -10116,7 +11538,7 @@ type ReportPolicyStatusResponse struct { func (x *ReportPolicyStatusResponse) Reset() { *x = ReportPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[141] + mi := &file_openshell_proto_msgTypes[152] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10128,7 +11550,7 @@ func (x *ReportPolicyStatusResponse) String() string { func (*ReportPolicyStatusResponse) ProtoMessage() {} func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[141] + mi := &file_openshell_proto_msgTypes[152] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10141,7 +11563,7 @@ func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{141} + return file_openshell_proto_rawDescGZIP(), []int{152} } // A versioned policy revision with metadata. @@ -10174,7 +11596,7 @@ type SandboxPolicyRevision struct { func (x *SandboxPolicyRevision) Reset() { *x = SandboxPolicyRevision{} - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[153] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10186,7 +11608,7 @@ func (x *SandboxPolicyRevision) String() string { func (*SandboxPolicyRevision) ProtoMessage() {} func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[153] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10199,7 +11621,7 @@ func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxPolicyRevision.ProtoReflect.Descriptor instead. func (*SandboxPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{142} + return file_openshell_proto_rawDescGZIP(), []int{153} } func (x *SandboxPolicyRevision) GetVersion() uint32 { @@ -10279,7 +11701,7 @@ type GetSandboxLogsRequest struct { func (x *GetSandboxLogsRequest) Reset() { *x = GetSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[154] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10291,7 +11713,7 @@ func (x *GetSandboxLogsRequest) String() string { func (*GetSandboxLogsRequest) ProtoMessage() {} func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[154] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10304,7 +11726,7 @@ func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*GetSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{143} + return file_openshell_proto_rawDescGZIP(), []int{154} } func (x *GetSandboxLogsRequest) GetSandboxId() string { @@ -10362,7 +11784,7 @@ type PushSandboxLogsRequest struct { func (x *PushSandboxLogsRequest) Reset() { *x = PushSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[155] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10374,7 +11796,7 @@ func (x *PushSandboxLogsRequest) String() string { func (*PushSandboxLogsRequest) ProtoMessage() {} func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[155] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10387,7 +11809,7 @@ func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*PushSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{144} + return file_openshell_proto_rawDescGZIP(), []int{155} } func (x *PushSandboxLogsRequest) GetSandboxId() string { @@ -10413,7 +11835,7 @@ type PushSandboxLogsResponse struct { func (x *PushSandboxLogsResponse) Reset() { *x = PushSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[156] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10425,7 +11847,7 @@ func (x *PushSandboxLogsResponse) String() string { func (*PushSandboxLogsResponse) ProtoMessage() {} func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[156] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10438,7 +11860,7 @@ func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*PushSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{145} + return file_openshell_proto_rawDescGZIP(), []int{156} } // Get sandbox logs response. @@ -10454,7 +11876,7 @@ type GetSandboxLogsResponse struct { func (x *GetSandboxLogsResponse) Reset() { *x = GetSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[157] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10466,7 +11888,7 @@ func (x *GetSandboxLogsResponse) String() string { func (*GetSandboxLogsResponse) ProtoMessage() {} func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[157] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10479,7 +11901,7 @@ func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*GetSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{146} + return file_openshell_proto_rawDescGZIP(), []int{157} } func (x *GetSandboxLogsResponse) GetLogs() []*SandboxLogLine { @@ -10512,7 +11934,7 @@ type SupervisorMessage struct { func (x *SupervisorMessage) Reset() { *x = SupervisorMessage{} - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[158] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10524,7 +11946,7 @@ func (x *SupervisorMessage) String() string { func (*SupervisorMessage) ProtoMessage() {} func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[158] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10537,7 +11959,7 @@ func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorMessage.ProtoReflect.Descriptor instead. func (*SupervisorMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{147} + return file_openshell_proto_rawDescGZIP(), []int{158} } func (x *SupervisorMessage) GetPayload() isSupervisorMessage_Payload { @@ -10628,7 +12050,7 @@ type GatewayMessage struct { func (x *GatewayMessage) Reset() { *x = GatewayMessage{} - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[159] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10640,7 +12062,7 @@ func (x *GatewayMessage) String() string { func (*GatewayMessage) ProtoMessage() {} func (x *GatewayMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[159] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10653,7 +12075,7 @@ func (x *GatewayMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayMessage.ProtoReflect.Descriptor instead. func (*GatewayMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{148} + return file_openshell_proto_rawDescGZIP(), []int{159} } func (x *GatewayMessage) GetPayload() isGatewayMessage_Payload { @@ -10748,14 +12170,16 @@ type SupervisorHello struct { // Sandbox ID this supervisor manages. SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` // Supervisor instance ID (e.g. boot id or process epoch). - InstanceId string `protobuf:"bytes,2,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + InstanceId string `protobuf:"bytes,2,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` + // The supervisor can report credential, policy, and launch-environment installation. + SupportsProviderReadiness bool `protobuf:"varint,4,opt,name=supports_provider_readiness,json=supportsProviderReadiness,proto3" json:"supports_provider_readiness,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SupervisorHello) Reset() { *x = SupervisorHello{} - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[160] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10767,7 +12191,7 @@ func (x *SupervisorHello) String() string { func (*SupervisorHello) ProtoMessage() {} func (x *SupervisorHello) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[160] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10780,7 +12204,7 @@ func (x *SupervisorHello) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHello.ProtoReflect.Descriptor instead. func (*SupervisorHello) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{149} + return file_openshell_proto_rawDescGZIP(), []int{160} } func (x *SupervisorHello) GetSandboxId() string { @@ -10797,6 +12221,13 @@ func (x *SupervisorHello) GetInstanceId() string { return "" } +func (x *SupervisorHello) GetSupportsProviderReadiness() bool { + if x != nil { + return x.SupportsProviderReadiness + } + return false +} + // Gateway accepts the supervisor session. type SessionAccepted struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -10810,7 +12241,7 @@ type SessionAccepted struct { func (x *SessionAccepted) Reset() { *x = SessionAccepted{} - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[161] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10822,7 +12253,7 @@ func (x *SessionAccepted) String() string { func (*SessionAccepted) ProtoMessage() {} func (x *SessionAccepted) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[161] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10835,7 +12266,7 @@ func (x *SessionAccepted) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionAccepted.ProtoReflect.Descriptor instead. func (*SessionAccepted) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{150} + return file_openshell_proto_rawDescGZIP(), []int{161} } func (x *SessionAccepted) GetSessionId() string { @@ -10863,7 +12294,7 @@ type SessionRejected struct { func (x *SessionRejected) Reset() { *x = SessionRejected{} - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[162] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10875,7 +12306,7 @@ func (x *SessionRejected) String() string { func (*SessionRejected) ProtoMessage() {} func (x *SessionRejected) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[162] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10888,7 +12319,7 @@ func (x *SessionRejected) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionRejected.ProtoReflect.Descriptor instead. func (*SessionRejected) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{151} + return file_openshell_proto_rawDescGZIP(), []int{162} } func (x *SessionRejected) GetReason() string { @@ -10907,7 +12338,7 @@ type SupervisorHeartbeat struct { func (x *SupervisorHeartbeat) Reset() { *x = SupervisorHeartbeat{} - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[163] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10919,7 +12350,7 @@ func (x *SupervisorHeartbeat) String() string { func (*SupervisorHeartbeat) ProtoMessage() {} func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[163] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10932,7 +12363,7 @@ func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHeartbeat.ProtoReflect.Descriptor instead. func (*SupervisorHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{152} + return file_openshell_proto_rawDescGZIP(), []int{163} } // Gateway heartbeat. @@ -10944,7 +12375,7 @@ type GatewayHeartbeat struct { func (x *GatewayHeartbeat) Reset() { *x = GatewayHeartbeat{} - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[164] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10956,7 +12387,7 @@ func (x *GatewayHeartbeat) String() string { func (*GatewayHeartbeat) ProtoMessage() {} func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[164] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10969,7 +12400,7 @@ func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayHeartbeat.ProtoReflect.Descriptor instead. func (*GatewayHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{153} + return file_openshell_proto_rawDescGZIP(), []int{164} } // Terminal result reported before the supervisor shuts down. A successful RPC @@ -10986,7 +12417,7 @@ type ReportMainProcessExitRequest struct { func (x *ReportMainProcessExitRequest) Reset() { *x = ReportMainProcessExitRequest{} - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[165] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10998,7 +12429,7 @@ func (x *ReportMainProcessExitRequest) String() string { func (*ReportMainProcessExitRequest) ProtoMessage() {} func (x *ReportMainProcessExitRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[165] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11011,7 +12442,7 @@ func (x *ReportMainProcessExitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportMainProcessExitRequest.ProtoReflect.Descriptor instead. func (*ReportMainProcessExitRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{154} + return file_openshell_proto_rawDescGZIP(), []int{165} } func (x *ReportMainProcessExitRequest) GetSandboxId() string { @@ -11043,7 +12474,7 @@ type ReportMainProcessExitResponse struct { func (x *ReportMainProcessExitResponse) Reset() { *x = ReportMainProcessExitResponse{} - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[166] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11055,7 +12486,7 @@ func (x *ReportMainProcessExitResponse) String() string { func (*ReportMainProcessExitResponse) ProtoMessage() {} func (x *ReportMainProcessExitResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[166] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11068,7 +12499,7 @@ func (x *ReportMainProcessExitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportMainProcessExitResponse.ProtoReflect.Descriptor instead. func (*ReportMainProcessExitResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{155} + return file_openshell_proto_rawDescGZIP(), []int{166} } // Terminal-delivery completion reported after all expected foreground SSH @@ -11083,7 +12514,7 @@ type FinalizeMainProcessExitRequest struct { func (x *FinalizeMainProcessExitRequest) Reset() { *x = FinalizeMainProcessExitRequest{} - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[167] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11095,7 +12526,7 @@ func (x *FinalizeMainProcessExitRequest) String() string { func (*FinalizeMainProcessExitRequest) ProtoMessage() {} func (x *FinalizeMainProcessExitRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[167] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11108,7 +12539,7 @@ func (x *FinalizeMainProcessExitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeMainProcessExitRequest.ProtoReflect.Descriptor instead. func (*FinalizeMainProcessExitRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{156} + return file_openshell_proto_rawDescGZIP(), []int{167} } func (x *FinalizeMainProcessExitRequest) GetSandboxId() string { @@ -11133,7 +12564,7 @@ type FinalizeMainProcessExitResponse struct { func (x *FinalizeMainProcessExitResponse) Reset() { *x = FinalizeMainProcessExitResponse{} - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[168] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11145,7 +12576,7 @@ func (x *FinalizeMainProcessExitResponse) String() string { func (*FinalizeMainProcessExitResponse) ProtoMessage() {} func (x *FinalizeMainProcessExitResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[168] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11158,7 +12589,7 @@ func (x *FinalizeMainProcessExitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeMainProcessExitResponse.ProtoReflect.Descriptor instead. func (*FinalizeMainProcessExitResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{157} + return file_openshell_proto_rawDescGZIP(), []int{168} } // Gateway requests the supervisor to open a relay channel. @@ -11187,7 +12618,7 @@ type RelayOpen struct { func (x *RelayOpen) Reset() { *x = RelayOpen{} - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[169] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11199,7 +12630,7 @@ func (x *RelayOpen) String() string { func (*RelayOpen) ProtoMessage() {} func (x *RelayOpen) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[169] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11212,7 +12643,7 @@ func (x *RelayOpen) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpen.ProtoReflect.Descriptor instead. func (*RelayOpen) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{158} + return file_openshell_proto_rawDescGZIP(), []int{169} } func (x *RelayOpen) GetChannelId() string { @@ -11279,7 +12710,7 @@ type SshRelayTarget struct { func (x *SshRelayTarget) Reset() { *x = SshRelayTarget{} - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[170] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11291,7 +12722,7 @@ func (x *SshRelayTarget) String() string { func (*SshRelayTarget) ProtoMessage() {} func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[170] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11304,7 +12735,7 @@ func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use SshRelayTarget.ProtoReflect.Descriptor instead. func (*SshRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{159} + return file_openshell_proto_rawDescGZIP(), []int{170} } // TCP target dialed by the supervisor from inside the sandbox. @@ -11320,7 +12751,7 @@ type TcpRelayTarget struct { func (x *TcpRelayTarget) Reset() { *x = TcpRelayTarget{} - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[171] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11332,7 +12763,7 @@ func (x *TcpRelayTarget) String() string { func (*TcpRelayTarget) ProtoMessage() {} func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[171] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11345,7 +12776,7 @@ func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpRelayTarget.ProtoReflect.Descriptor instead. func (*TcpRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{160} + return file_openshell_proto_rawDescGZIP(), []int{171} } func (x *TcpRelayTarget) GetHost() string { @@ -11373,7 +12804,7 @@ type RelayInit struct { func (x *RelayInit) Reset() { *x = RelayInit{} - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[172] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11385,7 +12816,7 @@ func (x *RelayInit) String() string { func (*RelayInit) ProtoMessage() {} func (x *RelayInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[172] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11398,7 +12829,7 @@ func (x *RelayInit) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayInit.ProtoReflect.Descriptor instead. func (*RelayInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{161} + return file_openshell_proto_rawDescGZIP(), []int{172} } func (x *RelayInit) GetChannelId() string { @@ -11425,7 +12856,7 @@ type RelayFrame struct { func (x *RelayFrame) Reset() { *x = RelayFrame{} - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[173] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11437,7 +12868,7 @@ func (x *RelayFrame) String() string { func (*RelayFrame) ProtoMessage() {} func (x *RelayFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[173] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11450,7 +12881,7 @@ func (x *RelayFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayFrame.ProtoReflect.Descriptor instead. func (*RelayFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{162} + return file_openshell_proto_rawDescGZIP(), []int{173} } func (x *RelayFrame) GetPayload() isRelayFrame_Payload { @@ -11509,7 +12940,7 @@ type RelayOpenResult struct { func (x *RelayOpenResult) Reset() { *x = RelayOpenResult{} - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[174] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11521,7 +12952,7 @@ func (x *RelayOpenResult) String() string { func (*RelayOpenResult) ProtoMessage() {} func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[174] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11534,7 +12965,7 @@ func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpenResult.ProtoReflect.Descriptor instead. func (*RelayOpenResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{163} + return file_openshell_proto_rawDescGZIP(), []int{174} } func (x *RelayOpenResult) GetChannelId() string { @@ -11571,7 +13002,7 @@ type RelayClose struct { func (x *RelayClose) Reset() { *x = RelayClose{} - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[175] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11583,7 +13014,7 @@ func (x *RelayClose) String() string { func (*RelayClose) ProtoMessage() {} func (x *RelayClose) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[175] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11596,7 +13027,7 @@ func (x *RelayClose) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayClose.ProtoReflect.Descriptor instead. func (*RelayClose) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{164} + return file_openshell_proto_rawDescGZIP(), []int{175} } func (x *RelayClose) GetChannelId() string { @@ -11630,7 +13061,7 @@ type L7RequestSample struct { func (x *L7RequestSample) Reset() { *x = L7RequestSample{} - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[176] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11642,7 +13073,7 @@ func (x *L7RequestSample) String() string { func (*L7RequestSample) ProtoMessage() {} func (x *L7RequestSample) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[176] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11655,7 +13086,7 @@ func (x *L7RequestSample) ProtoReflect() protoreflect.Message { // Deprecated: Use L7RequestSample.ProtoReflect.Descriptor instead. func (*L7RequestSample) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{165} + return file_openshell_proto_rawDescGZIP(), []int{176} } func (x *L7RequestSample) GetMethod() string { @@ -11729,7 +13160,7 @@ type DenialSummary struct { func (x *DenialSummary) Reset() { *x = DenialSummary{} - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[177] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11741,7 +13172,7 @@ func (x *DenialSummary) String() string { func (*DenialSummary) ProtoMessage() {} func (x *DenialSummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[177] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11754,7 +13185,7 @@ func (x *DenialSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialSummary.ProtoReflect.Descriptor instead. func (*DenialSummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{166} + return file_openshell_proto_rawDescGZIP(), []int{177} } func (x *DenialSummary) GetSandboxId() string { @@ -11889,7 +13320,7 @@ type DenialGroupCount struct { func (x *DenialGroupCount) Reset() { *x = DenialGroupCount{} - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[178] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11901,7 +13332,7 @@ func (x *DenialGroupCount) String() string { func (*DenialGroupCount) ProtoMessage() {} func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[178] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11914,7 +13345,7 @@ func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialGroupCount.ProtoReflect.Descriptor instead. func (*DenialGroupCount) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{167} + return file_openshell_proto_rawDescGZIP(), []int{178} } func (x *DenialGroupCount) GetDenyGroup() string { @@ -11947,7 +13378,7 @@ type NetworkActivitySummary struct { func (x *NetworkActivitySummary) Reset() { *x = NetworkActivitySummary{} - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[179] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11959,7 +13390,7 @@ func (x *NetworkActivitySummary) String() string { func (*NetworkActivitySummary) ProtoMessage() {} func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[179] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11972,7 +13403,7 @@ func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkActivitySummary.ProtoReflect.Descriptor instead. func (*NetworkActivitySummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{168} + return file_openshell_proto_rawDescGZIP(), []int{179} } func (x *NetworkActivitySummary) GetNetworkActivityCount() uint32 { @@ -12060,7 +13491,7 @@ type PolicyChunk struct { func (x *PolicyChunk) Reset() { *x = PolicyChunk{} - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[180] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12072,7 +13503,7 @@ func (x *PolicyChunk) String() string { func (*PolicyChunk) ProtoMessage() {} func (x *PolicyChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[180] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12085,7 +13516,7 @@ func (x *PolicyChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyChunk.ProtoReflect.Descriptor instead. func (*PolicyChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{169} + return file_openshell_proto_rawDescGZIP(), []int{180} } func (x *PolicyChunk) GetId() string { @@ -12273,7 +13704,7 @@ type DraftPolicyUpdate struct { func (x *DraftPolicyUpdate) Reset() { *x = DraftPolicyUpdate{} - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[181] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12285,7 +13716,7 @@ func (x *DraftPolicyUpdate) String() string { func (*DraftPolicyUpdate) ProtoMessage() {} func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[181] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12298,7 +13729,7 @@ func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftPolicyUpdate.ProtoReflect.Descriptor instead. func (*DraftPolicyUpdate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{170} + return file_openshell_proto_rawDescGZIP(), []int{181} } func (x *DraftPolicyUpdate) GetDraftVersion() uint64 { @@ -12356,7 +13787,7 @@ type SubmitPolicyAnalysisRequest struct { func (x *SubmitPolicyAnalysisRequest) Reset() { *x = SubmitPolicyAnalysisRequest{} - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[182] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12368,7 +13799,7 @@ func (x *SubmitPolicyAnalysisRequest) String() string { func (*SubmitPolicyAnalysisRequest) ProtoMessage() {} func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[182] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12381,7 +13812,7 @@ func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisRequest.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{171} + return file_openshell_proto_rawDescGZIP(), []int{182} } func (x *SubmitPolicyAnalysisRequest) GetSummaries() []*DenialSummary { @@ -12444,7 +13875,7 @@ type SubmitPolicyAnalysisResponse struct { func (x *SubmitPolicyAnalysisResponse) Reset() { *x = SubmitPolicyAnalysisResponse{} - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[183] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12456,7 +13887,7 @@ func (x *SubmitPolicyAnalysisResponse) String() string { func (*SubmitPolicyAnalysisResponse) ProtoMessage() {} func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[183] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12469,7 +13900,7 @@ func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisResponse.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{172} + return file_openshell_proto_rawDescGZIP(), []int{183} } func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunks() uint32 { @@ -12515,7 +13946,7 @@ type GetDraftPolicyRequest struct { func (x *GetDraftPolicyRequest) Reset() { *x = GetDraftPolicyRequest{} - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[184] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12527,7 +13958,7 @@ func (x *GetDraftPolicyRequest) String() string { func (*GetDraftPolicyRequest) ProtoMessage() {} func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[184] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12540,7 +13971,7 @@ func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyRequest.ProtoReflect.Descriptor instead. func (*GetDraftPolicyRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{173} + return file_openshell_proto_rawDescGZIP(), []int{184} } func (x *GetDraftPolicyRequest) GetName() string { @@ -12580,7 +14011,7 @@ type GetDraftPolicyResponse struct { func (x *GetDraftPolicyResponse) Reset() { *x = GetDraftPolicyResponse{} - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[185] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12592,7 +14023,7 @@ func (x *GetDraftPolicyResponse) String() string { func (*GetDraftPolicyResponse) ProtoMessage() {} func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[185] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12605,7 +14036,7 @@ func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyResponse.ProtoReflect.Descriptor instead. func (*GetDraftPolicyResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{174} + return file_openshell_proto_rawDescGZIP(), []int{185} } func (x *GetDraftPolicyResponse) GetChunks() []*PolicyChunk { @@ -12654,7 +14085,7 @@ type ApproveDraftChunkRequest struct { func (x *ApproveDraftChunkRequest) Reset() { *x = ApproveDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[186] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12666,7 +14097,7 @@ func (x *ApproveDraftChunkRequest) String() string { func (*ApproveDraftChunkRequest) ProtoMessage() {} func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[186] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12679,7 +14110,7 @@ func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkRequest.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{175} + return file_openshell_proto_rawDescGZIP(), []int{186} } func (x *ApproveDraftChunkRequest) GetName() string { @@ -12722,7 +14153,7 @@ type ApproveDraftChunkResponse struct { func (x *ApproveDraftChunkResponse) Reset() { *x = ApproveDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[187] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12734,7 +14165,7 @@ func (x *ApproveDraftChunkResponse) String() string { func (*ApproveDraftChunkResponse) ProtoMessage() {} func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[187] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12747,7 +14178,7 @@ func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkResponse.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{176} + return file_openshell_proto_rawDescGZIP(), []int{187} } func (x *ApproveDraftChunkResponse) GetPolicyVersion() uint32 { @@ -12781,7 +14212,7 @@ type RejectDraftChunkRequest struct { func (x *RejectDraftChunkRequest) Reset() { *x = RejectDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[188] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12793,7 +14224,7 @@ func (x *RejectDraftChunkRequest) String() string { func (*RejectDraftChunkRequest) ProtoMessage() {} func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[188] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12806,7 +14237,7 @@ func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkRequest.ProtoReflect.Descriptor instead. func (*RejectDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{177} + return file_openshell_proto_rawDescGZIP(), []int{188} } func (x *RejectDraftChunkRequest) GetName() string { @@ -12845,7 +14276,7 @@ type RejectDraftChunkResponse struct { func (x *RejectDraftChunkResponse) Reset() { *x = RejectDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[189] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12857,7 +14288,7 @@ func (x *RejectDraftChunkResponse) String() string { func (*RejectDraftChunkResponse) ProtoMessage() {} func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[189] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12870,7 +14301,7 @@ func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkResponse.ProtoReflect.Descriptor instead. func (*RejectDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{178} + return file_openshell_proto_rawDescGZIP(), []int{189} } // Approve all pending chunks. @@ -12884,7 +14315,7 @@ type DraftChunkApproval struct { func (x *DraftChunkApproval) Reset() { *x = DraftChunkApproval{} - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[190] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12896,7 +14327,7 @@ func (x *DraftChunkApproval) String() string { func (*DraftChunkApproval) ProtoMessage() {} func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[190] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12909,7 +14340,7 @@ func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftChunkApproval.ProtoReflect.Descriptor instead. func (*DraftChunkApproval) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{179} + return file_openshell_proto_rawDescGZIP(), []int{190} } func (x *DraftChunkApproval) GetChunkId() string { @@ -12943,7 +14374,7 @@ type ApproveAllDraftChunksRequest struct { func (x *ApproveAllDraftChunksRequest) Reset() { *x = ApproveAllDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[191] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12955,7 +14386,7 @@ func (x *ApproveAllDraftChunksRequest) String() string { func (*ApproveAllDraftChunksRequest) ProtoMessage() {} func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[191] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12968,7 +14399,7 @@ func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{180} + return file_openshell_proto_rawDescGZIP(), []int{191} } func (x *ApproveAllDraftChunksRequest) GetName() string { @@ -13016,7 +14447,7 @@ type ApproveAllDraftChunksResponse struct { func (x *ApproveAllDraftChunksResponse) Reset() { *x = ApproveAllDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[192] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13028,7 +14459,7 @@ func (x *ApproveAllDraftChunksResponse) String() string { func (*ApproveAllDraftChunksResponse) ProtoMessage() {} func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[192] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13041,7 +14472,7 @@ func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{181} + return file_openshell_proto_rawDescGZIP(), []int{192} } func (x *ApproveAllDraftChunksResponse) GetPolicyVersion() uint32 { @@ -13089,7 +14520,7 @@ type EditDraftChunkRequest struct { func (x *EditDraftChunkRequest) Reset() { *x = EditDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[193] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13101,7 +14532,7 @@ func (x *EditDraftChunkRequest) String() string { func (*EditDraftChunkRequest) ProtoMessage() {} func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[193] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13114,7 +14545,7 @@ func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkRequest.ProtoReflect.Descriptor instead. func (*EditDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{182} + return file_openshell_proto_rawDescGZIP(), []int{193} } func (x *EditDraftChunkRequest) GetName() string { @@ -13153,7 +14584,7 @@ type EditDraftChunkResponse struct { func (x *EditDraftChunkResponse) Reset() { *x = EditDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[194] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13165,7 +14596,7 @@ func (x *EditDraftChunkResponse) String() string { func (*EditDraftChunkResponse) ProtoMessage() {} func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[194] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13178,7 +14609,7 @@ func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkResponse.ProtoReflect.Descriptor instead. func (*EditDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{183} + return file_openshell_proto_rawDescGZIP(), []int{194} } // Reverse an approval (remove merged rule from active policy). @@ -13196,7 +14627,7 @@ type UndoDraftChunkRequest struct { func (x *UndoDraftChunkRequest) Reset() { *x = UndoDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[195] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13208,7 +14639,7 @@ func (x *UndoDraftChunkRequest) String() string { func (*UndoDraftChunkRequest) ProtoMessage() {} func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[195] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13221,7 +14652,7 @@ func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkRequest.ProtoReflect.Descriptor instead. func (*UndoDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{184} + return file_openshell_proto_rawDescGZIP(), []int{195} } func (x *UndoDraftChunkRequest) GetName() string { @@ -13257,7 +14688,7 @@ type UndoDraftChunkResponse struct { func (x *UndoDraftChunkResponse) Reset() { *x = UndoDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[196] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13269,7 +14700,7 @@ func (x *UndoDraftChunkResponse) String() string { func (*UndoDraftChunkResponse) ProtoMessage() {} func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[196] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13282,7 +14713,7 @@ func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkResponse.ProtoReflect.Descriptor instead. func (*UndoDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{185} + return file_openshell_proto_rawDescGZIP(), []int{196} } func (x *UndoDraftChunkResponse) GetPolicyVersion() uint32 { @@ -13312,7 +14743,7 @@ type ClearDraftChunksRequest struct { func (x *ClearDraftChunksRequest) Reset() { *x = ClearDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[197] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13324,7 +14755,7 @@ func (x *ClearDraftChunksRequest) String() string { func (*ClearDraftChunksRequest) ProtoMessage() {} func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[197] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13337,7 +14768,7 @@ func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ClearDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{186} + return file_openshell_proto_rawDescGZIP(), []int{197} } func (x *ClearDraftChunksRequest) GetName() string { @@ -13364,7 +14795,7 @@ type ClearDraftChunksResponse struct { func (x *ClearDraftChunksResponse) Reset() { *x = ClearDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[198] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13376,7 +14807,7 @@ func (x *ClearDraftChunksResponse) String() string { func (*ClearDraftChunksResponse) ProtoMessage() {} func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[198] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13389,7 +14820,7 @@ func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ClearDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{187} + return file_openshell_proto_rawDescGZIP(), []int{198} } func (x *ClearDraftChunksResponse) GetChunksCleared() uint32 { @@ -13412,7 +14843,7 @@ type GetDraftHistoryRequest struct { func (x *GetDraftHistoryRequest) Reset() { *x = GetDraftHistoryRequest{} - mi := &file_openshell_proto_msgTypes[188] + mi := &file_openshell_proto_msgTypes[199] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13424,7 +14855,7 @@ func (x *GetDraftHistoryRequest) String() string { func (*GetDraftHistoryRequest) ProtoMessage() {} func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[188] + mi := &file_openshell_proto_msgTypes[199] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13437,7 +14868,7 @@ func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryRequest.ProtoReflect.Descriptor instead. func (*GetDraftHistoryRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{188} + return file_openshell_proto_rawDescGZIP(), []int{199} } func (x *GetDraftHistoryRequest) GetName() string { @@ -13471,7 +14902,7 @@ type DraftHistoryEntry struct { func (x *DraftHistoryEntry) Reset() { *x = DraftHistoryEntry{} - mi := &file_openshell_proto_msgTypes[189] + mi := &file_openshell_proto_msgTypes[200] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13483,7 +14914,7 @@ func (x *DraftHistoryEntry) String() string { func (*DraftHistoryEntry) ProtoMessage() {} func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[189] + mi := &file_openshell_proto_msgTypes[200] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13496,7 +14927,7 @@ func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftHistoryEntry.ProtoReflect.Descriptor instead. func (*DraftHistoryEntry) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{189} + return file_openshell_proto_rawDescGZIP(), []int{200} } func (x *DraftHistoryEntry) GetTimestampMs() int64 { @@ -13537,7 +14968,7 @@ type GetDraftHistoryResponse struct { func (x *GetDraftHistoryResponse) Reset() { *x = GetDraftHistoryResponse{} - mi := &file_openshell_proto_msgTypes[190] + mi := &file_openshell_proto_msgTypes[201] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13549,7 +14980,7 @@ func (x *GetDraftHistoryResponse) String() string { func (*GetDraftHistoryResponse) ProtoMessage() {} func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[190] + mi := &file_openshell_proto_msgTypes[201] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13562,7 +14993,7 @@ func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryResponse.ProtoReflect.Descriptor instead. func (*GetDraftHistoryResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{190} + return file_openshell_proto_rawDescGZIP(), []int{201} } func (x *GetDraftHistoryResponse) GetEntries() []*DraftHistoryEntry { @@ -13585,7 +15016,7 @@ type CreateWorkspaceRequest struct { func (x *CreateWorkspaceRequest) Reset() { *x = CreateWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[202] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13597,7 +15028,7 @@ func (x *CreateWorkspaceRequest) String() string { func (*CreateWorkspaceRequest) ProtoMessage() {} func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[202] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13610,7 +15041,7 @@ func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceRequest.ProtoReflect.Descriptor instead. func (*CreateWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{191} + return file_openshell_proto_rawDescGZIP(), []int{202} } func (x *CreateWorkspaceRequest) GetName() string { @@ -13637,7 +15068,7 @@ type CreateWorkspaceResponse struct { func (x *CreateWorkspaceResponse) Reset() { *x = CreateWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[192] + mi := &file_openshell_proto_msgTypes[203] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13649,7 +15080,7 @@ func (x *CreateWorkspaceResponse) String() string { func (*CreateWorkspaceResponse) ProtoMessage() {} func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[192] + mi := &file_openshell_proto_msgTypes[203] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13662,7 +15093,7 @@ func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceResponse.ProtoReflect.Descriptor instead. func (*CreateWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{192} + return file_openshell_proto_rawDescGZIP(), []int{203} } func (x *CreateWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -13683,7 +15114,7 @@ type GetWorkspaceRequest struct { func (x *GetWorkspaceRequest) Reset() { *x = GetWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[193] + mi := &file_openshell_proto_msgTypes[204] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13695,7 +15126,7 @@ func (x *GetWorkspaceRequest) String() string { func (*GetWorkspaceRequest) ProtoMessage() {} func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[193] + mi := &file_openshell_proto_msgTypes[204] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13708,7 +15139,7 @@ func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceRequest.ProtoReflect.Descriptor instead. func (*GetWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{193} + return file_openshell_proto_rawDescGZIP(), []int{204} } func (x *GetWorkspaceRequest) GetName() string { @@ -13728,7 +15159,7 @@ type GetWorkspaceResponse struct { func (x *GetWorkspaceResponse) Reset() { *x = GetWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[194] + mi := &file_openshell_proto_msgTypes[205] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13740,7 +15171,7 @@ func (x *GetWorkspaceResponse) String() string { func (*GetWorkspaceResponse) ProtoMessage() {} func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[194] + mi := &file_openshell_proto_msgTypes[205] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13753,7 +15184,7 @@ func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceResponse.ProtoReflect.Descriptor instead. func (*GetWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{194} + return file_openshell_proto_rawDescGZIP(), []int{205} } func (x *GetWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -13780,7 +15211,7 @@ type ListWorkspacesRequest struct { func (x *ListWorkspacesRequest) Reset() { *x = ListWorkspacesRequest{} - mi := &file_openshell_proto_msgTypes[195] + mi := &file_openshell_proto_msgTypes[206] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13792,7 +15223,7 @@ func (x *ListWorkspacesRequest) String() string { func (*ListWorkspacesRequest) ProtoMessage() {} func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[195] + mi := &file_openshell_proto_msgTypes[206] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13805,7 +15236,7 @@ func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesRequest.ProtoReflect.Descriptor instead. func (*ListWorkspacesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{195} + return file_openshell_proto_rawDescGZIP(), []int{206} } func (x *ListWorkspacesRequest) GetPageSize() int32 { @@ -13841,7 +15272,7 @@ type ListWorkspacesResponse struct { func (x *ListWorkspacesResponse) Reset() { *x = ListWorkspacesResponse{} - mi := &file_openshell_proto_msgTypes[196] + mi := &file_openshell_proto_msgTypes[207] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13853,7 +15284,7 @@ func (x *ListWorkspacesResponse) String() string { func (*ListWorkspacesResponse) ProtoMessage() {} func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[196] + mi := &file_openshell_proto_msgTypes[207] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13866,7 +15297,7 @@ func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesResponse.ProtoReflect.Descriptor instead. func (*ListWorkspacesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{196} + return file_openshell_proto_rawDescGZIP(), []int{207} } func (x *ListWorkspacesResponse) GetWorkspaces() []*datamodelv1.Workspace { @@ -13894,7 +15325,7 @@ type DeleteWorkspaceRequest struct { func (x *DeleteWorkspaceRequest) Reset() { *x = DeleteWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[197] + mi := &file_openshell_proto_msgTypes[208] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13906,7 +15337,7 @@ func (x *DeleteWorkspaceRequest) String() string { func (*DeleteWorkspaceRequest) ProtoMessage() {} func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[197] + mi := &file_openshell_proto_msgTypes[208] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13919,7 +15350,7 @@ func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceRequest.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{197} + return file_openshell_proto_rawDescGZIP(), []int{208} } func (x *DeleteWorkspaceRequest) GetName() string { @@ -13939,7 +15370,7 @@ type DeleteWorkspaceResponse struct { func (x *DeleteWorkspaceResponse) Reset() { *x = DeleteWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[198] + mi := &file_openshell_proto_msgTypes[209] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13951,7 +15382,7 @@ func (x *DeleteWorkspaceResponse) String() string { func (*DeleteWorkspaceResponse) ProtoMessage() {} func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[198] + mi := &file_openshell_proto_msgTypes[209] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13964,7 +15395,7 @@ func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceResponse.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{198} + return file_openshell_proto_rawDescGZIP(), []int{209} } func (x *DeleteWorkspaceResponse) GetDeleted() bool { @@ -13988,7 +15419,7 @@ type WorkspaceMember struct { func (x *WorkspaceMember) Reset() { *x = WorkspaceMember{} - mi := &file_openshell_proto_msgTypes[199] + mi := &file_openshell_proto_msgTypes[210] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14000,7 +15431,7 @@ func (x *WorkspaceMember) String() string { func (*WorkspaceMember) ProtoMessage() {} func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[199] + mi := &file_openshell_proto_msgTypes[210] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14013,7 +15444,7 @@ func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceMember.ProtoReflect.Descriptor instead. func (*WorkspaceMember) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{199} + return file_openshell_proto_rawDescGZIP(), []int{210} } func (x *WorkspaceMember) GetMetadata() *datamodelv1.ObjectMeta { @@ -14052,7 +15483,7 @@ type AddWorkspaceMemberRequest struct { func (x *AddWorkspaceMemberRequest) Reset() { *x = AddWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[200] + mi := &file_openshell_proto_msgTypes[211] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14064,7 +15495,7 @@ func (x *AddWorkspaceMemberRequest) String() string { func (*AddWorkspaceMemberRequest) ProtoMessage() {} func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[200] + mi := &file_openshell_proto_msgTypes[211] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14077,7 +15508,7 @@ func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{200} + return file_openshell_proto_rawDescGZIP(), []int{211} } func (x *AddWorkspaceMemberRequest) GetWorkspace() string { @@ -14111,7 +15542,7 @@ type AddWorkspaceMemberResponse struct { func (x *AddWorkspaceMemberResponse) Reset() { *x = AddWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[201] + mi := &file_openshell_proto_msgTypes[212] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14123,7 +15554,7 @@ func (x *AddWorkspaceMemberResponse) String() string { func (*AddWorkspaceMemberResponse) ProtoMessage() {} func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[201] + mi := &file_openshell_proto_msgTypes[212] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14136,7 +15567,7 @@ func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{201} + return file_openshell_proto_rawDescGZIP(), []int{212} } func (x *AddWorkspaceMemberResponse) GetMember() *WorkspaceMember { @@ -14159,7 +15590,7 @@ type RemoveWorkspaceMemberRequest struct { func (x *RemoveWorkspaceMemberRequest) Reset() { *x = RemoveWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[202] + mi := &file_openshell_proto_msgTypes[213] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14171,7 +15602,7 @@ func (x *RemoveWorkspaceMemberRequest) String() string { func (*RemoveWorkspaceMemberRequest) ProtoMessage() {} func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[202] + mi := &file_openshell_proto_msgTypes[213] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14184,7 +15615,7 @@ func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{202} + return file_openshell_proto_rawDescGZIP(), []int{213} } func (x *RemoveWorkspaceMemberRequest) GetWorkspace() string { @@ -14211,7 +15642,7 @@ type RemoveWorkspaceMemberResponse struct { func (x *RemoveWorkspaceMemberResponse) Reset() { *x = RemoveWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[203] + mi := &file_openshell_proto_msgTypes[214] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14223,7 +15654,7 @@ func (x *RemoveWorkspaceMemberResponse) String() string { func (*RemoveWorkspaceMemberResponse) ProtoMessage() {} func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[203] + mi := &file_openshell_proto_msgTypes[214] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14236,7 +15667,7 @@ func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{203} + return file_openshell_proto_rawDescGZIP(), []int{214} } func (x *RemoveWorkspaceMemberResponse) GetRemoved() bool { @@ -14263,7 +15694,7 @@ type ListWorkspaceMembersRequest struct { func (x *ListWorkspaceMembersRequest) Reset() { *x = ListWorkspaceMembersRequest{} - mi := &file_openshell_proto_msgTypes[204] + mi := &file_openshell_proto_msgTypes[215] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14275,7 +15706,7 @@ func (x *ListWorkspaceMembersRequest) String() string { func (*ListWorkspaceMembersRequest) ProtoMessage() {} func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[204] + mi := &file_openshell_proto_msgTypes[215] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14288,7 +15719,7 @@ func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersRequest.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{204} + return file_openshell_proto_rawDescGZIP(), []int{215} } func (x *ListWorkspaceMembersRequest) GetWorkspace() string { @@ -14324,7 +15755,7 @@ type ListWorkspaceMembersResponse struct { func (x *ListWorkspaceMembersResponse) Reset() { *x = ListWorkspaceMembersResponse{} - mi := &file_openshell_proto_msgTypes[205] + mi := &file_openshell_proto_msgTypes[216] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14336,7 +15767,7 @@ func (x *ListWorkspaceMembersResponse) String() string { func (*ListWorkspaceMembersResponse) ProtoMessage() {} func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[205] + mi := &file_openshell_proto_msgTypes[216] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14349,7 +15780,7 @@ func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersResponse.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{205} + return file_openshell_proto_rawDescGZIP(), []int{216} } func (x *ListWorkspaceMembersResponse) GetMembers() []*WorkspaceMember { @@ -14384,7 +15815,7 @@ type ExtensionServiceCredential struct { func (x *ExtensionServiceCredential) Reset() { *x = ExtensionServiceCredential{} - mi := &file_openshell_proto_msgTypes[206] + mi := &file_openshell_proto_msgTypes[217] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14396,7 +15827,7 @@ func (x *ExtensionServiceCredential) String() string { func (*ExtensionServiceCredential) ProtoMessage() {} func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[206] + mi := &file_openshell_proto_msgTypes[217] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14409,7 +15840,7 @@ func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ExtensionServiceCredential.ProtoReflect.Descriptor instead. func (*ExtensionServiceCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{206} + return file_openshell_proto_rawDescGZIP(), []int{217} } func (x *ExtensionServiceCredential) GetServiceName() string { @@ -14446,7 +15877,7 @@ type EndpointObservation struct { func (x *EndpointObservation) Reset() { *x = EndpointObservation{} - mi := &file_openshell_proto_msgTypes[207] + mi := &file_openshell_proto_msgTypes[218] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14458,7 +15889,7 @@ func (x *EndpointObservation) String() string { func (*EndpointObservation) ProtoMessage() {} func (x *EndpointObservation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[207] + mi := &file_openshell_proto_msgTypes[218] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14471,7 +15902,7 @@ func (x *EndpointObservation) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointObservation.ProtoReflect.Descriptor instead. func (*EndpointObservation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{207} + return file_openshell_proto_rawDescGZIP(), []int{218} } func (x *EndpointObservation) GetEndpointId() string { @@ -14514,7 +15945,7 @@ type ReportEndpointStatusRequest struct { func (x *ReportEndpointStatusRequest) Reset() { *x = ReportEndpointStatusRequest{} - mi := &file_openshell_proto_msgTypes[208] + mi := &file_openshell_proto_msgTypes[219] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14526,7 +15957,7 @@ func (x *ReportEndpointStatusRequest) String() string { func (*ReportEndpointStatusRequest) ProtoMessage() {} func (x *ReportEndpointStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[208] + mi := &file_openshell_proto_msgTypes[219] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14539,7 +15970,7 @@ func (x *ReportEndpointStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportEndpointStatusRequest.ProtoReflect.Descriptor instead. func (*ReportEndpointStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{208} + return file_openshell_proto_rawDescGZIP(), []int{219} } func (x *ReportEndpointStatusRequest) GetSandboxId() string { @@ -14600,7 +16031,7 @@ type ReportEndpointStatusResponse struct { func (x *ReportEndpointStatusResponse) Reset() { *x = ReportEndpointStatusResponse{} - mi := &file_openshell_proto_msgTypes[209] + mi := &file_openshell_proto_msgTypes[220] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14612,7 +16043,7 @@ func (x *ReportEndpointStatusResponse) String() string { func (*ReportEndpointStatusResponse) ProtoMessage() {} func (x *ReportEndpointStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[209] + mi := &file_openshell_proto_msgTypes[220] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14625,7 +16056,7 @@ func (x *ReportEndpointStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportEndpointStatusResponse.ProtoReflect.Descriptor instead. func (*ReportEndpointStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{209} + return file_openshell_proto_rawDescGZIP(), []int{220} } // A configured endpoint and its last accepted network result in one record. @@ -14654,7 +16085,7 @@ type EndpointStatus struct { func (x *EndpointStatus) Reset() { *x = EndpointStatus{} - mi := &file_openshell_proto_msgTypes[210] + mi := &file_openshell_proto_msgTypes[221] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14666,7 +16097,7 @@ func (x *EndpointStatus) String() string { func (*EndpointStatus) ProtoMessage() {} func (x *EndpointStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[210] + mi := &file_openshell_proto_msgTypes[221] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14679,7 +16110,7 @@ func (x *EndpointStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointStatus.ProtoReflect.Descriptor instead. func (*EndpointStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{210} + return file_openshell_proto_rawDescGZIP(), []int{221} } func (x *EndpointStatus) GetEndpointId() string { @@ -14783,7 +16214,7 @@ const file_openshell_proto_rawDesc = "" + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12-\n" + "\x04spec\x18\x02 \x01(\v2\x19.openshell.v1.SandboxSpecR\x04spec\x123\n" + "\x06status\x18\x03 \x01(\v2\x1b.openshell.v1.SandboxStatusR\x06status\x12t\n" + - "\x1ecreated_from_workload_template\x18\x14 \x01(\v2/.openshell.v1.SandboxWorkloadTemplateProvenanceR\x1bcreatedFromWorkloadTemplateJ\x04\b\x04\x10\x05J\x04\b\x05\x10\x06R\x05phaseR\x16current_policy_version\"\x83\x04\n" + + "\x1ecreated_from_workload_template\x18\x14 \x01(\v2/.openshell.v1.SandboxWorkloadTemplateProvenanceR\x1bcreatedFromWorkloadTemplateJ\x04\b\x04\x10\x05J\x04\b\x05\x10\x06R\x05phaseR\x16current_policy_version\"\xbf\x04\n" + "\vSandboxSpec\x12\x1b\n" + "\tlog_level\x18\x01 \x01(\tR\blogLevel\x12L\n" + "\venvironment\x18\x05 \x03(\v2*.openshell.v1.SandboxSpec.EnvironmentEntryR\venvironment\x129\n" + @@ -14792,7 +16223,8 @@ const file_openshell_proto_rawDesc = "" + "\tproviders\x18\b \x03(\tR\tproviders\x12W\n" + "\x15resource_requirements\x18\t \x01(\v2\".openshell.v1.ResourceRequirementsR\x14resourceRequirements\x12\x18\n" + "\acommand\x18\f \x03(\tR\acommand\x12\x10\n" + - "\x03tty\x18\r \x01(\bR\x03tty\x1a>\n" + + "\x03tty\x18\r \x01(\bR\x03tty\x12:\n" + + "\x19provider_attachment_epoch\x18\x0e \x01(\tR\x17providerAttachmentEpoch\x1a>\n" + "\x10EnvironmentEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\n" + @@ -14968,13 +16400,101 @@ const file_openshell_proto_rawDesc = "" + "\tsandboxes\x18\x01 \x03(\v2\x15.openshell.v1.SandboxR\tsandboxes\x12&\n" + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"^\n" + "\x1cListSandboxProvidersResponse\x12>\n" + - "\tproviders\x18\x01 \x03(\v2 .openshell.datamodel.v1.ProviderR\tproviders\"l\n" + + "\tproviders\x18\x01 \x03(\v2 .openshell.datamodel.v1.ProviderR\tproviders\"\xad\x01\n" + "\x1dAttachSandboxProviderResponse\x12/\n" + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\x12\x1a\n" + - "\battached\x18\x02 \x01(\bR\battached\"l\n" + + "\battached\x18\x02 \x01(\bR\battached\x12?\n" + + "\areceipt\x18\x03 \x01(\v2%.openshell.v1.ProviderMutationReceiptR\areceipt\"\xad\x01\n" + "\x1dDetachSandboxProviderResponse\x12/\n" + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\x12\x1a\n" + - "\bdetached\x18\x02 \x01(\bR\bdetached\"1\n" + + "\bdetached\x18\x02 \x01(\bR\bdetached\x12?\n" + + "\areceipt\x18\x03 \x01(\v2%.openshell.v1.ProviderMutationReceiptR\areceipt\"\xe1\x02\n" + + "\x17ProviderDesiredIdentity\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12!\n" + + "\fsandbox_name\x18\x02 \x01(\tR\vsandboxName\x12)\n" + + "\x10attachment_epoch\x18\x03 \x01(\tR\x0fattachmentEpoch\x12\x1f\n" + + "\vprovider_id\x18\x04 \x01(\tR\n" + + "providerId\x12:\n" + + "\x19provider_resource_version\x18\x05 \x01(\x04R\x17providerResourceVersion\x122\n" + + "\x15provider_env_revision\x18\x06 \x01(\x04R\x13providerEnvRevision\x12'\n" + + "\x0fconfig_revision\x18\a \x01(\x04R\x0econfigRevision\x12\x1f\n" + + "\vpolicy_hash\x18\b \x01(\tR\n" + + "policyHash\"\xfa\x01\n" + + "\x16ConfigSnapshotRevision\x12L\n" + + "\x0esandbox_config\x18\x01 \x01(\v2#.openshell.v1.SandboxConfigRevisionH\x00R\rsandboxConfig\x123\n" + + "\x14provider_environment\x18\x02 \x01(\x04H\x00R\x13providerEnvironment\x12P\n" + + "\x0fprovider_target\x18\x03 \x01(\v2%.openshell.v1.ProviderDesiredIdentityH\x00R\x0eproviderTargetB\v\n" + + "\tcomponent\"\x91\x02\n" + + "\x15SandboxConfigRevision\x12'\n" + + "\x0fconfig_revision\x18\x01 \x01(\x04R\x0econfigRevision\x12%\n" + + "\x0epolicy_version\x18\x02 \x01(\rR\rpolicyVersion\x12G\n" + + "\rpolicy_source\x18\x03 \x01(\x0e2\".openshell.sandbox.v1.PolicySourceR\fpolicySource\x122\n" + + "\x15global_policy_version\x18\x04 \x01(\rR\x13globalPolicyVersion\x12+\n" + + "\x11settings_revision\x18\x05 \x01(\x04R\x10settingsRevision\"\xfa\x03\n" + + "\x15ConfigUpdateOperation\x12!\n" + + "\foperation_id\x18\x01 \x01(\tR\voperationId\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x02 \x01(\tR\tsandboxId\x12;\n" + + "\tcomponent\x18\x03 \x01(\x0e2\x1d.openshell.v1.ConfigComponentR\tcomponent\x12M\n" + + "\x0ftarget_revision\x18\x04 \x01(\v2$.openshell.v1.ConfigSnapshotRevisionR\x0etargetRevision\x12>\n" + + "\x05state\x18\x05 \x01(\x0e2(.openshell.v1.ConfigUpdateOperationStateR\x05state\x12:\n" + + "\aoutcome\x18\x06 \x01(\x0e2 .openshell.v1.ConfigApplyOutcomeR\aoutcome\x12'\n" + + "\x0fsanitized_error\x18\a \x01(\tR\x0esanitizedError\x12\"\n" + + "\rcreated_at_ms\x18\b \x01(\x03R\vcreatedAtMs\x12\"\n" + + "\rupdated_at_ms\x18\t \x01(\x03R\vupdatedAtMs\x12&\n" + + "\x0fcompleted_at_ms\x18\n" + + " \x01(\x03R\rcompletedAtMs\"\xbd\x02\n" + + "\x17ProviderMutationReceipt\x12\x1d\n" + + "\n" + + "receipt_id\x18\x01 \x01(\tR\treceiptId\x12\x1f\n" + + "\vmutation_id\x18\x02 \x01(\tR\n" + + "mutationId\x12#\n" + + "\rprovider_name\x18\x03 \x01(\tR\fproviderName\x12\x1c\n" + + "\tworkspace\x18\x04 \x01(\tR\tworkspace\x126\n" + + "\x04kind\x18\x05 \x01(\x0e2\".openshell.v1.ProviderMutationKindR\x04kind\x12?\n" + + "\adesired\x18\x06 \x01(\v2%.openshell.v1.ProviderDesiredIdentityR\adesired\x12&\n" + + "\x0fpersisted_at_ms\x18\a \x01(\x03R\rpersistedAtMs\"\x8d\x04\n" + + "\x1cProviderReadinessObservation\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x1a\n" + + "\bsequence\x18\x02 \x01(\x04R\bsequence\x12)\n" + + "\x10attachment_epoch\x18\x03 \x01(\tR\x0fattachmentEpoch\x122\n" + + "\x15provider_env_revision\x18\x04 \x01(\x04R\x13providerEnvRevision\x12'\n" + + "\x0fconfig_revision\x18\x05 \x01(\x04R\x0econfigRevision\x12\x1f\n" + + "\vpolicy_hash\x18\x06 \x01(\tR\n" + + "policyHash\x123\n" + + "\x15credentials_installed\x18\a \x01(\bR\x14credentialsInstalled\x12#\n" + + "\rpolicy_active\x18\b \x01(\bR\fpolicyActive\x12@\n" + + "\x1claunch_environment_installed\x18\t \x01(\bR\x1alaunchEnvironmentInstalled\x12.\n" + + "\x13process_instance_id\x18\n" + + " \x01(\tR\x11processInstanceId\x12=\n" + + "\x06reason\x18\v \x01(\x0e2%.openshell.v1.ProviderReadinessReasonR\x06reason\"\xde\x03\n" + + "\x17ProviderReadinessStatus\x12?\n" + + "\areceipt\x18\x01 \x01(\v2%.openshell.v1.ProviderMutationReceiptR\areceipt\x12:\n" + + "\x05state\x18\x02 \x01(\x0e2$.openshell.v1.ProviderReadinessStateR\x05state\x12=\n" + + "\x06reason\x18\x03 \x01(\x0e2%.openshell.v1.ProviderReadinessReasonR\x06reason\x12F\n" + + "\bobserved\x18\x04 \x01(\v2*.openshell.v1.ProviderReadinessObservationR\bobserved\x12.\n" + + "\x13network_instance_id\x18\x05 \x01(\tR\x11networkInstanceId\x12$\n" + + "\x0eobserved_at_ms\x18\x06 \x01(\x03R\fobservedAtMs\x12&\n" + + "\x0fevaluated_at_ms\x18\a \x01(\x03R\revaluatedAtMs\x12A\n" + + "\toperation\x18\b \x01(\v2#.openshell.v1.ConfigUpdateOperationR\toperation\"\xdc\x01\n" + + "\x1fGetSandboxProviderStatusRequest\x12!\n" + + "\fsandbox_name\x18\x01 \x01(\tR\vsandboxName\x12#\n" + + "\rprovider_name\x18\x02 \x01(\tR\fproviderName\x12\x1d\n" + + "\n" + + "receipt_id\x18\x03 \x01(\tR\treceiptId\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\"a\n" + + " GetSandboxProviderStatusResponse\x12=\n" + + "\x06status\x18\x01 \x01(\v2%.openshell.v1.ProviderReadinessStatusR\x06status\"\x8d\x01\n" + + "\x1eReportProviderReadinessRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12L\n" + + "\vobservation\x18\x02 \x01(\v2*.openshell.v1.ProviderReadinessObservationR\vobservation\"\xbe\x01\n" + + "\x1fReportProviderReadinessResponse\x12+\n" + + "\x11accepted_sequence\x18\x01 \x01(\x04R\x10acceptedSequence\x126\n" + + "\x17report_interval_seconds\x18\x02 \x01(\rR\x15reportIntervalSeconds\x126\n" + + "\x17observation_ttl_seconds\x18\x03 \x01(\rR\x15observationTtlSeconds\"1\n" + "\x15DeleteSandboxResponse\x12\x18\n" + "\adeleted\x18\x01 \x01(\bR\adeleted\"8\n" + "\x17CreateSshSessionRequest\x12\x1d\n" + @@ -15144,9 +16664,12 @@ const file_openshell_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01J\x04\b\x03\x10\x04R\tworkspace\"\x90\x01\n" + "\x15DeleteProviderRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12R\n" + - "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\tworkspace\"P\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScopeJ\x04\b\x02\x10\x03R\tworkspace\"\xc1\x01\n" + "\x10ProviderResponse\x12<\n" + - "\bprovider\x18\x01 \x01(\v2 .openshell.datamodel.v1.ProviderR\bprovider\"\x7f\n" + + "\bprovider\x18\x01 \x01(\v2 .openshell.datamodel.v1.ProviderR\bprovider\x12N\n" + + "\x0ftarget_receipts\x18\x02 \x03(\v2%.openshell.v1.ProviderMutationReceiptR\x0etargetReceipts\x12\x1f\n" + + "\vmutation_id\x18\x03 \x01(\tR\n" + + "mutationId\"\x7f\n" + "\x15ListProvidersResponse\x12>\n" + "\tproviders\x18\x01 \x03(\v2 .openshell.datamodel.v1.ProviderR\tproviders\x12&\n" + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"w\n" + @@ -15341,14 +16864,18 @@ const file_openshell_proto_rawDesc = "" + "\x17StaticCredentialBinding\x12K\n" + "\tendpoints\x18\x01 \x03(\v2-.openshell.v1.StaticCredentialEndpointBindingR\tendpoints\x12/\n" + "\x13credential_identity\x18\x02 \x01(\tR\x12credentialIdentity\x12<\n" + - "\x1aworkload_credential_handle\x18\x03 \x01(\tR\x18workloadCredentialHandle\"\x90\b\n" + + "\x1aworkload_credential_handle\x18\x03 \x01(\tR\x18workloadCredentialHandle\"\xbf\t\n" + "%GetSandboxProviderEnvironmentResponse\x12l\n" + "\venvironment\x18\x01 \x03(\v2D.openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntryB\x04\x88\xb5\x18\x01R\venvironment\x122\n" + "\x15provider_env_revision\x18\x02 \x01(\x04R\x13providerEnvRevision\x12\x87\x01\n" + "\x18credential_expires_at_ms\x18\x03 \x03(\v2N.openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntryR\x15credentialExpiresAtMs\x12|\n" + "\x13dynamic_credentials\x18\x04 \x03(\v2K.openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntryR\x12dynamicCredentials\x12\x8f\x01\n" + "\x1astatic_credential_bindings\x18\x05 \x03(\v2Q.openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntryR\x18staticCredentialBindings\x12=\n" + - "\x1bnon_secret_environment_keys\x18\x06 \x03(\tR\x18nonSecretEnvironmentKeys\x1a>\n" + + "\x1bnon_secret_environment_keys\x18\x06 \x03(\tR\x18nonSecretEnvironmentKeys\x12:\n" + + "\x19provider_attachment_epoch\x18\a \x01(\tR\x17providerAttachmentEpoch\x12\x1f\n" + + "\vpolicy_hash\x18\b \x01(\tR\n" + + "policyHash\x12P\n" + + "\x10readiness_reason\x18\t \x01(\x0e2%.openshell.v1.ProviderReadinessReasonR\x0freadinessReason\x1a>\n" + "\x10EnvironmentEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aH\n" + @@ -15504,12 +17031,13 @@ const file_openshell_proto_rawDesc = "" + "relay_open\x18\x04 \x01(\v2\x17.openshell.v1.RelayOpenH\x00R\trelayOpen\x12;\n" + "\vrelay_close\x18\x05 \x01(\v2\x18.openshell.v1.RelayCloseH\x00R\n" + "relayCloseB\t\n" + - "\apayload\"Q\n" + + "\apayload\"\x91\x01\n" + "\x0fSupervisorHello\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1f\n" + "\vinstance_id\x18\x02 \x01(\tR\n" + - "instanceId\"h\n" + + "instanceId\x12>\n" + + "\x1bsupports_provider_readiness\x18\x04 \x01(\bR\x19supportsProviderReadiness\"h\n" + "\x0fSessionAccepted\x12\x1d\n" + "\n" + "session_id\x18\x01 \x01(\tR\tsessionId\x126\n" + @@ -15801,7 +17329,62 @@ const file_openshell_proto_rawDesc = "" + "\x16SANDBOX_PHASE_STOPPING\x10\x06\x12\x19\n" + "\x15SANDBOX_PHASE_STOPPED\x10\a\x12\x1a\n" + "\x16SANDBOX_PHASE_STARTING\x10\b\x12\x1b\n" + - "\x17SANDBOX_PHASE_COMPLETED\x10\t*\xce\x01\n" + + "\x17SANDBOX_PHASE_COMPLETED\x10\t*\xcb\x01\n" + + "\x14ProviderMutationKind\x12&\n" + + "\"PROVIDER_MUTATION_KIND_UNSPECIFIED\x10\x00\x12!\n" + + "\x1dPROVIDER_MUTATION_KIND_ATTACH\x10\x01\x12!\n" + + "\x1dPROVIDER_MUTATION_KIND_DETACH\x10\x02\x12!\n" + + "\x1dPROVIDER_MUTATION_KIND_UPDATE\x10\x03\x12\"\n" + + "\x1ePROVIDER_MUTATION_KIND_OBSERVE\x10\x04*\xcf\x02\n" + + "\x16ProviderReadinessState\x12(\n" + + "$PROVIDER_READINESS_STATE_UNSPECIFIED\x10\x00\x12&\n" + + "\"PROVIDER_READINESS_STATE_PERSISTED\x10\x01\x12$\n" + + " PROVIDER_READINESS_STATE_PENDING\x10\x02\x12\"\n" + + "\x1ePROVIDER_READINESS_STATE_READY\x10\x03\x12%\n" + + "!PROVIDER_READINESS_STATE_WITHHELD\x10\x04\x12$\n" + + " PROVIDER_READINESS_STATE_REVOKED\x10\x05\x12#\n" + + "\x1fPROVIDER_READINESS_STATE_FAILED\x10\x06\x12'\n" + + "#PROVIDER_READINESS_STATE_SUPERSEDED\x10\a*\xda\x06\n" + + "\x17ProviderReadinessReason\x12)\n" + + "%PROVIDER_READINESS_REASON_UNSPECIFIED\x10\x00\x124\n" + + "0PROVIDER_READINESS_REASON_WAITING_FOR_SUPERVISOR\x10\x01\x125\n" + + "1PROVIDER_READINESS_REASON_WAITING_FOR_CREDENTIALS\x10\x02\x120\n" + + ",PROVIDER_READINESS_REASON_WAITING_FOR_POLICY\x10\x03\x121\n" + + "-PROVIDER_READINESS_REASON_WAITING_FOR_PROCESS\x10\x04\x124\n" + + "0PROVIDER_READINESS_REASON_UNSUPPORTED_SUPERVISOR\x10\x05\x122\n" + + ".PROVIDER_READINESS_REASON_CREDENTIALS_WITHHELD\x10\x06\x127\n" + + "3PROVIDER_READINESS_REASON_CREDENTIAL_INSTALL_FAILED\x10\a\x126\n" + + "2PROVIDER_READINESS_REASON_POLICY_ACTIVATION_FAILED\x10\b\x124\n" + + "0PROVIDER_READINESS_REASON_PROCESS_INSTALL_FAILED\x10\t\x125\n" + + "1PROVIDER_READINESS_REASON_SUPERVISOR_DISCONNECTED\x10\n" + + "\x126\n" + + "2PROVIDER_READINESS_REASON_SUPERVISOR_LEASE_EXPIRED\x10\v\x123\n" + + "/PROVIDER_READINESS_REASON_DESIRED_STATE_CHANGED\x10\f\x120\n" + + ",PROVIDER_READINESS_REASON_CREDENTIAL_EXPIRED\x10\r\x12*\n" + + "&PROVIDER_READINESS_REASON_LOCAL_POLICY\x10\x0e\x12/\n" + + "+PROVIDER_READINESS_REASON_SNAPSHOT_MISMATCH\x10\x0f*\x83\x01\n" + + "\x0fConfigComponent\x12 \n" + + "\x1cCONFIG_COMPONENT_UNSPECIFIED\x10\x00\x12#\n" + + "\x1fCONFIG_COMPONENT_SANDBOX_CONFIG\x10\x01\x12)\n" + + "%CONFIG_COMPONENT_PROVIDER_ENVIRONMENT\x10\x02*\x8d\x03\n" + + "\x12ConfigApplyOutcome\x12$\n" + + " CONFIG_APPLY_OUTCOME_UNSPECIFIED\x10\x00\x12 \n" + + "\x1cCONFIG_APPLY_OUTCOME_APPLIED\x10\x01\x12*\n" + + "&CONFIG_APPLY_OUTCOME_IGNORED_DUPLICATE\x10\x02\x12&\n" + + "\"CONFIG_APPLY_OUTCOME_IGNORED_STALE\x10\x03\x120\n" + + ",CONFIG_APPLY_OUTCOME_RETAINED_LOCAL_OVERRIDE\x10\x04\x12!\n" + + "\x1dCONFIG_APPLY_OUTCOME_DEGRADED\x10\x05\x128\n" + + "4CONFIG_APPLY_OUTCOME_FAILED_RETAINED_LAST_KNOWN_GOOD\x10\x06\x12&\n" + + "\"CONFIG_APPLY_OUTCOME_FAILED_CLOSED\x10\a\x12$\n" + + " CONFIG_APPLY_OUTCOME_UNSUPPORTED\x10\b*\xd2\x02\n" + + "\x1aConfigUpdateOperationState\x12-\n" + + ")CONFIG_UPDATE_OPERATION_STATE_UNSPECIFIED\x10\x00\x12)\n" + + "%CONFIG_UPDATE_OPERATION_STATE_PENDING\x10\x01\x12)\n" + + "%CONFIG_UPDATE_OPERATION_STATE_APPLIED\x10\x02\x12*\n" + + "&CONFIG_UPDATE_OPERATION_STATE_INACTIVE\x10\x03\x12(\n" + + "$CONFIG_UPDATE_OPERATION_STATE_FAILED\x10\x04\x12,\n" + + "(CONFIG_UPDATE_OPERATION_STATE_SUPERSEDED\x10\x05\x12+\n" + + "'CONFIG_UPDATE_OPERATION_STATE_CANCELLED\x10\x06*\xce\x01\n" + " ProviderCredentialTokenGrantType\x124\n" + "0PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED\x10\x00\x12;\n" + "7PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_CLIENT_CREDENTIALS\x10\x01\x127\n" + @@ -15852,7 +17435,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\x8bN\n" + + "!ENDPOINT_RESULT_UPSTREAM_REJECTED\x10\a2\xb1P\n" + "\tOpenShell\x12Z\n" + "\x06Health\x12\x1b.openshell.v1.HealthRequest\x1a\x1c.openshell.v1.HealthResponse\"\x15\x82\xb5\x18\x11\n" + "\x0funauthenticated\x12i\n" + @@ -15882,7 +17465,9 @@ const file_openshell_proto_rawDesc = "" + "\x15AttachSandboxProvider\x12*.openshell.v1.AttachSandboxProviderRequest\x1a+.openshell.v1.AttachSandboxProviderResponse\"!\x82\xb5\x18\x1d\n" + "\x06bearer\x12\x04user\"\rsandbox:write\x12\x93\x01\n" + "\x15DetachSandboxProvider\x12*.openshell.v1.DetachSandboxProviderRequest\x1a+.openshell.v1.DetachSandboxProviderResponse\"!\x82\xb5\x18\x1d\n" + - "\x06bearer\x12\x04user\"\rsandbox:write\x12{\n" + + "\x06bearer\x12\x04user\"\rsandbox:write\x12\x9b\x01\n" + + "\x18GetSandboxProviderStatus\x12-.openshell.v1.GetSandboxProviderStatusRequest\x1a..openshell.v1.GetSandboxProviderStatusResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12{\n" + "\rDeleteSandbox\x12\".openshell.v1.DeleteSandboxRequest\x1a#.openshell.v1.DeleteSandboxResponse\"!\x82\xb5\x18\x1d\n" + "\x06bearer\x12\x04user\"\rsandbox:write\x12q\n" + "\vStopSandbox\x12 .openshell.v1.StopSandboxRequest\x1a\x1d.openshell.v1.SandboxResponse\"!\x82\xb5\x18\x1d\n" + @@ -15952,6 +17537,8 @@ const file_openshell_proto_rawDesc = "" + "\x12ReportPolicyStatus\x12'.openshell.v1.ReportPolicyStatusRequest\x1a(.openshell.v1.ReportPolicyStatusResponse\"\r\x82\xb5\x18\t\n" + "\asandbox\x12|\n" + "\x14ReportEndpointStatus\x12).openshell.v1.ReportEndpointStatusRequest\x1a*.openshell.v1.ReportEndpointStatusResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox\x12\x85\x01\n" + + "\x17ReportProviderReadiness\x12,.openshell.v1.ReportProviderReadinessRequest\x1a-.openshell.v1.ReportProviderReadinessResponse\"\r\x82\xb5\x18\t\n" + "\asandbox\x12\x97\x01\n" + "\x1dGetSandboxProviderEnvironment\x122.openshell.v1.GetSandboxProviderEnvironmentRequest\x1a3.openshell.v1.GetSandboxProviderEnvironmentResponse\"\r\x82\xb5\x18\t\n" + "\asandbox\x12\x94\x01\n" + @@ -16020,632 +17607,676 @@ func file_openshell_proto_rawDescGZIP() []byte { return file_openshell_proto_rawDescData } -var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 9) -var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 232) +var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 15) +var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 243) var file_openshell_proto_goTypes = []any{ (SandboxPhase)(0), // 0: openshell.v1.SandboxPhase - (ProviderCredentialTokenGrantType)(0), // 1: openshell.v1.ProviderCredentialTokenGrantType - (ProviderCredentialRefreshStrategy)(0), // 2: openshell.v1.ProviderCredentialRefreshStrategy - (ProviderProfileCategory)(0), // 3: openshell.v1.ProviderProfileCategory - (PolicyStatus)(0), // 4: openshell.v1.PolicyStatus - (ServiceStatus)(0), // 5: openshell.v1.ServiceStatus - (WorkspaceRole)(0), // 6: openshell.v1.WorkspaceRole - (ProviderCredentialRefreshRecoveryAction)(0), // 7: openshell.v1.ProviderCredentialRefreshRecoveryAction - (EndpointResult)(0), // 8: openshell.v1.EndpointResult - (*IssueSandboxTokenRequest)(nil), // 9: openshell.v1.IssueSandboxTokenRequest - (*IssueSandboxTokenResponse)(nil), // 10: openshell.v1.IssueSandboxTokenResponse - (*RefreshSandboxTokenRequest)(nil), // 11: openshell.v1.RefreshSandboxTokenRequest - (*RefreshSandboxTokenResponse)(nil), // 12: openshell.v1.RefreshSandboxTokenResponse - (*HealthRequest)(nil), // 13: openshell.v1.HealthRequest - (*HealthResponse)(nil), // 14: openshell.v1.HealthResponse - (*GetCurrentUserRequest)(nil), // 15: openshell.v1.GetCurrentUserRequest - (*GetCurrentUserResponse)(nil), // 16: openshell.v1.GetCurrentUserResponse - (*GetGatewayInfoRequest)(nil), // 17: openshell.v1.GetGatewayInfoRequest - (*GetGatewayInfoResponse)(nil), // 18: openshell.v1.GetGatewayInfoResponse - (*ComputeDriverInfo)(nil), // 19: openshell.v1.ComputeDriverInfo - (*ComputeDriverCapabilities)(nil), // 20: openshell.v1.ComputeDriverCapabilities - (*ResourceCapabilities)(nil), // 21: openshell.v1.ResourceCapabilities - (*CpuResourceCapabilities)(nil), // 22: openshell.v1.CpuResourceCapabilities - (*MemoryResourceCapabilities)(nil), // 23: openshell.v1.MemoryResourceCapabilities - (*GpuResourceCapabilities)(nil), // 24: openshell.v1.GpuResourceCapabilities - (*Sandbox)(nil), // 25: openshell.v1.Sandbox - (*SandboxSpec)(nil), // 26: openshell.v1.SandboxSpec - (*ResourceRequirements)(nil), // 27: openshell.v1.ResourceRequirements - (*GpuResourceRequirements)(nil), // 28: openshell.v1.GpuResourceRequirements - (*SandboxTemplate)(nil), // 29: openshell.v1.SandboxTemplate - (*SandboxWorkloadTemplate)(nil), // 30: openshell.v1.SandboxWorkloadTemplate - (*SandboxWorkloadTemplateSpec)(nil), // 31: openshell.v1.SandboxWorkloadTemplateSpec - (*SandboxWorkloadConfig)(nil), // 32: openshell.v1.SandboxWorkloadConfig - (*SandboxResources)(nil), // 33: openshell.v1.SandboxResources - (*SandboxServiceLevel)(nil), // 34: openshell.v1.SandboxServiceLevel - (*SandboxStartup)(nil), // 35: openshell.v1.SandboxStartup - (*SandboxWorkloadTemplateProvenance)(nil), // 36: openshell.v1.SandboxWorkloadTemplateProvenance - (*SandboxStatus)(nil), // 37: openshell.v1.SandboxStatus - (*SandboxCondition)(nil), // 38: openshell.v1.SandboxCondition - (*PlatformEvent)(nil), // 39: openshell.v1.PlatformEvent - (*CreateSandboxRequest)(nil), // 40: openshell.v1.CreateSandboxRequest - (*CreateSandboxTemplateRequest)(nil), // 41: openshell.v1.CreateSandboxTemplateRequest - (*GetSandboxTemplateRequest)(nil), // 42: openshell.v1.GetSandboxTemplateRequest - (*ListSandboxTemplatesRequest)(nil), // 43: openshell.v1.ListSandboxTemplatesRequest - (*DeleteSandboxTemplateRequest)(nil), // 44: openshell.v1.DeleteSandboxTemplateRequest - (*SandboxTemplateResponse)(nil), // 45: openshell.v1.SandboxTemplateResponse - (*ListSandboxTemplatesResponse)(nil), // 46: openshell.v1.ListSandboxTemplatesResponse - (*DeleteSandboxTemplateResponse)(nil), // 47: openshell.v1.DeleteSandboxTemplateResponse - (*BeginRootfsTarStagingRequest)(nil), // 48: openshell.v1.BeginRootfsTarStagingRequest - (*BeginRootfsTarStagingResponse)(nil), // 49: openshell.v1.BeginRootfsTarStagingResponse - (*GetSandboxRequest)(nil), // 50: openshell.v1.GetSandboxRequest - (*ListSandboxesRequest)(nil), // 51: openshell.v1.ListSandboxesRequest - (*ListSandboxProvidersRequest)(nil), // 52: openshell.v1.ListSandboxProvidersRequest - (*AttachSandboxProviderRequest)(nil), // 53: openshell.v1.AttachSandboxProviderRequest - (*DetachSandboxProviderRequest)(nil), // 54: openshell.v1.DetachSandboxProviderRequest - (*DeleteSandboxRequest)(nil), // 55: openshell.v1.DeleteSandboxRequest - (*StopSandboxRequest)(nil), // 56: openshell.v1.StopSandboxRequest - (*StartSandboxRequest)(nil), // 57: openshell.v1.StartSandboxRequest - (*SandboxResponse)(nil), // 58: openshell.v1.SandboxResponse - (*ListSandboxesResponse)(nil), // 59: openshell.v1.ListSandboxesResponse - (*ListSandboxProvidersResponse)(nil), // 60: openshell.v1.ListSandboxProvidersResponse - (*AttachSandboxProviderResponse)(nil), // 61: openshell.v1.AttachSandboxProviderResponse - (*DetachSandboxProviderResponse)(nil), // 62: openshell.v1.DetachSandboxProviderResponse - (*DeleteSandboxResponse)(nil), // 63: openshell.v1.DeleteSandboxResponse - (*CreateSshSessionRequest)(nil), // 64: openshell.v1.CreateSshSessionRequest - (*CreateSshSessionResponse)(nil), // 65: openshell.v1.CreateSshSessionResponse - (*ExposeServiceRequest)(nil), // 66: openshell.v1.ExposeServiceRequest - (*GetServiceRequest)(nil), // 67: openshell.v1.GetServiceRequest - (*ListServicesRequest)(nil), // 68: openshell.v1.ListServicesRequest - (*ListServicesResponse)(nil), // 69: openshell.v1.ListServicesResponse - (*DeleteServiceRequest)(nil), // 70: openshell.v1.DeleteServiceRequest - (*DeleteServiceResponse)(nil), // 71: openshell.v1.DeleteServiceResponse - (*ServiceEndpoint)(nil), // 72: openshell.v1.ServiceEndpoint - (*ServiceEndpointResponse)(nil), // 73: openshell.v1.ServiceEndpointResponse - (*RevokeSshSessionRequest)(nil), // 74: openshell.v1.RevokeSshSessionRequest - (*RevokeSshSessionResponse)(nil), // 75: openshell.v1.RevokeSshSessionResponse - (*ExecSandboxRequest)(nil), // 76: openshell.v1.ExecSandboxRequest - (*ExecSandboxStdout)(nil), // 77: openshell.v1.ExecSandboxStdout - (*ExecSandboxStderr)(nil), // 78: openshell.v1.ExecSandboxStderr - (*ExecSandboxExit)(nil), // 79: openshell.v1.ExecSandboxExit - (*ExecSandboxEvent)(nil), // 80: openshell.v1.ExecSandboxEvent - (*TcpForwardInit)(nil), // 81: openshell.v1.TcpForwardInit - (*TcpForwardFrame)(nil), // 82: openshell.v1.TcpForwardFrame - (*ExecSandboxInput)(nil), // 83: openshell.v1.ExecSandboxInput - (*ExecSandboxWindowResize)(nil), // 84: openshell.v1.ExecSandboxWindowResize - (*SshSession)(nil), // 85: openshell.v1.SshSession - (*WatchSandboxRequest)(nil), // 86: openshell.v1.WatchSandboxRequest - (*SandboxStreamEvent)(nil), // 87: openshell.v1.SandboxStreamEvent - (*SandboxLogLine)(nil), // 88: openshell.v1.SandboxLogLine - (*SandboxStreamWarning)(nil), // 89: openshell.v1.SandboxStreamWarning - (*CreateProviderRequest)(nil), // 90: openshell.v1.CreateProviderRequest - (*GetProviderRequest)(nil), // 91: openshell.v1.GetProviderRequest - (*ListProvidersRequest)(nil), // 92: openshell.v1.ListProvidersRequest - (*UpdateProviderRequest)(nil), // 93: openshell.v1.UpdateProviderRequest - (*DeleteProviderRequest)(nil), // 94: openshell.v1.DeleteProviderRequest - (*ProviderResponse)(nil), // 95: openshell.v1.ProviderResponse - (*ListProvidersResponse)(nil), // 96: openshell.v1.ListProvidersResponse - (*ListProviderProfilesRequest)(nil), // 97: openshell.v1.ListProviderProfilesRequest - (*GetProviderProfileRequest)(nil), // 98: openshell.v1.GetProviderProfileRequest - (*ProviderProfileImportItem)(nil), // 99: openshell.v1.ProviderProfileImportItem - (*ProviderProfileDiagnostic)(nil), // 100: openshell.v1.ProviderProfileDiagnostic - (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 101: openshell.v1.ProviderCredentialTokenGrantAudienceOverride - (*ProviderCredentialTokenGrantSubjectToken)(nil), // 102: openshell.v1.ProviderCredentialTokenGrantSubjectToken - (*ProviderCredentialTokenGrant)(nil), // 103: openshell.v1.ProviderCredentialTokenGrant - (*ProviderProfileCredential)(nil), // 104: openshell.v1.ProviderProfileCredential - (*ProviderCredentialRefreshMaterial)(nil), // 105: openshell.v1.ProviderCredentialRefreshMaterial - (*ProviderCredentialRefreshOutput)(nil), // 106: openshell.v1.ProviderCredentialRefreshOutput - (*ProviderCredentialRefresh)(nil), // 107: openshell.v1.ProviderCredentialRefresh - (*ProviderCredentialRefreshStatus)(nil), // 108: openshell.v1.ProviderCredentialRefreshStatus - (*ProviderProfileDiscovery)(nil), // 109: openshell.v1.ProviderProfileDiscovery - (*GetProviderRefreshStatusRequest)(nil), // 110: openshell.v1.GetProviderRefreshStatusRequest - (*GetProviderRefreshStatusResponse)(nil), // 111: openshell.v1.GetProviderRefreshStatusResponse - (*ConfigureProviderRefreshRequest)(nil), // 112: openshell.v1.ConfigureProviderRefreshRequest - (*ConfigureProviderRefreshResponse)(nil), // 113: openshell.v1.ConfigureProviderRefreshResponse - (*RotateProviderCredentialRequest)(nil), // 114: openshell.v1.RotateProviderCredentialRequest - (*RotateProviderCredentialResponse)(nil), // 115: openshell.v1.RotateProviderCredentialResponse - (*DeleteProviderRefreshRequest)(nil), // 116: openshell.v1.DeleteProviderRefreshRequest - (*DeleteProviderRefreshResponse)(nil), // 117: openshell.v1.DeleteProviderRefreshResponse - (*ProviderProfile)(nil), // 118: openshell.v1.ProviderProfile - (*ProviderProfileResponse)(nil), // 119: openshell.v1.ProviderProfileResponse - (*ListProviderProfilesResponse)(nil), // 120: openshell.v1.ListProviderProfilesResponse - (*ImportProviderProfilesRequest)(nil), // 121: openshell.v1.ImportProviderProfilesRequest - (*ImportProviderProfilesResponse)(nil), // 122: openshell.v1.ImportProviderProfilesResponse - (*UpdateProviderProfilesRequest)(nil), // 123: openshell.v1.UpdateProviderProfilesRequest - (*UpdateProviderProfilesResponse)(nil), // 124: openshell.v1.UpdateProviderProfilesResponse - (*LintProviderProfilesRequest)(nil), // 125: openshell.v1.LintProviderProfilesRequest - (*LintProviderProfilesResponse)(nil), // 126: openshell.v1.LintProviderProfilesResponse - (*DeleteProviderResponse)(nil), // 127: openshell.v1.DeleteProviderResponse - (*DeleteProviderProfileRequest)(nil), // 128: openshell.v1.DeleteProviderProfileRequest - (*DeleteProviderProfileResponse)(nil), // 129: openshell.v1.DeleteProviderProfileResponse - (*GetSandboxProviderEnvironmentRequest)(nil), // 130: openshell.v1.GetSandboxProviderEnvironmentRequest - (*StaticCredentialEndpointBinding)(nil), // 131: openshell.v1.StaticCredentialEndpointBinding - (*StaticCredentialBinding)(nil), // 132: openshell.v1.StaticCredentialBinding - (*GetSandboxProviderEnvironmentResponse)(nil), // 133: openshell.v1.GetSandboxProviderEnvironmentResponse - (*ExchangeProviderSubjectTokenRequest)(nil), // 134: openshell.v1.ExchangeProviderSubjectTokenRequest - (*ExchangeProviderSubjectTokenResponse)(nil), // 135: openshell.v1.ExchangeProviderSubjectTokenResponse - (*UpdateConfigRequest)(nil), // 136: openshell.v1.UpdateConfigRequest - (*PolicyMergeOperation)(nil), // 137: openshell.v1.PolicyMergeOperation - (*AddNetworkRule)(nil), // 138: openshell.v1.AddNetworkRule - (*RemoveNetworkEndpoint)(nil), // 139: openshell.v1.RemoveNetworkEndpoint - (*RemoveNetworkRule)(nil), // 140: openshell.v1.RemoveNetworkRule - (*AddDenyRules)(nil), // 141: openshell.v1.AddDenyRules - (*AddAllowRules)(nil), // 142: openshell.v1.AddAllowRules - (*RemoveNetworkBinary)(nil), // 143: openshell.v1.RemoveNetworkBinary - (*UpdateConfigResponse)(nil), // 144: openshell.v1.UpdateConfigResponse - (*GetSandboxPolicyStatusRequest)(nil), // 145: openshell.v1.GetSandboxPolicyStatusRequest - (*GetSandboxPolicyStatusResponse)(nil), // 146: openshell.v1.GetSandboxPolicyStatusResponse - (*ListSandboxPoliciesRequest)(nil), // 147: openshell.v1.ListSandboxPoliciesRequest - (*ListSandboxPoliciesResponse)(nil), // 148: openshell.v1.ListSandboxPoliciesResponse - (*ReportPolicyStatusRequest)(nil), // 149: openshell.v1.ReportPolicyStatusRequest - (*ReportPolicyStatusResponse)(nil), // 150: openshell.v1.ReportPolicyStatusResponse - (*SandboxPolicyRevision)(nil), // 151: openshell.v1.SandboxPolicyRevision - (*GetSandboxLogsRequest)(nil), // 152: openshell.v1.GetSandboxLogsRequest - (*PushSandboxLogsRequest)(nil), // 153: openshell.v1.PushSandboxLogsRequest - (*PushSandboxLogsResponse)(nil), // 154: openshell.v1.PushSandboxLogsResponse - (*GetSandboxLogsResponse)(nil), // 155: openshell.v1.GetSandboxLogsResponse - (*SupervisorMessage)(nil), // 156: openshell.v1.SupervisorMessage - (*GatewayMessage)(nil), // 157: openshell.v1.GatewayMessage - (*SupervisorHello)(nil), // 158: openshell.v1.SupervisorHello - (*SessionAccepted)(nil), // 159: openshell.v1.SessionAccepted - (*SessionRejected)(nil), // 160: openshell.v1.SessionRejected - (*SupervisorHeartbeat)(nil), // 161: openshell.v1.SupervisorHeartbeat - (*GatewayHeartbeat)(nil), // 162: openshell.v1.GatewayHeartbeat - (*ReportMainProcessExitRequest)(nil), // 163: openshell.v1.ReportMainProcessExitRequest - (*ReportMainProcessExitResponse)(nil), // 164: openshell.v1.ReportMainProcessExitResponse - (*FinalizeMainProcessExitRequest)(nil), // 165: openshell.v1.FinalizeMainProcessExitRequest - (*FinalizeMainProcessExitResponse)(nil), // 166: openshell.v1.FinalizeMainProcessExitResponse - (*RelayOpen)(nil), // 167: openshell.v1.RelayOpen - (*SshRelayTarget)(nil), // 168: openshell.v1.SshRelayTarget - (*TcpRelayTarget)(nil), // 169: openshell.v1.TcpRelayTarget - (*RelayInit)(nil), // 170: openshell.v1.RelayInit - (*RelayFrame)(nil), // 171: openshell.v1.RelayFrame - (*RelayOpenResult)(nil), // 172: openshell.v1.RelayOpenResult - (*RelayClose)(nil), // 173: openshell.v1.RelayClose - (*L7RequestSample)(nil), // 174: openshell.v1.L7RequestSample - (*DenialSummary)(nil), // 175: openshell.v1.DenialSummary - (*DenialGroupCount)(nil), // 176: openshell.v1.DenialGroupCount - (*NetworkActivitySummary)(nil), // 177: openshell.v1.NetworkActivitySummary - (*PolicyChunk)(nil), // 178: openshell.v1.PolicyChunk - (*DraftPolicyUpdate)(nil), // 179: openshell.v1.DraftPolicyUpdate - (*SubmitPolicyAnalysisRequest)(nil), // 180: openshell.v1.SubmitPolicyAnalysisRequest - (*SubmitPolicyAnalysisResponse)(nil), // 181: openshell.v1.SubmitPolicyAnalysisResponse - (*GetDraftPolicyRequest)(nil), // 182: openshell.v1.GetDraftPolicyRequest - (*GetDraftPolicyResponse)(nil), // 183: openshell.v1.GetDraftPolicyResponse - (*ApproveDraftChunkRequest)(nil), // 184: openshell.v1.ApproveDraftChunkRequest - (*ApproveDraftChunkResponse)(nil), // 185: openshell.v1.ApproveDraftChunkResponse - (*RejectDraftChunkRequest)(nil), // 186: openshell.v1.RejectDraftChunkRequest - (*RejectDraftChunkResponse)(nil), // 187: openshell.v1.RejectDraftChunkResponse - (*DraftChunkApproval)(nil), // 188: openshell.v1.DraftChunkApproval - (*ApproveAllDraftChunksRequest)(nil), // 189: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 190: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 191: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 192: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 193: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 194: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 195: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 196: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 197: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 198: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 199: openshell.v1.GetDraftHistoryResponse - (*CreateWorkspaceRequest)(nil), // 200: openshell.v1.CreateWorkspaceRequest - (*CreateWorkspaceResponse)(nil), // 201: openshell.v1.CreateWorkspaceResponse - (*GetWorkspaceRequest)(nil), // 202: openshell.v1.GetWorkspaceRequest - (*GetWorkspaceResponse)(nil), // 203: openshell.v1.GetWorkspaceResponse - (*ListWorkspacesRequest)(nil), // 204: openshell.v1.ListWorkspacesRequest - (*ListWorkspacesResponse)(nil), // 205: openshell.v1.ListWorkspacesResponse - (*DeleteWorkspaceRequest)(nil), // 206: openshell.v1.DeleteWorkspaceRequest - (*DeleteWorkspaceResponse)(nil), // 207: openshell.v1.DeleteWorkspaceResponse - (*WorkspaceMember)(nil), // 208: openshell.v1.WorkspaceMember - (*AddWorkspaceMemberRequest)(nil), // 209: openshell.v1.AddWorkspaceMemberRequest - (*AddWorkspaceMemberResponse)(nil), // 210: openshell.v1.AddWorkspaceMemberResponse - (*RemoveWorkspaceMemberRequest)(nil), // 211: openshell.v1.RemoveWorkspaceMemberRequest - (*RemoveWorkspaceMemberResponse)(nil), // 212: openshell.v1.RemoveWorkspaceMemberResponse - (*ListWorkspaceMembersRequest)(nil), // 213: openshell.v1.ListWorkspaceMembersRequest - (*ListWorkspaceMembersResponse)(nil), // 214: openshell.v1.ListWorkspaceMembersResponse - (*ExtensionServiceCredential)(nil), // 215: openshell.v1.ExtensionServiceCredential - (*EndpointObservation)(nil), // 216: openshell.v1.EndpointObservation - (*ReportEndpointStatusRequest)(nil), // 217: openshell.v1.ReportEndpointStatusRequest - (*ReportEndpointStatusResponse)(nil), // 218: openshell.v1.ReportEndpointStatusResponse - (*EndpointStatus)(nil), // 219: openshell.v1.EndpointStatus - nil, // 220: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 221: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 222: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 223: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 224: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry - nil, // 225: openshell.v1.PlatformEvent.MetadataEntry - nil, // 226: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 227: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 228: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 229: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 230: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - nil, // 231: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 232: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 233: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 234: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - nil, // 235: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 236: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 237: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 238: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 239: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 240: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*datamodelv1.ObjectMeta)(nil), // 241: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 242: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 243: google.protobuf.Struct - (*durationpb.Duration)(nil), // 244: google.protobuf.Duration - (*datamodelv1.WorkspaceSelector)(nil), // 245: openshell.datamodel.v1.WorkspaceSelector - (*datamodelv1.Provider)(nil), // 246: openshell.datamodel.v1.Provider - (*sandboxv1.NetworkEndpoint)(nil), // 247: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 248: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 249: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 250: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 251: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 252: openshell.sandbox.v1.L7Rule - (*datamodelv1.Workspace)(nil), // 253: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 254: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 255: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 256: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 257: openshell.sandbox.v1.GetGatewayConfigResponse + (ProviderMutationKind)(0), // 1: openshell.v1.ProviderMutationKind + (ProviderReadinessState)(0), // 2: openshell.v1.ProviderReadinessState + (ProviderReadinessReason)(0), // 3: openshell.v1.ProviderReadinessReason + (ConfigComponent)(0), // 4: openshell.v1.ConfigComponent + (ConfigApplyOutcome)(0), // 5: openshell.v1.ConfigApplyOutcome + (ConfigUpdateOperationState)(0), // 6: openshell.v1.ConfigUpdateOperationState + (ProviderCredentialTokenGrantType)(0), // 7: openshell.v1.ProviderCredentialTokenGrantType + (ProviderCredentialRefreshStrategy)(0), // 8: openshell.v1.ProviderCredentialRefreshStrategy + (ProviderProfileCategory)(0), // 9: openshell.v1.ProviderProfileCategory + (PolicyStatus)(0), // 10: openshell.v1.PolicyStatus + (ServiceStatus)(0), // 11: openshell.v1.ServiceStatus + (WorkspaceRole)(0), // 12: openshell.v1.WorkspaceRole + (ProviderCredentialRefreshRecoveryAction)(0), // 13: openshell.v1.ProviderCredentialRefreshRecoveryAction + (EndpointResult)(0), // 14: openshell.v1.EndpointResult + (*IssueSandboxTokenRequest)(nil), // 15: openshell.v1.IssueSandboxTokenRequest + (*IssueSandboxTokenResponse)(nil), // 16: openshell.v1.IssueSandboxTokenResponse + (*RefreshSandboxTokenRequest)(nil), // 17: openshell.v1.RefreshSandboxTokenRequest + (*RefreshSandboxTokenResponse)(nil), // 18: openshell.v1.RefreshSandboxTokenResponse + (*HealthRequest)(nil), // 19: openshell.v1.HealthRequest + (*HealthResponse)(nil), // 20: openshell.v1.HealthResponse + (*GetCurrentUserRequest)(nil), // 21: openshell.v1.GetCurrentUserRequest + (*GetCurrentUserResponse)(nil), // 22: openshell.v1.GetCurrentUserResponse + (*GetGatewayInfoRequest)(nil), // 23: openshell.v1.GetGatewayInfoRequest + (*GetGatewayInfoResponse)(nil), // 24: openshell.v1.GetGatewayInfoResponse + (*ComputeDriverInfo)(nil), // 25: openshell.v1.ComputeDriverInfo + (*ComputeDriverCapabilities)(nil), // 26: openshell.v1.ComputeDriverCapabilities + (*ResourceCapabilities)(nil), // 27: openshell.v1.ResourceCapabilities + (*CpuResourceCapabilities)(nil), // 28: openshell.v1.CpuResourceCapabilities + (*MemoryResourceCapabilities)(nil), // 29: openshell.v1.MemoryResourceCapabilities + (*GpuResourceCapabilities)(nil), // 30: openshell.v1.GpuResourceCapabilities + (*Sandbox)(nil), // 31: openshell.v1.Sandbox + (*SandboxSpec)(nil), // 32: openshell.v1.SandboxSpec + (*ResourceRequirements)(nil), // 33: openshell.v1.ResourceRequirements + (*GpuResourceRequirements)(nil), // 34: openshell.v1.GpuResourceRequirements + (*SandboxTemplate)(nil), // 35: openshell.v1.SandboxTemplate + (*SandboxWorkloadTemplate)(nil), // 36: openshell.v1.SandboxWorkloadTemplate + (*SandboxWorkloadTemplateSpec)(nil), // 37: openshell.v1.SandboxWorkloadTemplateSpec + (*SandboxWorkloadConfig)(nil), // 38: openshell.v1.SandboxWorkloadConfig + (*SandboxResources)(nil), // 39: openshell.v1.SandboxResources + (*SandboxServiceLevel)(nil), // 40: openshell.v1.SandboxServiceLevel + (*SandboxStartup)(nil), // 41: openshell.v1.SandboxStartup + (*SandboxWorkloadTemplateProvenance)(nil), // 42: openshell.v1.SandboxWorkloadTemplateProvenance + (*SandboxStatus)(nil), // 43: openshell.v1.SandboxStatus + (*SandboxCondition)(nil), // 44: openshell.v1.SandboxCondition + (*PlatformEvent)(nil), // 45: openshell.v1.PlatformEvent + (*CreateSandboxRequest)(nil), // 46: openshell.v1.CreateSandboxRequest + (*CreateSandboxTemplateRequest)(nil), // 47: openshell.v1.CreateSandboxTemplateRequest + (*GetSandboxTemplateRequest)(nil), // 48: openshell.v1.GetSandboxTemplateRequest + (*ListSandboxTemplatesRequest)(nil), // 49: openshell.v1.ListSandboxTemplatesRequest + (*DeleteSandboxTemplateRequest)(nil), // 50: openshell.v1.DeleteSandboxTemplateRequest + (*SandboxTemplateResponse)(nil), // 51: openshell.v1.SandboxTemplateResponse + (*ListSandboxTemplatesResponse)(nil), // 52: openshell.v1.ListSandboxTemplatesResponse + (*DeleteSandboxTemplateResponse)(nil), // 53: openshell.v1.DeleteSandboxTemplateResponse + (*BeginRootfsTarStagingRequest)(nil), // 54: openshell.v1.BeginRootfsTarStagingRequest + (*BeginRootfsTarStagingResponse)(nil), // 55: openshell.v1.BeginRootfsTarStagingResponse + (*GetSandboxRequest)(nil), // 56: openshell.v1.GetSandboxRequest + (*ListSandboxesRequest)(nil), // 57: openshell.v1.ListSandboxesRequest + (*ListSandboxProvidersRequest)(nil), // 58: openshell.v1.ListSandboxProvidersRequest + (*AttachSandboxProviderRequest)(nil), // 59: openshell.v1.AttachSandboxProviderRequest + (*DetachSandboxProviderRequest)(nil), // 60: openshell.v1.DetachSandboxProviderRequest + (*DeleteSandboxRequest)(nil), // 61: openshell.v1.DeleteSandboxRequest + (*StopSandboxRequest)(nil), // 62: openshell.v1.StopSandboxRequest + (*StartSandboxRequest)(nil), // 63: openshell.v1.StartSandboxRequest + (*SandboxResponse)(nil), // 64: openshell.v1.SandboxResponse + (*ListSandboxesResponse)(nil), // 65: openshell.v1.ListSandboxesResponse + (*ListSandboxProvidersResponse)(nil), // 66: openshell.v1.ListSandboxProvidersResponse + (*AttachSandboxProviderResponse)(nil), // 67: openshell.v1.AttachSandboxProviderResponse + (*DetachSandboxProviderResponse)(nil), // 68: openshell.v1.DetachSandboxProviderResponse + (*ProviderDesiredIdentity)(nil), // 69: openshell.v1.ProviderDesiredIdentity + (*ConfigSnapshotRevision)(nil), // 70: openshell.v1.ConfigSnapshotRevision + (*SandboxConfigRevision)(nil), // 71: openshell.v1.SandboxConfigRevision + (*ConfigUpdateOperation)(nil), // 72: openshell.v1.ConfigUpdateOperation + (*ProviderMutationReceipt)(nil), // 73: openshell.v1.ProviderMutationReceipt + (*ProviderReadinessObservation)(nil), // 74: openshell.v1.ProviderReadinessObservation + (*ProviderReadinessStatus)(nil), // 75: openshell.v1.ProviderReadinessStatus + (*GetSandboxProviderStatusRequest)(nil), // 76: openshell.v1.GetSandboxProviderStatusRequest + (*GetSandboxProviderStatusResponse)(nil), // 77: openshell.v1.GetSandboxProviderStatusResponse + (*ReportProviderReadinessRequest)(nil), // 78: openshell.v1.ReportProviderReadinessRequest + (*ReportProviderReadinessResponse)(nil), // 79: openshell.v1.ReportProviderReadinessResponse + (*DeleteSandboxResponse)(nil), // 80: openshell.v1.DeleteSandboxResponse + (*CreateSshSessionRequest)(nil), // 81: openshell.v1.CreateSshSessionRequest + (*CreateSshSessionResponse)(nil), // 82: openshell.v1.CreateSshSessionResponse + (*ExposeServiceRequest)(nil), // 83: openshell.v1.ExposeServiceRequest + (*GetServiceRequest)(nil), // 84: openshell.v1.GetServiceRequest + (*ListServicesRequest)(nil), // 85: openshell.v1.ListServicesRequest + (*ListServicesResponse)(nil), // 86: openshell.v1.ListServicesResponse + (*DeleteServiceRequest)(nil), // 87: openshell.v1.DeleteServiceRequest + (*DeleteServiceResponse)(nil), // 88: openshell.v1.DeleteServiceResponse + (*ServiceEndpoint)(nil), // 89: openshell.v1.ServiceEndpoint + (*ServiceEndpointResponse)(nil), // 90: openshell.v1.ServiceEndpointResponse + (*RevokeSshSessionRequest)(nil), // 91: openshell.v1.RevokeSshSessionRequest + (*RevokeSshSessionResponse)(nil), // 92: openshell.v1.RevokeSshSessionResponse + (*ExecSandboxRequest)(nil), // 93: openshell.v1.ExecSandboxRequest + (*ExecSandboxStdout)(nil), // 94: openshell.v1.ExecSandboxStdout + (*ExecSandboxStderr)(nil), // 95: openshell.v1.ExecSandboxStderr + (*ExecSandboxExit)(nil), // 96: openshell.v1.ExecSandboxExit + (*ExecSandboxEvent)(nil), // 97: openshell.v1.ExecSandboxEvent + (*TcpForwardInit)(nil), // 98: openshell.v1.TcpForwardInit + (*TcpForwardFrame)(nil), // 99: openshell.v1.TcpForwardFrame + (*ExecSandboxInput)(nil), // 100: openshell.v1.ExecSandboxInput + (*ExecSandboxWindowResize)(nil), // 101: openshell.v1.ExecSandboxWindowResize + (*SshSession)(nil), // 102: openshell.v1.SshSession + (*WatchSandboxRequest)(nil), // 103: openshell.v1.WatchSandboxRequest + (*SandboxStreamEvent)(nil), // 104: openshell.v1.SandboxStreamEvent + (*SandboxLogLine)(nil), // 105: openshell.v1.SandboxLogLine + (*SandboxStreamWarning)(nil), // 106: openshell.v1.SandboxStreamWarning + (*CreateProviderRequest)(nil), // 107: openshell.v1.CreateProviderRequest + (*GetProviderRequest)(nil), // 108: openshell.v1.GetProviderRequest + (*ListProvidersRequest)(nil), // 109: openshell.v1.ListProvidersRequest + (*UpdateProviderRequest)(nil), // 110: openshell.v1.UpdateProviderRequest + (*DeleteProviderRequest)(nil), // 111: openshell.v1.DeleteProviderRequest + (*ProviderResponse)(nil), // 112: openshell.v1.ProviderResponse + (*ListProvidersResponse)(nil), // 113: openshell.v1.ListProvidersResponse + (*ListProviderProfilesRequest)(nil), // 114: openshell.v1.ListProviderProfilesRequest + (*GetProviderProfileRequest)(nil), // 115: openshell.v1.GetProviderProfileRequest + (*ProviderProfileImportItem)(nil), // 116: openshell.v1.ProviderProfileImportItem + (*ProviderProfileDiagnostic)(nil), // 117: openshell.v1.ProviderProfileDiagnostic + (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 118: openshell.v1.ProviderCredentialTokenGrantAudienceOverride + (*ProviderCredentialTokenGrantSubjectToken)(nil), // 119: openshell.v1.ProviderCredentialTokenGrantSubjectToken + (*ProviderCredentialTokenGrant)(nil), // 120: openshell.v1.ProviderCredentialTokenGrant + (*ProviderProfileCredential)(nil), // 121: openshell.v1.ProviderProfileCredential + (*ProviderCredentialRefreshMaterial)(nil), // 122: openshell.v1.ProviderCredentialRefreshMaterial + (*ProviderCredentialRefreshOutput)(nil), // 123: openshell.v1.ProviderCredentialRefreshOutput + (*ProviderCredentialRefresh)(nil), // 124: openshell.v1.ProviderCredentialRefresh + (*ProviderCredentialRefreshStatus)(nil), // 125: openshell.v1.ProviderCredentialRefreshStatus + (*ProviderProfileDiscovery)(nil), // 126: openshell.v1.ProviderProfileDiscovery + (*GetProviderRefreshStatusRequest)(nil), // 127: openshell.v1.GetProviderRefreshStatusRequest + (*GetProviderRefreshStatusResponse)(nil), // 128: openshell.v1.GetProviderRefreshStatusResponse + (*ConfigureProviderRefreshRequest)(nil), // 129: openshell.v1.ConfigureProviderRefreshRequest + (*ConfigureProviderRefreshResponse)(nil), // 130: openshell.v1.ConfigureProviderRefreshResponse + (*RotateProviderCredentialRequest)(nil), // 131: openshell.v1.RotateProviderCredentialRequest + (*RotateProviderCredentialResponse)(nil), // 132: openshell.v1.RotateProviderCredentialResponse + (*DeleteProviderRefreshRequest)(nil), // 133: openshell.v1.DeleteProviderRefreshRequest + (*DeleteProviderRefreshResponse)(nil), // 134: openshell.v1.DeleteProviderRefreshResponse + (*ProviderProfile)(nil), // 135: openshell.v1.ProviderProfile + (*ProviderProfileResponse)(nil), // 136: openshell.v1.ProviderProfileResponse + (*ListProviderProfilesResponse)(nil), // 137: openshell.v1.ListProviderProfilesResponse + (*ImportProviderProfilesRequest)(nil), // 138: openshell.v1.ImportProviderProfilesRequest + (*ImportProviderProfilesResponse)(nil), // 139: openshell.v1.ImportProviderProfilesResponse + (*UpdateProviderProfilesRequest)(nil), // 140: openshell.v1.UpdateProviderProfilesRequest + (*UpdateProviderProfilesResponse)(nil), // 141: openshell.v1.UpdateProviderProfilesResponse + (*LintProviderProfilesRequest)(nil), // 142: openshell.v1.LintProviderProfilesRequest + (*LintProviderProfilesResponse)(nil), // 143: openshell.v1.LintProviderProfilesResponse + (*DeleteProviderResponse)(nil), // 144: openshell.v1.DeleteProviderResponse + (*DeleteProviderProfileRequest)(nil), // 145: openshell.v1.DeleteProviderProfileRequest + (*DeleteProviderProfileResponse)(nil), // 146: openshell.v1.DeleteProviderProfileResponse + (*GetSandboxProviderEnvironmentRequest)(nil), // 147: openshell.v1.GetSandboxProviderEnvironmentRequest + (*StaticCredentialEndpointBinding)(nil), // 148: openshell.v1.StaticCredentialEndpointBinding + (*StaticCredentialBinding)(nil), // 149: openshell.v1.StaticCredentialBinding + (*GetSandboxProviderEnvironmentResponse)(nil), // 150: openshell.v1.GetSandboxProviderEnvironmentResponse + (*ExchangeProviderSubjectTokenRequest)(nil), // 151: openshell.v1.ExchangeProviderSubjectTokenRequest + (*ExchangeProviderSubjectTokenResponse)(nil), // 152: openshell.v1.ExchangeProviderSubjectTokenResponse + (*UpdateConfigRequest)(nil), // 153: openshell.v1.UpdateConfigRequest + (*PolicyMergeOperation)(nil), // 154: openshell.v1.PolicyMergeOperation + (*AddNetworkRule)(nil), // 155: openshell.v1.AddNetworkRule + (*RemoveNetworkEndpoint)(nil), // 156: openshell.v1.RemoveNetworkEndpoint + (*RemoveNetworkRule)(nil), // 157: openshell.v1.RemoveNetworkRule + (*AddDenyRules)(nil), // 158: openshell.v1.AddDenyRules + (*AddAllowRules)(nil), // 159: openshell.v1.AddAllowRules + (*RemoveNetworkBinary)(nil), // 160: openshell.v1.RemoveNetworkBinary + (*UpdateConfigResponse)(nil), // 161: openshell.v1.UpdateConfigResponse + (*GetSandboxPolicyStatusRequest)(nil), // 162: openshell.v1.GetSandboxPolicyStatusRequest + (*GetSandboxPolicyStatusResponse)(nil), // 163: openshell.v1.GetSandboxPolicyStatusResponse + (*ListSandboxPoliciesRequest)(nil), // 164: openshell.v1.ListSandboxPoliciesRequest + (*ListSandboxPoliciesResponse)(nil), // 165: openshell.v1.ListSandboxPoliciesResponse + (*ReportPolicyStatusRequest)(nil), // 166: openshell.v1.ReportPolicyStatusRequest + (*ReportPolicyStatusResponse)(nil), // 167: openshell.v1.ReportPolicyStatusResponse + (*SandboxPolicyRevision)(nil), // 168: openshell.v1.SandboxPolicyRevision + (*GetSandboxLogsRequest)(nil), // 169: openshell.v1.GetSandboxLogsRequest + (*PushSandboxLogsRequest)(nil), // 170: openshell.v1.PushSandboxLogsRequest + (*PushSandboxLogsResponse)(nil), // 171: openshell.v1.PushSandboxLogsResponse + (*GetSandboxLogsResponse)(nil), // 172: openshell.v1.GetSandboxLogsResponse + (*SupervisorMessage)(nil), // 173: openshell.v1.SupervisorMessage + (*GatewayMessage)(nil), // 174: openshell.v1.GatewayMessage + (*SupervisorHello)(nil), // 175: openshell.v1.SupervisorHello + (*SessionAccepted)(nil), // 176: openshell.v1.SessionAccepted + (*SessionRejected)(nil), // 177: openshell.v1.SessionRejected + (*SupervisorHeartbeat)(nil), // 178: openshell.v1.SupervisorHeartbeat + (*GatewayHeartbeat)(nil), // 179: openshell.v1.GatewayHeartbeat + (*ReportMainProcessExitRequest)(nil), // 180: openshell.v1.ReportMainProcessExitRequest + (*ReportMainProcessExitResponse)(nil), // 181: openshell.v1.ReportMainProcessExitResponse + (*FinalizeMainProcessExitRequest)(nil), // 182: openshell.v1.FinalizeMainProcessExitRequest + (*FinalizeMainProcessExitResponse)(nil), // 183: openshell.v1.FinalizeMainProcessExitResponse + (*RelayOpen)(nil), // 184: openshell.v1.RelayOpen + (*SshRelayTarget)(nil), // 185: openshell.v1.SshRelayTarget + (*TcpRelayTarget)(nil), // 186: openshell.v1.TcpRelayTarget + (*RelayInit)(nil), // 187: openshell.v1.RelayInit + (*RelayFrame)(nil), // 188: openshell.v1.RelayFrame + (*RelayOpenResult)(nil), // 189: openshell.v1.RelayOpenResult + (*RelayClose)(nil), // 190: openshell.v1.RelayClose + (*L7RequestSample)(nil), // 191: openshell.v1.L7RequestSample + (*DenialSummary)(nil), // 192: openshell.v1.DenialSummary + (*DenialGroupCount)(nil), // 193: openshell.v1.DenialGroupCount + (*NetworkActivitySummary)(nil), // 194: openshell.v1.NetworkActivitySummary + (*PolicyChunk)(nil), // 195: openshell.v1.PolicyChunk + (*DraftPolicyUpdate)(nil), // 196: openshell.v1.DraftPolicyUpdate + (*SubmitPolicyAnalysisRequest)(nil), // 197: openshell.v1.SubmitPolicyAnalysisRequest + (*SubmitPolicyAnalysisResponse)(nil), // 198: openshell.v1.SubmitPolicyAnalysisResponse + (*GetDraftPolicyRequest)(nil), // 199: openshell.v1.GetDraftPolicyRequest + (*GetDraftPolicyResponse)(nil), // 200: openshell.v1.GetDraftPolicyResponse + (*ApproveDraftChunkRequest)(nil), // 201: openshell.v1.ApproveDraftChunkRequest + (*ApproveDraftChunkResponse)(nil), // 202: openshell.v1.ApproveDraftChunkResponse + (*RejectDraftChunkRequest)(nil), // 203: openshell.v1.RejectDraftChunkRequest + (*RejectDraftChunkResponse)(nil), // 204: openshell.v1.RejectDraftChunkResponse + (*DraftChunkApproval)(nil), // 205: openshell.v1.DraftChunkApproval + (*ApproveAllDraftChunksRequest)(nil), // 206: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 207: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 208: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 209: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 210: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 211: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 212: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 213: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 214: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 215: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 216: openshell.v1.GetDraftHistoryResponse + (*CreateWorkspaceRequest)(nil), // 217: openshell.v1.CreateWorkspaceRequest + (*CreateWorkspaceResponse)(nil), // 218: openshell.v1.CreateWorkspaceResponse + (*GetWorkspaceRequest)(nil), // 219: openshell.v1.GetWorkspaceRequest + (*GetWorkspaceResponse)(nil), // 220: openshell.v1.GetWorkspaceResponse + (*ListWorkspacesRequest)(nil), // 221: openshell.v1.ListWorkspacesRequest + (*ListWorkspacesResponse)(nil), // 222: openshell.v1.ListWorkspacesResponse + (*DeleteWorkspaceRequest)(nil), // 223: openshell.v1.DeleteWorkspaceRequest + (*DeleteWorkspaceResponse)(nil), // 224: openshell.v1.DeleteWorkspaceResponse + (*WorkspaceMember)(nil), // 225: openshell.v1.WorkspaceMember + (*AddWorkspaceMemberRequest)(nil), // 226: openshell.v1.AddWorkspaceMemberRequest + (*AddWorkspaceMemberResponse)(nil), // 227: openshell.v1.AddWorkspaceMemberResponse + (*RemoveWorkspaceMemberRequest)(nil), // 228: openshell.v1.RemoveWorkspaceMemberRequest + (*RemoveWorkspaceMemberResponse)(nil), // 229: openshell.v1.RemoveWorkspaceMemberResponse + (*ListWorkspaceMembersRequest)(nil), // 230: openshell.v1.ListWorkspaceMembersRequest + (*ListWorkspaceMembersResponse)(nil), // 231: openshell.v1.ListWorkspaceMembersResponse + (*ExtensionServiceCredential)(nil), // 232: openshell.v1.ExtensionServiceCredential + (*EndpointObservation)(nil), // 233: openshell.v1.EndpointObservation + (*ReportEndpointStatusRequest)(nil), // 234: openshell.v1.ReportEndpointStatusRequest + (*ReportEndpointStatusResponse)(nil), // 235: openshell.v1.ReportEndpointStatusResponse + (*EndpointStatus)(nil), // 236: openshell.v1.EndpointStatus + nil, // 237: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 238: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 239: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 240: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 241: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + nil, // 242: openshell.v1.PlatformEvent.MetadataEntry + nil, // 243: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 244: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 245: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 246: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 247: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + nil, // 248: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 249: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 250: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 251: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + nil, // 252: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 253: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 254: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 255: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 256: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 257: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*datamodelv1.ObjectMeta)(nil), // 258: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 259: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 260: google.protobuf.Struct + (*durationpb.Duration)(nil), // 261: google.protobuf.Duration + (*datamodelv1.WorkspaceSelector)(nil), // 262: openshell.datamodel.v1.WorkspaceSelector + (*datamodelv1.Provider)(nil), // 263: openshell.datamodel.v1.Provider + (sandboxv1.PolicySource)(0), // 264: openshell.sandbox.v1.PolicySource + (*sandboxv1.NetworkEndpoint)(nil), // 265: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 266: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 267: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 268: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 269: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 270: openshell.sandbox.v1.L7Rule + (*datamodelv1.Workspace)(nil), // 271: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 272: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 273: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 274: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 275: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 215, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential - 5, // 1: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus - 5, // 2: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus - 19, // 3: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo - 20, // 4: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities - 21, // 5: openshell.v1.ComputeDriverCapabilities.resource_capabilities:type_name -> openshell.v1.ResourceCapabilities - 22, // 6: openshell.v1.ResourceCapabilities.cpu:type_name -> openshell.v1.CpuResourceCapabilities - 23, // 7: openshell.v1.ResourceCapabilities.memory:type_name -> openshell.v1.MemoryResourceCapabilities - 24, // 8: openshell.v1.ResourceCapabilities.gpu:type_name -> openshell.v1.GpuResourceCapabilities - 241, // 9: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 26, // 10: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec - 37, // 11: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus - 36, // 12: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance - 220, // 13: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry - 29, // 14: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 242, // 15: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 27, // 16: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements - 28, // 17: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 221, // 18: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 222, // 19: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 223, // 20: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 243, // 21: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 243, // 22: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 241, // 23: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 31, // 24: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec - 32, // 25: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig - 243, // 26: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct - 34, // 27: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel - 224, // 28: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry - 33, // 29: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources - 28, // 30: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements - 35, // 31: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup - 244, // 32: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration - 38, // 33: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition + 232, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential + 11, // 1: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus + 11, // 2: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus + 25, // 3: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo + 26, // 4: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities + 27, // 5: openshell.v1.ComputeDriverCapabilities.resource_capabilities:type_name -> openshell.v1.ResourceCapabilities + 28, // 6: openshell.v1.ResourceCapabilities.cpu:type_name -> openshell.v1.CpuResourceCapabilities + 29, // 7: openshell.v1.ResourceCapabilities.memory:type_name -> openshell.v1.MemoryResourceCapabilities + 30, // 8: openshell.v1.ResourceCapabilities.gpu:type_name -> openshell.v1.GpuResourceCapabilities + 258, // 9: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 32, // 10: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec + 43, // 11: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus + 42, // 12: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance + 237, // 13: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 35, // 14: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate + 259, // 15: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 33, // 16: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements + 34, // 17: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements + 238, // 18: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 239, // 19: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 240, // 20: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 260, // 21: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 260, // 22: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 258, // 23: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 37, // 24: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec + 38, // 25: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig + 260, // 26: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct + 40, // 27: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel + 241, // 28: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + 39, // 29: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources + 34, // 30: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements + 41, // 31: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup + 261, // 32: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration + 44, // 33: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition 0, // 34: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 219, // 35: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus - 225, // 36: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 26, // 37: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 226, // 38: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 227, // 39: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 245, // 40: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 30, // 41: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 245, // 42: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 43: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 44: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 45: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 30, // 46: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 30, // 47: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate - 245, // 48: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 49: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 50: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 51: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 52: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 53: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 54: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 55: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 56: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 25, // 57: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 25, // 58: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 246, // 59: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 25, // 60: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 25, // 61: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 245, // 62: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 63: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 64: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 73, // 65: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 245, // 66: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 241, // 67: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 72, // 68: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 228, // 69: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 77, // 70: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 78, // 71: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 79, // 72: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 168, // 73: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 169, // 74: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 81, // 75: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 76, // 76: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 84, // 77: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 241, // 78: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 25, // 79: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 88, // 80: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 39, // 81: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 89, // 82: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 179, // 83: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 229, // 84: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 246, // 85: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 245, // 86: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 87: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 88: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 246, // 89: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 230, // 90: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - 245, // 91: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 92: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 246, // 93: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 246, // 94: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 118, // 95: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 101, // 96: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride - 1, // 97: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType - 102, // 98: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken - 107, // 99: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 103, // 100: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant - 2, // 101: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 105, // 102: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 106, // 103: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput - 2, // 104: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 7, // 105: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 245, // 106: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 108, // 107: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 2, // 108: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 231, // 109: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 245, // 110: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 108, // 111: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 245, // 112: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 108, // 113: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 245, // 114: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 3, // 115: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 104, // 116: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 247, // 117: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 248, // 118: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 109, // 119: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 232, // 120: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 118, // 121: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 118, // 122: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 99, // 123: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 100, // 124: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 118, // 125: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 99, // 126: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 100, // 127: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 118, // 128: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 99, // 129: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 100, // 130: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 131, // 131: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 233, // 132: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 234, // 133: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - 235, // 134: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 236, // 135: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 242, // 136: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 249, // 137: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 137, // 138: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 237, // 139: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 245, // 140: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 138, // 141: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 139, // 142: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 140, // 143: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 141, // 144: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 142, // 145: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 143, // 146: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 250, // 147: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 251, // 148: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 252, // 149: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 238, // 150: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 245, // 151: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 151, // 152: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 245, // 153: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 151, // 154: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 4, // 155: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 4, // 156: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 242, // 157: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 239, // 158: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 245, // 159: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 88, // 160: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 88, // 161: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 158, // 162: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 161, // 163: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 172, // 164: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 173, // 165: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 159, // 166: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 160, // 167: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 162, // 168: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 167, // 169: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 173, // 170: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 168, // 171: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 169, // 172: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 170, // 173: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 174, // 174: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 176, // 175: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 250, // 176: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 242, // 177: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 242, // 178: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 175, // 179: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 178, // 180: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 177, // 181: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 245, // 182: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 178, // 183: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 245, // 184: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 185: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 188, // 186: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 245, // 187: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 250, // 188: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 245, // 189: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 190: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 191: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 245, // 192: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 198, // 193: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 240, // 194: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 253, // 195: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 253, // 196: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 253, // 197: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 241, // 198: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 6, // 199: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 6, // 200: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 208, // 201: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 208, // 202: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 8, // 203: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult - 216, // 204: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation - 8, // 205: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult - 104, // 206: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 132, // 207: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 13, // 208: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 15, // 209: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 17, // 210: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 40, // 211: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 48, // 212: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest - 50, // 213: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 51, // 214: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 41, // 215: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest - 42, // 216: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest - 43, // 217: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest - 44, // 218: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest - 52, // 219: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 53, // 220: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 54, // 221: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 55, // 222: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 56, // 223: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 57, // 224: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 64, // 225: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 66, // 226: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 67, // 227: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 68, // 228: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 70, // 229: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 74, // 230: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 76, // 231: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 82, // 232: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 83, // 233: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 90, // 234: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 91, // 235: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 92, // 236: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 97, // 237: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 98, // 238: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 121, // 239: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 123, // 240: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 125, // 241: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 93, // 242: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 110, // 243: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 112, // 244: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 114, // 245: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 116, // 246: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 94, // 247: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 128, // 248: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 254, // 249: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 255, // 250: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 136, // 251: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 145, // 252: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 147, // 253: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 149, // 254: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 217, // 255: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest - 130, // 256: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 134, // 257: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 152, // 258: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 153, // 259: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 156, // 260: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 163, // 261: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 165, // 262: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest - 171, // 263: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 86, // 264: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 180, // 265: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 182, // 266: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 184, // 267: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 186, // 268: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 189, // 269: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 191, // 270: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 193, // 271: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 195, // 272: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 197, // 273: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 9, // 274: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 11, // 275: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 200, // 276: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 202, // 277: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 204, // 278: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 206, // 279: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 209, // 280: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 211, // 281: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 213, // 282: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 14, // 283: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 16, // 284: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 18, // 285: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 58, // 286: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 49, // 287: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse - 58, // 288: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 59, // 289: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 45, // 290: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 45, // 291: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 46, // 292: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse - 47, // 293: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse - 60, // 294: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 61, // 295: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 62, // 296: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 63, // 297: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 58, // 298: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 58, // 299: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 65, // 300: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 73, // 301: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 73, // 302: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 69, // 303: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 71, // 304: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 75, // 305: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 80, // 306: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 82, // 307: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 80, // 308: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 95, // 309: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 95, // 310: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 96, // 311: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 120, // 312: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 119, // 313: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 122, // 314: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 124, // 315: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 126, // 316: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 95, // 317: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 111, // 318: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 113, // 319: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 115, // 320: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 117, // 321: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 127, // 322: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 129, // 323: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 256, // 324: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 257, // 325: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 144, // 326: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 146, // 327: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 148, // 328: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 150, // 329: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 218, // 330: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse - 133, // 331: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 135, // 332: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 155, // 333: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 154, // 334: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 157, // 335: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 164, // 336: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 166, // 337: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse - 171, // 338: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 87, // 339: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 181, // 340: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 183, // 341: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 185, // 342: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 187, // 343: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 190, // 344: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 192, // 345: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 194, // 346: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 196, // 347: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 199, // 348: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 10, // 349: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 12, // 350: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 201, // 351: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 203, // 352: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 205, // 353: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 207, // 354: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 210, // 355: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 212, // 356: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 214, // 357: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 283, // [283:358] is the sub-list for method output_type - 208, // [208:283] is the sub-list for method input_type - 208, // [208:208] is the sub-list for extension type_name - 208, // [208:208] is the sub-list for extension extendee - 0, // [0:208] is the sub-list for field type_name + 236, // 35: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus + 242, // 36: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 32, // 37: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec + 243, // 38: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 244, // 39: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 262, // 40: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 36, // 41: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 262, // 42: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 43: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 44: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 45: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 36, // 46: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 36, // 47: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate + 262, // 48: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 49: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 50: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 51: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 52: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 53: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 54: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 55: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 56: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 31, // 57: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 31, // 58: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 263, // 59: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 31, // 60: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 73, // 61: openshell.v1.AttachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 31, // 62: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 73, // 63: openshell.v1.DetachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 71, // 64: openshell.v1.ConfigSnapshotRevision.sandbox_config:type_name -> openshell.v1.SandboxConfigRevision + 69, // 65: openshell.v1.ConfigSnapshotRevision.provider_target:type_name -> openshell.v1.ProviderDesiredIdentity + 264, // 66: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource + 4, // 67: openshell.v1.ConfigUpdateOperation.component:type_name -> openshell.v1.ConfigComponent + 70, // 68: openshell.v1.ConfigUpdateOperation.target_revision:type_name -> openshell.v1.ConfigSnapshotRevision + 6, // 69: openshell.v1.ConfigUpdateOperation.state:type_name -> openshell.v1.ConfigUpdateOperationState + 5, // 70: openshell.v1.ConfigUpdateOperation.outcome:type_name -> openshell.v1.ConfigApplyOutcome + 1, // 71: openshell.v1.ProviderMutationReceipt.kind:type_name -> openshell.v1.ProviderMutationKind + 69, // 72: openshell.v1.ProviderMutationReceipt.desired:type_name -> openshell.v1.ProviderDesiredIdentity + 3, // 73: openshell.v1.ProviderReadinessObservation.reason:type_name -> openshell.v1.ProviderReadinessReason + 73, // 74: openshell.v1.ProviderReadinessStatus.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 2, // 75: openshell.v1.ProviderReadinessStatus.state:type_name -> openshell.v1.ProviderReadinessState + 3, // 76: openshell.v1.ProviderReadinessStatus.reason:type_name -> openshell.v1.ProviderReadinessReason + 74, // 77: openshell.v1.ProviderReadinessStatus.observed:type_name -> openshell.v1.ProviderReadinessObservation + 72, // 78: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation + 262, // 79: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 75, // 80: openshell.v1.GetSandboxProviderStatusResponse.status:type_name -> openshell.v1.ProviderReadinessStatus + 74, // 81: openshell.v1.ReportProviderReadinessRequest.observation:type_name -> openshell.v1.ProviderReadinessObservation + 262, // 82: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 83: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 84: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 90, // 85: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 262, // 86: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 258, // 87: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 89, // 88: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 245, // 89: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 94, // 90: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 95, // 91: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 96, // 92: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 185, // 93: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 186, // 94: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 98, // 95: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 93, // 96: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 101, // 97: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 258, // 98: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 31, // 99: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 105, // 100: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 45, // 101: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 106, // 102: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 196, // 103: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 246, // 104: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 263, // 105: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 262, // 106: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 107: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 108: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 263, // 109: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 247, // 110: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + 262, // 111: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 112: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 263, // 113: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 73, // 114: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt + 263, // 115: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 135, // 116: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 118, // 117: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 7, // 118: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType + 119, // 119: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 124, // 120: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 120, // 121: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 8, // 122: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 122, // 123: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 123, // 124: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 8, // 125: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 13, // 126: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 262, // 127: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 125, // 128: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 8, // 129: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 248, // 130: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 262, // 131: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 125, // 132: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 262, // 133: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 125, // 134: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 262, // 135: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 9, // 136: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 121, // 137: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 265, // 138: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 266, // 139: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 126, // 140: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 249, // 141: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 135, // 142: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 135, // 143: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 116, // 144: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 117, // 145: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 135, // 146: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 116, // 147: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 117, // 148: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 135, // 149: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 116, // 150: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 117, // 151: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 148, // 152: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 250, // 153: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 251, // 154: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + 252, // 155: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 253, // 156: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 3, // 157: openshell.v1.GetSandboxProviderEnvironmentResponse.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason + 259, // 158: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 267, // 159: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 154, // 160: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 254, // 161: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 262, // 162: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 155, // 163: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 156, // 164: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 157, // 165: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 158, // 166: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 159, // 167: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 160, // 168: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 268, // 169: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 269, // 170: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 270, // 171: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 255, // 172: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 262, // 173: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 168, // 174: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 262, // 175: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 168, // 176: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 10, // 177: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 10, // 178: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 259, // 179: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 256, // 180: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 262, // 181: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 105, // 182: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 105, // 183: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 175, // 184: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 178, // 185: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 189, // 186: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 190, // 187: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 176, // 188: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 177, // 189: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 179, // 190: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 184, // 191: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 190, // 192: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 185, // 193: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 186, // 194: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 187, // 195: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 191, // 196: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 193, // 197: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 268, // 198: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 259, // 199: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 259, // 200: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 192, // 201: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 195, // 202: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 194, // 203: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 262, // 204: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 195, // 205: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 262, // 206: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 207: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 205, // 208: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 262, // 209: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 268, // 210: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 262, // 211: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 212: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 213: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 214: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 215, // 215: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 257, // 216: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 271, // 217: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 271, // 218: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 271, // 219: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 258, // 220: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 12, // 221: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 12, // 222: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 225, // 223: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 225, // 224: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 14, // 225: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult + 233, // 226: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation + 14, // 227: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult + 121, // 228: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 149, // 229: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 19, // 230: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 21, // 231: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 23, // 232: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 46, // 233: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 54, // 234: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest + 56, // 235: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 57, // 236: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 47, // 237: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest + 48, // 238: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest + 49, // 239: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest + 50, // 240: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest + 58, // 241: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 59, // 242: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 60, // 243: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 76, // 244: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest + 61, // 245: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 62, // 246: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 63, // 247: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 81, // 248: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 83, // 249: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 84, // 250: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 85, // 251: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 87, // 252: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 91, // 253: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 93, // 254: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 99, // 255: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 100, // 256: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 107, // 257: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 108, // 258: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 109, // 259: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 114, // 260: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 115, // 261: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 138, // 262: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 140, // 263: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 142, // 264: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 110, // 265: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 127, // 266: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 129, // 267: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 131, // 268: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 133, // 269: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 111, // 270: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 145, // 271: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 272, // 272: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 273, // 273: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 153, // 274: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 162, // 275: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 164, // 276: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 166, // 277: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 234, // 278: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest + 78, // 279: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest + 147, // 280: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 151, // 281: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 169, // 282: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 170, // 283: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 173, // 284: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 180, // 285: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 182, // 286: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest + 188, // 287: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 103, // 288: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 197, // 289: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 199, // 290: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 201, // 291: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 203, // 292: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 206, // 293: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 208, // 294: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 210, // 295: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 212, // 296: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 214, // 297: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 15, // 298: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 17, // 299: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 217, // 300: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 219, // 301: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 221, // 302: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 223, // 303: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 226, // 304: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 228, // 305: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 230, // 306: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 20, // 307: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 22, // 308: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 24, // 309: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 64, // 310: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 55, // 311: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse + 64, // 312: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 65, // 313: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 51, // 314: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 51, // 315: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 52, // 316: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse + 53, // 317: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse + 66, // 318: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 67, // 319: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 68, // 320: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 77, // 321: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse + 80, // 322: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 64, // 323: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 64, // 324: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 82, // 325: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 90, // 326: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 90, // 327: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 86, // 328: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 88, // 329: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 92, // 330: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 97, // 331: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 99, // 332: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 97, // 333: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 112, // 334: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 112, // 335: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 113, // 336: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 137, // 337: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 136, // 338: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 139, // 339: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 141, // 340: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 143, // 341: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 112, // 342: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 128, // 343: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 130, // 344: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 132, // 345: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 134, // 346: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 144, // 347: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 146, // 348: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 274, // 349: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 275, // 350: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 161, // 351: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 163, // 352: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 165, // 353: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 167, // 354: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 235, // 355: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse + 79, // 356: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse + 150, // 357: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 152, // 358: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 172, // 359: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 171, // 360: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 174, // 361: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 181, // 362: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 183, // 363: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse + 188, // 364: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 104, // 365: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 198, // 366: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 200, // 367: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 202, // 368: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 204, // 369: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 207, // 370: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 209, // 371: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 211, // 372: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 213, // 373: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 216, // 374: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 16, // 375: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 18, // 376: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 218, // 377: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 220, // 378: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 222, // 379: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 224, // 380: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 227, // 381: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 229, // 382: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 231, // 383: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 307, // [307:384] is the sub-list for method output_type + 230, // [230:307] is the sub-list for method input_type + 230, // [230:230] is the sub-list for extension type_name + 230, // [230:230] is the sub-list for extension extendee + 0, // [0:230] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -16656,33 +18287,38 @@ func file_openshell_proto_init() { file_openshell_proto_msgTypes[19].OneofWrappers = []any{} file_openshell_proto_msgTypes[20].OneofWrappers = []any{} file_openshell_proto_msgTypes[28].OneofWrappers = []any{} - file_openshell_proto_msgTypes[71].OneofWrappers = []any{ + file_openshell_proto_msgTypes[55].OneofWrappers = []any{ + (*ConfigSnapshotRevision_SandboxConfig)(nil), + (*ConfigSnapshotRevision_ProviderEnvironment)(nil), + (*ConfigSnapshotRevision_ProviderTarget)(nil), + } + file_openshell_proto_msgTypes[82].OneofWrappers = []any{ (*ExecSandboxEvent_Stdout)(nil), (*ExecSandboxEvent_Stderr)(nil), (*ExecSandboxEvent_Exit)(nil), } - file_openshell_proto_msgTypes[72].OneofWrappers = []any{ + file_openshell_proto_msgTypes[83].OneofWrappers = []any{ (*TcpForwardInit_Ssh)(nil), (*TcpForwardInit_Tcp)(nil), } - file_openshell_proto_msgTypes[73].OneofWrappers = []any{ + file_openshell_proto_msgTypes[84].OneofWrappers = []any{ (*TcpForwardFrame_Init)(nil), (*TcpForwardFrame_Data)(nil), } - file_openshell_proto_msgTypes[74].OneofWrappers = []any{ + file_openshell_proto_msgTypes[85].OneofWrappers = []any{ (*ExecSandboxInput_Start)(nil), (*ExecSandboxInput_Stdin)(nil), (*ExecSandboxInput_Resize)(nil), } - file_openshell_proto_msgTypes[78].OneofWrappers = []any{ + file_openshell_proto_msgTypes[89].OneofWrappers = []any{ (*SandboxStreamEvent_Sandbox)(nil), (*SandboxStreamEvent_Log)(nil), (*SandboxStreamEvent_Event)(nil), (*SandboxStreamEvent_Warning)(nil), (*SandboxStreamEvent_DraftPolicyUpdate)(nil), } - file_openshell_proto_msgTypes[103].OneofWrappers = []any{} - file_openshell_proto_msgTypes[128].OneofWrappers = []any{ + file_openshell_proto_msgTypes[114].OneofWrappers = []any{} + file_openshell_proto_msgTypes[139].OneofWrappers = []any{ (*PolicyMergeOperation_AddRule)(nil), (*PolicyMergeOperation_RemoveEndpoint)(nil), (*PolicyMergeOperation_RemoveRule)(nil), @@ -16690,24 +18326,24 @@ func file_openshell_proto_init() { (*PolicyMergeOperation_AddAllowRules)(nil), (*PolicyMergeOperation_RemoveBinary)(nil), } - file_openshell_proto_msgTypes[147].OneofWrappers = []any{ + file_openshell_proto_msgTypes[158].OneofWrappers = []any{ (*SupervisorMessage_Hello)(nil), (*SupervisorMessage_Heartbeat)(nil), (*SupervisorMessage_RelayOpenResult)(nil), (*SupervisorMessage_RelayClose)(nil), } - file_openshell_proto_msgTypes[148].OneofWrappers = []any{ + file_openshell_proto_msgTypes[159].OneofWrappers = []any{ (*GatewayMessage_SessionAccepted)(nil), (*GatewayMessage_SessionRejected)(nil), (*GatewayMessage_Heartbeat)(nil), (*GatewayMessage_RelayOpen)(nil), (*GatewayMessage_RelayClose)(nil), } - file_openshell_proto_msgTypes[158].OneofWrappers = []any{ + file_openshell_proto_msgTypes[169].OneofWrappers = []any{ (*RelayOpen_Ssh)(nil), (*RelayOpen_Tcp)(nil), } - file_openshell_proto_msgTypes[162].OneofWrappers = []any{ + file_openshell_proto_msgTypes[173].OneofWrappers = []any{ (*RelayFrame_Init)(nil), (*RelayFrame_Data)(nil), } @@ -16716,8 +18352,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: 9, - NumMessages: 232, + NumEnums: 15, + NumMessages: 243, 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 42587e10ef..fab991628c 100644 --- a/sdk/go/proto/openshellv1/openshell_grpc.pb.go +++ b/sdk/go/proto/openshellv1/openshell_grpc.pb.go @@ -37,6 +37,7 @@ const ( OpenShell_ListSandboxProviders_FullMethodName = "/openshell.v1.OpenShell/ListSandboxProviders" OpenShell_AttachSandboxProvider_FullMethodName = "/openshell.v1.OpenShell/AttachSandboxProvider" OpenShell_DetachSandboxProvider_FullMethodName = "/openshell.v1.OpenShell/DetachSandboxProvider" + OpenShell_GetSandboxProviderStatus_FullMethodName = "/openshell.v1.OpenShell/GetSandboxProviderStatus" OpenShell_DeleteSandbox_FullMethodName = "/openshell.v1.OpenShell/DeleteSandbox" OpenShell_StopSandbox_FullMethodName = "/openshell.v1.OpenShell/StopSandbox" OpenShell_StartSandbox_FullMethodName = "/openshell.v1.OpenShell/StartSandbox" @@ -71,6 +72,7 @@ const ( OpenShell_ListSandboxPolicies_FullMethodName = "/openshell.v1.OpenShell/ListSandboxPolicies" OpenShell_ReportPolicyStatus_FullMethodName = "/openshell.v1.OpenShell/ReportPolicyStatus" OpenShell_ReportEndpointStatus_FullMethodName = "/openshell.v1.OpenShell/ReportEndpointStatus" + OpenShell_ReportProviderReadiness_FullMethodName = "/openshell.v1.OpenShell/ReportProviderReadiness" OpenShell_GetSandboxProviderEnvironment_FullMethodName = "/openshell.v1.OpenShell/GetSandboxProviderEnvironment" OpenShell_ExchangeProviderSubjectToken_FullMethodName = "/openshell.v1.OpenShell/ExchangeProviderSubjectToken" OpenShell_GetSandboxLogs_FullMethodName = "/openshell.v1.OpenShell/GetSandboxLogs" @@ -148,6 +150,8 @@ type OpenShellClient interface { AttachSandboxProvider(ctx context.Context, in *AttachSandboxProviderRequest, opts ...grpc.CallOption) (*AttachSandboxProviderResponse, error) // Detach a provider record from an existing sandbox. DetachSandboxProvider(ctx context.Context, in *DetachSandboxProviderRequest, opts ...grpc.CallOption) (*DetachSandboxProviderResponse, error) + // Inspect the installed authority for one sandbox provider mutation. + GetSandboxProviderStatus(ctx context.Context, in *GetSandboxProviderStatusRequest, opts ...grpc.CallOption) (*GetSandboxProviderStatusResponse, error) // Delete a sandbox by name. DeleteSandbox(ctx context.Context, in *DeleteSandboxRequest, opts ...grpc.CallOption) (*DeleteSandboxResponse, error) // Stop a sandbox while retaining its persistent state. @@ -224,6 +228,9 @@ type OpenShellClient interface { ReportPolicyStatus(ctx context.Context, in *ReportPolicyStatusRequest, opts ...grpc.CallOption) (*ReportPolicyStatusResponse, error) // Replace the gateway's observed tool server endpoint status for one sandbox. ReportEndpointStatus(ctx context.Context, in *ReportEndpointStatusRequest, opts ...grpc.CallOption) (*ReportEndpointStatusResponse, error) + // Report installed provider state for the current ConnectSupervisor session. + // Replacing or losing that session invalidates its observations. + ReportProviderReadiness(ctx context.Context, in *ReportProviderReadinessRequest, opts ...grpc.CallOption) (*ReportProviderReadinessResponse, error) // Get provider environment for a sandbox (called by sandbox supervisor at startup). GetSandboxProviderEnvironment(ctx context.Context, in *GetSandboxProviderEnvironmentRequest, opts ...grpc.CallOption) (*GetSandboxProviderEnvironmentResponse, error) // Exchange a stored provider subject token for an intermediate token scoped @@ -459,6 +466,16 @@ func (c *openShellClient) DetachSandboxProvider(ctx context.Context, in *DetachS return out, nil } +func (c *openShellClient) GetSandboxProviderStatus(ctx context.Context, in *GetSandboxProviderStatusRequest, opts ...grpc.CallOption) (*GetSandboxProviderStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetSandboxProviderStatusResponse) + err := c.cc.Invoke(ctx, OpenShell_GetSandboxProviderStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *openShellClient) DeleteSandbox(ctx context.Context, in *DeleteSandboxRequest, opts ...grpc.CallOption) (*DeleteSandboxResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DeleteSandboxResponse) @@ -814,6 +831,16 @@ func (c *openShellClient) ReportEndpointStatus(ctx context.Context, in *ReportEn return out, nil } +func (c *openShellClient) ReportProviderReadiness(ctx context.Context, in *ReportProviderReadinessRequest, opts ...grpc.CallOption) (*ReportProviderReadinessResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReportProviderReadinessResponse) + err := c.cc.Invoke(ctx, OpenShell_ReportProviderReadiness_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *openShellClient) GetSandboxProviderEnvironment(ctx context.Context, in *GetSandboxProviderEnvironmentRequest, opts ...grpc.CallOption) (*GetSandboxProviderEnvironmentResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetSandboxProviderEnvironmentResponse) @@ -1150,6 +1177,8 @@ type OpenShellServer interface { AttachSandboxProvider(context.Context, *AttachSandboxProviderRequest) (*AttachSandboxProviderResponse, error) // Detach a provider record from an existing sandbox. DetachSandboxProvider(context.Context, *DetachSandboxProviderRequest) (*DetachSandboxProviderResponse, error) + // Inspect the installed authority for one sandbox provider mutation. + GetSandboxProviderStatus(context.Context, *GetSandboxProviderStatusRequest) (*GetSandboxProviderStatusResponse, error) // Delete a sandbox by name. DeleteSandbox(context.Context, *DeleteSandboxRequest) (*DeleteSandboxResponse, error) // Stop a sandbox while retaining its persistent state. @@ -1226,6 +1255,9 @@ type OpenShellServer interface { ReportPolicyStatus(context.Context, *ReportPolicyStatusRequest) (*ReportPolicyStatusResponse, error) // Replace the gateway's observed tool server endpoint status for one sandbox. ReportEndpointStatus(context.Context, *ReportEndpointStatusRequest) (*ReportEndpointStatusResponse, error) + // Report installed provider state for the current ConnectSupervisor session. + // Replacing or losing that session invalidates its observations. + ReportProviderReadiness(context.Context, *ReportProviderReadinessRequest) (*ReportProviderReadinessResponse, error) // Get provider environment for a sandbox (called by sandbox supervisor at startup). GetSandboxProviderEnvironment(context.Context, *GetSandboxProviderEnvironmentRequest) (*GetSandboxProviderEnvironmentResponse, error) // Exchange a stored provider subject token for an intermediate token scoped @@ -1363,6 +1395,9 @@ func (UnimplementedOpenShellServer) AttachSandboxProvider(context.Context, *Atta func (UnimplementedOpenShellServer) DetachSandboxProvider(context.Context, *DetachSandboxProviderRequest) (*DetachSandboxProviderResponse, error) { return nil, status.Error(codes.Unimplemented, "method DetachSandboxProvider not implemented") } +func (UnimplementedOpenShellServer) GetSandboxProviderStatus(context.Context, *GetSandboxProviderStatusRequest) (*GetSandboxProviderStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSandboxProviderStatus not implemented") +} func (UnimplementedOpenShellServer) DeleteSandbox(context.Context, *DeleteSandboxRequest) (*DeleteSandboxResponse, error) { return nil, status.Error(codes.Unimplemented, "method DeleteSandbox not implemented") } @@ -1465,6 +1500,9 @@ func (UnimplementedOpenShellServer) ReportPolicyStatus(context.Context, *ReportP func (UnimplementedOpenShellServer) ReportEndpointStatus(context.Context, *ReportEndpointStatusRequest) (*ReportEndpointStatusResponse, error) { return nil, status.Error(codes.Unimplemented, "method ReportEndpointStatus not implemented") } +func (UnimplementedOpenShellServer) ReportProviderReadiness(context.Context, *ReportProviderReadinessRequest) (*ReportProviderReadinessResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReportProviderReadiness not implemented") +} func (UnimplementedOpenShellServer) GetSandboxProviderEnvironment(context.Context, *GetSandboxProviderEnvironmentRequest) (*GetSandboxProviderEnvironmentResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetSandboxProviderEnvironment not implemented") } @@ -1819,6 +1857,24 @@ func _OpenShell_DetachSandboxProvider_Handler(srv interface{}, ctx context.Conte return interceptor(ctx, in, info, handler) } +func _OpenShell_GetSandboxProviderStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSandboxProviderStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetSandboxProviderStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetSandboxProviderStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetSandboxProviderStatus(ctx, req.(*GetSandboxProviderStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _OpenShell_DeleteSandbox_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteSandboxRequest) if err := dec(in); err != nil { @@ -2402,6 +2458,24 @@ func _OpenShell_ReportEndpointStatus_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _OpenShell_ReportProviderReadiness_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportProviderReadinessRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ReportProviderReadiness(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ReportProviderReadiness_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ReportProviderReadiness(ctx, req.(*ReportProviderReadinessRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _OpenShell_GetSandboxProviderEnvironment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetSandboxProviderEnvironmentRequest) if err := dec(in); err != nil { @@ -2911,6 +2985,10 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "DetachSandboxProvider", Handler: _OpenShell_DetachSandboxProvider_Handler, }, + { + MethodName: "GetSandboxProviderStatus", + Handler: _OpenShell_GetSandboxProviderStatus_Handler, + }, { MethodName: "DeleteSandbox", Handler: _OpenShell_DeleteSandbox_Handler, @@ -3035,6 +3113,10 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "ReportEndpointStatus", Handler: _OpenShell_ReportEndpointStatus_Handler, }, + { + MethodName: "ReportProviderReadiness", + Handler: _OpenShell_ReportProviderReadiness_Handler, + }, { MethodName: "GetSandboxProviderEnvironment", Handler: _OpenShell_GetSandboxProviderEnvironment_Handler, diff --git a/sdk/go/proto/sandboxv1/sandbox.pb.go b/sdk/go/proto/sandboxv1/sandbox.pb.go index 17d8909f83..f4d97b43d6 100644 --- a/sdk/go/proto/sandboxv1/sandbox.pb.go +++ b/sdk/go/proto/sandboxv1/sandbox.pb.go @@ -1825,8 +1825,11 @@ type GetSandboxConfigResponse struct { // False also covers older gateways that do not advertise this capability; // supervisors preserve their legacy unauthenticated connection behavior. ExtensionAuthenticationEnabled bool `protobuf:"varint,12,opt,name=extension_authentication_enabled,json=extensionAuthenticationEnabled,proto3" json:"extension_authentication_enabled,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Gateway-owned attachment identity captured with this desired configuration. + // Compare for equality; reattachment invalidates previous installation evidence. + ProviderAttachmentEpoch string `protobuf:"bytes,14,opt,name=provider_attachment_epoch,json=providerAttachmentEpoch,proto3" json:"provider_attachment_epoch,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetSandboxConfigResponse) Reset() { @@ -1943,6 +1946,13 @@ func (x *GetSandboxConfigResponse) GetExtensionAuthenticationEnabled() bool { return false } +func (x *GetSandboxConfigResponse) GetProviderAttachmentEpoch() string { + if x != nil { + return x.ProviderAttachmentEpoch + } + return "" +} + // Connection details for one operator-registered supervisor middleware service. // V1 supports plaintext and server-authenticated TLS gRPC. type SupervisorMiddlewareService struct { @@ -2207,7 +2217,7 @@ const file_sandbox_proto_rawDesc = "" + "\x05value\"\x86\x01\n" + "\x10EffectiveSetting\x128\n" + "\x05value\x18\x01 \x01(\v2\".openshell.sandbox.v1.SettingValueR\x05value\x128\n" + - "\x05scope\x18\x02 \x01(\x0e2\".openshell.sandbox.v1.SettingScopeR\x05scope\"\xd1\x06\n" + + "\x05scope\x18\x02 \x01(\x0e2\".openshell.sandbox.v1.SettingScopeR\x05scope\"\x8d\a\n" + "\x18GetSandboxConfigResponse\x12;\n" + "\x06policy\x18\x01 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x06policy\x12\x18\n" + "\aversion\x18\x02 \x01(\rR\aversion\x12\x1f\n" + @@ -2222,7 +2232,8 @@ const file_sandbox_proto_rawDesc = "" + "\tworkspace\x18\n" + " \x01(\tR\tworkspace\x12C\n" + "\x1epolicy_validation_failure_mode\x18\v \x01(\tR\x1bpolicyValidationFailureMode\x12H\n" + - " extension_authentication_enabled\x18\f \x01(\bR\x1eextensionAuthenticationEnabled\x1ac\n" + + " extension_authentication_enabled\x18\f \x01(\bR\x1eextensionAuthenticationEnabled\x12:\n" + + "\x19provider_attachment_epoch\x18\x0e \x01(\tR\x17providerAttachmentEpoch\x1ac\n" + "\rSettingsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12<\n" + "\x05value\x18\x02 \x01(\v2&.openshell.sandbox.v1.EffectiveSettingR\x05value:\x028\x01\"\x99\x02\n" + diff --git a/skills/debug-inference/SKILL.md b/skills/debug-inference/SKILL.md index 1ba416ce3e..6c9580a9f6 100644 --- a/skills/debug-inference/SKILL.md +++ b/skills/debug-inference/SKILL.md @@ -62,21 +62,23 @@ binding error. ```bash openshell sandbox provider list -openshell sandbox provider attach +openshell sandbox provider attach --wait --timeout 30 ``` -Launch a new process after attaching a provider so it inherits newly available -credential placeholders: +Save the change's `receipt_id` and use `openshell sandbox provider status --receipt --wait --timeout 30` to check when it takes effect. Success confirms that the sandbox applied the credentials, policy, and environment for new processes. If the result is pending, failed, withheld, or superseded, inspect its reason before launching the client. + +Launch the client after the attachment wait succeeds so it receives the updated environment: ```bash -openshell sandbox exec -- env +openshell sandbox exec -- ``` -Do not print or copy credential values into diagnostic output. Detaching a -provider revokes its policy and credential access: +After updating an ordinary static provider, wait for that change and launch a new client. An existing process keeps its revision-scoped reference; readiness does not make the old reference resolve the replacement value. Diagnose managed-refresh credentials according to their own lifecycle. + +Keep credentials and issued references out of diagnostic output. Acknowledged detach revokes future credential resolution and removes the reference from future process environments. Requests already forwarded may still finish: ```bash -openshell sandbox provider detach +openshell sandbox provider detach --wait --timeout 30 ``` ### 4. Verify Native Client Configuration diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index 0d30aacb50..7e37cfd88d 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -164,6 +164,8 @@ openshell provider profile import --file ./my-profile.yaml ### List, inspect, update, delete +Use `openshell sandbox provider status --help` and the attach, detach, and update help to find the installed version's wait options. Add `--wait` when the next step depends on a provider change taking effect. Without it, a successful command only confirms that the gateway saved the change. Save the returned `receipt_id` to check that same change later, and inspect the result for every selected sandbox. Credential refresh status confirms that OpenShell obtained credentials; provider status confirms that the sandbox applied them, activated the policy, and updated the environment for new processes. If the status is `superseded`, explain that a later change replaced the request and inspect that change separately. + ```bash openshell provider list openshell provider list --output json @@ -378,8 +380,9 @@ provider instead of passing API keys, tokens, or other secrets to `sandbox exec` ```bash openshell sandbox provider list my-sandbox openshell sandbox provider list my-sandbox --output json -openshell sandbox provider attach my-sandbox my-github -openshell sandbox provider detach my-sandbox my-github +openshell sandbox provider attach my-sandbox my-github --wait --timeout 30 +openshell sandbox provider status my-sandbox my-github --output json +openshell sandbox provider detach my-sandbox my-github --wait --timeout 30 ``` Structured attachment output contains provider names, types, and sorted @@ -714,14 +717,14 @@ endpoint, create the provider, and attach it only to sandboxes that need it: ```bash openshell provider profile import -f ./inference-provider.yaml openshell provider create --name model-provider --type --credential -openshell sandbox provider attach work-session model-provider +openshell sandbox provider attach work-session model-provider --wait --timeout 30 openshell sandbox exec work-session -- ``` The application owns the native base URL, model, request shape, and timeout. -Launch a new process after attaching a provider so it inherits the provider -credential placeholder. Use the `debug-inference` skill for endpoint, policy, -credential-binding, or migration failures. +Launch a new process after attachment readiness so it inherits the installed provider environment. Use the `debug-inference` skill for endpoint, policy, credential-binding, or migration failures. + +For an ordinary static provider update, wait for the update and launch a new client process to obtain the new reference. Do not claim that readiness updates the environment of an existing process or retargets its old reference. Acknowledged detach revokes retained references and removes them from future process environments. ## Workflow 8: Gateway Management From 8177a08627e2a92410e98e4213dbf8112549d344 Mon Sep 17 00:00:00 2001 From: Shiju Date: Thu, 17 Sep 2026 10:48:02 +0530 Subject: [PATCH 2/4] fix(providers): align readiness times with protobuf contracts Represent readiness receipts, status, and operation times with Timestamp and report intervals with Duration. Reserve the scalar field tags, update all consumers and generated bindings, and preserve timestamp presence and nanosecond identity through storage and client validation. Qualify both empty-map constructors in the Linux boundary test so its module compiles while retaining the explicit default required by Clippy. Signed-off-by: Shiju --- architecture/gateway.md | 2 + .../src/commands/provider_readiness.rs | 49 +- .../tests/provider_commands_integration.rs | 25 +- .../openshell-sandbox/src/boundary_server.rs | 4 +- .../openshell-sdk/src/provider_readiness.rs | 84 +- crates/openshell-server/proto/storage.proto | 5 +- .../src/config_update_operation.rs | 142 ++- .../src/grpc/provider_readiness.rs | 44 +- .../src/grpc/provider_readiness_tests.rs | 55 +- crates/openshell-server/src/storage_proto.rs | 6 +- .../src/provider_readiness.rs | 51 +- docs/providers/profiles.mdx | 2 + e2e/rust/tests/provider_readiness.rs | 18 +- proto/openshell.proto | 26 +- sdk/go/proto/openshellv1/openshell.pb.go | 833 +++++++++--------- 15 files changed, 853 insertions(+), 493 deletions(-) diff --git a/architecture/gateway.md b/architecture/gateway.md index 407bda4ec7..5840a00fc8 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -710,6 +710,8 @@ Each receipt projects a common configuration operation in the `config_update_ope Provider mutation and operation-result writes are separate. A result-storage failure can follow a saved mutation and returns structured uncertainty without a rollback or safe-retry claim. A failed initial snapshot remains failed rather than acquiring a different target during a later lookup. Operations contain only identities, revisions, timestamps, and closed reason categories. +Provider receipts, installation status, and common operations represent absolute times with protobuf `Timestamp`; report intervals and evidence lifetimes use protobuf `Duration`. Receipt identity compares the full canonical timestamp without truncating nanoseconds. An absent observation or completion time represents missing evidence or an unfinished operation, independently of the Unix epoch. + Provider installation reports belong to the existing `ConnectSupervisor` session. Each report names that session, has an increasing sequence, and expires unless the supervisor reports again. Reconnection or disconnect invalidates prior observations; stored change records survive a gateway restart, but runtime evidence does not. Replaying an identical report cannot extend its lifetime. Reports and status also compare the supervisor instance with the sandbox's persisted current instance. A different supervisor becoming current invalidates an older connection, including one retained by another gateway replica. Observations stay local to the gateway holding the supervisor session; a status request reaching a replica without that session returns pending. Multi-replica deployments therefore retain the existing supervisor-session routing requirement. diff --git a/crates/openshell-cli/src/commands/provider_readiness.rs b/crates/openshell-cli/src/commands/provider_readiness.rs index d68d3a610d..c0472853b1 100644 --- a/crates/openshell-cli/src/commands/provider_readiness.rs +++ b/crates/openshell-cli/src/commands/provider_readiness.rs @@ -364,7 +364,7 @@ fn receipt_json(receipt: &ProviderMutationReceipt) -> serde_json::Value { "workspace": receipt.workspace, "kind": receipt.kind().as_str_name().trim_start_matches("PROVIDER_MUTATION_KIND_").to_ascii_lowercase(), "desired": desired, - "persisted_at_ms": receipt.persisted_at_ms, + "persisted_time": receipt.persisted_time.as_ref().map(ToString::to_string), }) } @@ -390,8 +390,8 @@ fn status_json(result: &DisplayStatus) -> serde_json::Value { "reason": reason_label(result.status.reason), "observed": observed, "network_instance_id": result.status.network_instance_id, - "observed_at_ms": result.status.observed_at_ms, - "evaluated_at_ms": result.status.evaluated_at_ms, + "observed_time": result.status.observed_time.as_ref().map(ToString::to_string), + "evaluated_time": result.status.evaluated_time.as_ref().map(ToString::to_string), "wait_outcome": result.outcome, }) } @@ -433,7 +433,10 @@ fn print_statuses(mutation_id: &str, results: &[DisplayStatus], output: &str) -> ); println!( " Persisted: {}", - crate::commands::common::format_epoch_ms(receipt.persisted_at_ms) + receipt + .persisted_time + .as_ref() + .map_or_else(|| "-".to_string(), ToString::to_string) ); if let Some(observed) = result.status.observed.as_ref() { println!( @@ -480,7 +483,7 @@ mod tests { provider_name: "provider".to_string(), workspace: "default".to_string(), kind: ProviderMutationKind::Update.into(), - persisted_at_ms: 1, + persisted_time: Some(openshell_core::time::timestamp_from_millis(1).unwrap()), desired: Some(ProviderDesiredIdentity { sandbox_id: "sandbox-id".to_string(), sandbox_name: "sandbox".to_string(), @@ -519,6 +522,42 @@ mod tests { ); } + #[test] + fn structured_status_renders_rfc3339_timestamps_without_losing_nanos() { + let timestamp = prost_types::Timestamp { + seconds: 1_000, + nanos: 123_456_789, + }; + let receipt = ProviderMutationReceipt { + persisted_time: Some(timestamp), + ..Default::default() + }; + let mut status = persisted_status(receipt); + status.observed_time = Some(timestamp); + status.evaluated_time = Some(timestamp); + let value = status_json(&DisplayStatus { + status, + outcome: "not_requested", + complete: false, + }); + let expected = "1970-01-01T00:16:40.123456789Z"; + assert_eq!(value["receipt"]["persisted_time"], expected); + assert_eq!(value["observed_time"], expected); + assert_eq!(value["evaluated_time"], expected); + assert!(value["receipt"].get("persisted_at_ms").is_none()); + assert!(value.get("observed_at_ms").is_none()); + assert!(value.get("evaluated_at_ms").is_none()); + + let value = status_json(&DisplayStatus { + status: persisted_status(ProviderMutationReceipt::default()), + outcome: "not_requested", + complete: false, + }); + assert!(value["receipt"]["persisted_time"].is_null()); + assert!(value["observed_time"].is_null()); + assert!(value["evaluated_time"].is_null()); + } + #[test] fn structured_status_preserves_opaque_revisions_and_separates_timeout() { let receipt = ProviderMutationReceipt { diff --git a/crates/openshell-cli/tests/provider_commands_integration.rs b/crates/openshell-cli/tests/provider_commands_integration.rs index c00ca05253..b02e256e9d 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -191,7 +191,10 @@ impl TestOpenShell { config_revision: 17, policy_hash: "effective-policy".to_string(), }), - persisted_at_ms: i64::try_from(sequence).unwrap(), + persisted_time: Some( + openshell_core::time::timestamp_from_millis(i64::try_from(sequence).unwrap()) + .unwrap(), + ), }; let corrupt = *self.state.corrupt_mutation_receipt.lock().await; if let Some(corrupt) = corrupt { @@ -525,7 +528,12 @@ impl OpenShell for TestOpenShell { .as_ref() .is_some_and(|desired| desired.sandbox_name == request.sandbox_name) }) - .max_by_key(|receipt| receipt.persisted_at_ms) + .max_by_key(|receipt| { + receipt + .persisted_time + .as_ref() + .map(|time| (time.seconds, time.nanos)) + }) } else { receipts .get(&request.receipt_id) @@ -1568,8 +1576,8 @@ fn readiness_status( ..Default::default() }), network_instance_id: "network-instance".to_string(), - observed_at_ms: 1000, - evaluated_at_ms: 1000, + observed_time: Some(openshell_core::time::timestamp_from_millis(1000).unwrap()), + evaluated_time: Some(openshell_core::time::timestamp_from_millis(1000).unwrap()), ..Default::default() } } @@ -1634,7 +1642,12 @@ async fn latest_readiness_receipt( .as_ref() .is_some_and(|desired| desired.sandbox_name == sandbox_name) }) - .max_by_key(|receipt| receipt.persisted_at_ms) + .max_by_key(|receipt| { + receipt + .persisted_time + .as_ref() + .map(|time| (time.seconds, time.nanos)) + }) .cloned() .expect("sandbox mutation receipt") } @@ -1727,7 +1740,7 @@ async fn provider_readiness_mutations_reject_unbound_receipts_before_output_or_p .sandbox_id = "other-id".to_string(); }), ("desired", |receipt| receipt.desired = None), - ("timestamp", |receipt| receipt.persisted_at_ms = 0), + ("timestamp", |receipt| receipt.persisted_time = None), ("provider_presence", |receipt| { let detached = receipt.kind == i32::from(ProviderMutationKind::Detach); receipt diff --git a/crates/openshell-sandbox/src/boundary_server.rs b/crates/openshell-sandbox/src/boundary_server.rs index 6144491288..a926074f95 100644 --- a/crates/openshell-sandbox/src/boundary_server.rs +++ b/crates/openshell-sandbox/src/boundary_server.rs @@ -4722,7 +4722,7 @@ mod linux { // fingerprint. Distinct publications must still replace the map, // while a delayed older clear must never undo the repair. assert_eq!( - boundary.update_provider_environment(2, 7, HashMap::default()), + boundary.update_provider_environment(2, 7, std::collections::HashMap::default()), Response::ProviderEnvironmentUpdated { revision: 7, generation: 2, @@ -4745,7 +4745,7 @@ mod linux { } ); assert_eq!( - boundary.update_provider_environment(2, 7, HashMap::default()), + boundary.update_provider_environment(2, 7, std::collections::HashMap::default()), Response::ProviderEnvironmentUpdated { revision: 7, generation: 3, diff --git a/crates/openshell-sdk/src/provider_readiness.rs b/crates/openshell-sdk/src/provider_readiness.rs index c28bf245bf..e11f3c8977 100644 --- a/crates/openshell-sdk/src/provider_readiness.rs +++ b/crates/openshell-sdk/src/provider_readiness.rs @@ -220,6 +220,15 @@ fn validate_status(status: &ProviderReadinessStatus) -> Result<(), ProviderReadi { return Err(ProviderReadinessError::InvalidStatus); } + // Absent observation times are expected before a report arrives. Present + // times must be canonical so output cannot normalize malformed timestamps. + for timestamp in [&status.observed_time, &status.evaluated_time] + .into_iter() + .flatten() + { + openshell_core::time::validate_timestamp(timestamp) + .map_err(|_| ProviderReadinessError::InvalidStatus)?; + } let receipt = status .receipt .as_ref() @@ -269,7 +278,8 @@ fn completion_state( /// the mutation kind must still agree with the presence of a provider identity. /// /// # Errors -/// Rejects incomplete receipts and inconsistent mutation/provider identities. +/// Rejects incomplete receipts, invalid persistence timestamps, and inconsistent +/// mutation/provider identities. pub fn validate_provider_receipt( receipt: &ProviderMutationReceipt, ) -> Result<(), ProviderReadinessError> { @@ -277,9 +287,14 @@ pub fn validate_provider_receipt( .desired .as_ref() .ok_or(ProviderReadinessError::InvalidReceipt)?; + let persisted_time = receipt + .persisted_time + .as_ref() + .ok_or(ProviderReadinessError::InvalidReceipt)?; + openshell_core::time::validate_timestamp(persisted_time) + .map_err(|_| ProviderReadinessError::InvalidReceipt)?; if receipt.receipt_id.is_empty() || receipt.mutation_id.is_empty() - || receipt.persisted_at_ms <= 0 || desired.sandbox_id.is_empty() || desired.sandbox_name.is_empty() || receipt.provider_name.is_empty() @@ -331,7 +346,7 @@ fn disposition( && (observed_receipt.receipt_id != receipt.receipt_id || observed_receipt.mutation_id != receipt.mutation_id || observed_receipt.kind != receipt.kind - || observed_receipt.persisted_at_ms != receipt.persisted_at_ms)) + || observed_receipt.persisted_time != receipt.persisted_time)) { status.receipt = Some(receipt.clone()); status.state = ProviderReadinessState::Superseded.into(); @@ -466,7 +481,7 @@ mod tests { mutation_id: "mutation".into(), provider_name: "provider".into(), workspace: "default".into(), - persisted_at_ms: 1, + persisted_time: Some(openshell_core::time::timestamp_from_millis(1).unwrap()), kind: kind.into(), desired: Some(ProviderDesiredIdentity { sandbox_id: "sandbox-id".into(), @@ -509,6 +524,67 @@ mod tests { } } + #[test] + fn receipt_requires_a_present_canonical_persistence_time() { + let mut receipt = receipt(ProviderMutationKind::Attach); + for (seconds, nanos, valid) in [ + (0, 1, true), + (0, 0, true), + (-1, 999_999_999, true), + (1, -1, false), + (1, 1_000_000_000, false), + (253_402_300_800, 0, false), + (-62_135_596_801, 0, false), + ] { + let timestamp = receipt.persisted_time.as_mut().unwrap(); + timestamp.seconds = seconds; + timestamp.nanos = nanos; + assert_eq!( + validate_provider_receipt(&receipt).is_ok(), + valid, + "seconds={seconds}, nanos={nanos}" + ); + } + receipt.persisted_time = None; + assert!(matches!( + validate_provider_receipt(&receipt), + Err(ProviderReadinessError::InvalidReceipt) + )); + } + + #[test] + fn changed_persistence_nanoseconds_supersede_the_original_receipt() { + let receipt = receipt(ProviderMutationKind::Attach); + let mut changed = receipt.clone(); + changed.persisted_time.as_mut().unwrap().nanos += 1; + let mut status = completed_status(&changed, ProviderReadinessState::Ready); + assert_eq!( + disposition(&receipt, &mut status).unwrap(), + Some(ProviderWaitOutcome::Terminal) + ); + assert_eq!(status.state, i32::from(ProviderReadinessState::Superseded)); + assert_eq!(status.receipt.as_ref(), Some(&receipt)); + } + + #[test] + fn status_rejects_malformed_observation_and_evaluation_times() { + let receipt = receipt(ProviderMutationKind::Attach); + let mut malformed = openshell_core::time::timestamp_from_millis(1).unwrap(); + malformed.nanos = -1; + for observation in [true, false] { + let mut status = completed_status(&receipt, ProviderReadinessState::Ready); + if observation { + status.observed_time = Some(malformed); + } else { + status.evaluated_time = Some(malformed); + } + assert!(matches!( + validate_status(&status), + Err(ProviderReadinessError::InvalidStatus) + )); + } + } + #[test] fn direct_status_accepts_receipt_only_and_explicit_provider_selectors() { let receipt = receipt(ProviderMutationKind::Attach); diff --git a/crates/openshell-server/proto/storage.proto b/crates/openshell-server/proto/storage.proto index d7dfaa8985..28e9e9e4a1 100644 --- a/crates/openshell-server/proto/storage.proto +++ b/crates/openshell-server/proto/storage.proto @@ -9,6 +9,7 @@ package openshell.storage.v1; import "datamodel.proto"; import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; import "openshell.proto"; import "options.proto"; import "sandbox.proto"; @@ -119,7 +120,9 @@ message StoredConfigUpdateOperation { openshell.v1.SandboxPhase initial_phase = 5; string idempotency_key = 6; uint32 attempt_count = 7; - int64 next_attempt_at_ms = 8; + reserved 8; + reserved "next_attempt_at_ms"; + google.protobuf.Timestamp next_attempt_time = 108; uint32 response_policy_version = 9; string response_policy_hash = 10; uint64 response_settings_revision = 11; diff --git a/crates/openshell-server/src/config_update_operation.rs b/crates/openshell-server/src/config_update_operation.rs index 7ec7d604dc..2588bdf6b4 100644 --- a/crates/openshell-server/src/config_update_operation.rs +++ b/crates/openshell-server/src/config_update_operation.rs @@ -22,9 +22,7 @@ use prost::Message; use sha2::{Digest, Sha256}; use tonic::{Code, Status}; -use crate::persistence::{ - ObjectRecord, ObjectType, PersistenceError, Store, WriteCondition, current_time_ms, -}; +use crate::persistence::{ObjectRecord, ObjectType, PersistenceError, Store, WriteCondition}; use crate::storage_proto::StoredConfigUpdateOperation; /// Object-store namespace shared by configuration completion resources. @@ -74,6 +72,14 @@ fn invalid_record() -> Status { ) } +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, @@ -129,6 +135,7 @@ pub async fn record_provider_operation( { 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(), @@ -154,18 +161,16 @@ pub async fn record_provider_operation( } else { String::new() }, - created_at_ms: receipt.persisted_at_ms, - updated_at_ms: receipt.persisted_at_ms, - completed_at_ms: if failed { receipt.persisted_at_ms } else { 0 }, + 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: openshell_core::time::timestamp_from_millis(receipt.persisted_at_ms) - .map(Some) - .map_err(|_| invalid_record())?, + created_time: Some(persisted_time), ..Default::default() }), operation: Some(operation), @@ -220,6 +225,7 @@ fn decode_provider_operation( 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 @@ -228,6 +234,8 @@ fn decode_provider_operation( || 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 @@ -372,8 +380,13 @@ pub async fn observe_provider_status( } else { String::new() }; - operation.updated_at_ms = current_time_ms(); - operation.completed_at_ms = operation.updated_at_ms; + 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, @@ -427,7 +440,10 @@ mod tests { config_revision: 7, policy_hash: "policy".to_string(), }), - persisted_at_ms: current_time_ms(), + persisted_time: Some(prost_types::Timestamp { + seconds: 1_700_000_000, + nanos: 123_456_789, + }), } } @@ -470,6 +486,9 @@ mod tests { .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 @@ -503,10 +522,103 @@ mod tests { .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!( - status.operation.unwrap().state, - ConfigUpdateOperationState::Failed as i32 + 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] @@ -618,7 +730,7 @@ mod tests { .await .unwrap(); observed.mutation_id = Uuid::new_v4().to_string(); - observed.persisted_at_ms += 1; + observed.persisted_time.as_mut().unwrap().nanos += 1; let repeated = record_provider_operation( &store, observed.clone(), diff --git a/crates/openshell-server/src/grpc/provider_readiness.rs b/crates/openshell-server/src/grpc/provider_readiness.rs index 0342878d08..0afb4bf6c9 100644 --- a/crates/openshell-server/src/grpc/provider_readiness.rs +++ b/crates/openshell-server/src/grpc/provider_readiness.rs @@ -6,7 +6,7 @@ #![allow(clippy::result_large_err)] // The RPC boundary returns tonic status values. use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::time::{Duration, Instant, SystemTime}; use openshell_core::proto::{ GetSandboxProviderStatusRequest, GetSandboxProviderStatusResponse, Provider, @@ -23,7 +23,7 @@ use crate::ServerState; use crate::auth::guard::{enforce_sandbox_scope, ensure_sandbox_principal_scope}; use crate::auth::workspace_authz::{MinWorkspaceRole, authorize_workspace_selector}; use crate::config_update_operation; -use crate::persistence::{ObjectType, current_time_ms}; +use crate::persistence::ObjectType; const REPORT_INTERVAL_SECONDS: u32 = 5; const OBSERVATION_TTL_SECONDS: u32 = 15; @@ -37,7 +37,7 @@ pub struct ProviderReadinessEvidence { process_instance_id: Option, last_seen: Instant, observation: Option, - observed_at_ms: i64, + observed_time: Option, } impl ProviderReadinessEvidence { @@ -59,7 +59,7 @@ impl ProviderReadinessEvidence { process_instance_id: None, last_seen: Instant::now(), observation: None, - observed_at_ms: 0, + observed_time: None, }) } @@ -116,6 +116,7 @@ impl ProviderReadinessEvidence { "process instance identity is required", )); } + let observed_time = now_timestamp()?; if !observation.process_instance_id.is_empty() { canonical_uuid(&observation.process_instance_id)?; if self @@ -130,7 +131,7 @@ impl ProviderReadinessEvidence { self.process_instance_id = Some(observation.process_instance_id.clone()); } self.last_seen = Instant::now(); - self.observed_at_ms = current_time_ms(); + self.observed_time = Some(observed_time); self.observation = Some(observation); Ok(()) } @@ -140,6 +141,11 @@ fn registry_unavailable() -> Status { Status::unavailable("provider readiness state is unavailable") } +fn now_timestamp() -> Result { + openshell_core::time::timestamp_from_system_time(SystemTime::now()) + .map_err(|error| Status::internal(format!("create provider readiness timestamp: {error}"))) +} + fn canonical_uuid(value: &str) -> Result<(), Status> { let parsed = Uuid::parse_str(value) .map_err(|_| Status::invalid_argument("invalid readiness instance identity"))?; @@ -178,7 +184,7 @@ pub(super) async fn record_provider_mutation( workspace: sandbox.object_workspace().to_string(), kind: kind.into(), desired: Some(desired), - persisted_at_ms: current_time_ms(), + persisted_time: Some(now_timestamp()?), }; config_update_operation::record_provider_operation( state.store.as_ref(), @@ -407,7 +413,7 @@ pub(super) async fn handle_get_sandbox_provider_status( active_sandbox.phase() == SandboxPhase::Ready as i32, active_instance_id, session.as_ref(), - ); + )?; config_update_operation::observe_provider_status(state.store.as_ref(), &mut status).await?; Ok(Response::new(GetSandboxProviderStatusResponse { status: Some(status), @@ -445,7 +451,7 @@ fn evaluate_status( running: bool, active_instance_id: &str, session: Option<&ProviderReadinessEvidence>, -) -> ProviderReadinessStatus { +) -> Result { let mut status = ProviderReadinessStatus { receipt: Some(receipt.clone()), state: ProviderReadinessState::Persisted.into(), @@ -454,8 +460,8 @@ fn evaluate_status( network_instance_id: session .map(|session| session.network_instance_id.clone()) .unwrap_or_default(), - observed_at_ms: session.map_or(0, |session| session.observed_at_ms), - evaluated_at_ms: current_time_ms(), + observed_time: session.and_then(|session| session.observed_time), + evaluated_time: Some(now_timestamp()?), operation: None, }; let set = |status: &mut ProviderReadinessStatus, @@ -470,7 +476,7 @@ fn evaluate_status( ProviderReadinessState::Failed, ProviderReadinessReason::SnapshotMismatch, ); - return status; + return Ok(status); }; if !same_authority(desired, current) || (snapshot_reason == ProviderReadinessReason::Unspecified && desired != current) @@ -554,7 +560,7 @@ fn evaluate_status( } } } - status + Ok(status) } fn authorize_provider_readiness( @@ -602,8 +608,18 @@ pub(super) async fn handle_report_provider_readiness( )?; Ok(Response::new(ReportProviderReadinessResponse { accepted_sequence, - report_interval_seconds: REPORT_INTERVAL_SECONDS, - observation_ttl_seconds: OBSERVATION_TTL_SECONDS, + report_interval: Some( + openshell_core::time::duration_from_std(Duration::from_secs(u64::from( + REPORT_INTERVAL_SECONDS, + ))) + .map_err(|error| Status::internal(format!("create report interval: {error}")))?, + ), + observation_ttl: Some( + openshell_core::time::duration_from_std(Duration::from_secs(u64::from( + OBSERVATION_TTL_SECONDS, + ))) + .map_err(|error| Status::internal(format!("create observation TTL: {error}")))?, + ), })) } diff --git a/crates/openshell-server/src/grpc/provider_readiness_tests.rs b/crates/openshell-server/src/grpc/provider_readiness_tests.rs index b89109f357..ae865afa6e 100644 --- a/crates/openshell-server/src/grpc/provider_readiness_tests.rs +++ b/crates/openshell-server/src/grpc/provider_readiness_tests.rs @@ -91,7 +91,10 @@ fn receipt(hello: &SupervisorHello) -> ProviderMutationReceipt { config_revision: u64::MAX, policy_hash: "abcd".to_string(), }), - persisted_at_ms: 1, + persisted_time: Some(prost_types::Timestamp { + seconds: 0, + nanos: 1, + }), } } @@ -129,6 +132,34 @@ fn evaluate( session.map_or("", |evidence| evidence.network_instance_id.as_str()), session, ) + .unwrap() +} + +#[test] +fn readiness_timestamps_distinguish_missing_observations_and_preserve_nanos() { + let hello = hello(); + let receipt = receipt(&hello); + let disconnected = evaluate(&receipt, None); + assert!(disconnected.observed_time.is_none()); + assert!(disconnected.evaluated_time.is_some()); + + let mut evidence = ProviderReadinessEvidence::from_hello(&hello).unwrap(); + assert!(evaluate(&receipt, Some(&evidence)).observed_time.is_none()); + let observation = installed(&hello, &Uuid::new_v4().to_string(), &receipt); + evidence.accept(observation.clone()).unwrap(); + assert!(evidence.observed_time.is_some()); + + // A captured timestamp must survive status projection and report retries + // without millisecond truncation or a fabricated new observation time. + let captured = prost_types::Timestamp { + seconds: 0, + nanos: 1, + }; + evidence.observed_time = Some(captured); + evidence.accept(observation).unwrap(); + let status = evaluate(&receipt, Some(&evidence)); + assert_eq!(status.observed_time, Some(captured)); + openshell_core::time::validate_timestamp(status.evaluated_time.as_ref().unwrap()).unwrap(); } #[test] @@ -233,7 +264,14 @@ async fn report_rpc_requires_the_authenticated_sandboxes_current_session() { .unwrap() .into_inner(); assert_eq!(response.accepted_sequence, 1); - assert_eq!(response.observation_ttl_seconds, OBSERVATION_TTL_SECONDS); + assert_eq!( + openshell_core::time::duration_to_std(response.observation_ttl.as_ref().unwrap()).unwrap(), + Duration::from_secs(u64::from(OBSERVATION_TTL_SECONDS)) + ); + assert_eq!( + openshell_core::time::duration_to_std(response.report_interval.as_ref().unwrap()).unwrap(), + Duration::from_secs(u64::from(REPORT_INTERVAL_SECONDS)) + ); // Another gateway replica can replace the persisted instance without // touching this process's session registry. The old report must fail. let mut sandbox = state @@ -297,7 +335,7 @@ fn report_retries_preserve_evidence_and_reject_reordering_or_changed_content() { let retried = registry.snapshot(&hello.sandbox_id).unwrap().unwrap(); assert_eq!(retried.observation, Some(observation)); assert_eq!(retried.last_seen, accepted.last_seen); - assert_eq!(retried.observed_at_ms, accepted.observed_at_ms); + assert_eq!(retried.observed_time, accepted.observed_time); } #[test] @@ -430,7 +468,7 @@ fn replacement_disconnect_and_replayed_reports_cannot_restore_readiness() { .unwrap(); let after_retry = registry.snapshot(&hello.sandbox_id).unwrap().unwrap(); assert_eq!(before_retry.last_seen, after_retry.last_seen); - assert_eq!(before_retry.observed_at_ms, after_retry.observed_at_ms); + assert_eq!(before_retry.observed_time, after_retry.observed_time); let second = registry.register(&hello).unwrap(); assert_ne!(first, second); @@ -557,7 +595,8 @@ fn partial_failed_incompatible_stopped_and_expired_installations_are_not_ready() false, &hello.instance_id, Some(&complete), - ); + ) + .unwrap(); assert_eq!( stopped.reason, ProviderReadinessReason::SupervisorDisconnected as i32 @@ -654,7 +693,8 @@ fn superseded_authority_cannot_complete_an_older_receipt() { true, &hello.instance_id, session.as_ref(), - ); + ) + .unwrap(); assert_eq!(status.state, ProviderReadinessState::Superseded as i32); } let failed_snapshot = evaluate_status( @@ -665,7 +705,8 @@ fn superseded_authority_cannot_complete_an_older_receipt() { true, &hello.instance_id, session.as_ref(), - ); + ) + .unwrap(); assert_eq!(failed_snapshot.state, ProviderReadinessState::Failed as i32); } diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index 3667896b46..b27b18c53a 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -116,11 +116,11 @@ mod tests { use std::collections::{BTreeMap, BTreeSet, VecDeque}; const STORAGE_V1_SCHEMA_SHA256: &str = - "6c31e100348f9880a1f8a26f44afa6c583bddcb6cfb7915766c172e517472611"; + "d68401809d8cea445c35233ef32412bbd041cb2ac5acaf368a0d0bf74d2ddf17"; const PUBLIC_RPC_SCHEMA_SHA256: &str = - "6b7970718a68b2641adaf54917c063754692012e8c26db04ee9d6f022556ddb7"; + "40d1aba0dbe3e0e5bb3c30bb94cc49b338e1b9cc23a0032f988e69012ecdbbc6"; const DURABLE_SCHEMA_SHA256: &str = - "8231d551f29bc671e200ea5b0b2d2f5887f5bc13a26ec7cb6a427c27fbd06acc"; + "557ca283c55fd46b213d5573b950ba8604cc3f4b31bad3e433eb9c5f9975138d"; const PUBLIC_DURABLE_OVERLAP_SHA256: &str = "f541c25bb3e1e5806865bc61470c66d10c16cca7399bf5909c77dc384d469171"; // A persisted Sandbox without endpoint status retains its lifecycle fields; diff --git a/crates/openshell-supervisor/src/provider_readiness.rs b/crates/openshell-supervisor/src/provider_readiness.rs index 6f7479489a..fae5e71455 100644 --- a/crates/openshell-supervisor/src/provider_readiness.rs +++ b/crates/openshell-supervisor/src/provider_readiness.rs @@ -366,7 +366,7 @@ impl Tracker { response = report => response, }; if let Ok(Ok(response)) = response - && (response.accepted_sequence != sequence || response.observation_ttl_seconds == 0) + && !report_acknowledged(&response, sequence) { // A gateway rejection does not invalidate the boundary's // installation. Retry on the normal cadence or a real state @@ -377,6 +377,16 @@ impl Tracker { } } +/// An acknowledgment must bind this report and a valid positive evidence lease. +fn report_acknowledged(response: &ReportProviderReadinessResponse, sequence: u64) -> bool { + response.accepted_sequence == sequence + && response + .observation_ttl + .as_ref() + .and_then(|ttl| openshell_core::time::duration_to_std(ttl).ok()) + .is_some_and(|ttl| !ttl.is_zero()) +} + #[tonic::async_trait] trait ReportClient: Send + Sync { async fn report( @@ -419,6 +429,37 @@ mod tests { use super::*; use std::collections::HashMap; + #[test] + fn acknowledgment_requires_matching_sequence_and_valid_positive_ttl() { + let mut response = ReportProviderReadinessResponse { + accepted_sequence: 7, + ..Default::default() + }; + assert!(!report_acknowledged(&response, 7)); + for (seconds, nanos, valid) in [ + (15, 0, true), + (0, 1, true), + (0, 0, false), + (-1, 0, false), + (0, -1, false), + (1, -1, false), + (0, 1_000_000_000, false), + (315_576_000_001, 0, false), + ] { + response.observation_ttl = Some(prost_types::Duration { seconds, nanos }); + assert_eq!( + report_acknowledged(&response, 7), + valid, + "seconds={seconds}, nanos={nanos}" + ); + } + response.observation_ttl = Some(prost_types::Duration { + seconds: 15, + nanos: 0, + }); + assert!(!report_acknowledged(&response, 8)); + } + struct CapturingReporter(tokio::sync::mpsc::UnboundedSender); #[tonic::async_trait] @@ -431,8 +472,12 @@ mod tests { self.0.send(observation).unwrap(); Ok(ReportProviderReadinessResponse { accepted_sequence: sequence, - report_interval_seconds: 5, - observation_ttl_seconds: 15, + report_interval: Some( + openshell_core::time::duration_from_std(Duration::from_secs(5)).unwrap(), + ), + observation_ttl: Some( + openshell_core::time::duration_from_std(Duration::from_secs(15)).unwrap(), + ), }) } } diff --git a/docs/providers/profiles.mdx b/docs/providers/profiles.mdx index 327ef07edc..578eccfc86 100644 --- a/docs/providers/profiles.mdx +++ b/docs/providers/profiles.mdx @@ -1011,6 +1011,8 @@ An unavailable supervisor, an expired report, a failed installation, or a missin JSON and YAML output include change IDs, requested and installed revisions, timestamps, reason categories, and a result for each sandbox. Revision strings identify configurations; compare them for equality rather than numerical order. Status output excludes credential values, credential references, authorization headers, and raw installation errors. +The `persisted_time`, `observed_time`, and `evaluated_time` fields use RFC 3339 timestamp strings. An absent `observed_time` means the current supervisor session has not supplied accepted evidence. API operation timestamps use protobuf `Timestamp`, and report intervals and observation lifetimes use protobuf `Duration`, following the [protobuf time representation](/reference/protobuf-time-types). + The API's `operation` field records the common operation's historical outcome; its `operation_id` equals the receipt ID. Use the provider `state` and the wait result for current readiness. A disconnected supervisor can make the live state pending even after the operation previously applied, and a newer change can supersede the live result. Historical completion does not override those checks. ### Runtime Limitations diff --git a/e2e/rust/tests/provider_readiness.rs b/e2e/rust/tests/provider_readiness.rs index 1cfb5dde5a..7736681d19 100644 --- a/e2e/rust/tests/provider_readiness.rs +++ b/e2e/rust/tests/provider_readiness.rs @@ -1047,12 +1047,7 @@ impl MutationReceipt { } else { nonempty_string(desired, "provider_id")?; } - if receipt["persisted_at_ms"] - .as_i64() - .is_none_or(|time| time <= 0) - { - return Err("provider mutation receipt persistence time was absent".to_string()); - } + nonempty_string(receipt, "persisted_time")?; Ok(Self { mutation_id: mutation_id.to_string(), receipt: receipt.clone(), @@ -1160,14 +1155,9 @@ impl MutationReceipt { nonempty_string(observed, field)?; } nonempty_string(status, "network_instance_id")?; - if observed["reason"] != "unspecified" - || status["observed_at_ms"] - .as_i64() - .is_none_or(|time| time <= 0) - || status["evaluated_at_ms"] - .as_i64() - .is_none_or(|time| time <= 0) - { + nonempty_string(status, "observed_time")?; + nonempty_string(status, "evaluated_time")?; + if observed["reason"] != "unspecified" { return Err( "provider installation observation lacked a successful timestamped acknowledgment" .to_string(), diff --git a/proto/openshell.proto b/proto/openshell.proto index 42d29015e3..3e80a42b4d 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -1532,9 +1532,12 @@ message ConfigUpdateOperation { ConfigUpdateOperationState state = 5; ConfigApplyOutcome outcome = 6; string sanitized_error = 7; - int64 created_at_ms = 8; - int64 updated_at_ms = 9; - int64 completed_at_ms = 10; + reserved 8, 9, 10; + reserved "created_at_ms", "updated_at_ms", "completed_at_ms"; + google.protobuf.Timestamp created_time = 108; + google.protobuf.Timestamp updated_time = 109; + // Absent until the operation reaches a terminal state. + google.protobuf.Timestamp completed_time = 110; } // Immutable, secret-free record of one sandbox's intended provider mutation. @@ -1546,7 +1549,9 @@ message ProviderMutationReceipt { string workspace = 4; ProviderMutationKind kind = 5; ProviderDesiredIdentity desired = 6; - int64 persisted_at_ms = 7; + reserved 7; + reserved "persisted_at_ms"; + google.protobuf.Timestamp persisted_time = 107; } // Installed state reported by the current supervisor. Process installation is @@ -1575,8 +1580,11 @@ message ProviderReadinessStatus { ProviderReadinessReason reason = 3; ProviderReadinessObservation observed = 4; string network_instance_id = 5; - int64 observed_at_ms = 6; - int64 evaluated_at_ms = 7; + reserved 6, 7; + reserved "observed_at_ms", "evaluated_at_ms"; + // Absent until the current supervisor session supplies accepted evidence. + google.protobuf.Timestamp observed_time = 106; + google.protobuf.Timestamp evaluated_time = 107; // Durable operation for the receipt, including its terminal apply outcome. ConfigUpdateOperation operation = 8; } @@ -1605,8 +1613,10 @@ message ReportProviderReadinessRequest { // Identical retries do not extend the evidence's original acceptance time. message ReportProviderReadinessResponse { uint64 accepted_sequence = 1; - uint32 report_interval_seconds = 2; - uint32 observation_ttl_seconds = 3; + reserved 2, 3; + reserved "report_interval_seconds", "observation_ttl_seconds"; + google.protobuf.Duration report_interval = 102; + google.protobuf.Duration observation_ttl = 103; } // Delete sandbox response. diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 155c0480e0..692a8b576c 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -4755,11 +4755,12 @@ type ConfigUpdateOperation struct { State ConfigUpdateOperationState `protobuf:"varint,5,opt,name=state,proto3,enum=openshell.v1.ConfigUpdateOperationState" json:"state,omitempty"` Outcome ConfigApplyOutcome `protobuf:"varint,6,opt,name=outcome,proto3,enum=openshell.v1.ConfigApplyOutcome" json:"outcome,omitempty"` SanitizedError string `protobuf:"bytes,7,opt,name=sanitized_error,json=sanitizedError,proto3" json:"sanitized_error,omitempty"` - CreatedAtMs int64 `protobuf:"varint,8,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` - UpdatedAtMs int64 `protobuf:"varint,9,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` - CompletedAtMs int64 `protobuf:"varint,10,opt,name=completed_at_ms,json=completedAtMs,proto3" json:"completed_at_ms,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + CreatedTime *timestamppb.Timestamp `protobuf:"bytes,108,opt,name=created_time,json=createdTime,proto3" json:"created_time,omitempty"` + UpdatedTime *timestamppb.Timestamp `protobuf:"bytes,109,opt,name=updated_time,json=updatedTime,proto3" json:"updated_time,omitempty"` + // Absent until the operation reaches a terminal state. + CompletedTime *timestamppb.Timestamp `protobuf:"bytes,110,opt,name=completed_time,json=completedTime,proto3" json:"completed_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ConfigUpdateOperation) Reset() { @@ -4841,25 +4842,25 @@ func (x *ConfigUpdateOperation) GetSanitizedError() string { return "" } -func (x *ConfigUpdateOperation) GetCreatedAtMs() int64 { +func (x *ConfigUpdateOperation) GetCreatedTime() *timestamppb.Timestamp { if x != nil { - return x.CreatedAtMs + return x.CreatedTime } - return 0 + return nil } -func (x *ConfigUpdateOperation) GetUpdatedAtMs() int64 { +func (x *ConfigUpdateOperation) GetUpdatedTime() *timestamppb.Timestamp { if x != nil { - return x.UpdatedAtMs + return x.UpdatedTime } - return 0 + return nil } -func (x *ConfigUpdateOperation) GetCompletedAtMs() int64 { +func (x *ConfigUpdateOperation) GetCompletedTime() *timestamppb.Timestamp { if x != nil { - return x.CompletedAtMs + return x.CompletedTime } - return 0 + return nil } // Immutable, secret-free record of one sandbox's intended provider mutation. @@ -4872,7 +4873,7 @@ type ProviderMutationReceipt struct { Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` Kind ProviderMutationKind `protobuf:"varint,5,opt,name=kind,proto3,enum=openshell.v1.ProviderMutationKind" json:"kind,omitempty"` Desired *ProviderDesiredIdentity `protobuf:"bytes,6,opt,name=desired,proto3" json:"desired,omitempty"` - PersistedAtMs int64 `protobuf:"varint,7,opt,name=persisted_at_ms,json=persistedAtMs,proto3" json:"persisted_at_ms,omitempty"` + PersistedTime *timestamppb.Timestamp `protobuf:"bytes,107,opt,name=persisted_time,json=persistedTime,proto3" json:"persisted_time,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4949,11 +4950,11 @@ func (x *ProviderMutationReceipt) GetDesired() *ProviderDesiredIdentity { return nil } -func (x *ProviderMutationReceipt) GetPersistedAtMs() int64 { +func (x *ProviderMutationReceipt) GetPersistedTime() *timestamppb.Timestamp { if x != nil { - return x.PersistedAtMs + return x.PersistedTime } - return 0 + return nil } // Installed state reported by the current supervisor. Process installation is @@ -5093,8 +5094,9 @@ type ProviderReadinessStatus struct { Reason ProviderReadinessReason `protobuf:"varint,3,opt,name=reason,proto3,enum=openshell.v1.ProviderReadinessReason" json:"reason,omitempty"` Observed *ProviderReadinessObservation `protobuf:"bytes,4,opt,name=observed,proto3" json:"observed,omitempty"` NetworkInstanceId string `protobuf:"bytes,5,opt,name=network_instance_id,json=networkInstanceId,proto3" json:"network_instance_id,omitempty"` - ObservedAtMs int64 `protobuf:"varint,6,opt,name=observed_at_ms,json=observedAtMs,proto3" json:"observed_at_ms,omitempty"` - EvaluatedAtMs int64 `protobuf:"varint,7,opt,name=evaluated_at_ms,json=evaluatedAtMs,proto3" json:"evaluated_at_ms,omitempty"` + // Absent until the current supervisor session supplies accepted evidence. + ObservedTime *timestamppb.Timestamp `protobuf:"bytes,106,opt,name=observed_time,json=observedTime,proto3" json:"observed_time,omitempty"` + EvaluatedTime *timestamppb.Timestamp `protobuf:"bytes,107,opt,name=evaluated_time,json=evaluatedTime,proto3" json:"evaluated_time,omitempty"` // Durable operation for the receipt, including its terminal apply outcome. Operation *ConfigUpdateOperation `protobuf:"bytes,8,opt,name=operation,proto3" json:"operation,omitempty"` unknownFields protoimpl.UnknownFields @@ -5166,18 +5168,18 @@ func (x *ProviderReadinessStatus) GetNetworkInstanceId() string { return "" } -func (x *ProviderReadinessStatus) GetObservedAtMs() int64 { +func (x *ProviderReadinessStatus) GetObservedTime() *timestamppb.Timestamp { if x != nil { - return x.ObservedAtMs + return x.ObservedTime } - return 0 + return nil } -func (x *ProviderReadinessStatus) GetEvaluatedAtMs() int64 { +func (x *ProviderReadinessStatus) GetEvaluatedTime() *timestamppb.Timestamp { if x != nil { - return x.EvaluatedAtMs + return x.EvaluatedTime } - return 0 + return nil } func (x *ProviderReadinessStatus) GetOperation() *ConfigUpdateOperation { @@ -5358,12 +5360,12 @@ func (x *ReportProviderReadinessRequest) GetObservation() *ProviderReadinessObse // Acknowledges accepted evidence without granting a separate session authority. // Identical retries do not extend the evidence's original acceptance time. type ReportProviderReadinessResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - AcceptedSequence uint64 `protobuf:"varint,1,opt,name=accepted_sequence,json=acceptedSequence,proto3" json:"accepted_sequence,omitempty"` - ReportIntervalSeconds uint32 `protobuf:"varint,2,opt,name=report_interval_seconds,json=reportIntervalSeconds,proto3" json:"report_interval_seconds,omitempty"` - ObservationTtlSeconds uint32 `protobuf:"varint,3,opt,name=observation_ttl_seconds,json=observationTtlSeconds,proto3" json:"observation_ttl_seconds,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + AcceptedSequence uint64 `protobuf:"varint,1,opt,name=accepted_sequence,json=acceptedSequence,proto3" json:"accepted_sequence,omitempty"` + ReportInterval *durationpb.Duration `protobuf:"bytes,102,opt,name=report_interval,json=reportInterval,proto3" json:"report_interval,omitempty"` + ObservationTtl *durationpb.Duration `protobuf:"bytes,103,opt,name=observation_ttl,json=observationTtl,proto3" json:"observation_ttl,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReportProviderReadinessResponse) Reset() { @@ -5403,18 +5405,18 @@ func (x *ReportProviderReadinessResponse) GetAcceptedSequence() uint64 { return 0 } -func (x *ReportProviderReadinessResponse) GetReportIntervalSeconds() uint32 { +func (x *ReportProviderReadinessResponse) GetReportInterval() *durationpb.Duration { if x != nil { - return x.ReportIntervalSeconds + return x.ReportInterval } - return 0 + return nil } -func (x *ReportProviderReadinessResponse) GetObservationTtlSeconds() uint32 { +func (x *ReportProviderReadinessResponse) GetObservationTtl() *durationpb.Duration { if x != nil { - return x.ObservationTtlSeconds + return x.ObservationTtl } - return 0 + return nil } // Delete sandbox response. @@ -16588,7 +16590,7 @@ const file_openshell_proto_rawDesc = "" + "\x0epolicy_version\x18\x02 \x01(\rR\rpolicyVersion\x12G\n" + "\rpolicy_source\x18\x03 \x01(\x0e2\".openshell.sandbox.v1.PolicySourceR\fpolicySource\x122\n" + "\x15global_policy_version\x18\x04 \x01(\rR\x13globalPolicyVersion\x12+\n" + - "\x11settings_revision\x18\x05 \x01(\x04R\x10settingsRevision\"\xfa\x03\n" + + "\x11settings_revision\x18\x05 \x01(\x04R\x10settingsRevision\"\x8c\x05\n" + "\x15ConfigUpdateOperation\x12!\n" + "\foperation_id\x18\x01 \x01(\tR\voperationId\x12\x1d\n" + "\n" + @@ -16597,11 +16599,12 @@ const file_openshell_proto_rawDesc = "" + "\x0ftarget_revision\x18\x04 \x01(\v2$.openshell.v1.ConfigSnapshotRevisionR\x0etargetRevision\x12>\n" + "\x05state\x18\x05 \x01(\x0e2(.openshell.v1.ConfigUpdateOperationStateR\x05state\x12:\n" + "\aoutcome\x18\x06 \x01(\x0e2 .openshell.v1.ConfigApplyOutcomeR\aoutcome\x12'\n" + - "\x0fsanitized_error\x18\a \x01(\tR\x0esanitizedError\x12\"\n" + - "\rcreated_at_ms\x18\b \x01(\x03R\vcreatedAtMs\x12\"\n" + - "\rupdated_at_ms\x18\t \x01(\x03R\vupdatedAtMs\x12&\n" + - "\x0fcompleted_at_ms\x18\n" + - " \x01(\x03R\rcompletedAtMs\"\xbd\x02\n" + + "\x0fsanitized_error\x18\a \x01(\tR\x0esanitizedError\x12=\n" + + "\fcreated_time\x18l \x01(\v2\x1a.google.protobuf.TimestampR\vcreatedTime\x12=\n" + + "\fupdated_time\x18m \x01(\v2\x1a.google.protobuf.TimestampR\vupdatedTime\x12A\n" + + "\x0ecompleted_time\x18n \x01(\v2\x1a.google.protobuf.TimestampR\rcompletedTimeJ\x04\b\b\x10\tJ\x04\b\t\x10\n" + + "J\x04\b\n" + + "\x10\vR\rcreated_at_msR\rupdated_at_msR\x0fcompleted_at_ms\"\xef\x02\n" + "\x17ProviderMutationReceipt\x12\x1d\n" + "\n" + "receipt_id\x18\x01 \x01(\tR\treceiptId\x12\x1f\n" + @@ -16610,8 +16613,8 @@ const file_openshell_proto_rawDesc = "" + "\rprovider_name\x18\x03 \x01(\tR\fproviderName\x12\x1c\n" + "\tworkspace\x18\x04 \x01(\tR\tworkspace\x126\n" + "\x04kind\x18\x05 \x01(\x0e2\".openshell.v1.ProviderMutationKindR\x04kind\x12?\n" + - "\adesired\x18\x06 \x01(\v2%.openshell.v1.ProviderDesiredIdentityR\adesired\x12&\n" + - "\x0fpersisted_at_ms\x18\a \x01(\x03R\rpersistedAtMs\"\x8d\x04\n" + + "\adesired\x18\x06 \x01(\v2%.openshell.v1.ProviderDesiredIdentityR\adesired\x12A\n" + + "\x0epersisted_time\x18k \x01(\v2\x1a.google.protobuf.TimestampR\rpersistedTimeJ\x04\b\a\x10\bR\x0fpersisted_at_ms\"\x8d\x04\n" + "\x1cProviderReadinessObservation\x12\x1d\n" + "\n" + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x1a\n" + @@ -16626,16 +16629,16 @@ const file_openshell_proto_rawDesc = "" + "\x1claunch_environment_installed\x18\t \x01(\bR\x1alaunchEnvironmentInstalled\x12.\n" + "\x13process_instance_id\x18\n" + " \x01(\tR\x11processInstanceId\x12=\n" + - "\x06reason\x18\v \x01(\x0e2%.openshell.v1.ProviderReadinessReasonR\x06reason\"\xde\x03\n" + + "\x06reason\x18\v \x01(\x0e2%.openshell.v1.ProviderReadinessReasonR\x06reason\"\xc1\x04\n" + "\x17ProviderReadinessStatus\x12?\n" + "\areceipt\x18\x01 \x01(\v2%.openshell.v1.ProviderMutationReceiptR\areceipt\x12:\n" + "\x05state\x18\x02 \x01(\x0e2$.openshell.v1.ProviderReadinessStateR\x05state\x12=\n" + "\x06reason\x18\x03 \x01(\x0e2%.openshell.v1.ProviderReadinessReasonR\x06reason\x12F\n" + "\bobserved\x18\x04 \x01(\v2*.openshell.v1.ProviderReadinessObservationR\bobserved\x12.\n" + - "\x13network_instance_id\x18\x05 \x01(\tR\x11networkInstanceId\x12$\n" + - "\x0eobserved_at_ms\x18\x06 \x01(\x03R\fobservedAtMs\x12&\n" + - "\x0fevaluated_at_ms\x18\a \x01(\x03R\revaluatedAtMs\x12A\n" + - "\toperation\x18\b \x01(\v2#.openshell.v1.ConfigUpdateOperationR\toperation\"\xdc\x01\n" + + "\x13network_instance_id\x18\x05 \x01(\tR\x11networkInstanceId\x12?\n" + + "\robserved_time\x18j \x01(\v2\x1a.google.protobuf.TimestampR\fobservedTime\x12A\n" + + "\x0eevaluated_time\x18k \x01(\v2\x1a.google.protobuf.TimestampR\revaluatedTime\x12A\n" + + "\toperation\x18\b \x01(\v2#.openshell.v1.ConfigUpdateOperationR\toperationJ\x04\b\x06\x10\aJ\x04\b\a\x10\bR\x0eobserved_at_msR\x0fevaluated_at_ms\"\xdc\x01\n" + "\x1fGetSandboxProviderStatusRequest\x12!\n" + "\fsandbox_name\x18\x01 \x01(\tR\vsandboxName\x12#\n" + "\rprovider_name\x18\x02 \x01(\tR\fproviderName\x12\x1d\n" + @@ -16647,11 +16650,11 @@ const file_openshell_proto_rawDesc = "" + "\x1eReportProviderReadinessRequest\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12L\n" + - "\vobservation\x18\x02 \x01(\v2*.openshell.v1.ProviderReadinessObservationR\vobservation\"\xbe\x01\n" + + "\vobservation\x18\x02 \x01(\v2*.openshell.v1.ProviderReadinessObservationR\vobservation\"\x94\x02\n" + "\x1fReportProviderReadinessResponse\x12+\n" + - "\x11accepted_sequence\x18\x01 \x01(\x04R\x10acceptedSequence\x126\n" + - "\x17report_interval_seconds\x18\x02 \x01(\rR\x15reportIntervalSeconds\x126\n" + - "\x17observation_ttl_seconds\x18\x03 \x01(\rR\x15observationTtlSeconds\"~\n" + + "\x11accepted_sequence\x18\x01 \x01(\x04R\x10acceptedSequence\x12B\n" + + "\x0freport_interval\x18f \x01(\v2\x19.google.protobuf.DurationR\x0ereportInterval\x12B\n" + + "\x0fobservation_ttl\x18g \x01(\v2\x19.google.protobuf.DurationR\x0eobservationTtlJ\x04\b\x02\x10\x03J\x04\b\x03\x10\x04R\x17report_interval_secondsR\x17observation_ttl_seconds\"~\n" + "\x15DeleteSandboxResponse\x127\n" + "\aoutcome\x18\x02 \x01(\x0e2\x1d.openshell.v1.DeletionOutcomeR\aoutcome\x12\x1d\n" + "\n" + @@ -18142,362 +18145,370 @@ var file_openshell_proto_depIdxs = []int32{ 71, // 75: openshell.v1.ConfigUpdateOperation.target_revision:type_name -> openshell.v1.ConfigSnapshotRevision 6, // 76: openshell.v1.ConfigUpdateOperation.state:type_name -> openshell.v1.ConfigUpdateOperationState 5, // 77: openshell.v1.ConfigUpdateOperation.outcome:type_name -> openshell.v1.ConfigApplyOutcome - 1, // 78: openshell.v1.ProviderMutationReceipt.kind:type_name -> openshell.v1.ProviderMutationKind - 70, // 79: openshell.v1.ProviderMutationReceipt.desired:type_name -> openshell.v1.ProviderDesiredIdentity - 3, // 80: openshell.v1.ProviderReadinessObservation.reason:type_name -> openshell.v1.ProviderReadinessReason - 74, // 81: openshell.v1.ProviderReadinessStatus.receipt:type_name -> openshell.v1.ProviderMutationReceipt - 2, // 82: openshell.v1.ProviderReadinessStatus.state:type_name -> openshell.v1.ProviderReadinessState - 3, // 83: openshell.v1.ProviderReadinessStatus.reason:type_name -> openshell.v1.ProviderReadinessReason - 75, // 84: openshell.v1.ProviderReadinessStatus.observed:type_name -> openshell.v1.ProviderReadinessObservation - 73, // 85: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation - 264, // 86: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 76, // 87: openshell.v1.GetSandboxProviderStatusResponse.status:type_name -> openshell.v1.ProviderReadinessStatus - 75, // 88: openshell.v1.ReportProviderReadinessRequest.observation:type_name -> openshell.v1.ProviderReadinessObservation - 14, // 89: openshell.v1.DeleteSandboxResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 259, // 90: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp - 264, // 91: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 264, // 92: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 264, // 93: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 91, // 94: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 264, // 95: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 14, // 96: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 260, // 97: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 90, // 98: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 14, // 99: openshell.v1.RevokeSshSessionResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 246, // 100: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 263, // 101: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration - 95, // 102: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 96, // 103: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 97, // 104: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 186, // 105: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 187, // 106: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 99, // 107: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 94, // 108: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 102, // 109: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 260, // 110: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 259, // 111: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp - 259, // 112: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp - 32, // 113: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 106, // 114: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 46, // 115: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 107, // 116: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 197, // 117: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 259, // 118: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp - 247, // 119: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 265, // 120: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 264, // 121: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 264, // 122: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 264, // 123: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 124: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 248, // 125: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry - 264, // 126: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 264, // 127: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 128: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 74, // 129: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt - 265, // 130: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 136, // 131: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 263, // 132: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration - 119, // 133: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride - 7, // 134: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType - 120, // 135: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken - 125, // 136: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 121, // 137: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant - 8, // 138: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 263, // 139: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration - 263, // 140: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration - 123, // 141: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 124, // 142: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput - 8, // 143: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 259, // 144: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp - 259, // 145: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp - 259, // 146: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp - 13, // 147: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 259, // 148: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp - 264, // 149: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 126, // 150: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 8, // 151: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 249, // 152: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 259, // 153: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp - 264, // 154: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 126, // 155: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 264, // 156: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 126, // 157: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 264, // 158: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 14, // 159: openshell.v1.DeleteProviderRefreshResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 9, // 160: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 122, // 161: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 267, // 162: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 268, // 163: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 127, // 164: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 250, // 165: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 136, // 166: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 136, // 167: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 117, // 168: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 118, // 169: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 136, // 170: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 117, // 171: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 118, // 172: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 136, // 173: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 117, // 174: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 118, // 175: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 14, // 176: openshell.v1.DeleteProviderResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 14, // 177: openshell.v1.DeleteProviderProfileResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 149, // 178: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 251, // 179: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 252, // 180: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry - 253, // 181: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 254, // 182: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 3, // 183: openshell.v1.GetSandboxProviderEnvironmentResponse.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason - 263, // 184: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration - 261, // 185: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 269, // 186: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 155, // 187: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 255, // 188: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 264, // 189: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 156, // 190: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 157, // 191: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 158, // 192: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 159, // 193: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 160, // 194: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 161, // 195: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 270, // 196: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 271, // 197: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 272, // 198: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 256, // 199: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 264, // 200: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 169, // 201: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 264, // 202: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 169, // 203: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 10, // 204: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 10, // 205: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 259, // 206: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp - 259, // 207: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp - 261, // 208: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 257, // 209: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 259, // 210: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp - 264, // 211: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 106, // 212: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 106, // 213: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 176, // 214: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 179, // 215: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 190, // 216: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 191, // 217: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 177, // 218: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 178, // 219: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 180, // 220: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 185, // 221: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 191, // 222: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 263, // 223: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration - 186, // 224: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 187, // 225: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 188, // 226: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 259, // 227: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp - 259, // 228: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp - 192, // 229: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 194, // 230: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 270, // 231: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 259, // 232: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp - 259, // 233: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp - 259, // 234: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp - 259, // 235: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp - 261, // 236: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 261, // 237: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 193, // 238: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 196, // 239: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 195, // 240: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 264, // 241: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 196, // 242: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 259, // 243: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp - 264, // 244: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 264, // 245: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 206, // 246: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 264, // 247: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 248: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 264, // 249: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 264, // 250: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 264, // 251: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 264, // 252: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 259, // 253: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp - 216, // 254: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 258, // 255: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 273, // 256: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 273, // 257: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 273, // 258: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 14, // 259: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 260, // 260: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 12, // 261: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 12, // 262: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 226, // 263: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 14, // 264: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 226, // 265: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 259, // 266: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp - 15, // 267: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult - 234, // 268: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation - 15, // 269: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult - 259, // 270: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp - 259, // 271: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 259, // 272: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 122, // 273: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 150, // 274: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 20, // 275: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 22, // 276: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 24, // 277: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 47, // 278: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 55, // 279: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest - 57, // 280: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 58, // 281: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 48, // 282: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest - 49, // 283: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest - 50, // 284: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest - 51, // 285: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest - 59, // 286: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 60, // 287: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 61, // 288: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 77, // 289: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest - 62, // 290: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 63, // 291: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 64, // 292: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 82, // 293: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 84, // 294: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 85, // 295: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 86, // 296: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 88, // 297: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 92, // 298: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 94, // 299: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 100, // 300: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 101, // 301: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 108, // 302: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 109, // 303: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 110, // 304: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 115, // 305: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 116, // 306: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 139, // 307: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 141, // 308: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 143, // 309: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 111, // 310: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 128, // 311: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 130, // 312: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 132, // 313: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 134, // 314: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 112, // 315: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 146, // 316: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 274, // 317: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 275, // 318: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 154, // 319: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 163, // 320: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 165, // 321: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 167, // 322: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 235, // 323: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest - 79, // 324: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest - 148, // 325: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 152, // 326: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 170, // 327: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 171, // 328: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 174, // 329: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 181, // 330: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 183, // 331: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest - 189, // 332: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 104, // 333: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 198, // 334: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 200, // 335: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 202, // 336: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 204, // 337: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 207, // 338: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 209, // 339: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 211, // 340: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 213, // 341: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 215, // 342: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 16, // 343: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 18, // 344: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 218, // 345: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 220, // 346: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 222, // 347: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 224, // 348: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 227, // 349: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 229, // 350: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 231, // 351: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 21, // 352: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 23, // 353: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 25, // 354: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 65, // 355: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 56, // 356: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse - 65, // 357: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 66, // 358: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 52, // 359: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 52, // 360: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 53, // 361: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse - 54, // 362: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse - 67, // 363: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 68, // 364: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 69, // 365: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 78, // 366: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse - 81, // 367: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 65, // 368: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 65, // 369: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 83, // 370: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 91, // 371: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 91, // 372: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 87, // 373: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 89, // 374: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 93, // 375: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 98, // 376: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 100, // 377: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 98, // 378: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 113, // 379: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 113, // 380: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 114, // 381: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 138, // 382: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 137, // 383: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 140, // 384: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 142, // 385: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 144, // 386: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 113, // 387: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 129, // 388: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 131, // 389: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 133, // 390: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 135, // 391: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 145, // 392: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 147, // 393: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 276, // 394: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 277, // 395: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 162, // 396: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 164, // 397: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 166, // 398: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 168, // 399: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 236, // 400: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse - 80, // 401: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse - 151, // 402: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 153, // 403: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 173, // 404: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 172, // 405: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 175, // 406: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 182, // 407: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 184, // 408: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse - 189, // 409: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 105, // 410: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 199, // 411: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 201, // 412: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 203, // 413: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 205, // 414: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 208, // 415: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 210, // 416: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 212, // 417: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 214, // 418: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 217, // 419: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 17, // 420: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 19, // 421: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 219, // 422: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 221, // 423: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 223, // 424: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 225, // 425: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 228, // 426: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 230, // 427: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 232, // 428: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 352, // [352:429] is the sub-list for method output_type - 275, // [275:352] is the sub-list for method input_type - 275, // [275:275] is the sub-list for extension type_name - 275, // [275:275] is the sub-list for extension extendee - 0, // [0:275] is the sub-list for field type_name + 259, // 78: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp + 259, // 79: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp + 259, // 80: openshell.v1.ConfigUpdateOperation.completed_time:type_name -> google.protobuf.Timestamp + 1, // 81: openshell.v1.ProviderMutationReceipt.kind:type_name -> openshell.v1.ProviderMutationKind + 70, // 82: openshell.v1.ProviderMutationReceipt.desired:type_name -> openshell.v1.ProviderDesiredIdentity + 259, // 83: openshell.v1.ProviderMutationReceipt.persisted_time:type_name -> google.protobuf.Timestamp + 3, // 84: openshell.v1.ProviderReadinessObservation.reason:type_name -> openshell.v1.ProviderReadinessReason + 74, // 85: openshell.v1.ProviderReadinessStatus.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 2, // 86: openshell.v1.ProviderReadinessStatus.state:type_name -> openshell.v1.ProviderReadinessState + 3, // 87: openshell.v1.ProviderReadinessStatus.reason:type_name -> openshell.v1.ProviderReadinessReason + 75, // 88: openshell.v1.ProviderReadinessStatus.observed:type_name -> openshell.v1.ProviderReadinessObservation + 259, // 89: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp + 259, // 90: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp + 73, // 91: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation + 264, // 92: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 76, // 93: openshell.v1.GetSandboxProviderStatusResponse.status:type_name -> openshell.v1.ProviderReadinessStatus + 75, // 94: openshell.v1.ReportProviderReadinessRequest.observation:type_name -> openshell.v1.ProviderReadinessObservation + 263, // 95: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration + 263, // 96: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration + 14, // 97: openshell.v1.DeleteSandboxResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 259, // 98: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp + 264, // 99: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 264, // 100: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 264, // 101: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 91, // 102: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 264, // 103: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 14, // 104: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 260, // 105: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 90, // 106: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 14, // 107: openshell.v1.RevokeSshSessionResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 246, // 108: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 263, // 109: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration + 95, // 110: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 96, // 111: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 97, // 112: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 186, // 113: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 187, // 114: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 99, // 115: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 94, // 116: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 102, // 117: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 260, // 118: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 259, // 119: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp + 259, // 120: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp + 32, // 121: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 106, // 122: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 46, // 123: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 107, // 124: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 197, // 125: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 259, // 126: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp + 247, // 127: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 265, // 128: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 264, // 129: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 264, // 130: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 264, // 131: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 265, // 132: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 248, // 133: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + 264, // 134: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 264, // 135: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 265, // 136: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 74, // 137: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt + 265, // 138: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 136, // 139: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 263, // 140: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration + 119, // 141: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 7, // 142: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType + 120, // 143: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 125, // 144: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 121, // 145: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 8, // 146: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 263, // 147: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration + 263, // 148: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration + 123, // 149: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 124, // 150: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 8, // 151: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 259, // 152: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp + 259, // 153: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp + 259, // 154: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp + 13, // 155: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 259, // 156: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp + 264, // 157: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 126, // 158: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 8, // 159: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 249, // 160: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 259, // 161: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp + 264, // 162: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 126, // 163: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 264, // 164: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 126, // 165: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 264, // 166: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 14, // 167: openshell.v1.DeleteProviderRefreshResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 9, // 168: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 122, // 169: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 267, // 170: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 268, // 171: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 127, // 172: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 250, // 173: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 136, // 174: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 136, // 175: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 117, // 176: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 118, // 177: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 136, // 178: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 117, // 179: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 118, // 180: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 136, // 181: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 117, // 182: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 118, // 183: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 14, // 184: openshell.v1.DeleteProviderResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 14, // 185: openshell.v1.DeleteProviderProfileResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 149, // 186: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 251, // 187: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 252, // 188: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + 253, // 189: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 254, // 190: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 3, // 191: openshell.v1.GetSandboxProviderEnvironmentResponse.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason + 263, // 192: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration + 261, // 193: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 269, // 194: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 155, // 195: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 255, // 196: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 264, // 197: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 156, // 198: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 157, // 199: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 158, // 200: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 159, // 201: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 160, // 202: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 161, // 203: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 270, // 204: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 271, // 205: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 272, // 206: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 256, // 207: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 264, // 208: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 169, // 209: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 264, // 210: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 169, // 211: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 10, // 212: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 10, // 213: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 259, // 214: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp + 259, // 215: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp + 261, // 216: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 257, // 217: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 259, // 218: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp + 264, // 219: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 106, // 220: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 106, // 221: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 176, // 222: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 179, // 223: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 190, // 224: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 191, // 225: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 177, // 226: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 178, // 227: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 180, // 228: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 185, // 229: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 191, // 230: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 263, // 231: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration + 186, // 232: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 187, // 233: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 188, // 234: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 259, // 235: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp + 259, // 236: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp + 192, // 237: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 194, // 238: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 270, // 239: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 259, // 240: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp + 259, // 241: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp + 259, // 242: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp + 259, // 243: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp + 261, // 244: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 261, // 245: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 193, // 246: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 196, // 247: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 195, // 248: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 264, // 249: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 196, // 250: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 259, // 251: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp + 264, // 252: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 264, // 253: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 206, // 254: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 264, // 255: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 256: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 264, // 257: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 264, // 258: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 264, // 259: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 264, // 260: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 259, // 261: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp + 216, // 262: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 258, // 263: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 273, // 264: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 273, // 265: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 273, // 266: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 14, // 267: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 260, // 268: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 12, // 269: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 12, // 270: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 226, // 271: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 14, // 272: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 226, // 273: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 259, // 274: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp + 15, // 275: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult + 234, // 276: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation + 15, // 277: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult + 259, // 278: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp + 259, // 279: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 259, // 280: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 122, // 281: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 150, // 282: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 20, // 283: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 22, // 284: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 24, // 285: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 47, // 286: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 55, // 287: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest + 57, // 288: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 58, // 289: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 48, // 290: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest + 49, // 291: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest + 50, // 292: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest + 51, // 293: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest + 59, // 294: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 60, // 295: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 61, // 296: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 77, // 297: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest + 62, // 298: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 63, // 299: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 64, // 300: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 82, // 301: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 84, // 302: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 85, // 303: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 86, // 304: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 88, // 305: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 92, // 306: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 94, // 307: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 100, // 308: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 101, // 309: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 108, // 310: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 109, // 311: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 110, // 312: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 115, // 313: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 116, // 314: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 139, // 315: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 141, // 316: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 143, // 317: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 111, // 318: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 128, // 319: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 130, // 320: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 132, // 321: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 134, // 322: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 112, // 323: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 146, // 324: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 274, // 325: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 275, // 326: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 154, // 327: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 163, // 328: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 165, // 329: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 167, // 330: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 235, // 331: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest + 79, // 332: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest + 148, // 333: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 152, // 334: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 170, // 335: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 171, // 336: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 174, // 337: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 181, // 338: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 183, // 339: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest + 189, // 340: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 104, // 341: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 198, // 342: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 200, // 343: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 202, // 344: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 204, // 345: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 207, // 346: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 209, // 347: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 211, // 348: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 213, // 349: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 215, // 350: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 16, // 351: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 18, // 352: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 218, // 353: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 220, // 354: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 222, // 355: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 224, // 356: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 227, // 357: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 229, // 358: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 231, // 359: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 21, // 360: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 23, // 361: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 25, // 362: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 65, // 363: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 56, // 364: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse + 65, // 365: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 66, // 366: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 52, // 367: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 52, // 368: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 53, // 369: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse + 54, // 370: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse + 67, // 371: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 68, // 372: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 69, // 373: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 78, // 374: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse + 81, // 375: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 65, // 376: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 65, // 377: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 83, // 378: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 91, // 379: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 91, // 380: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 87, // 381: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 89, // 382: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 93, // 383: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 98, // 384: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 100, // 385: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 98, // 386: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 113, // 387: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 113, // 388: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 114, // 389: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 138, // 390: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 137, // 391: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 140, // 392: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 142, // 393: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 144, // 394: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 113, // 395: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 129, // 396: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 131, // 397: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 133, // 398: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 135, // 399: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 145, // 400: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 147, // 401: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 276, // 402: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 277, // 403: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 162, // 404: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 164, // 405: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 166, // 406: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 168, // 407: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 236, // 408: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse + 80, // 409: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse + 151, // 410: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 153, // 411: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 173, // 412: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 172, // 413: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 175, // 414: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 182, // 415: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 184, // 416: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse + 189, // 417: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 105, // 418: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 199, // 419: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 201, // 420: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 203, // 421: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 205, // 422: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 208, // 423: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 210, // 424: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 212, // 425: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 214, // 426: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 217, // 427: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 17, // 428: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 19, // 429: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 219, // 430: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 221, // 431: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 223, // 432: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 225, // 433: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 228, // 434: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 230, // 435: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 232, // 436: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 360, // [360:437] is the sub-list for method output_type + 283, // [283:360] is the sub-list for method input_type + 283, // [283:283] is the sub-list for extension type_name + 283, // [283:283] is the sub-list for extension extendee + 0, // [0:283] is the sub-list for field type_name } func init() { file_openshell_proto_init() } From 1d92000e94ef764ec7784e227b026c0a32a1f548 Mon Sep 17 00:00:00 2001 From: Shiju Date: Thu, 17 Sep 2026 11:12:21 +0530 Subject: [PATCH 3/4] fix(cli): preserve provider mutation storage uncertainty Recognize the gateway's exact structured storage-uncertainty reason for provider attach, detach, and update. Explain that the change may already be saved and must be reconciled before retrying, without exposing server messages or metadata. Preserve uncertainty ahead of generic retry hints. Exercise saved mutations through the CLI and verify single submission, redaction, missing receipt handling, and untrusted error-detail rejection. Document the recovery guidance for users and the public CLI skill. Signed-off-by: Shiju --- architecture/gateway.md | 2 + crates/openshell-cli/src/commands/provider.rs | 41 ++- .../tests/provider_commands_integration.rs | 256 ++++++++++++++++++ docs/providers/profiles.mdx | 2 + skills/openshell-cli/SKILL.md | 2 + 5 files changed, 298 insertions(+), 5 deletions(-) diff --git a/architecture/gateway.md b/architecture/gateway.md index 5840a00fc8..9d899489db 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -710,6 +710,8 @@ Each receipt projects a common configuration operation in the `config_update_ope Provider mutation and operation-result writes are separate. A result-storage failure can follow a saved mutation and returns structured uncertainty without a rollback or safe-retry claim. A failed initial snapshot remains failed rather than acquiring a different target during a later lookup. Operations contain only identities, revisions, timestamps, and closed reason categories. +The CLI recognizes the gateway's `CONFIG_OPERATION_STORAGE_UNCERTAIN` error reason and domain for attach, detach, and update. It reports fixed guidance to inspect and reconcile the saved change before retrying, while withholding arbitrary server messages and error metadata. An uncertain mutation never starts a readiness wait or automatic replay. + Provider receipts, installation status, and common operations represent absolute times with protobuf `Timestamp`; report intervals and evidence lifetimes use protobuf `Duration`. Receipt identity compares the full canonical timestamp without truncating nanoseconds. An absent observation or completion time represents missing evidence or an unfinished operation, independently of the Unix epoch. Provider installation reports belong to the existing `ConnectSupervisor` session. Each report names that session, has an increasing sequence, and expires unless the supervisor reports again. Reconnection or disconnect invalidates prior observations; stored change records survive a gateway restart, but runtime evidence does not. Replaying an identical report cannot extend its lifetime. diff --git a/crates/openshell-cli/src/commands/provider.rs b/crates/openshell-cli/src/commands/provider.rs index 8c7d1d97db..f849162f5e 100644 --- a/crates/openshell-cli/src/commands/provider.rs +++ b/crates/openshell-cli/src/commands/provider.rs @@ -26,6 +26,7 @@ use openshell_core::proto::{ ProviderProfileDiagnostic, ProviderProfileImportItem, RotateProviderCredentialRequest, UpdateProviderProfilesRequest, UpdateProviderRequest, }; +use openshell_core::rpc_error::{ERROR_DOMAIN, decode_details}; use openshell_core::{ObjectId, ObjectName, ObjectWorkspace}; use openshell_providers::{ ProviderTypeProfile, RealDiscoveryContext, detect_provider_from_command, discover_from_profile, @@ -37,6 +38,30 @@ use std::io::IsTerminal; use std::path::{Path, PathBuf}; use tonic::{Code, Status}; +fn provider_mutation_is_uncertain(status: &Status) -> bool { + // Only a validated gateway ErrorInfo identifies a possibly saved mutation. + // Message text, metadata, foreign domains, and malformed details are untrusted. + decode_details(status).is_some_and(|details| { + details.error_info().is_some_and(|info| { + info.domain == ERROR_DOMAIN && info.reason == "CONFIG_OPERATION_STORAGE_UNCERTAIN" + }) + }) +} + +fn provider_mutation_error(status: &Status, operation: &str) -> miette::Report { + if provider_mutation_is_uncertain(status) { + // Emit fixed guidance without chaining the server's potentially sensitive + // message or metadata. An error does not establish that the write rolled back. + miette!( + "provider change may already be saved (CONFIG_OPERATION_STORAGE_UNCERTAIN); \ + readiness receipt could not be recorded. Do not blindly retry the mutation; \ + check provider and sandbox status and reconcile the saved change first." + ) + } else { + miette!("provider {operation} failed ({})", status.code()) + } +} + fn proto_timestamp_ms(timestamp: Option<&prost_types::Timestamp>) -> i64 { timestamp .and_then(|value| openshell_core::time::timestamp_to_millis(value).ok()) @@ -123,13 +148,16 @@ pub async fn sandbox_provider_attach( .await { Ok(response) => response.into_inner(), - Err(status) if status.code() == Code::Aborted => { + // Explicit post-save uncertainty takes precedence over a generic retry hint. + Err(status) + if status.code() == Code::Aborted && !provider_mutation_is_uncertain(&status) => + { return Err(miette::miette!( "Failed to attach provider: sandbox was modified by another operation.\n\ Please retry the command." )); } - Err(error) => return Err(miette!("provider attachment failed ({})", error.code())), + Err(error) => return Err(provider_mutation_error(&error, "attachment")), }; let receipt = response.receipt.ok_or_else(|| { @@ -190,13 +218,16 @@ pub async fn sandbox_provider_detach( .await { Ok(response) => response.into_inner(), - Err(status) if status.code() == Code::Aborted => { + // Explicit post-save uncertainty takes precedence over a generic retry hint. + Err(status) + if status.code() == Code::Aborted && !provider_mutation_is_uncertain(&status) => + { return Err(miette::miette!( "Failed to detach provider: sandbox was modified by another operation.\n\ Please retry the command." )); } - Err(error) => return Err(miette!("provider detachment failed ({})", error.code())), + Err(error) => return Err(provider_mutation_error(&error, "detachment")), }; let receipt = response.receipt.ok_or_else(|| miette!("gateway did not return a provider receipt; saved detachment cannot establish revocation"))?; @@ -2405,7 +2436,7 @@ pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> { clear_credential_expiration_keys, }) .await - .map_err(|error| miette!("provider update failed ({})", error.code()))?; + .map_err(|error| provider_mutation_error(&error, "update"))?; let response = response.into_inner(); if response.mutation_id.is_empty() { diff --git a/crates/openshell-cli/tests/provider_commands_integration.rs b/crates/openshell-cli/tests/provider_commands_integration.rs index b02e256e9d..9f901c04c9 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -31,6 +31,7 @@ use openshell_core::proto::{ RotateProviderCredentialResponse, Sandbox, SandboxResponse, SandboxStreamEvent, ServiceStatus, SettingValue, SupervisorMessage, UpdateProviderRequest, WatchSandboxRequest, }; +use openshell_core::rpc_error::{ERROR_DOMAIN, ErrorDetails, StatusExt}; use openshell_core::{ObjectId, ObjectName}; use std::collections::{HashMap, VecDeque}; use std::sync::Arc; @@ -58,6 +59,8 @@ const READINESS_PROVIDER: &str = "readiness-provider"; const SYNTHETIC_READINESS_CREDENTIAL: &str = "fixture-provider-credential"; const SYNTHETIC_READINESS_BACKEND_ERROR: &str = "fixture-backend-authorization-details"; const SYNTHETIC_PROFILE_BACKEND_ERROR: &str = "TESTLEAK"; +const SYNTHETIC_MUTATION_ERROR_METADATA: &str = "fixture-mutation-error-metadata"; +const STORAGE_UNCERTAIN_REASON: &str = "CONFIG_OPERATION_STORAGE_UNCERTAIN"; fn selected_workspace( scope: &Option, @@ -98,6 +101,7 @@ struct ProviderState { readiness_requests: Arc>>, readiness_sequence: Arc, corrupt_mutation_receipt: Arc>>, + fail_mutation_after_save: Arc>>, global_settings: Arc>>, } @@ -145,6 +149,13 @@ struct TestOpenShell { } impl TestOpenShell { + // A durable mutation can fail before its receipt is stored. Keep the failure + // active for every call so a client replay remains visible in request logs. + async fn check_mutation_receipt_storage(&self) -> Result<(), Status> { + let failure = self.state.fail_mutation_after_save.lock().await.clone(); + failure.map_or(Ok(()), Err) + } + async fn provider_receipt( &self, sandbox_name: &str, @@ -388,6 +399,7 @@ impl OpenShell for TestOpenShell { }; let provider_names = providers.clone(); drop(sandbox_providers); + self.check_mutation_receipt_storage().await?; let receipt = self .provider_receipt( &request.sandbox_name, @@ -440,6 +452,7 @@ impl OpenShell for TestOpenShell { let detached = providers.len() != before_len; let provider_names = providers.clone(); drop(sandbox_providers); + self.check_mutation_receipt_storage().await?; let receipt = self .provider_receipt( &request.sandbox_name, @@ -999,6 +1012,7 @@ impl OpenShell for TestOpenShell { let updated_name = updated.object_name().to_string(); providers.insert(updated_name.clone(), updated.clone()); drop(providers); + self.check_mutation_receipt_storage().await?; let mutation_id = format!( "update-{}", self.state.readiness_sequence.fetch_add(1, Ordering::SeqCst) + 1 @@ -1703,6 +1717,248 @@ fn assert_readiness_output_redacted(output: &std::process::Output) { assert!(!text.contains(SYNTHETIC_READINESS_CREDENTIAL)); assert!(!text.contains(SYNTHETIC_READINESS_BACKEND_ERROR)); assert!(!text.contains(SYNTHETIC_PROFILE_BACKEND_ERROR)); + assert!(!text.contains(SYNTHETIC_MUTATION_ERROR_METADATA)); + } +} + +fn mutation_error_status(code: Code, reason: &str, domain: &str) -> Status { + Status::with_error_details( + code, + SYNTHETIC_READINESS_BACKEND_ERROR, + ErrorDetails::with_error_info( + reason, + domain, + HashMap::from([( + "backend".to_string(), + SYNTHETIC_MUTATION_ERROR_METADATA.to_string(), + )]), + ), + ) +} + +// Exercise the actual CLI process and establish that the mock saved exactly one +// mutation before returning the error, without producing or polling a receipt. +async fn run_saved_provider_mutation_error( + server: &TestServer, + action: &str, + status: Status, + wait: bool, +) -> String { + let sandbox_name = "storage-uncertain"; + seed_readiness_provider(server).await; + server.state.sandbox_providers.lock().await.insert( + sandbox_name.to_string(), + if action == "attach" { + Vec::new() + } else { + vec![READINESS_PROVIDER.to_string()] + }, + ); + server.state.sandbox_provider_requests.lock().await.clear(); + server.state.provider_update_requests.lock().await.clear(); + *server.state.fail_mutation_after_save.lock().await = Some(status); + let mut args = if action == "update" { + vec![ + "provider", + "update", + READINESS_PROVIDER, + "--config", + "region=changed", + ] + } else { + vec![ + "sandbox", + "provider", + action, + sandbox_name, + READINESS_PROVIDER, + ] + }; + args.extend(["--output", "json"]); + if wait { + args.extend(["--wait", "--timeout", "1"]); + } + let output = run_readiness_cli(server, &args).await; + assert!(!output.status.success(), "{action}, wait={wait}"); + assert!(output.stdout.is_empty(), "failed mutation printed a result"); + assert_readiness_output_redacted(&output); + assert!(server.state.readiness_receipts.lock().await.is_empty()); + assert!(server.state.readiness_requests.lock().await.is_empty()); + + let attachment_requests = server.state.sandbox_provider_requests.lock().await; + let update_requests = server.state.provider_update_requests.lock().await; + if action == "update" { + assert!(attachment_requests.is_empty()); + assert_eq!(update_requests.len(), 1, "update was replayed"); + let providers = server.state.providers.lock().await; + let provider = providers.get(READINESS_PROVIDER).unwrap(); + assert_eq!( + provider.config.get("region").map(String::as_str), + Some("changed") + ); + assert_eq!(provider.metadata.as_ref().unwrap().resource_version, 2); + } else { + assert!(update_requests.is_empty()); + let expected_request = if action == "attach" { + SandboxProviderRequestLog::Attach { + sandbox_name: sandbox_name.to_string(), + provider_name: READINESS_PROVIDER.to_string(), + } + } else { + SandboxProviderRequestLog::Detach { + sandbox_name: sandbox_name.to_string(), + provider_name: READINESS_PROVIDER.to_string(), + } + }; + assert_eq!( + *attachment_requests, + vec![expected_request], + "mutation was replayed" + ); + let attachments = server.state.sandbox_providers.lock().await; + assert_eq!( + attachments + .get(sandbox_name) + .unwrap() + .contains(&READINESS_PROVIDER.to_string()), + action == "attach", + "attachment mutation was not saved" + ); + } + String::from_utf8(output.stderr).unwrap() +} + +#[tokio::test] +async fn provider_readiness_storage_uncertainty_preserves_safe_recovery_guidance() { + let server = run_server().await; + for action in ["attach", "detach", "update"] { + for wait in [false, true] { + // Structured uncertainty takes precedence over the ordinary Aborted + // conflict hint: this saved mutation must not invite a blind retry. + for code in [Code::Unavailable, Code::Aborted] { + let stderr = run_saved_provider_mutation_error( + &server, + action, + mutation_error_status(code, STORAGE_UNCERTAIN_REASON, ERROR_DOMAIN), + wait, + ) + .await; + for expected in [ + STORAGE_UNCERTAIN_REASON, + "may already be saved", + "Do not blindly retry", + "reconcile", + ] { + assert!( + stderr.contains(expected), + "{action}, wait={wait}, {code:?}: {stderr}" + ); + } + assert!(!stderr.contains("Please retry the command")); + } + } + } +} + +#[tokio::test] +async fn provider_readiness_storage_uncertainty_requires_trusted_error_info() { + let server = run_server().await; + let valid = mutation_error_status(Code::Unavailable, STORAGE_UNCERTAIN_REASON, ERROR_DOMAIN); + let mut malformed_error_info = valid.details().to_vec(); + let reason_offset = malformed_error_info + .windows(STORAGE_UNCERTAIN_REASON.len()) + .position(|bytes| bytes == STORAGE_UNCERTAIN_REASON.as_bytes()) + .unwrap(); + // Invalid UTF-8 breaks only the nested ErrorInfo reason; its outer status + // envelope remains valid and cannot authorize the special recovery hint. + malformed_error_info[reason_offset] = 0xff; + let cases = [ + ( + "unrelated", + mutation_error_status(Code::Unavailable, "OTHER_REASON", ERROR_DOMAIN), + ), + ( + "wrong domain", + mutation_error_status(Code::Unavailable, STORAGE_UNCERTAIN_REASON, "other.example"), + ), + ( + "reason case", + mutation_error_status( + Code::Unavailable, + "config_operation_storage_uncertain", + ERROR_DOMAIN, + ), + ), + ( + "domain case", + mutation_error_status( + Code::Unavailable, + STORAGE_UNCERTAIN_REASON, + "OPENSHELL.NVIDIA.COM", + ), + ), + ( + "missing ErrorInfo", + Status::with_error_details( + Code::Unavailable, + SYNTHETIC_READINESS_BACKEND_ERROR, + ErrorDetails::new(), + ), + ), + ( + "message only", + Status::unavailable(format!( + "{STORAGE_UNCERTAIN_REASON}: {SYNTHETIC_READINESS_BACKEND_ERROR}" + )), + ), + ( + "malformed ErrorInfo", + Status::with_details( + Code::Unavailable, + SYNTHETIC_READINESS_BACKEND_ERROR, + malformed_error_info.into(), + ), + ), + ( + "malformed envelope", + Status::with_details( + Code::Unavailable, + SYNTHETIC_READINESS_BACKEND_ERROR, + vec![0xff].into(), + ), + ), + ( + "mismatched envelope message", + Status::with_details( + Code::Unavailable, + "different message", + valid.details().to_vec().into(), + ), + ), + ]; + for action in ["attach", "detach", "update"] { + let error_prefix = match action { + "attach" => "provider attachment failed", + "detach" => "provider detachment failed", + _ => "provider update failed", + }; + for (case, status) in &cases { + let stderr = + run_saved_provider_mutation_error(&server, action, status.clone(), true).await; + assert!(stderr.contains(error_prefix), "{action}, {case}: {stderr}"); + assert!( + stderr.contains(&Code::Unavailable.to_string()), + "{action}, {case}: {stderr}" + ); + assert!( + !stderr.contains(STORAGE_UNCERTAIN_REASON), + "{action}, {case}: {stderr}" + ); + assert!( + !stderr.contains("may already be saved"), + "{action}, {case}: {stderr}" + ); + } } } diff --git a/docs/providers/profiles.mdx b/docs/providers/profiles.mdx index 578eccfc86..06f17a42fb 100644 --- a/docs/providers/profiles.mdx +++ b/docs/providers/profiles.mdx @@ -995,6 +995,8 @@ openshell sandbox provider status provider-demo work-github --receipt RECEIPT_ID Replace `RECEIPT_ID` with the `receipt_id` returned by attach, detach, or update. The ID always refers to the original change. If a later change replaces it, the status is `superseded` and the wait ends without reporting success. +If attach, detach, or update reports `CONFIG_OPERATION_STORAGE_UNCERTAIN`, the change may already be saved even though the gateway could not record its readiness receipt. Do not blindly retry the mutation: inspect the provider and sandbox state and reconcile the saved change first. A receipt may be unavailable, so an error does not establish that the mutation was rolled back or that the sandbox is ready. + Readiness states have these meanings: - `persisted`: the gateway saved the change; the command has not checked the sandbox yet. diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index f2449c6a60..e619ae3334 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -166,6 +166,8 @@ openshell provider profile import --file ./my-profile.yaml Use `openshell sandbox provider status --help` and the attach, detach, and update help to find the installed version's wait options. Add `--wait` when the next step depends on a provider change taking effect. Without it, a successful command only confirms that the gateway saved the change. Save the returned `receipt_id` to check that same change later, and inspect the result for every selected sandbox. Credential refresh status confirms that OpenShell obtained credentials; provider status confirms that the sandbox applied them, activated the policy, and updated the environment for new processes. If the status is `superseded`, explain that a later change replaced the request and inspect that change separately. +If attach, detach, or update reports `CONFIG_OPERATION_STORAGE_UNCERTAIN`, explain that the change may already be saved and its readiness receipt may be unavailable. Do not blindly retry the mutation. Inspect the provider and sandbox state and reconcile the saved change before deciding on another mutation; the error proves neither rollback nor readiness. + ```bash openshell provider list openshell provider list --output json From 8b5fd3fa37f2340799862cd99a255c089f3dd564 Mon Sep 17 00:00:00 2001 From: Shiju Date: Thu, 17 Sep 2026 14:05:34 +0530 Subject: [PATCH 4/4] fix(cli): explain denied provider profile lookups Report exact and alias profile lookup denials with fixed permission and workspace guidance. Keep backend details redacted and stop before provider mutations. Cover denied create and update calls through the CLI. Verify the complete provider list independently in the cross-workspace OIDC regression, extracting its JSON object from surrounding startup diagnostics. Signed-off-by: Shiju --- crates/openshell-cli/src/commands/provider.rs | 31 +++--- .../tests/provider_commands_integration.rs | 96 +++++++++++++++++++ e2e/rust/tests/oidc_pkce.rs | 50 +++++++++- 3 files changed, 161 insertions(+), 16 deletions(-) diff --git a/crates/openshell-cli/src/commands/provider.rs b/crates/openshell-cli/src/commands/provider.rs index f849162f5e..88607413de 100644 --- a/crates/openshell-cli/src/commands/provider.rs +++ b/crates/openshell-cli/src/commands/provider.rs @@ -757,6 +757,19 @@ async fn rollback_provider_create_after_gcloud_adc_failure( } } +fn provider_profile_lookup_error(status: &Status) -> miette::Report { + // A permission code supports recovery guidance, but cannot distinguish a + // missing membership from an insufficient role. Never expose backend text. + if status.code() == Code::PermissionDenied { + miette!( + "provider profile lookup denied (PERMISSION_DENIED): \ + verify workspace membership and required permissions" + ) + } else { + miette!("provider profile lookup failed ({})", status.code()) + } +} + async fn fetch_provider_profile( client: &mut crate::tls::GrpcClient, provider_type: &str, @@ -781,16 +794,11 @@ async fn fetch_provider_profile( "provider profile '{requested}' not found; import a matching profile before using this provider type" ) } else { - miette!("provider profile lookup failed ({})", fallback_status.code()) + provider_profile_lookup_error(&fallback_status) } })? } - Err(status) => { - return Err(miette!( - "provider profile lookup failed ({})", - status.code() - )); - } + Err(status) => return Err(provider_profile_lookup_error(&status)), }; Ok(response) @@ -1077,11 +1085,10 @@ pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) -> if profile_id.is_empty() { return Err(miette::miette!("provider type is required")); } - let provider_profile = fetch_provider_profile(&mut client, profile_id, profile_workspace) - .await - .map_err(|err| { - miette::miette!("unsupported provider type or profile: {profile_id} ({err})") - })?; + // Lookup already distinguishes absent profiles from permission and transport + // failures; those failures do not establish that the profile is unsupported. + let provider_profile = + fetch_provider_profile(&mut client, profile_id, profile_workspace).await?; let provider_type = provider_profile.id.clone(); let adc_credential_key = if from_gcloud_adc { diff --git a/crates/openshell-cli/tests/provider_commands_integration.rs b/crates/openshell-cli/tests/provider_commands_integration.rs index 9f901c04c9..182c46d3e6 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -82,6 +82,7 @@ struct ProviderState { scoped_profiles: Arc>>, refresh_statuses: Arc>>, refresh_requests: Arc>>, + provider_create_requests: Arc, provider_update_requests: Arc>>, deny_provider_reads: Arc, fail_provider_reads: Arc, @@ -667,6 +668,9 @@ impl OpenShell for TestOpenShell { &self, request: tonic::Request, ) -> Result, Status> { + self.state + .provider_create_requests + .fetch_add(1, Ordering::SeqCst); let mut provider = request .into_inner() .provider @@ -2163,6 +2167,98 @@ async fn provider_readiness_update_with_no_targets_preserves_saved_success() { } } +#[tokio::test] +async fn provider_profile_permission_denial_preserves_safe_workspace_guidance() { + let server = run_server().await; + seed_readiness_provider(&server).await; + for (profile, failed_lookup, expected_lookups) in [ + ("openai", "openai", vec!["openai"]), + ("gh", "github", vec!["gh", "github"]), + ] { + server + .state + .providers + .lock() + .await + .get_mut(READINESS_PROVIDER) + .expect("seeded provider") + .r#type = profile.to_string(); + let mut errors = server.state.profile_read_errors.lock().await; + errors.clear(); + errors.insert(failed_lookup.to_string(), Code::PermissionDenied); + drop(errors); + for args in [ + vec![ + "provider", + "create", + "--name", + "denied-new-provider", + "--type", + profile, + "--credential", + "OPENAI_API_KEY=fixture-provider-credential", + ], + vec![ + "provider", + "update", + READINESS_PROVIDER, + "--from-existing", + "--wait", + "--output", + "json", + ], + ] { + server.state.profile_read_requests.lock().await.clear(); + let output = run_readiness_cli(&server, &args).await; + assert!(!output.status.success(), "{args:?}"); + assert!(output.stdout.is_empty()); + assert_readiness_output_redacted(&output); + let diagnostic = String::from_utf8_lossy(&output.stderr); + let compact: String = diagnostic + .chars() + .filter(|character| !character.is_whitespace() && *character != '│') + .collect(); + assert!( + compact.contains("providerprofilelookupdenied"), + "{diagnostic}" + ); + // The permission code supports recovery guidance without revealing + // backend text or inferring which membership or role check failed. + assert!(compact.contains("PERMISSION_DENIED"), "{diagnostic}"); + assert!( + compact.contains("verifyworkspacemembershipandrequiredpermissions"), + "{diagnostic}" + ); + assert!(!diagnostic.contains("unsupported provider type or profile")); + assert_eq!( + *server.state.profile_read_requests.lock().await, + expected_lookups + ); + assert!( + !server + .state + .providers + .lock() + .await + .contains_key("denied-new-provider") + ); + assert_eq!( + server.state.provider_create_requests.load(Ordering::SeqCst), + 0 + ); + assert!( + server + .state + .provider_update_requests + .lock() + .await + .is_empty() + ); + assert!(server.state.readiness_requests.lock().await.is_empty()); + } + } +} + #[tokio::test] async fn provider_readiness_update_redacts_exact_and_alias_profile_lookup_errors() { let server = run_server().await; diff --git a/e2e/rust/tests/oidc_pkce.rs b/e2e/rust/tests/oidc_pkce.rs index a64dab3476..8798670e7a 100644 --- a/e2e/rust/tests/oidc_pkce.rs +++ b/e2e/rust/tests/oidc_pkce.rs @@ -808,6 +808,7 @@ async fn workspace_admin_cannot_manage_another_workspace_members() { async fn workspace_admin_cannot_manage_another_workspace_providers() { const WORKSPACE_A: &str = "oidc-wsa-xprov-a"; const WORKSPACE_B: &str = "oidc-wsa-xprov-b"; + const PROVIDER: &str = "oidc-wsa-xprovider"; let (admin, workspace_admin, _user_b) = prepare_isolated_workspaces_with_admin(WORKSPACE_A, WORKSPACE_B).await; @@ -818,7 +819,7 @@ async fn workspace_admin_cannot_manage_another_workspace_providers() { "provider", "create", "--name", - "oidc-wsa-xprovider", + PROVIDER, "--type", "openai", "--credential", @@ -826,9 +827,50 @@ async fn workspace_admin_cannot_manage_another_workspace_providers() { ], ) .await; - assert_non_member_denial( - &denied, - "manage another workspace's providers as workspace admin", + let diagnostic = combined_output(&denied); + let compact: String = diagnostic + .chars() + .filter(|character| !character.is_whitespace() && *character != '│') + .collect(); + // Profile lookup redacts backend diagnostics. Check the permission code and + // safe recovery guidance without requiring the server's membership details. + assert!( + !denied.status.success() + && compact.contains("PERMISSION_DENIED") + && compact.contains("verifyworkspacemembershipandrequiredpermissions"), + "cross-workspace provider creation did not report a safe permission denial:\n{diagnostic}" + ); + assert!(!diagnostic.contains("e2e-test-value")); + + // Query with an independent authorized identity so a failed command alone + // cannot hide a provider created before the denial was returned. + let listed = assert_workspace_allowed( + &admin, + WORKSPACE_B, + &["provider", "list", "--output", "json"], + "verify denied creation left the target workspace empty", + ) + .await; + // CLI startup diagnostics may precede the JSON object on stdout. + let stdout = String::from_utf8(listed.stdout).expect("provider list output should be UTF-8"); + let json_start = stdout + .find('{') + .expect("provider list output should contain JSON"); + let json_end = stdout + .rfind('}') + .expect("provider list output should contain a complete JSON object"); + let listing: Value = serde_json::from_str(&stdout[json_start..=json_end]) + .expect("provider list --output json should return JSON on stdout"); + assert_eq!( + listing["next_page_token"], "", + "provider listing is incomplete" + ); + assert!( + listing["providers"] + .as_array() + .expect("provider collection") + .is_empty(), + "denied creation added a provider to the isolated target workspace" ); delete_workspace(&admin, WORKSPACE_B).await;