From db18050e8cc8ce0db5e8e43b56c3a87fa29a48ec Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Thu, 10 Sep 2026 09:54:31 -0700 Subject: [PATCH 01/23] feat(sandbox): validate configuration before workload activation Closes #3145 Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- architecture/sandbox.md | 49 +- architecture/security-policy.md | 8 + crates/openshell-cli/src/run.rs | 107 +++- .../tests/ensure_providers_integration.rs | 7 + .../openshell-cli/tests/mtls_integration.rs | 7 + .../tests/provider_commands_integration.rs | 7 + .../sandbox_create_lifecycle_integration.rs | 7 + .../sandbox_name_fallback_integration.rs | 7 + crates/openshell-core/src/grpc_client.rs | 40 ++ .../src/provider_credentials.rs | 117 ++++ crates/openshell-sdk/tests/client_mock.rs | 7 + .../openshell-server/src/auth/method_authz.rs | 3 + crates/openshell-server/src/compute/mod.rs | 205 +++++++ crates/openshell-server/src/grpc/mod.rs | 7 + crates/openshell-server/src/grpc/policy.rs | 578 +++++++++++++++--- crates/openshell-server/src/grpc/sandbox.rs | 8 + crates/openshell-server/src/policy_store.rs | 14 + crates/openshell-server/tests/common/mod.rs | 7 + .../tests/supervisor_relay_integration.rs | 7 + .../openshell-supervisor-network/src/opa.rs | 98 ++- docs/reference/gateway-config.mdx | 2 +- docs/sandboxes/manage-sandboxes.mdx | 8 + docs/sandboxes/policies.mdx | 25 +- e2e/rust/Cargo.toml | 5 + e2e/rust/tests/policy_activation.rs | 354 +++++++++++ proto/openshell.proto | 34 ++ proto/sandbox.proto | 7 + .../v1/internal/converter/coverage_test.go | 1 + .../v1/internal/converter/sandbox.go | 19 + .../v1/internal/converter/sandbox_test.go | 27 + sdk/go/openshell/v1/types/sandbox.go | 23 + skills/debug-openshell-cluster/SKILL.md | 10 + skills/generate-sandbox-policy/SKILL.md | 6 + skills/openshell-cli/SKILL.md | 9 +- 34 files changed, 1707 insertions(+), 113 deletions(-) create mode 100644 e2e/rust/tests/policy_activation.rs diff --git a/architecture/sandbox.md b/architecture/sandbox.md index 12b3d74315..6cadfa58f6 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -612,6 +612,44 @@ the structured 403 and authors the narrowest rule. Mechanistically mapping L7 would either over-broaden rules or require path-templating logic that rots quickly. +## Configuration Admission + +Gateway-managed supervisors reconcile configuration before launching the main +process or exposing workload services. Admission covers the effective policy, +provider layers, credential bindings, and gateway-derived provenance. Explicit +user and global policy precedence is unchanged; an image without a policy uses +the restrictive baseline. An invalid image policy does not become a launchable +default. + +The gateway tracks configuration admission independently of compute health. +A blocked startup remains `Provisioning` with a `ConfigurationInvalid` readiness +condition, even when the container backend reports readiness. Gateway management +operations remain available. Replacing the policy or repairing providers allows +the same supervisor to reconcile and launch; it does not recreate the sandbox. +Static policy fields can be replaced before the first accepted activation. +Admission validates policy composition; image and host setup failures, such as +an unresolved OCI user or unavailable isolation facilities, retain their existing +startup error behavior. + +Acceptance identifies the effective policy hash/version, configuration revision, +provider-environment revision, and reporting supervisor instance. Startup captures +the matching provider environment and constructs the runtime before reporting +acceptance. Live reconciliation begins only after the main process has spawned, +so it cannot replace the configuration captured for that launch. Restart resets +admission and requires a fresh accepted configuration. + +Policy and provider refreshes are prepared before publication. Publication +invalidates prior policy guards before exposing new provider material and swaps +the policy under the same publication locks. Rejected candidates cannot install +their credentials alongside the previous policy. Existing runtime fail-closed +checks remain necessary for in-flight traffic and invalid live updates. + +In sidecar topology, the authenticated process supervisor supplies discovery +from the workload image over the existing control socket. The network supervisor +withholds bootstrap until admission succeeds, then sends the accepted policy and +child environment together. Subsequent configuration messages carry both parts +and an ordered generation; older messages cannot restore stale child credentials. + ## Policy Revision Acknowledgement When the supervisor loads a sandbox-scoped policy from the gateway, it retains @@ -647,12 +685,11 @@ outages cannot block policy polling, enforcement, settings, or provider refreshes and cannot permanently lose the initial acknowledgement. Only sandbox-scoped revisions (`PolicySource::Sandbox`, version greater than -zero) are acknowledged. Global policies and local-file development policies do -not use the sandbox revision API and produce no acknowledgement. When explicit -local Rego and data files are provisioned into the supervisor, it continues -polling the gateway for settings and provider refreshes but never replaces the -local OPA engine with a gateway policy revision. Workload image files and -environment variables do not configure the separately isolated supervisor. +zero) use the policy revision acknowledgement API. Global policies use the +configuration admission contract without a sandbox policy revision acknowledgement. +Local Rego/data overrides remain available for standalone development; combining +them with a gateway-managed sandbox is rejected because the gateway cannot admit +the runtime policy it would enforce. ## Failure Behavior diff --git a/architecture/security-policy.md b/architecture/security-policy.md index 3b490ceee4..667694a1fc 100644 --- a/architecture/security-policy.md +++ b/architecture/security-policy.md @@ -160,6 +160,14 @@ flag defaults to `false` and is security-flagged in policy approval flows. Incremental merges only ever add the flag to a matching endpoint; clearing it requires removing the endpoint or replacing the policy. +Image discovery may persist a desired policy for repair, but does not authorize +workload activation. The gateway applies the credential gate after full provider +composition and provenance derivation. A rejected effective configuration keeps +startup blocked with a bounded diagnostic; the supervisor waits for management +repair instead of launching with connection-time denials or a fallback policy. +Accepted runtime state includes the matching provider-environment revision, so +policy and credential updates cannot activate independently. + The network supervisor independently enforces the same boundary. Credentialed WebSocket upgrades use the parsed relay, binary frames fail closed, and text placeholders require rewrite. REST bodies continue streaming when body rewrite is disabled. The relay holds diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index 87af586f8e..0ce00051f2 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -1567,11 +1567,23 @@ where sandbox.object_id().to_string() }; - let config = client + let config_result = client .get_sandbox_config(GetSandboxConfigRequest { sandbox_id }) - .await - .into_diagnostic()? - .into_inner(); + .await; + let config = match config_result { + Ok(response) => response.into_inner(), + Err(_) if !policy_only && configuration_failure_message(&sandbox).is_some() => { + // An invalid desired policy must not hide the status needed to + // repair it. Keep payload-only reads strict. + GetSandboxConfigResponse { + configuration_error: configuration_failure_message(&sandbox) + .unwrap_or_default() + .to_string(), + ..Default::default() + } + } + Err(error) => return Err(error).into_diagnostic(), + }; if policy_only { let Some(ref policy) = config.policy else { @@ -1605,6 +1617,22 @@ where println!(" {} {}", "Id:".dimmed(), id); println!(" {} {}", "Name:".dimmed(), name); println!(" {} {}", "Phase:".dimmed(), phase_name(sandbox.phase())); + if let Some(status) = sandbox.status.as_ref() { + for condition in &status.conditions { + if matches!( + condition.r#type.as_str(), + "ConfigurationReady" | "DesiredConfigurationReady" + ) && condition.status.eq_ignore_ascii_case("false") + { + println!( + " {} {}: {}", + "Configuration:".dimmed(), + condition.reason, + condition.message + ); + } + } + } if let Some(exit_code) = sandbox.status.as_ref().and_then(|status| status.exit_code) { println!(" {} {}", "Exit Code:".dimmed(), exit_code); } @@ -2467,7 +2495,19 @@ pub async fn sandbox_list( Ok(()) } +fn configuration_failure_message(sandbox: &Sandbox) -> Option<&str> { + sandbox + .status + .as_ref()? + .configuration_admission + .as_ref() + .map(|admission| admission.error.as_str()) + .filter(|message| !message.is_empty()) +} + fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value { + use openshell_core::proto::ConfigurationAdmissionState; + let meta = sandbox.metadata.as_ref(); let labels = meta.map_or_else(|| serde_json::json!({}), |m| serde_json::json!(m.labels)); let annotations = meta.map_or_else( @@ -2507,6 +2547,25 @@ fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value { }) .collect::>() }); + let admission = sandbox + .status + .as_ref() + .and_then(|status| status.configuration_admission.as_ref()) + .map(|admission| { + serde_json::json!({ + "state": match ConfigurationAdmissionState::try_from(admission.state) { + Ok(ConfigurationAdmissionState::Pending) => "pending", + Ok(ConfigurationAdmissionState::Accepted) => "accepted", + Ok(ConfigurationAdmissionState::Rejected) => "rejected", + _ => "unknown", + }, + "error": admission.error, + "policy_version": admission.policy_version, + "policy_hash": admission.policy_hash, + "config_revision": admission.config_revision, + "provider_env_revision": admission.provider_env_revision, + }) + }); serde_json::json!({ "id": sandbox.object_id(), "name": sandbox.object_name(), @@ -2520,6 +2579,7 @@ fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value { "exit_code": sandbox.status.as_ref().and_then(|status| status.exit_code), "conditions": conditions, "endpoint_statuses": endpoint_statuses, + "configuration_admission": admission, "created_from_workload_template": created_from_workload_template, }) } @@ -7365,6 +7425,45 @@ mod tests { assert_eq!(json["resources"]["gpu"], 2); } + #[test] + fn sandbox_json_exposes_repair_diagnostic_and_accepted_generation() { + use openshell_core::proto::{ConfigurationAdmissionState, SandboxConfigurationAdmission}; + + let mut sandbox = Sandbox::default(); + sandbox.set_phase(SandboxPhase::Provisioning as i32); + let status = sandbox.status.as_mut().unwrap(); + status.configuration_admission = Some(SandboxConfigurationAdmission { + state: ConfigurationAdmissionState::Rejected as i32, + error: "rule image_api requires L7 inspection".to_string(), + policy_hash: "candidate-hash".to_string(), + ..Default::default() + }); + status.conditions.push(SandboxCondition { + r#type: "ConfigurationReady".to_string(), + status: "False".to_string(), + reason: "ConfigurationInvalid".to_string(), + message: "rule image_api requires L7 inspection".to_string(), + ..Default::default() + }); + let json = super::sandbox_to_json(&sandbox); + assert_eq!(json["configuration_admission"]["state"], "rejected"); + assert_eq!(json["conditions"][0]["reason"], "ConfigurationInvalid"); + assert_eq!( + super::configuration_failure_message(&sandbox), + Some("rule image_api requires L7 inspection") + ); + sandbox + .status + .as_mut() + .unwrap() + .configuration_admission + .as_mut() + .unwrap() + .error + .clear(); + assert_eq!(super::configuration_failure_message(&sandbox), None); + } + #[test] fn sandbox_detail_to_json_includes_policy_fields() { let mut sandbox = Sandbox { diff --git a/crates/openshell-cli/tests/ensure_providers_integration.rs b/crates/openshell-cli/tests/ensure_providers_integration.rs index 730c7271d0..9541d38bf4 100644 --- a/crates/openshell-cli/tests/ensure_providers_integration.rs +++ b/crates/openshell-cli/tests/ensure_providers_integration.rs @@ -586,6 +586,13 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not implemented in test")) } + async fn report_sandbox_configuration( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + async fn report_policy_status( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/mtls_integration.rs b/crates/openshell-cli/tests/mtls_integration.rs index e188d541f1..59ba2d287d 100644 --- a/crates/openshell-cli/tests/mtls_integration.rs +++ b/crates/openshell-cli/tests/mtls_integration.rs @@ -438,6 +438,13 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not implemented in test")) } + async fn report_sandbox_configuration( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + async fn report_policy_status( &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 a8b4579287..e6e5a10567 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -1318,6 +1318,13 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not implemented in test")) } + async fn report_sandbox_configuration( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + async fn report_policy_status( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs index 5a5e00accb..033bd2479e 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -862,6 +862,13 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not implemented in test")) } + async fn report_sandbox_configuration( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + async fn report_policy_status( &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 316cd3d1d0..27cf9d63ea 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -528,6 +528,13 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not implemented in test")) } + async fn report_sandbox_configuration( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + async fn report_policy_status( &self, _request: tonic::Request, diff --git a/crates/openshell-core/src/grpc_client.rs b/crates/openshell-core/src/grpc_client.rs index fcdbe907e3..426ef3b6c9 100644 --- a/crates/openshell-core/src/grpc_client.rs +++ b/crates/openshell-core/src/grpc_client.rs @@ -1022,6 +1022,40 @@ pub async fn sync_policy_and_fetch_snapshot( fetch_settings_snapshot_with_client(&mut client, sandbox_id).await } +/// Report an exact runtime configuration generation. Pending registration uses +/// the snapshot's instance fence; retain that snapshot across registration retries. +pub async fn report_sandbox_configuration( + endpoint: &str, + sandbox_id: &str, + instance_id: &str, + snapshot: Option<&SettingsPollResult>, + state: crate::proto::ConfigurationAdmissionState, + error: &str, +) -> Result<()> { + let mut client = connect(endpoint).await?; + client + .report_sandbox_configuration(crate::proto::ReportSandboxConfigurationRequest { + sandbox_id: sandbox_id.to_string(), + expected_instance_id: snapshot.map_or_else(String::new, |snapshot| { + snapshot.configuration_instance_id.clone() + }), + admission: Some(crate::proto::SandboxConfigurationAdmission { + instance_id: instance_id.to_string(), + state: state.into(), + policy_version: snapshot.map_or(0, |snapshot| snapshot.version), + policy_hash: snapshot + .map_or_else(String::new, |snapshot| snapshot.policy_hash.clone()), + config_revision: snapshot.map_or(0, |snapshot| snapshot.config_revision), + provider_env_revision: snapshot + .map_or(0, |snapshot| snapshot.provider_env_revision), + error: error.to_string(), + }), + }) + .await + .into_diagnostic()?; + Ok(()) +} + /// Fetch provider environment variables for a sandbox from `OpenShell` server via gRPC. /// /// Returns the credential snapshot and its exact readiness identity. An empty @@ -1218,6 +1252,9 @@ pub struct CachedOpenShellClient { /// Settings poll result returned by [`CachedOpenShellClient::poll_settings`]. #[derive(Clone, Debug)] pub struct SettingsPollResult { + pub configuration_instance_id: String, + pub configuration_admitted: bool, + pub configuration_error: String, pub policy: Option, pub version: u32, pub policy_hash: String, @@ -1241,6 +1278,9 @@ pub struct SettingsPollResult { fn settings_poll_result(inner: crate::proto::GetSandboxConfigResponse) -> SettingsPollResult { SettingsPollResult { + configuration_instance_id: inner.configuration_instance_id, + configuration_admitted: inner.configuration_admitted, + configuration_error: inner.configuration_error, policy: inner.policy, version: inner.version, policy_hash: inner.policy_hash, diff --git a/crates/openshell-core/src/provider_credentials.rs b/crates/openshell-core/src/provider_credentials.rs index beec21de37..f5456f4eaa 100644 --- a/crates/openshell-core/src/provider_credentials.rs +++ b/crates/openshell-core/src/provider_credentials.rs @@ -729,6 +729,62 @@ impl ProviderCredentialState { Ok(inner.current.child_env.len()) } + /// Install a validated candidate without repeating fallible compilation. + /// + /// Existing clones keep observing this live state. Preserve suppressed + /// environment keys and identity history so refresh cannot restore removed + /// keys or authorize old placeholders for a different provider. Callers + /// must serialize installs and revocations, as for bound-environment installs. + pub fn install_prepared(&self, prepared: &Self) -> usize { + // Release the candidate lock before taking the live lock, including + // when a caller passes another handle to the same state. + let (snapshot, generations, current_resolver, bindings, non_secret_keys) = { + let candidate = prepared + .inner + .read() + .expect("provider credential state poisoned"); + ( + (*candidate.current).clone(), + candidate.generations.clone(), + candidate.current_resolver.clone(), + candidate.static_credential_bindings.clone(), + candidate.non_secret_environment_keys.clone(), + ) + }; + let mut inner = self + .inner + .write() + .expect("provider credential state poisoned"); + let mut snapshot = snapshot; + for key in &inner.suppressed_keys { + snapshot.child_env.remove(key); + } + if static_credential_identities(&inner.static_credential_bindings) + != static_credential_identities(&bindings) + { + inner.generations.clear(); + } + inner.generations.extend(generations); + while inner.generations.len() > MAX_RETAINED_CREDENTIAL_GENERATIONS { + inner.generations.pop_front(); + } + inner.current_resolver = current_resolver; + inner.combined_resolver = + merge_resolvers(&inner.generations, inner.current_resolver.as_ref()); + inner + .known_static_credential_keys + .extend(bindings.keys().cloned()); + update_static_credential_identity_epochs( + &mut inner.static_credential_identity_epochs, + snapshot.revision, + &bindings, + ); + inner.static_credential_bindings = bindings; + inner.non_secret_environment_keys = non_secret_keys; + inner.current = Arc::new(snapshot); + inner.current.child_env.len() + } + /// Atomically remove static provider material after a failed refresh. /// /// Dynamic token grants retain their independently endpoint-bound state @@ -2369,6 +2425,67 @@ mod tests { ); } + #[test] + fn prepared_install_retains_placeholders_for_same_provider_identity() { + let make_state = |revision, secret: &str| { + ProviderCredentialState::from_bound_environment( + revision, + HashMap::from([("API_KEY".to_string(), secret.to_string())]), + HashMap::new(), + HashMap::new(), + HashMap::from([( + "API_KEY".to_string(), + binding("api.example.com", 443, "/**"), + )]), + Vec::new(), + ) + .unwrap() + }; + let live = make_state(1, "first-secret"); + let old_placeholder = live.snapshot().child_env["API_KEY"].clone(); + live.install_prepared(&make_state(2, "second-secret")); + let resolver = live + .resolver_for_endpoint("api.example.com", 443, "/") + .unwrap(); + assert_eq!( + resolver.resolve_placeholder(&old_placeholder), + Some("first-secret") + ); + assert_eq!( + resolver.resolve_placeholder(&live.snapshot().child_env["API_KEY"]), + Some("second-secret"), + ); + } + + #[test] + fn prepared_install_preserves_suppression_and_rejects_replaced_identity() { + let make_state = |revision, identity: &str, secret: &str| { + let mut binding = binding("api.example.com", 443, "/**"); + binding.credential_identity = identity.to_string(); + ProviderCredentialState::from_bound_environment( + revision, + HashMap::from([("API_KEY".to_string(), secret.to_string())]), + HashMap::new(), + HashMap::new(), + HashMap::from([("API_KEY".to_string(), binding)]), + Vec::new(), + ) + .unwrap() + }; + let live = make_state(1, "first:API_KEY", "first-secret"); + let observer = live.clone(); + let old_placeholder = live.snapshot().child_env["API_KEY"].clone(); + live.remove_env_key("API_KEY"); + let candidate = make_state(2, "second:API_KEY", "second-secret"); + live.install_prepared(&candidate); + assert_eq!(observer.snapshot().revision, 2); + assert!(!observer.snapshot().child_env.contains_key("API_KEY")); + let resolver = observer + .resolver_for_endpoint("api.example.com", 443, "/") + .unwrap(); + assert!(resolver.resolve_placeholder(&old_placeholder).is_none()); + } + #[test] fn suppressed_keys_survive_install_environment() { let state = ProviderCredentialState::from_environment( diff --git a/crates/openshell-sdk/tests/client_mock.rs b/crates/openshell-sdk/tests/client_mock.rs index f2fc52b5ff..34678e23e1 100644 --- a/crates/openshell-sdk/tests/client_mock.rs +++ b/crates/openshell-sdk/tests/client_mock.rs @@ -701,6 +701,13 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("unused")) } + async fn report_sandbox_configuration( + &self, + _: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + async fn report_policy_status( &self, _: tonic::Request, diff --git a/crates/openshell-server/src/auth/method_authz.rs b/crates/openshell-server/src/auth/method_authz.rs index 35ff82e925..92d3b0a56a 100644 --- a/crates/openshell-server/src/auth/method_authz.rs +++ b/crates/openshell-server/src/auth/method_authz.rs @@ -128,6 +128,9 @@ mod tests { assert!(!is_user_callable( "/openshell.v1.OpenShell/ReportPolicyStatus" )); + assert!(!is_user_callable( + "/openshell.v1.OpenShell/ReportSandboxConfiguration" + )); assert!(!is_user_callable("/openshell.v1.OpenShell/PushSandboxLogs")); assert!(!is_user_callable( "/openshell.v1.OpenShell/GetSandboxProviderEnvironment" diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 4cca02d6ac..56377127b9 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -1533,6 +1533,17 @@ impl ComputeRuntime { // Retain the previous instance id as a tombstone until // the restarted supervisor registers its new id. status.exit_code = None; + if phase == SandboxPhase::Starting { + status.configuration_admission = + Some(openshell_core::proto::SandboxConfigurationAdmission { + // Fence delayed registrations from the previous runtime. + instance_id: uuid::Uuid::new_v4().to_string(), + state: + openshell_core::proto::ConfigurationAdmissionState::Pending + .into(), + ..Default::default() + }); + } } upsert_ready_condition( &mut sandbox.status, @@ -3277,6 +3288,7 @@ impl ComputeRuntime { ensure_supervisor_not_ready_status(&mut sandbox.status, &sandbox_name); sandbox.set_phase(SandboxPhase::Provisioning as i32); } + apply_configuration_readiness(sandbox); }, ) .await; @@ -4525,6 +4537,7 @@ fn public_status_from_driver( main_process_instance_id: String::new(), exit_code: None, endpoint_statuses: Vec::new(), + configuration_admission: None, } } @@ -4615,6 +4628,21 @@ fn apply_driver_snapshot( SandboxPhase::Stopped if driver_snapshot_confirms_starting(incoming) => phase, SandboxPhase::Stopped => SandboxPhase::Stopped, SandboxPhase::Completed => SandboxPhase::Completed, + SandboxPhase::Starting + if phase != SandboxPhase::Error + && sandbox + .status + .as_ref() + .and_then(|status| status.configuration_admission.as_ref()) + .is_some_and(|admission| { + admission.state + != i32::from( + openshell_core::proto::ConfigurationAdmissionState::Accepted, + ) + }) => + { + SandboxPhase::Starting + } SandboxPhase::Starting if !matches!(phase, SandboxPhase::Ready | SandboxPhase::Error) => { SandboxPhase::Starting } @@ -4636,6 +4664,9 @@ fn apply_driver_snapshot( .main_process_instance_id .clone_from(¤t_status.main_process_instance_id); status.exit_code = current_status.exit_code; + status + .configuration_admission + .clone_from(¤t_status.configuration_admission); } if old_phase != phase { info!( @@ -4672,6 +4703,71 @@ fn apply_driver_snapshot( sandbox.status = status; sandbox.set_phase(phase as i32); sandbox.set_current_policy_version(cpv); + apply_configuration_readiness(sandbox); +} + +/// Configuration readiness is independent of compute/container readiness. +pub fn apply_configuration_readiness(sandbox: &mut Sandbox) { + use openshell_core::proto::ConfigurationAdmissionState; + let Some(status) = sandbox.status.as_mut() else { + return; + }; + let Some(admission) = status.configuration_admission.as_ref() else { + return; + }; + let accepted = admission.state == i32::from(ConfigurationAdmissionState::Accepted); + let reason = if accepted { + "ConfigurationAccepted" + } else if admission.state == i32::from(ConfigurationAdmissionState::Rejected) { + "ConfigurationInvalid" + } else { + "ConfigurationPending" + }; + let desired_error = admission.error.clone(); + let message = if accepted { + String::new() + } else if admission.error.is_empty() { + "Waiting for effective configuration validation before workload activation".to_string() + } else { + admission.error.clone() + }; + status.conditions.retain(|condition| { + condition.r#type != "ConfigurationReady" && condition.r#type != "DesiredConfigurationReady" + }); + status.conditions.push(SandboxCondition { + r#type: "ConfigurationReady".to_string(), + status: if accepted { "True" } else { "False" }.to_string(), + reason: reason.to_string(), + message: message.clone(), + ..Default::default() + }); + if accepted && !desired_error.is_empty() { + status.conditions.push(SandboxCondition { + r#type: "DesiredConfigurationReady".to_string(), + status: "False".to_string(), + reason: "ConfigurationInvalid".to_string(), + message: desired_error, + ..Default::default() + }); + } + if !accepted + && matches!( + SandboxPhase::try_from(status.phase), + Ok(SandboxPhase::Ready | SandboxPhase::Provisioning) + ) + { + status.phase = SandboxPhase::Provisioning as i32; + status + .conditions + .retain(|condition| condition.r#type != "Ready"); + status.conditions.push(SandboxCondition { + r#type: "Ready".to_string(), + status: "False".to_string(), + reason: reason.to_string(), + message, + ..Default::default() + }); + } } fn driver_snapshot_confirms_stopped(incoming: &DriverSandbox) -> bool { @@ -5241,6 +5337,82 @@ mod tests { use tokio::sync::{Notify, Semaphore, mpsc, oneshot}; use tokio_stream::wrappers::UnboundedReceiverStream; + #[test] + fn configuration_admission_survives_driver_ready_observations() { + use openshell_core::proto::{ + ConfigurationAdmissionState as Admission, SandboxConfigurationAdmission, + }; + let mut sandbox = Sandbox::default(); + sandbox.set_phase(SandboxPhase::Provisioning as i32); + sandbox.status.as_mut().unwrap().configuration_admission = + Some(SandboxConfigurationAdmission { + instance_id: "instance".to_string(), + state: Admission::Rejected.into(), + error: "Invalid credentialed endpoint in rule image".to_string(), + ..Default::default() + }); + let incoming = ready_driver_sandbox("sandbox", "sandbox"); + apply_driver_snapshot(&mut sandbox, &incoming, true, true); + assert_eq!(sandbox.phase(), SandboxPhase::Provisioning as i32); + assert!( + sandbox + .status + .as_ref() + .unwrap() + .conditions + .iter() + .any(|condition| condition.reason == "ConfigurationInvalid" + && condition.status == "False") + ); + sandbox + .status + .as_mut() + .unwrap() + .configuration_admission + .as_mut() + .unwrap() + .state = Admission::Accepted.into(); + apply_driver_snapshot(&mut sandbox, &incoming, true, true); + assert_eq!(sandbox.phase(), SandboxPhase::Ready as i32); + assert!( + sandbox + .status + .as_ref() + .unwrap() + .conditions + .iter() + .any(|condition| condition.reason == "ConfigurationAccepted" + && condition.status == "True") + ); + } + + #[test] + fn configuration_admission_preserves_starting_for_early_exit_reports() { + use openshell_core::proto::{ + ConfigurationAdmissionState as Admission, SandboxConfigurationAdmission, + }; + let mut sandbox = Sandbox::default(); + sandbox.set_phase(SandboxPhase::Starting as i32); + let status = sandbox.status.as_mut().unwrap(); + status.main_process_instance_id = "previous-instance".to_string(); + status.configuration_admission = Some(SandboxConfigurationAdmission { + state: Admission::Pending.into(), + ..Default::default() + }); + apply_configuration_readiness(&mut sandbox); + apply_driver_snapshot( + &mut sandbox, + &ready_driver_sandbox("sandbox", "sandbox"), + false, + true, + ); + assert_eq!(sandbox.phase(), SandboxPhase::Starting as i32); + assert_eq!( + sandbox.status.as_ref().unwrap().main_process_instance_id, + "previous-instance" + ); + } + fn string_value(value: &str) -> prost_types::Value { prost_types::Value { kind: Some(prost_types::value::Kind::StringValue(value.to_string())), @@ -7494,6 +7666,39 @@ mod tests { driver.set_get_outcome(ControlledGetOutcome::Sandbox(Box::new( ready_driver_sandbox(sandbox.object_id(), sandbox.object_name()), ))); + runtime + .apply_sandbox_update(ready_driver_sandbox( + sandbox.object_id(), + sandbox.object_name(), + )) + .await + .unwrap(); + let blocked = runtime + .store + .get_message::(sandbox.object_id()) + .await + .unwrap() + .unwrap(); + assert_eq!( + blocked.phase(), + SandboxPhase::Starting as i32, + "driver/session readiness cannot bypass restart configuration admission" + ); + // Simulate the supervisor's successful exact-generation admission report. + runtime + .store + .update_message_cas::(sandbox.object_id(), 0, |sandbox| { + sandbox + .status + .as_mut() + .unwrap() + .configuration_admission + .as_mut() + .unwrap() + .state = openshell_core::proto::ConfigurationAdmissionState::Accepted.into(); + }) + .await + .unwrap(); runtime .apply_sandbox_update(ready_driver_sandbox( sandbox.object_id(), diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index ac87ebcddf..1381faa583 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -633,6 +633,13 @@ impl OpenShell for OpenShellService { policy::handle_report_endpoint_status(&self.state, request).await } + async fn report_sandbox_configuration( + &self, + request: Request, + ) -> Result, Status> { + policy::handle_report_sandbox_configuration(&self.state, request).await + } + // --- Sandbox logs --- async fn get_sandbox_logs( diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 8434377c1d..d933a9425c 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -1755,20 +1755,32 @@ async fn current_effective_policy_for_sandbox( .as_ref() .map(|spec| spec.providers.clone()) .unwrap_or_default(); + let records = super::provider::load_provider_environment_records( + state.store.as_ref(), + workspace, + &provider_names, + ) + .await?; + current_effective_policy_from_records(state, catalog, sandbox, sandbox_id, &records).await +} + +async fn current_effective_policy_from_records( + state: &ServerState, + catalog: &EffectiveProviderProfileCatalog, + sandbox: &Sandbox, + sandbox_id: &str, + records: &[super::provider::ProviderEnvironmentRecord], +) -> Result { let global_settings = load_global_settings(state.store.as_ref()).await?; if let Some(global_policy) = decode_policy_from_global_settings(&global_settings)? { // A global policy is the complete effective policy. Dormant sandbox // history and specs may predate the current schema, but they must not // prevent the valid global policy from being served. - return apply_effective_policy_context( - state, - catalog, - workspace, - &provider_names, + return apply_captured_policy_context( + provider_policy_context_from_records(catalog, records), global_policy, PolicySource::Global, - ) - .await; + ); } let policy = if let Some(record) = state @@ -1787,15 +1799,11 @@ async fn current_effective_policy_for_sandbox( } }; - apply_effective_policy_context( - state, - catalog, - workspace, - &provider_names, + apply_captured_policy_context( + provider_policy_context_from_records(catalog, records), policy, PolicySource::Sandbox, ) - .await } async fn effective_policy_for_source( @@ -1830,17 +1838,25 @@ async fn apply_effective_policy_context( catalog: &EffectiveProviderProfileCatalog, workspace: &str, provider_names: &[String], - mut policy: ProtoSandboxPolicy, + policy: ProtoSandboxPolicy, policy_source: PolicySource, ) -> Result { - clear_provider_credentialed_markers(&mut policy); - let mut provider_context = provider_policy_context_with_catalog( + let provider_context = provider_policy_context_with_catalog( state.store.as_ref(), catalog, workspace, provider_names, ) .await?; + apply_captured_policy_context(provider_context, policy, policy_source) +} + +fn apply_captured_policy_context( + mut provider_context: ProviderPolicyContext, + mut policy: ProtoSandboxPolicy, + policy_source: PolicySource, +) -> Result { + clear_provider_credentialed_markers(&mut policy); if !matches!(policy_source, PolicySource::Global) && !provider_context.layers.is_empty() { policy = compose_effective_policy(&policy, &provider_context.layers); } @@ -2455,9 +2471,17 @@ async fn persist_existing_policy_projection( let updated = state .store .update_message_cas::(sandbox_id, expected_resource_version, |sandbox| { + let startup_blocked = sandbox + .status + .as_ref() + .and_then(|status| status.configuration_admission.as_ref()) + .is_some_and(|admission| { + admission.state + != i32::from(openshell_core::proto::ConfigurationAdmissionState::Accepted) + }); if let Some(policy) = backfill_policy.as_ref() && let Some(spec) = sandbox.spec.as_mut() - && spec.policy.is_none() + && (spec.policy.is_none() || startup_blocked) { spec.policy = Some(policy.clone()); } @@ -2514,6 +2538,42 @@ async fn resolve_sandbox_by_name_for_principal( pub(super) async fn handle_get_sandbox_config( state: &Arc, request: Request, +) -> Result, Status> { + let principal = super::extract_principal(&request)?; + let sandbox_id = request.get_ref().sandbox_id.clone(); + let result = handle_get_sandbox_config_inner(state, request).await; + match result { + Err(error) + if matches!(principal, Principal::Sandbox(_)) + && matches!( + error.code(), + tonic::Code::FailedPrecondition | tonic::Code::InvalidArgument + ) => + { + // A malformed stored candidate must not prevent a supervisor + // from registering its startup fence and waiting for repair. + // Do not expose parser payloads or copy malformed policy history. + let sandbox = + super::sandbox::fetch_and_authorize_sandbox(state, &principal, &sandbox_id).await?; + Ok(Response::new(GetSandboxConfigResponse { + configuration_admitted: false, + configuration_error: configuration_failure_diagnostic(&error).to_string(), + configuration_instance_id: sandbox + .status + .as_ref() + .and_then(|status| status.configuration_admission.as_ref()) + .map_or_else(String::new, |admission| admission.instance_id.clone()), + workspace: sandbox.object_workspace().to_string(), + ..Default::default() + })) + } + result => result, + } +} + +async fn handle_get_sandbox_config_inner( + state: &Arc, + request: Request, ) -> Result, Status> { let principal = super::extract_principal(&request)?; let sandbox_id = request.get_ref().sandbox_id.clone(); @@ -2641,13 +2701,14 @@ pub(super) async fn load_sandbox_config( let global_settings = load_global_settings(state.store.as_ref()).await?; let sandbox_settings = load_sandbox_settings(state.store.as_ref(), &workspace, sandbox.object_name()).await?; - let mut provider_policy_context = provider_policy_context_with_catalog( + let provider_records = super::provider::load_provider_environment_records( state.store.as_ref(), - &provider_profile_catalog, &workspace, &sandbox_provider_names, ) .await?; + let mut provider_policy_context = + provider_policy_context_from_records(&provider_profile_catalog, &provider_records); if matches!(policy_source, PolicySource::Global) && let Ok(Some(global_rev)) = state @@ -2693,12 +2754,15 @@ pub(super) async fn load_sandbox_config( &policy_credential_bindings, &provider_policy_context.endpointless_provider_names, ); + let mut configuration_error = String::new(); if let Some(effective_policy) = policy.as_mut() { stamp_provider_credentialed_endpoints( effective_policy, &provider_policy_context.credentialed_scopes, ); - report_uninspected_credentialed_endpoints(effective_policy, &sandbox_id); + if let Err(error) = validate_uninspected_credentialed_endpoints(effective_policy) { + configuration_error = bounded_configuration_diagnostic(error.message()); + } policy_hash = deterministic_policy_hash(effective_policy); } @@ -2725,25 +2789,27 @@ pub(super) async fn load_sandbox_config( state.sandbox_jwt_issuer.is_some(), ); if let Some(policy) = policy.as_ref() { - validate_policy_credential_bindings_for_sandbox( - state.as_ref(), + validate_policy_credential_binding_context( &provider_profile_catalog, - &workspace, - &sandbox_provider_names, + &provider_records, policy, - ) - .await?; + &policy_credential_bindings, + )?; } - let provider_env_revision = compute_provider_env_revision_with_catalog_and_policy_bindings( - state.store.as_ref(), + let provider_env_revision = compute_provider_env_revision_from_records_and_policy_bindings( &provider_profile_catalog, - &workspace, - &sandbox_provider_names, + &provider_records, &policy_credential_bindings, - ) - .await?; + )?; Ok(GetSandboxConfigResponse { + configuration_instance_id: sandbox + .status + .as_ref() + .and_then(|status| status.configuration_admission.as_ref()) + .map_or_else(String::new, |admission| admission.instance_id.clone()), + configuration_admitted: policy.is_some() && configuration_error.is_empty(), + configuration_error, policy, version, policy_hash, @@ -2797,6 +2863,7 @@ pub(super) async fn compute_provider_env_revision_with_catalog( .await } +#[cfg(test)] async fn compute_provider_env_revision_with_catalog_and_policy_bindings( store: &Store, catalog: &EffectiveProviderProfileCatalog, @@ -3029,16 +3096,23 @@ async fn provider_policy_context_with_catalog( workspace: &str, provider_names: &[String], ) -> Result { + let records = + super::provider::load_provider_environment_records(store, workspace, provider_names) + .await?; + Ok(provider_policy_context_from_records(catalog, &records)) +} + +fn provider_policy_context_from_records( + catalog: &EffectiveProviderProfileCatalog, + records: &[super::provider::ProviderEnvironmentRecord], +) -> ProviderPolicyContext { let mut layers = Vec::new(); let mut credentialed_scopes = Vec::new(); let mut endpointless_provider_names = HashSet::new(); - for name in provider_names { - let provider = store - .get_message_by_name::(workspace, name) - .await - .map_err(|e| Status::internal(format!("failed to fetch provider '{name}': {e}")))? - .ok_or_else(|| Status::failed_precondition(format!("provider '{name}' not found")))?; + for record in records { + let name = &record.name; + let provider = &record.provider; let provider_type = provider.r#type.trim(); let Some(profile) = super::provider::get_provider_type_profile_for_scope( @@ -3054,7 +3128,7 @@ async fn provider_policy_context_with_catalog( continue; }; - if !super::provider::provider_profile_endpoints_are_active(&profile, &provider) { + if !super::provider::provider_profile_endpoints_are_active(&profile, provider) { endpointless_provider_names.insert(name.clone()); continue; } @@ -3083,11 +3157,11 @@ async fn provider_policy_context_with_catalog( }); } - Ok(ProviderPolicyContext { + ProviderPolicyContext { layers, credentialed_scopes, endpointless_provider_names, - }) + } } fn endpoint_ports(endpoint: &NetworkEndpoint) -> Vec { @@ -3240,22 +3314,6 @@ fn validate_uninspected_credentialed_endpoints(policy: &ProtoSandboxPolicy) -> R ))) } -/// Delivery-path reporting for an already-persisted policy. Sandbox config -/// delivery must not fail closed here: refusing the config would crash-loop a -/// running supervisor. The runtime backstop denies the traffic instead. -fn report_uninspected_credentialed_endpoints(policy: &ProtoSandboxPolicy, sandbox_id: &str) { - if let Some(violation) = find_uninspected_credentialed_endpoint(policy) { - warn!( - sandbox_id, - rule_name = %violation.rule_name, - host = %violation.host, - port = violation.port, - mode = violation.mode, - "delivering credentialed endpoint without L7 inspection; the sandbox proxy will deny this traffic unless allow_uninspected_credentials is set" - ); - } -} - pub(super) async fn handle_get_gateway_config( state: &Arc, _request: Request, @@ -3315,12 +3373,12 @@ pub(super) async fn load_sandbox_provider_environment( &provider_names, ) .await?; - let effective_policy = current_effective_policy_for_sandbox( + let effective_policy = current_effective_policy_from_records( state.as_ref(), &provider_profile_catalog, - &workspace, sandbox, &sandbox_id, + &provider_records, ) .await?; let policy_credential_bindings = @@ -3946,7 +4004,19 @@ async fn handle_update_config_inner( validate_no_reserved_provider_policy_keys(&new_policy)?; } - let should_backfill_policy = if let Some(baseline_policy) = spec.policy.as_ref() { + let startup_blocked = sandbox + .status + .as_ref() + .and_then(|status| status.configuration_admission.as_ref()) + .is_some_and(|admission| { + admission.state + != i32::from(openshell_core::proto::ConfigurationAdmissionState::Accepted) + }); + let should_backfill_policy = if startup_blocked && !sandbox_caller { + // No child has consumed static restrictions yet. A complete replacement + // must be able to repair every field before the first activation. + true + } else if let Some(baseline_policy) = spec.policy.as_ref() { let comparable_baseline = baseline_policy.clone(); validate_static_fields_unchanged(&comparable_baseline, &new_policy)?; false @@ -3978,9 +4048,9 @@ async fn handle_update_config_inner( &effective_policy, ) .await?; - // Sandbox-authored syncs replay a policy the supervisor already discovered - // on disk. Rejecting it here would crash-loop the sandbox instead of - // surfacing an operator decision, so only operator-authored updates gate. + // Image discovery persists the desired candidate for management repair. + // It never admits workload activation: GetSandboxConfig applies the complete + // composition gate and ReportSandboxConfiguration checks the exact result. if !sandbox_caller { validate_candidate_sandbox_credential_policy( state, @@ -4310,6 +4380,153 @@ pub(super) async fn handle_list_sandbox_policies( })) } +fn bounded_configuration_diagnostic(message: &str) -> String { + message + .chars() + .filter(|character| !character.is_control()) + .take(512) + .collect() +} + +fn configuration_failure_diagnostic(error: &Status) -> &'static str { + let message = error.message(); + if message.contains("middleware") { + "Effective middleware configuration is invalid; repair the policy middleware bindings or registered services" + } else if message.contains("credential") || message.contains("provider") { + "Effective provider configuration is invalid; repair credential bindings, attached providers, or their policy layers" + } else { + "Stored policy structure or safety validation failed; submit a complete valid replacement policy" + } +} + +fn configuration_generation_matches( + admission: &openshell_core::proto::SandboxConfigurationAdmission, + config: &GetSandboxConfigResponse, +) -> bool { + ( + admission.policy_version, + &admission.policy_hash, + admission.config_revision, + admission.provider_env_revision, + ) == ( + config.version, + &config.policy_hash, + config.config_revision, + config.provider_env_revision, + ) +} + +pub(super) async fn handle_report_sandbox_configuration( + state: &Arc, + request: Request, +) -> Result, Status> { + use openshell_core::proto::ConfigurationAdmissionState; + let principal = super::extract_principal(&request)?; + let sandbox_id = request.get_ref().sandbox_id.clone(); + crate::auth::guard::enforce_sandbox_scope(&request, &sandbox_id)?; + let mut admission = request + .get_ref() + .admission + .clone() + .ok_or_else(|| Status::invalid_argument("admission is required"))?; + if uuid::Uuid::parse_str(&admission.instance_id).is_err() { + return Err(Status::invalid_argument("instance_id must be a UUID")); + } + let reported = ConfigurationAdmissionState::try_from(admission.state) + .map_err(|_| Status::invalid_argument("invalid admission state"))?; + if reported == ConfigurationAdmissionState::Unspecified { + return Err(Status::invalid_argument("admission state is required")); + } + let sandbox = + super::sandbox::fetch_and_authorize_sandbox(state, &principal, &sandbox_id).await?; + let current = sandbox + .status + .as_ref() + .and_then(|status| status.configuration_admission.as_ref()); + if reported == ConfigurationAdmissionState::Pending + && current.is_some_and(|current| current.instance_id != admission.instance_id) + && current.map_or("", |current| current.instance_id.as_str()) + != request.get_ref().expected_instance_id + { + return Err(Status::aborted("supervisor registration fence has changed")); + } + if reported != ConfigurationAdmissionState::Pending + && current.is_none_or(|current| current.instance_id != admission.instance_id) + { + return Err(Status::aborted( + "supervisor configuration instance has changed", + )); + } + if reported == ConfigurationAdmissionState::Accepted { + let mut config_request = Request::new(GetSandboxConfigRequest { + sandbox_id: sandbox_id.clone(), + }); + *config_request.extensions_mut() = request.extensions().clone(); + let config = handle_get_sandbox_config(state, config_request) + .await? + .into_inner(); + if !config.configuration_admitted || !configuration_generation_matches(&admission, &config) + { + return Err(Status::aborted( + "configuration changed or is not admitted; fetch and validate again", + )); + } + admission.error.clear(); + } else if reported == ConfigurationAdmissionState::Rejected { + // Runtime error strings may contain parser payloads. Only gateway-authored + // diagnostics may be exposed verbatim through public sandbox status. + let mut config_request = Request::new(GetSandboxConfigRequest { + sandbox_id: sandbox_id.clone(), + }); + *config_request.extensions_mut() = request.extensions().clone(); + admission.error = match handle_get_sandbox_config(state, config_request).await { + Ok(config) if !configuration_generation_matches(&admission, config.get_ref()) => { + return Err(Status::aborted("rejected configuration generation has changed")); + } + Ok(config) if !config.get_ref().configuration_error.is_empty() => config.into_inner().configuration_error, + _ => "Effective configuration could not be activated; replace the policy or repair attached providers".to_string(), + }; + if let Some(current) = current + && current.state == i32::from(ConfigurationAdmissionState::Accepted) + { + // A rejected desired update does not invalidate an accepted runtime. + let error = admission.error; + admission = current.clone(); + admission.error = error; + } + } else { + admission.error.clear(); + if let Some(current) = current + && current.instance_id == admission.instance_id + { + admission = current.clone(); + } + } + let expected_version = sandbox + .metadata + .as_ref() + .map_or(0, |metadata| metadata.resource_version); + let _guard = state.compute.sandbox_sync_guard().await; + let updated = state + .store + .update_message_cas::(&sandbox_id, expected_version, |sandbox| { + sandbox + .status + .get_or_insert_with(Default::default) + .configuration_admission = Some(admission.clone()); + crate::compute::apply_configuration_readiness(sandbox); + }) + .await + .map_err(|error| { + super::persistence_error_to_status(error, "report configuration admission") + })?; + state.sandbox_index.update_from_sandbox(&updated); + state.sandbox_watch_bus.notify(&sandbox_id); + Ok(Response::new( + openshell_core::proto::ReportSandboxConfigurationResponse {}, + )) +} + pub(super) async fn handle_report_policy_status( state: &Arc, request: Request, @@ -7554,6 +7771,229 @@ mod tests { request } + #[tokio::test] + async fn configuration_admission_rejects_stale_generation_and_instance() { + use openshell_core::proto::{ + ConfigurationAdmissionState as Admission, ReportSandboxConfigurationRequest, + SandboxConfigurationAdmission, + }; + let state = test_server_state().await; + let sandbox_id = "sb-admission"; + state + .store + .put_message(&test_sandbox( + sandbox_id, + "admission", + openshell_policy::restrictive_default_policy(), + Vec::new(), + )) + .await + .unwrap(); + let instance_id = uuid::Uuid::new_v4().to_string(); + let report = |admission| { + with_sandbox( + Request::new(ReportSandboxConfigurationRequest { + sandbox_id: sandbox_id.to_string(), + admission: Some(admission), + expected_instance_id: String::new(), + }), + sandbox_id, + ) + }; + handle_report_sandbox_configuration( + &state, + report(SandboxConfigurationAdmission { + instance_id: instance_id.clone(), + state: Admission::Pending.into(), + ..Default::default() + }), + ) + .await + .unwrap(); + let config = handle_get_sandbox_config( + &state, + with_sandbox( + Request::new(GetSandboxConfigRequest { + sandbox_id: sandbox_id.to_string(), + }), + sandbox_id, + ), + ) + .await + .unwrap() + .into_inner(); + assert!(config.configuration_admitted); + let accepted = SandboxConfigurationAdmission { + instance_id: instance_id.clone(), + state: Admission::Accepted.into(), + policy_version: config.version, + policy_hash: config.policy_hash, + config_revision: config.config_revision, + provider_env_revision: config.provider_env_revision, + error: String::new(), + }; + let mut outdated_admission = accepted.clone(); + outdated_admission.provider_env_revision = + outdated_admission.provider_env_revision.wrapping_add(1); + assert_eq!( + handle_report_sandbox_configuration(&state, report(outdated_admission)) + .await + .unwrap_err() + .code(), + Code::Aborted + ); + handle_report_sandbox_configuration(&state, report(accepted.clone())) + .await + .unwrap(); + let mut stale_rejection = accepted.clone(); + stale_rejection.state = Admission::Rejected.into(); + stale_rejection.policy_version += 1; + assert_eq!( + handle_report_sandbox_configuration(&state, report(stale_rejection)) + .await + .unwrap_err() + .code(), + Code::Aborted, + "a delayed rejection must not mark the accepted current generation invalid" + ); + let mut restart = report(SandboxConfigurationAdmission { + instance_id: uuid::Uuid::new_v4().to_string(), + state: Admission::Pending.into(), + ..Default::default() + }); + restart.get_mut().expected_instance_id = instance_id.clone(); + handle_report_sandbox_configuration(&state, restart) + .await + .unwrap(); + assert_eq!( + handle_report_sandbox_configuration( + &state, + report(SandboxConfigurationAdmission { + instance_id, + state: Admission::Pending.into(), + ..Default::default() + }) + ) + .await + .unwrap_err() + .code(), + Code::Aborted, + "delayed old Pending must not reclaim registration" + ); + assert_eq!( + handle_report_sandbox_configuration(&state, report(accepted)) + .await + .unwrap_err() + .code(), + Code::Aborted + ); + } + + #[test] + fn configuration_diagnostic_is_bounded_and_removes_control_characters() { + let diagnostic = bounded_configuration_diagnostic(&format!("rule\n{}", "é".repeat(1000))); + assert_eq!(diagnostic.chars().count(), 512); + assert!(!diagnostic.contains('\n')); + } + + #[tokio::test] + async fn configuration_admission_retains_invalid_image_composition_for_repair() { + use openshell_core::proto::{ + ConfigurationAdmissionState as Admission, ReportSandboxConfigurationRequest, + SandboxConfigurationAdmission, + }; + let state = test_server_state().await; + let sandbox_id = "sb-image-admission"; + let mut sandbox = test_sandbox( + sandbox_id, + "image-admission", + ProtoSandboxPolicy::default(), + vec!["work-github".to_string()], + ); + sandbox.spec.as_mut().unwrap().policy = None; + state + .store + .put_message(&test_provider("work-github", "github")) + .await + .unwrap(); + state.store.put_message(&sandbox).await.unwrap(); + let instance_id = uuid::Uuid::new_v4().to_string(); + handle_report_sandbox_configuration( + &state, + with_sandbox( + Request::new(ReportSandboxConfigurationRequest { + sandbox_id: sandbox_id.to_string(), + expected_instance_id: String::new(), + admission: Some(SandboxConfigurationAdmission { + instance_id, + state: Admission::Pending.into(), + ..Default::default() + }), + }), + sandbox_id, + ), + ) + .await + .unwrap(); + let image = test_policy_with_rule("image_github", "api.github.com"); + handle_update_config( + &state, + with_sandbox( + Request::new(UpdateConfigRequest { + name: "image-admission".to_string(), + policy: Some(image), + ..Default::default() + }), + sandbox_id, + ), + ) + .await + .expect("image candidate remains available for repair"); + let rejected = handle_get_sandbox_config( + &state, + with_sandbox( + Request::new(GetSandboxConfigRequest { + sandbox_id: sandbox_id.to_string(), + }), + sandbox_id, + ), + ) + .await + .unwrap() + .into_inner(); + assert!(!rejected.configuration_admitted); + assert!(rejected.configuration_error.contains("image_github")); + assert!(!rejected.configuration_error.contains("ghp-test")); + assert!(rejected.policy.is_some()); + handle_update_config( + &state, + with_user(Request::new(UpdateConfigRequest { + name: "image-admission".to_string(), + policy: Some(openshell_policy::restrictive_default_policy()), + ..Default::default() + })), + ) + .await + .expect("operator can replace static sections before first launch"); + let repaired = handle_get_sandbox_config( + &state, + with_sandbox( + Request::new(GetSandboxConfigRequest { + sandbox_id: sandbox_id.to_string(), + }), + sandbox_id, + ), + ) + .await + .unwrap() + .into_inner(); + assert!( + repaired.configuration_admitted, + "{}", + repaired.configuration_error + ); + } + fn security_notes_for_host(host: &str) -> String { generate_security_notes(&NetworkPolicyRule { endpoints: vec![NetworkEndpoint { @@ -7785,7 +8225,7 @@ mod tests { .await .expect("store legacy sandbox spec"); - let error = handle_get_sandbox_config( + let error = handle_get_sandbox_config_inner( &state, with_sandbox( Request::new(GetSandboxConfigRequest { @@ -8032,7 +8472,7 @@ mod tests { .await .expect("store legacy invalid history"); - let error = handle_get_sandbox_config( + let rejected = handle_get_sandbox_config( &state, with_sandbox( Request::new(GetSandboxConfigRequest { @@ -8042,10 +8482,12 @@ mod tests { ), ) .await - .expect_err("invalid latest history must fail closed"); + .expect("invalid latest history must remain repairable") + .into_inner(); - assert_eq!(error.code(), Code::FailedPrecondition); - assert!(error.message().contains(STORED_POLICY_SOURCE_HISTORY)); + assert!(!rejected.configuration_admitted); + assert!(rejected.policy.is_none()); + assert!(!rejected.configuration_error.is_empty()); let record = state .store diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index e72f5631e4..777d8000cd 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -490,6 +490,14 @@ async fn handle_create_sandbox_inner( created_from_workload_template, }; sandbox.set_phase(SandboxPhase::Provisioning as i32); + sandbox + .status + .get_or_insert_with(Default::default) + .configuration_admission = Some(openshell_core::proto::SandboxConfigurationAdmission { + state: openshell_core::proto::ConfigurationAdmissionState::Pending.into(), + ..Default::default() + }); + crate::compute::apply_configuration_readiness(&mut sandbox); // Ensure metadata is valid (defense in depth - should always be true for server-constructed metadata) super::validation::validate_object_metadata(sandbox.metadata.as_ref(), "sandbox")?; diff --git a/crates/openshell-server/src/policy_store.rs b/crates/openshell-server/src/policy_store.rs index 464957b44f..dccde69ee7 100644 --- a/crates/openshell-server/src/policy_store.rs +++ b/crates/openshell-server/src/policy_store.rs @@ -34,6 +34,8 @@ pub struct AtomicPolicyRevisionWrite { pub provenance: HashMap, pub expected_resource_version: u64, pub annotations: HashMap, + /// Populate the create-time baseline, or replace it while startup admission + /// is blocked and no workload has consumed the static restrictions. pub backfill_policy: Option, } @@ -74,6 +76,14 @@ pub fn project_policy_revision_onto_sandbox( sandbox.set_resource_version(current_resource_version); let mut changed = false; + let startup_blocked = sandbox + .status + .as_ref() + .and_then(|status| status.configuration_admission.as_ref()) + .is_some_and(|admission| { + admission.state + != i32::from(openshell_core::proto::ConfigurationAdmissionState::Accepted) + }); if let Some(backfill_policy) = write.backfill_policy.as_ref() { let spec = sandbox .spec @@ -85,6 +95,10 @@ pub fn project_policy_revision_onto_sandbox( changed = true; } Some(current) if current == backfill_policy => {} + Some(_) if startup_blocked => { + spec.policy = Some(backfill_policy.clone()); + changed = true; + } Some(_) => { return Err(PersistenceError::Conflict { current_resource_version: Some(current_resource_version), diff --git a/crates/openshell-server/tests/common/mod.rs b/crates/openshell-server/tests/common/mod.rs index 5391a3ab4e..64e2762e2a 100644 --- a/crates/openshell-server/tests/common/mod.rs +++ b/crates/openshell-server/tests/common/mod.rs @@ -463,6 +463,13 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not implemented in test")) } + async fn report_sandbox_configuration( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + async fn report_policy_status( &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 f3ae883f78..31ab9cb1eb 100644 --- a/crates/openshell-server/tests/supervisor_relay_integration.rs +++ b/crates/openshell-server/tests/supervisor_relay_integration.rs @@ -434,6 +434,13 @@ impl OpenShell for RelayGateway { ) -> Result, Status> { Err(Status::unimplemented("unused")) } + async fn report_sandbox_configuration( + &self, + _: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + async fn report_policy_status( &self, _: tonic::Request, diff --git a/crates/openshell-supervisor-network/src/opa.rs b/crates/openshell-supervisor-network/src/opa.rs index 5a7056a79c..ba1f8d1a28 100644 --- a/crates/openshell-supervisor-network/src/opa.rs +++ b/crates/openshell-supervisor-network/src/opa.rs @@ -728,47 +728,39 @@ impl OpaEngine { proto: &ProtoSandboxPolicy, entrypoint_pid: u32, ) -> 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 - .engine - .into_inner() - .map_err(|_| miette::miette!("lock poisoned on new engine"))?; - let mut engine = self - .engine - .lock() - .map_err(|_| miette::miette!("OPA engine lock poisoned"))?; - *engine = new_engine; - *self - .fail_closed_reason - .write() - .map_err(|_| miette::miette!("OPA fail-closed state lock poisoned"))? = None; - 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) + self.reload_configuration_from_proto_with_pid(proto, entrypoint_pid, None, || {}) } /// Reload the policy and middleware registry as one runtime generation. - /// - /// Both replacements are prepared before the live locks are acquired. The - /// 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 { + self.reload_configuration_from_proto_with_pid(proto, entrypoint_pid, Some(registry), || {}) + } + + /// Validate a complete candidate before publishing policy, middleware, and + /// prepared credentials together. A validation or lock failure leaves the + /// active configuration untouched and never invokes `commit_credentials`. + /// + /// The callback must be infallible and must not call back into this engine. + /// Existing policy guards become stale before credentials change; new + /// policy readers remain blocked until the complete configuration is live. + pub fn reload_configuration_from_proto_with_pid( + &self, + proto: &ProtoSandboxPolicy, + entrypoint_pid: u32, + registry: Option, + commit_credentials: impl FnOnce(), ) -> Result { let new = Self::from_proto_with_pid(proto, entrypoint_pid)?; let new_engine = new .engine .into_inner() .map_err(|_| miette::miette!("lock poisoned on new engine"))?; - // Match clone_engine_for_tunnel's lock order (engine, then runner) so - // readers can observe only the old pair or the new pair. + // Match clone_engine_for_tunnel's lock order (engine, then runner). let mut engine = self .engine .lock() @@ -777,14 +769,18 @@ impl OpaEngine { .middleware_runner .write() .map_err(|_| miette::miette!("middleware runner lock poisoned"))?; - let new_runner = runner.with_replacement_registry(registry); - *engine = new_engine; - *runner = new_runner; - *self + let mut fail_closed_reason = self .fail_closed_reason .write() - .map_err(|_| miette::miette!("OPA fail-closed state lock poisoned"))? = None; + .map_err(|_| miette::miette!("OPA fail-closed state lock poisoned"))?; + let new_runner = registry.map(|registry| runner.with_replacement_registry(registry)); let generation = self.advance_generation(); + commit_credentials(); + *engine = new_engine; + if let Some(new_runner) = new_runner { + *runner = new_runner; + } + *fail_closed_reason = None; self.generation_guard(generation) } @@ -10592,6 +10588,44 @@ network_policies: assert!(described[0].is_resolved()); } + #[test] + fn rejected_configuration_never_commits_credentials() { + let mut proto = test_proto(); + let engine = OpaEngine::from_proto(&proto).unwrap(); + proto.network_middlewares.insert( + String::new(), + NetworkMiddlewareConfig { + middleware: openshell_supervisor_middleware_builtins::BUILTIN_REGEX.into(), + ..Default::default() + }, + ); + engine + .reload_configuration_from_proto_with_pid(&proto, 0, None, || { + panic!("invalid candidate must not publish credentials"); + }) + .expect_err("invalid candidate"); + assert_eq!(engine.current_generation(), 0); + } + + #[test] + fn configuration_commit_invalidates_old_guards_before_credentials_change() { + let proto = test_proto(); + let engine = OpaEngine::from_proto(&proto).unwrap(); + let old = engine.clone_engine_for_tunnel(0).unwrap(); + engine.enter_fail_closed("invalid candidate").unwrap(); + let mut committed = false; + engine + .reload_configuration_from_proto_with_pid(&proto, 0, None, || { + assert!(old.generation_guard().is_stale()); + assert_eq!(engine.current_generation(), 2); + committed = true; + }) + .unwrap(); + assert!(committed); + assert!(engine.fail_closed_reason().is_none()); + assert!(engine.clone_engine_for_tunnel(2).is_ok()); + } + #[tokio::test] async fn failed_combined_reload_preserves_policy_registry_and_generation() { let proto = test_proto(); diff --git a/docs/reference/gateway-config.mdx b/docs/reference/gateway-config.mdx index abced28b74..75599ae19f 100644 --- a/docs/reference/gateway-config.mdx +++ b/docs/reference/gateway-config.mdx @@ -266,7 +266,7 @@ The client-certificate handshake policy is derived and has no `require_client_au `[openshell.gateway.tls]` supports optional SNI-based dual-certificate mode for deployments that need separate internal and external server certificates. Set `external_cert_path` and `external_key_path` to point at the external (e.g. ACME/publicly-trusted) certificate and key. List the hostnames that should be served with the external certificate in `external_server_names`. Connections whose TLS SNI hostname matches one of those names receive the external certificate; all other connections (including those with no SNI) receive the primary internal certificate from `cert_path`/`key_path`. Both fields must be set together — providing only one is a configuration error. On Kubernetes with the Helm chart, the external certificate is managed automatically when `certManager.serverIssuerRef.name` is set; the chart populates these fields from the cert-manager-issued external server certificate. -`[openshell.gateway] policy_validation_failure_mode` controls what sandbox supervisors do when a complete candidate policy fails runtime validation. The default, `fail_closed`, deactivates the previous network policy, closes relays pinned to it, and denies new egress until a valid generation loads. `retain_last_valid` leaves the previous valid generation active. Both modes reject the candidate atomically; startup always fails closed when no previous valid generation exists. Gateway mutation paths that can preflight a known effective scope reject invalid candidates before persistence and leave the active policy unchanged regardless of this setting. Changing the value requires restarting the gateway so it can reload `gateway.toml` and distribute the new posture to sandbox supervisors. +`[openshell.gateway] policy_validation_failure_mode` controls what sandbox supervisors do when a complete candidate policy fails runtime validation. The default, `fail_closed`, deactivates the previous network policy, closes relays pinned to it, and denies new egress until a valid generation loads. `retain_last_valid` leaves the previous valid generation active. Both modes reject the candidate atomically; startup keeps the workload unstarted until the effective policy and matching provider configuration pass admission. A rejected startup exposes `ConfigurationInvalid` and remains available for policy/provider repair in either mode. Gateway mutation paths that can preflight a known effective scope reject invalid candidates before persistence and leave the active policy unchanged regardless of this setting. Changing the value requires restarting the gateway so it can reload `gateway.toml` and distribute the new posture to sandbox supervisors. `[openshell.gateway.gateway_jwt] ttl_secs` controls gateway-minted sandbox JWT lifetime. Omit it for a non-expiring token: the token has no `exp` claim and the response omits `expiration_time`. Use this only for local single-player Docker, Podman, or VM gateways. Explicit `0` is invalid. Kubernetes and other shared deployments should set a positive TTL; Helm renders `3600` seconds by default, and the gateway logs a warning when a Kubernetes gateway omits the field. diff --git a/docs/sandboxes/manage-sandboxes.mdx b/docs/sandboxes/manage-sandboxes.mdx index 86968a9910..c3e777f3a4 100644 --- a/docs/sandboxes/manage-sandboxes.mdx +++ b/docs/sandboxes/manage-sandboxes.mdx @@ -760,6 +760,14 @@ after it attempts every requested deletion if any entry failed. Every sandbox moves through a defined set of phases: +Before workload activation, OpenShell validates the effective policy and matching +provider configuration. A rejection keeps the workload unstarted and exposes a +`ConfigurationInvalid` condition in `Provisioning`. Use `openshell sandbox get` +to inspect the diagnostic, then [repair the policy or provider configuration](/sandboxes/policies#validation-failures). +Management operations remain available while startup is blocked. After repair, +the supervisor completes startup without recreating the sandbox. Starting a +stopped sandbox repeats configuration admission before launching its workload. + | Phase | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Provisioning | The runtime is setting up the sandbox environment, or the gateway is waiting for the sandbox supervisor to establish its authenticated control session. | diff --git a/docs/sandboxes/policies.mdx b/docs/sandboxes/policies.mdx index 749dd0c5a4..31aaed3855 100644 --- a/docs/sandboxes/policies.mdx +++ b/docs/sandboxes/policies.mdx @@ -269,6 +269,29 @@ The following steps outline the hot-reload policy update workflow. ### Validation failures +Before starting a workload, OpenShell validates its effective policy together +with attached provider rules and credential bindings. An image policy can be +valid alone and fail after composition, for example when an L4-only endpoint +overlaps a credentialed provider that requires L7 inspection. + +When startup validation fails, the sandbox stays in `Provisioning` with a +`ConfigurationInvalid` readiness condition. Its workload has not started. Inspect +the condition with `openshell sandbox get `, then submit a complete repaired +policy or detach the conflicting provider: + +```shell +openshell policy set --policy repaired-policy.yaml --wait +openshell sandbox provider detach +``` + +These are alternative repairs; choose the one that matches the intended access. +The supervisor starts the workload after the repaired configuration passes +validation. You can replace static policy fields while startup is blocked; +after activation, the usual static-field restrictions apply. Images without an +embedded policy use the restrictive baseline and gain network access only from +operator-selected configuration. Explicit user/global policy precedence remains +unchanged. + OpenShell validates a complete candidate policy before activating any part of it. Endpoints may overlap when their connection and request-processing metadata agree. For example, two `api.example.com:443` REST entries can contribute different allow and deny rules when they use the same TLS, destination, credential, parser, and enforcement settings. A plain L4 endpoint may overlap an L7 endpoint because it authorizes the destination without contributing request-processing metadata. A more-specific path endpoint may override request-processing metadata from a broader endpoint, such as a `/graphql` GraphQL endpoint alongside a general REST endpoint for the same host. OpenShell rejects the candidate when overlapping exact or wildcard host selectors can both contribute equally specific endpoint configuration and disagree on those fields. Internal policy-advisor provenance does not make otherwise compatible endpoints ambiguous. This lets an advisor proposal extend a provider-covered host without modifying the provider rule. TLS, destination IP constraints, credential handling, protocol, parser, and equally specific enforcement settings must still agree. @@ -303,7 +326,7 @@ Operators that explicitly prioritize availability can retain the previous genera policy_validation_failure_mode = "retain_last_valid" ``` -In `retain_last_valid` mode, the rejected candidate remains inactive and the previous valid generation remains active. If no previous valid generation exists, such as during initial startup, OpenShell still fails closed. Restart the gateway after changing `gateway.toml`; connected sandbox supervisors receive the configured posture from the restarted gateway. Individual sandboxes cannot override it. +In `retain_last_valid` mode, the rejected candidate remains inactive and the previous valid generation remains active. During initial startup, a rejected configuration keeps the workload unstarted in either mode. Restart the gateway after changing `gateway.toml`; connected sandbox supervisors receive the configured posture from the restarted gateway. Individual sandboxes cannot override it. OCSF configuration and finding events identify the rejected candidate, validation rationale, configured and effective modes, active generation, and whether the previous policy is active. When `retain_last_valid` is configured without a previous valid generation, the effective mode remains `fail_closed`. Connection denials during quarantine include the validation failure as their policy denial rationale. diff --git a/e2e/rust/Cargo.toml b/e2e/rust/Cargo.toml index 51f30c37fc..e2ed363915 100644 --- a/e2e/rust/Cargo.toml +++ b/e2e/rust/Cargo.toml @@ -38,6 +38,11 @@ e2e-oidc-pkce = [] e2e-provider-refresh-keycloak = [] e2e-vm = ["e2e", "e2e-host-gateway"] +[[test]] +name = "policy_activation" +path = "tests/policy_activation.rs" +required-features = ["e2e-docker"] + [[test]] name = "oidc_pkce" path = "tests/oidc_pkce.rs" diff --git a/e2e/rust/tests/policy_activation.rs b/e2e/rust/tests/policy_activation.rs new file mode 100644 index 0000000000..45c9019a63 --- /dev/null +++ b/e2e/rust/tests/policy_activation.rs @@ -0,0 +1,354 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(feature = "e2e-docker")] + +//! A rejected image/provider composition must never launch its main process. + +use std::process::Stdio; +use std::time::Duration; + +use openshell_e2e::harness::binary::{openshell_bin, openshell_cmd}; +use openshell_e2e::harness::cli::run_cli; +use openshell_e2e::harness::container::{ContainerEngine, ImageGuard}; +use openshell_e2e::harness::output::strip_ansi; + +const MARKER: &str = "/sandbox/activation-count"; +const WORKLOAD: &str = "echo started >> /sandbox/activation-count; exec sleep infinity"; +const POLICY: &str = r"version: 1 +filesystem_policy: + include_workdir: true + read_only: [/usr, /lib, /proc, /etc, /dev/urandom] + read_write: [/sandbox, /tmp, /dev/null] +landlock: + compatibility: best_effort +process: + run_as_user: sandbox + run_as_group: sandbox +network_policies: + image_api: + endpoints: + - host: api.example.com + port: 443 + binaries: + - path: /usr/bin/curl +"; + +struct Resources { + sandbox: String, + standalone_sandbox: String, + provider: String, +} + +impl Drop for Resources { + fn drop(&mut self) { + // Deletion drains asynchronously; retry dependencies on panic as well. + let bin = openshell_bin(); + let _ = std::process::Command::new(&bin) + .args(["sandbox", "delete", &self.sandbox]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + let _ = std::process::Command::new(&bin) + .args(["sandbox", "delete", &self.standalone_sandbox]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + for _ in 0..20 { + let deleted = std::process::Command::new(&bin) + .args(["provider", "delete", &self.provider]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + if deleted.is_ok_and(|status| status.success()) { + break; + } + std::thread::sleep(Duration::from_millis(250)); + } + let _ = std::process::Command::new(&bin) + .args(["provider", "profile", "delete", &self.provider]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } +} + +async fn cli_ok(args: &[&str]) { + let (output, code) = run_cli(args).await; + assert_eq!(code, 0, "{} failed:\n{output}", args.join(" ")); +} + +fn container_id(engine: &ContainerEngine, name: &str) -> String { + let output = engine + .command() + .args([ + "ps", + "--quiet", + "--filter", + &format!("label=openshell.ai/sandbox-name={name}"), + ]) + .output() + .expect("find sandbox container"); + assert!(output.status.success(), "container lookup failed"); + let ids = String::from_utf8_lossy(&output.stdout); + let ids: Vec<_> = ids.split_whitespace().collect(); + assert_eq!( + ids.len(), + 1, + "expected one running sandbox container: {ids:?}" + ); + ids[0].to_string() +} + +fn assert_marker(engine: &ContainerEngine, container: &str, started: bool) { + let script = if started { + format!("test -f {MARKER} && test \"$(wc -l < {MARKER})\" -eq 1") + } else { + format!("test ! -e {MARKER}") + }; + let output = engine + .command() + .args(["exec", container, "sh", "-c", &script]) + .output() + .expect("inspect actual workload marker"); + assert!( + output.status.success(), + "workload marker assertion failed (started={started}): {}", + String::from_utf8_lossy(&output.stderr) + ); +} + +async fn wait_for_marker(engine: &ContainerEngine, container: &str) { + let deadline = tokio::time::Instant::now() + Duration::from_secs(30); + loop { + let output = engine + .command() + .args(["exec", container, "test", "-f", MARKER]) + .output() + .unwrap(); + if output.status.success() { + break; + } + assert!( + tokio::time::Instant::now() < deadline, + "admitted workload did not start" + ); + tokio::time::sleep(Duration::from_millis(250)).await; + } + assert_marker(engine, container, true); +} + +#[tokio::test] +async fn invalid_image_provider_bundle_waits_for_repair_before_launch() { + let suffix = format!("{:016x}", rand::random::()); + let resources = Resources { + sandbox: format!("ac-{suffix}"), + standalone_sandbox: format!("al-{suffix}"), + provider: format!("ap-{suffix}"), + }; + let context = tempfile::tempdir().unwrap(); + std::fs::write(context.path().join("policy.yaml"), POLICY).unwrap(); + std::fs::write(context.path().join("Dockerfile"), r#"FROM public.ecr.aws/docker/library/python:3.13-slim +RUN apt-get update && apt-get install -y --no-install-recommends iproute2 && rm -rf /var/lib/apt/lists/* \ + && groupadd sandbox && useradd -m -g sandbox sandbox && mkdir -p /sandbox && chown sandbox:sandbox /sandbox +COPY policy.yaml /etc/openshell/policy.yaml +WORKDIR /sandbox +USER sandbox +CMD ["sh", "-c", "echo started >> /sandbox/activation-count; exec sleep infinity"] +"#).unwrap(); + let image = ImageGuard::build( + "policy-activation", + &context.path().join("Dockerfile"), + context.path(), + ) + .unwrap(); + // The very same embedded policy is valid before provider composition. + tokio::time::timeout( + Duration::from_secs(120), + cli_ok(&[ + "sandbox", + "create", + "--name", + &resources.standalone_sandbox, + "--detach", + "--from", + image.tag(), + "--", + "sh", + "-c", + WORKLOAD, + ]), + ) + .await + .expect("standalone image policy activates"); + let engine = ContainerEngine::from_env().unwrap(); + wait_for_marker( + &engine, + &container_id(&engine, &resources.standalone_sandbox), + ) + .await; + cli_ok(&["sandbox", "delete", &resources.standalone_sandbox]).await; + + let profile = context.path().join("provider.yaml"); + std::fs::write( + &profile, + format!( + r"id: {} +display_name: Activation test +category: other +credentials: + - name: token + env_vars: [ACTIVATION_TOKEN] + required: true + auth_style: bearer + header_name: authorization +endpoints: + - host: api.example.com + port: 443 + protocol: rest + access: full +binaries: + - path: /usr/bin/curl +", + resources.provider + ), + ) + .unwrap(); + cli_ok(&[ + "provider", + "profile", + "import", + "--file", + profile.to_str().unwrap(), + ]) + .await; + cli_ok(&[ + "provider", + "create", + "--name", + &resources.provider, + "--type", + &resources.provider, + "--credential", + "ACTIVATION_TOKEN=activation-test-not-a-real-secret", + ]) + .await; + let mut create = openshell_cmd() + .args([ + "sandbox", + "create", + "--name", + &resources.sandbox, + "--detach", + "--from", + image.tag(), + "--provider", + &resources.provider, + "--", + "sh", + "-c", + WORKLOAD, + ]) + .stdout(Stdio::null()) + .stderr(Stdio::inherit()) + .spawn() + .expect("start sandbox create"); + let deadline = tokio::time::Instant::now() + Duration::from_secs(120); + loop { + let (output, code) = + run_cli(&["sandbox", "get", &resources.sandbox, "--output", "json"]).await; + let clean = strip_ansi(&output); + if code == 0 && clean.contains("ConfigurationInvalid") { + let details: serde_json::Value = serde_json::from_str(&clean).expect("sandbox JSON"); + let phase = details + .get("phase") + .and_then(serde_json::Value::as_str) + .expect("sandbox detail must expose a phase string"); + assert_eq!( + phase, "Provisioning", + "rejected configuration must not be usable" + ); + assert!(!clean.contains("activation-test-not-a-real-secret")); + break; + } + assert!( + tokio::time::Instant::now() < deadline, + "configuration did not reject:\n{clean}" + ); + tokio::time::sleep(Duration::from_millis(500)).await; + } + let container = container_id(&engine, &resources.sandbox); + assert_marker(&engine, &container, false); + // Repeated observation distinguishes a stable gate from a crash/relaunch loop. + tokio::time::sleep(Duration::from_secs(3)).await; + assert_eq!(container_id(&engine, &resources.sandbox), container); + let restarts = engine + .command() + .args(["inspect", "--format", "{{.RestartCount}}", &container]) + .output() + .expect("inspect supervisor restart count"); + assert!(restarts.status.success()); + assert_eq!( + String::from_utf8_lossy(&restarts.stdout).trim(), + "0", + "invalid configuration must not crash-loop" + ); + assert_marker(&engine, &container, false); + let repaired = context.path().join("repaired.yaml"); + std::fs::write( + &repaired, + POLICY.replace( + " port: 443", + " port: 443\n protocol: rest\n access: full", + ), + ) + .unwrap(); + cli_ok(&[ + "policy", + "set", + &resources.sandbox, + "--policy", + repaired.to_str().unwrap(), + ]) + .await; + let status = tokio::time::timeout(Duration::from_secs(120), create.wait()) + .await + .expect("create completes after repair") + .expect("wait for create"); + assert!(status.success(), "create failed after valid repair"); + wait_for_marker(&engine, &container).await; + // An invalid later replacement must not displace the admitted live policy. + let (output, code) = run_cli(&[ + "policy", + "set", + &resources.sandbox, + "--policy", + context.path().join("policy.yaml").to_str().unwrap(), + ]) + .await; + assert_ne!( + code, 0, + "unsafe replacement unexpectedly succeeded: {output}" + ); + assert_marker(&engine, &container, true); + // An explicit stop/start must run the admission gate again before the saved + // command launches. Clear the marker to distinguish that new launch. + let removed = engine + .command() + .args(["exec", &container, "rm", "-f", MARKER]) + .status() + .unwrap(); + assert!(removed.success()); + cli_ok(&["sandbox", "stop", &resources.sandbox]).await; + tokio::time::timeout( + Duration::from_secs(120), + cli_ok(&["sandbox", "start", &resources.sandbox]), + ) + .await + .expect("repaired configuration revalidates on restart"); + let restarted_container = container_id(&engine, &resources.sandbox); + wait_for_marker(&engine, &restarted_container).await; + drop(create); + drop(resources); + drop(image); +} diff --git a/proto/openshell.proto b/proto/openshell.proto index 91c0365863..f664a2faa1 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -503,6 +503,12 @@ service OpenShell { }; } + // Register startup and acknowledge an exact validated runtime configuration. + rpc ReportSandboxConfiguration(ReportSandboxConfigurationRequest) + returns (ReportSandboxConfigurationResponse) { + 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) { @@ -1110,6 +1116,8 @@ message SandboxStatus { // Currently populated for MCP-over-HTTP endpoints. These passive results // remain separate from sandbox lifecycle conditions and readiness. repeated EndpointStatus endpoint_statuses = 10; + // Independent of infrastructure phase; retained across driver observations. + SandboxConfigurationAdmission configuration_admission = 11; } // User-facing sandbox condition derived from platform or gateway observations. @@ -2772,6 +2780,32 @@ message ReportPolicyStatusRequest { // Report policy status response. message ReportPolicyStatusResponse {} +enum ConfigurationAdmissionState { + CONFIGURATION_ADMISSION_STATE_UNSPECIFIED = 0; + CONFIGURATION_ADMISSION_STATE_PENDING = 1; + CONFIGURATION_ADMISSION_STATE_ACCEPTED = 2; + CONFIGURATION_ADMISSION_STATE_REJECTED = 3; +} + +message SandboxConfigurationAdmission { + string instance_id = 1; + ConfigurationAdmissionState state = 2; + uint32 policy_version = 3; + string policy_hash = 4; + uint64 config_revision = 5; + uint64 provider_env_revision = 6; + string error = 7; +} + +message ReportSandboxConfigurationRequest { + string sandbox_id = 1; + SandboxConfigurationAdmission admission = 2; + // Pending registration replaces only this previously observed instance. + string expected_instance_id = 3; +} + +message ReportSandboxConfigurationResponse {} + // A versioned policy revision with metadata. message SandboxPolicyRevision { reserved 5, 6; diff --git a/proto/sandbox.proto b/proto/sandbox.proto index 8b15056455..2c9d71e879 100644 --- a/proto/sandbox.proto +++ b/proto/sandbox.proto @@ -402,6 +402,13 @@ message GetSandboxConfigResponse { // Gateway-owned attachment identity captured with this desired configuration. // Compare for equality; reattachment invalidates previous installation evidence. string provider_attachment_epoch = 14; + // True only after validating this complete policy/provider composition. + // Missing (older gateway) is deliberately not admission. + bool configuration_admitted = 13; + // Bounded, credential-free admission diagnostic. Empty for admitted policy. + string configuration_error = 16; + // Registration fence for a new supervisor; capture once and retain on retry. + string configuration_instance_id = 15; } // 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 4777942a02..c09454add6 100644 --- a/sdk/go/openshell/v1/internal/converter/coverage_test.go +++ b/sdk/go/openshell/v1/internal/converter/coverage_test.go @@ -124,6 +124,7 @@ func TestConverterCoversAllProtoFields_SandboxStatus(t *testing.T) { "endpoint_statuses": true, "current_policy_version": true, "exit_code": true, + "configuration_admission": true, } // These fields coordinate internal gateway/supervisor lifecycle fencing // and idempotent status reconciliation. They remain available only through diff --git a/sdk/go/openshell/v1/internal/converter/sandbox.go b/sdk/go/openshell/v1/internal/converter/sandbox.go index ab064c2231..2db7002b73 100644 --- a/sdk/go/openshell/v1/internal/converter/sandbox.go +++ b/sdk/go/openshell/v1/internal/converter/sandbox.go @@ -125,6 +125,25 @@ func sandboxStatusFromProto(status *pb.SandboxStatus) types.SandboxStatus { }) } result.ExitCode = CopyInt32Ptr(status.ExitCode) + if admission := status.GetConfigurationAdmission(); admission != nil { + state := types.ConfigurationAdmissionUnknown + switch admission.GetState() { + case pb.ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_PENDING: + state = types.ConfigurationAdmissionPending + case pb.ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_ACCEPTED: + state = types.ConfigurationAdmissionAccepted + case pb.ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_REJECTED: + state = types.ConfigurationAdmissionRejected + } + result.ConfigurationAdmission = &types.SandboxConfigurationAdmission{ + State: state, + PolicyVersion: admission.GetPolicyVersion(), + PolicyHash: admission.GetPolicyHash(), + ConfigRevision: admission.GetConfigRevision(), + ProviderEnvRevision: admission.GetProviderEnvRevision(), + Error: admission.GetError(), + } + } return result } diff --git a/sdk/go/openshell/v1/internal/converter/sandbox_test.go b/sdk/go/openshell/v1/internal/converter/sandbox_test.go index f80fef832e..81dc260cf8 100644 --- a/sdk/go/openshell/v1/internal/converter/sandbox_test.go +++ b/sdk/go/openshell/v1/internal/converter/sandbox_test.go @@ -19,6 +19,33 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) +func TestSandboxConfigurationAdmissionFromProto(t *testing.T) { + for _, tc := range []struct { + wire pb.ConfigurationAdmissionState + want v1.ConfigurationAdmissionState + }{ + {pb.ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_PENDING, v1.ConfigurationAdmissionPending}, + {pb.ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_ACCEPTED, v1.ConfigurationAdmissionAccepted}, + {pb.ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_REJECTED, v1.ConfigurationAdmissionRejected}, + {pb.ConfigurationAdmissionState(99), v1.ConfigurationAdmissionUnknown}, + } { + t.Run(string(tc.want), func(t *testing.T) { + wire := &pb.SandboxStatus{ConfigurationAdmission: &pb.SandboxConfigurationAdmission{ + State: tc.wire, PolicyVersion: 4, PolicyHash: "hash", ConfigRevision: 5, + ProviderEnvRevision: 6, Error: "invalid endpoint", + }} + got := sandboxStatusFromProto(wire) + assert.Equal(t, &v1.SandboxConfigurationAdmission{ + State: tc.want, PolicyVersion: 4, PolicyHash: "hash", ConfigRevision: 5, + ProviderEnvRevision: 6, Error: "invalid endpoint", + }, got.ConfigurationAdmission) + wire.ConfigurationAdmission.Error = "changed" + assert.Equal(t, "invalid endpoint", got.ConfigurationAdmission.Error) + }) + } + assert.Nil(t, sandboxStatusFromProto(&pb.SandboxStatus{}).ConfigurationAdmission) +} + func TestSandboxFromProto(t *testing.T) { userNS := true gpuCount := uint32(2) diff --git a/sdk/go/openshell/v1/types/sandbox.go b/sdk/go/openshell/v1/types/sandbox.go index 47d4a71b18..25db870811 100644 --- a/sdk/go/openshell/v1/types/sandbox.go +++ b/sdk/go/openshell/v1/types/sandbox.go @@ -120,6 +120,29 @@ type SandboxStatus struct { // EndpointStatuses describes configured external tool endpoints and their // last accepted network results, independently of sandbox readiness. EndpointStatuses []EndpointStatus + ConfigurationAdmission *SandboxConfigurationAdmission +} + +// ConfigurationAdmissionState describes validation of an effective configuration. +type ConfigurationAdmissionState string + +// Configuration admission states reported by the gateway. +const ( + ConfigurationAdmissionUnknown ConfigurationAdmissionState = "unknown" + ConfigurationAdmissionPending ConfigurationAdmissionState = "pending" + ConfigurationAdmissionAccepted ConfigurationAdmissionState = "accepted" + ConfigurationAdmissionRejected ConfigurationAdmissionState = "rejected" +) + +// SandboxConfigurationAdmission identifies a validated or rejected configuration. +// Supervisor instance fencing remains available through the raw protobuf API. +type SandboxConfigurationAdmission struct { + State ConfigurationAdmissionState + PolicyVersion uint32 + PolicyHash string + ConfigRevision uint64 + ProviderEnvRevision uint64 + Error string } // EndpointStatus holds a configured tool endpoint and its last accepted network result. diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index e53f571108..0710aff65c 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -185,6 +185,16 @@ are unavailable. Workload shells belong to the separate sandbox image. Preserve the driver-selected UID and writable runtime/log mounts when reproducing a supervisor startup failure. +A `ConfigurationInvalid` readiness condition means startup admission rejected +the image/effective policy or provider configuration. The supervisor remains +alive while the workload stays unstarted. Inspect `openshell sandbox get` and +repair the desired configuration with a complete policy replacement or provider +change; do not treat a healthy container as proof that the workload is ready. +See [policy validation and repair](https://docs.nvidia.com/openshell/latest/sandboxes/policies.md). +In sidecar topology, the process supervisor sends image-policy discovery over +the authenticated control socket and waits for an accepted bootstrap. A process +container waiting there can be expected during repair, rather than a crash loop. + ### Step 4: Check Docker-Backed Gateways ```bash diff --git a/skills/generate-sandbox-policy/SKILL.md b/skills/generate-sandbox-policy/SKILL.md index 39ab4cadef..89e6223ac2 100644 --- a/skills/generate-sandbox-policy/SKILL.md +++ b/skills/generate-sandbox-policy/SKILL.md @@ -173,6 +173,12 @@ When middleware is requested, also read the published [supervisor middleware gui For enforcement concepts and the shipped baseline, read [sandbox policies](https://docs.nvidia.com/openshell/latest/sandboxes/policies.md) and the [default policy reference](https://docs.nvidia.com/openshell/latest/reference/default-policy.md). The default policy is baked into the community base image (`ghcr.io/nvidia/openshell-community/sandboxes/base:latest`). +Validate the intended provider combination as well as the authored policy. +An image endpoint can become credentialed after provider composition and block +startup with `ConfigurationInvalid`. Repair the complete policy or provider +selection using the published policy workflow; do not add +`allow_uninspected_credentials` merely to bypass a startup error. + ## Step 4: Choose Policy Shape Follow this decision tree based on the detail tier and user intent: diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index cb23c6e6f8..7926263e57 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -447,7 +447,14 @@ the operation that removes retained state. This is the most important multi-step workflow. It enables a tight feedback cycle where sandbox policy is refined based on observed activity. -**Key concept**: Policies have static fields (immutable after creation: `filesystem_policy`, `landlock`, `process`) and two dynamic fields: `network_policies` and `network_middlewares`. Both dynamic fields can be updated without recreating the sandbox when the selected compute driver supports live policy updates. Drivers without the standard supervisor fetch revisions through the sandbox configuration API and report whether they loaded them. +**Key concept**: Policies have static fields (immutable after activation: `filesystem_policy`, `landlock`, `process`) and two dynamic fields: `network_policies` and `network_middlewares`. Both dynamic fields can be updated without recreating the sandbox when the selected compute driver supports live policy updates. Drivers without the standard supervisor fetch revisions through the sandbox configuration API and report whether they loaded them. + +If startup reports `ConfigurationInvalid`, inspect `openshell sandbox get` and +repair the complete policy or provider set through the gateway. The workload +has not started, so static fields can also be replaced during this repair. +After validation succeeds, the supervisor completes startup. Follow the +published [policy repair guidance](https://docs.nvidia.com/openshell/latest/sandboxes/policies.md) +and confirm current replacement/detach syntax with installed CLI help. An endpoint with omitted `protocol` retains explicit-proxy behavior. Explicit `protocol: tcp` requests policy DNS and transparent TCP and currently requires From f7bd1a9664b443911e13b79d8ced68509cdb801d Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Mon, 14 Sep 2026 13:38:10 -0700 Subject: [PATCH 02/23] fix(sandbox): bound startup failures and preserve activation history Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- architecture/gateway.md | 8 ++ architecture/sandbox.md | 8 +- crates/openshell-core/src/grpc_client.rs | 17 +++- crates/openshell-server/src/compute/mod.rs | 12 ++- crates/openshell-server/src/grpc/policy.rs | 89 +++++++++++++----- crates/openshell-server/src/grpc/sandbox.rs | 5 + crates/openshell-server/src/policy_store.rs | 93 +++++++++++++++++-- crates/openshell-server/src/storage_proto.rs | 18 ++++ docs/sandboxes/policies.mdx | 9 +- proto/openshell.proto | 2 + .../v1/internal/converter/coverage_test.go | 10 +- skills/debug-openshell-cluster/SKILL.md | 3 + skills/openshell-cli/SKILL.md | 4 +- 13 files changed, 229 insertions(+), 49 deletions(-) diff --git a/architecture/gateway.md b/architecture/gateway.md index 6d944cd0d0..0b7fbc643a 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -436,6 +436,14 @@ and number; this coordinated pre-1.0 API change does not alter durable schemas. The outcome alone does not provide request deduplication. Opted-in unary methods require a request UUID for the admission contract. +Configuration admission adds `SandboxStatus.configuration_admission` at field +11 and optional `configuration_activated` at field 12, extending the public and +durable closures. New sandboxes explicitly store `false` until first acceptance; +acceptance stores `true` permanently, including across restart. Legacy rows +have neither field and conservatively retain static-policy restrictions. No +database rewrite is required. A pre-admission byte fixture verifies that legacy +phase and policy-version fields survive without fabricated admission or activation. + | Dual-purpose encoded root | Current decision | |---|---| | `Sandbox` | Defer a storage twin; govern its complete dependency closure as durable. | diff --git a/architecture/sandbox.md b/architecture/sandbox.md index 6cadfa58f6..9f3c274922 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -627,6 +627,9 @@ condition, even when the container backend reports readiness. Gateway management operations remain available. Replacing the policy or repairing providers allows the same supervisor to reconcile and launch; it does not recreate the sandbox. Static policy fields can be replaced before the first accepted activation. +A durable first-activation marker closes this repair window permanently, including +across stop/start and later rejected configurations. Legacy records without the +marker retain static-field immutability. Admission validates policy composition; image and host setup failures, such as an unresolved OCI user or unavailable isolation facilities, retain their existing startup error behavior. @@ -636,7 +639,10 @@ provider-environment revision, and reporting supervisor instance. Startup captur the matching provider environment and constructs the runtime before reporting acceptance. Live reconciliation begins only after the main process has spawned, so it cannot replace the configuration captured for that launch. Restart resets -admission and requires a fresh accepted configuration. +admission and requires a fresh accepted configuration. Permanent gateway errors +and exhausted transient retries terminate startup; only acknowledged configuration +rejections wait indefinitely for repair. Process-sidecar discovery uses the +existing sidecar readiness timeout. Policy and provider refreshes are prepared before publication. Publication invalidates prior policy guards before exposing new provider material and swaps diff --git a/crates/openshell-core/src/grpc_client.rs b/crates/openshell-core/src/grpc_client.rs index 426ef3b6c9..a64fbaf918 100644 --- a/crates/openshell-core/src/grpc_client.rs +++ b/crates/openshell-core/src/grpc_client.rs @@ -43,6 +43,15 @@ use tonic::service::interceptor::InterceptedService; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity}; use tracing::{debug, info, warn}; +/// Preserve the gRPC status as a source so callers can classify retryable errors. +/// `IntoDiagnostic` alone hides the wrapped error's concrete type. +pub fn grpc_status_error(status: Status) -> miette::Report { + #[derive(Debug, thiserror::Error, miette::Diagnostic)] + #[error("{0}")] + struct GrpcStatusError(#[source] Status); + GrpcStatusError(status).into() +} + /// Channel type after the [`AuthInterceptor`] is applied. Aliased so the /// generated client type signatures stay readable. pub type AuthedChannel = InterceptedService; @@ -920,7 +929,7 @@ async fn fetch_settings_snapshot_with_client( sandbox_id: sandbox_id.to_string(), }) .await - .into_diagnostic()?; + .map_err(grpc_status_error)?; Ok(settings_poll_result(response.into_inner())) } @@ -957,7 +966,7 @@ async fn sync_policy_with_client( ..Default::default() }) .await - .into_diagnostic() + .map_err(grpc_status_error) .wrap_err("failed to sync policy to server")?; Ok(()) @@ -1052,7 +1061,7 @@ pub async fn report_sandbox_configuration( }), }) .await - .into_diagnostic()?; + .map_err(grpc_status_error)?; Ok(()) } @@ -1075,7 +1084,7 @@ pub async fn fetch_provider_environment( supports_static_credential_bindings: true, }) .await - .into_diagnostic()?; + .map_err(grpc_status_error)?; provider_environment_result(response.into_inner()) } diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 56377127b9..5dbb697175 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -4538,6 +4538,7 @@ fn public_status_from_driver( exit_code: None, endpoint_statuses: Vec::new(), configuration_admission: None, + configuration_activated: None, } } @@ -4667,6 +4668,7 @@ fn apply_driver_snapshot( status .configuration_admission .clone_from(¤t_status.configuration_admission); + status.configuration_activated = current_status.configuration_activated; } if old_phase != phase { info!( @@ -7614,7 +7616,8 @@ mod tests { async fn stop_and_start_follow_durable_state_machine() { let driver = ControlledDriver::new(); let runtime = test_runtime(driver.clone()).await; - let sandbox = sandbox_record("sb-lifecycle", "sandbox-lifecycle", SandboxPhase::Ready); + let mut sandbox = sandbox_record("sb-lifecycle", "sandbox-lifecycle", SandboxPhase::Ready); + sandbox.status.as_mut().unwrap().configuration_activated = Some(true); runtime.store.put_message(&sandbox).await.unwrap(); let session = ssh_session_record("lifecycle-session", sandbox.object_id()); runtime.store.put_message(&session).await.unwrap(); @@ -7684,6 +7687,13 @@ mod tests { SandboxPhase::Starting as i32, "driver/session readiness cannot bypass restart configuration admission" ); + assert_eq!( + blocked.status.as_ref().unwrap().configuration_activated, + Some(true) + ); + assert!(!crate::policy_store::permits_initial_static_policy_repair( + &blocked + )); // Simulate the supervisor's successful exact-generation admission report. runtime .store diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index d933a9425c..63e0347127 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -25,9 +25,10 @@ use crate::auth::workspace_authz::{ require_platform_admin, selected_workspace_name, }; use crate::pagination::Pagination; +#[cfg(test)] +use crate::persistence::ObjectType; use crate::persistence::{ - DraftChunkRecord, ObjectId, ObjectListQuery, ObjectName, ObjectType, ObjectWorkspace, - PolicyRecord, Store, + DraftChunkRecord, ObjectId, ObjectListQuery, ObjectName, ObjectWorkspace, PolicyRecord, Store, }; use crate::policy_store::{AtomicPolicyRevisionWrite, PolicyStoreExt}; use crate::provider_profile_sources::EffectiveProviderProfileCatalog; @@ -2471,14 +2472,8 @@ async fn persist_existing_policy_projection( let updated = state .store .update_message_cas::(sandbox_id, expected_resource_version, |sandbox| { - let startup_blocked = sandbox - .status - .as_ref() - .and_then(|status| status.configuration_admission.as_ref()) - .is_some_and(|admission| { - admission.state - != i32::from(openshell_core::proto::ConfigurationAdmissionState::Accepted) - }); + let startup_blocked = + crate::policy_store::permits_initial_static_policy_repair(sandbox); if let Some(policy) = backfill_policy.as_ref() && let Some(spec) = sandbox.spec.as_mut() && (spec.policy.is_none() || startup_blocked) @@ -4004,14 +3999,7 @@ async fn handle_update_config_inner( validate_no_reserved_provider_policy_keys(&new_policy)?; } - let startup_blocked = sandbox - .status - .as_ref() - .and_then(|status| status.configuration_admission.as_ref()) - .is_some_and(|admission| { - admission.state - != i32::from(openshell_core::proto::ConfigurationAdmissionState::Accepted) - }); + let startup_blocked = crate::policy_store::permits_initial_static_policy_repair(&sandbox); let should_backfill_policy = if startup_blocked && !sandbox_caller { // No child has consumed static restrictions yet. A complete replacement // must be able to repair every field before the first activation. @@ -4448,12 +4436,14 @@ pub(super) async fn handle_report_sandbox_configuration( && current.map_or("", |current| current.instance_id.as_str()) != request.get_ref().expected_instance_id { - return Err(Status::aborted("supervisor registration fence has changed")); + return Err(Status::failed_precondition( + "supervisor registration fence has changed", + )); } if reported != ConfigurationAdmissionState::Pending && current.is_none_or(|current| current.instance_id != admission.instance_id) { - return Err(Status::aborted( + return Err(Status::failed_precondition( "supervisor configuration instance has changed", )); } @@ -4514,6 +4504,13 @@ pub(super) async fn handle_report_sandbox_configuration( .status .get_or_insert_with(Default::default) .configuration_admission = Some(admission.clone()); + if reported == ConfigurationAdmissionState::Accepted { + sandbox + .status + .as_mut() + .expect("status initialized") + .configuration_activated = Some(true); + } crate::compute::apply_configuration_readiness(sandbox); }) .await @@ -7856,8 +7853,9 @@ mod tests { Code::Aborted, "a delayed rejection must not mark the accepted current generation invalid" ); + let restart_instance = uuid::Uuid::new_v4().to_string(); let mut restart = report(SandboxConfigurationAdmission { - instance_id: uuid::Uuid::new_v4().to_string(), + instance_id: restart_instance.clone(), state: Admission::Pending.into(), ..Default::default() }); @@ -7877,16 +7875,54 @@ mod tests { .await .unwrap_err() .code(), - Code::Aborted, + Code::FailedPrecondition, "delayed old Pending must not reclaim registration" ); assert_eq!( - handle_report_sandbox_configuration(&state, report(accepted)) + handle_report_sandbox_configuration(&state, report(accepted.clone())) .await .unwrap_err() .code(), - Code::Aborted - ); + Code::FailedPrecondition + ); + for admission_state in [Admission::Pending, Admission::Rejected] { + if admission_state == Admission::Rejected { + let mut rejected = accepted.clone(); + rejected.instance_id = restart_instance.clone(); + rejected.state = Admission::Rejected.into(); + handle_report_sandbox_configuration(&state, report(rejected)) + .await + .unwrap(); + } + let persisted = state + .store + .get_message::(sandbox_id) + .await + .unwrap() + .unwrap(); + assert_eq!( + persisted.status.as_ref().unwrap().configuration_activated, + Some(true) + ); + assert!(!crate::policy_store::permits_initial_static_policy_repair( + &persisted + )); + let mut replacement = persisted.spec.as_ref().unwrap().policy.clone().unwrap(); + replacement.filesystem.as_mut().unwrap().read_only.clear(); + let error = handle_update_config( + &state, + with_user(Request::new(UpdateConfigRequest { + name: "admission".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + policy: Some(replacement), + ..Default::default() + })), + ) + .await + .unwrap_err(); + assert_eq!(error.code(), Code::InvalidArgument); + assert!(error.message().contains("filesystem"), "{error}"); + } } #[test] @@ -7911,6 +7947,7 @@ mod tests { vec!["work-github".to_string()], ); sandbox.spec.as_mut().unwrap().policy = None; + sandbox.status.as_mut().unwrap().configuration_activated = Some(false); state .store .put_message(&test_provider("work-github", "github")) @@ -7941,6 +7978,7 @@ mod tests { with_sandbox( Request::new(UpdateConfigRequest { name: "image-admission".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), policy: Some(image), ..Default::default() }), @@ -7969,6 +8007,7 @@ mod tests { &state, with_user(Request::new(UpdateConfigRequest { name: "image-admission".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), policy: Some(openshell_policy::restrictive_default_policy()), ..Default::default() })), diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 777d8000cd..962c42282e 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -497,6 +497,11 @@ async fn handle_create_sandbox_inner( state: openshell_core::proto::ConfigurationAdmissionState::Pending.into(), ..Default::default() }); + sandbox + .status + .as_mut() + .expect("status initialized") + .configuration_activated = Some(false); crate::compute::apply_configuration_readiness(&mut sandbox); // Ensure metadata is valid (defense in depth - should always be true for server-constructed metadata) diff --git a/crates/openshell-server/src/policy_store.rs b/crates/openshell-server/src/policy_store.rs index dccde69ee7..cf6e23ef51 100644 --- a/crates/openshell-server/src/policy_store.rs +++ b/crates/openshell-server/src/policy_store.rs @@ -57,6 +57,26 @@ pub fn policy_record_for_atomic_write( } } +/// Only a new sandbox with durable evidence of no prior activation can replace +/// static restrictions. Legacy records are conservative: absence is not false. +pub fn permits_initial_static_policy_repair(sandbox: &Sandbox) -> bool { + sandbox.status.as_ref().is_some_and(|status| { + status.configuration_activated == Some(false) + && status + .configuration_admission + .as_ref() + .is_some_and(|admission| { + matches!( + openshell_core::proto::ConfigurationAdmissionState::try_from( + admission.state + ), + Ok(openshell_core::proto::ConfigurationAdmissionState::Pending + | openshell_core::proto::ConfigurationAdmissionState::Rejected) + ) + }) + }) +} + pub fn project_policy_revision_onto_sandbox( write: &AtomicPolicyRevisionWrite, payload: &[u8], @@ -76,14 +96,7 @@ pub fn project_policy_revision_onto_sandbox( sandbox.set_resource_version(current_resource_version); let mut changed = false; - let startup_blocked = sandbox - .status - .as_ref() - .and_then(|status| status.configuration_admission.as_ref()) - .is_some_and(|admission| { - admission.state - != i32::from(openshell_core::proto::ConfigurationAdmissionState::Accepted) - }); + let startup_blocked = permits_initial_static_policy_repair(&sandbox); if let Some(backfill_policy) = write.backfill_policy.as_ref() { let spec = sandbox .spec @@ -611,3 +624,67 @@ pub fn draft_chunk_record_from_parts( candidate_effective_policy: wrapper.candidate_effective_policy, }) } + +#[cfg(test)] +mod tests { + use super::*; + use openshell_core::proto::{ + ConfigurationAdmissionState as Admission, SandboxConfigurationAdmission, SandboxSpec, + SandboxStatus, + }; + + #[test] + fn static_projection_requires_durable_evidence_of_no_previous_activation() { + let baseline = openshell_policy::restrictive_default_policy(); + let mut replacement = baseline.clone(); + replacement + .filesystem + .as_mut() + .unwrap() + .read_write + .push("/new-static-path".to_string()); + let write = AtomicPolicyRevisionWrite { + id: "revision".to_string(), + sandbox_id: "sandbox".to_string(), + workspace: "default".to_string(), + version: 2, + policy_payload: replacement.encode_to_vec(), + policy_hash: String::new(), + provenance: HashMap::new(), + expected_resource_version: 0, + annotations: HashMap::new(), + backfill_policy: Some(replacement.clone()), + }; + for activated in [None, Some(false), Some(true)] { + for state in [Admission::Pending, Admission::Rejected, Admission::Accepted] { + let sandbox = Sandbox { + spec: Some(SandboxSpec { + policy: Some(baseline.clone()), + ..Default::default() + }), + status: Some(SandboxStatus { + configuration_activated: activated, + configuration_admission: Some(SandboxConfigurationAdmission { + state: state.into(), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }; + let result = + project_policy_revision_onto_sandbox(&write, &sandbox.encode_to_vec(), 1); + if activated == Some(false) && state != Admission::Accepted { + let (projected, changed) = result.unwrap(); + assert!(changed); + assert_eq!(projected.spec.unwrap().policy, Some(replacement.clone())); + } else { + assert!( + matches!(result, Err(PersistenceError::Conflict { .. })), + "{activated:?} {state:?}" + ); + } + } + } + } +} diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index 6e53903178..a0dcc3521d 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -593,6 +593,24 @@ mod tests { assert!(spec.provider_attachment_epoch.is_empty()); } + #[test] + fn pre_admission_sandbox_bytes_preserve_legacy_status() { + use openshell_core::proto::{Sandbox, SandboxPhase}; + // Synthetic Sandbox encoded with main 0357daee, before admission fields. + let bytes = + legacy_bytes("0a180a096c65676163792d6964120b6c65676163792d6e616d651a0430023807"); + let sandbox = Sandbox::decode(bytes.as_slice()).unwrap(); + let status = sandbox.status.as_ref().unwrap(); + assert_eq!(status.phase, SandboxPhase::Ready as i32); + assert_eq!(status.current_policy_version, 7); + assert!(status.configuration_admission.is_none()); + assert_eq!(status.configuration_activated, None); + assert!(!crate::policy_store::permits_initial_static_policy_repair( + &sandbox + )); + assert_eq!(sandbox.encode_to_vec(), bytes); + } + fn legacy_bytes(encoded: &str) -> Vec { hex::decode(encoded).expect("checked-in legacy fixture must be valid hex") } diff --git a/docs/sandboxes/policies.mdx b/docs/sandboxes/policies.mdx index 31aaed3855..c404d5acc4 100644 --- a/docs/sandboxes/policies.mdx +++ b/docs/sandboxes/policies.mdx @@ -286,11 +286,14 @@ openshell sandbox provider detach These are alternative repairs; choose the one that matches the intended access. The supervisor starts the workload after the repaired configuration passes -validation. You can replace static policy fields while startup is blocked; -after activation, the usual static-field restrictions apply. Images without an +validation. You can replace static policy fields while the first startup is +blocked. After the first accepted activation, the usual static-field restrictions +apply permanently, including while a restart is pending or rejected. Images without an embedded policy use the restrictive baseline and gain network access only from operator-selected configuration. Explicit user/global policy precedence remains -unchanged. +unchanged. Gateway authorization or compatibility errors, exhausted connection +retries, and missing process-sidecar connections terminate startup rather than +waiting for policy repair. OpenShell validates a complete candidate policy before activating any part of it. Endpoints may overlap when their connection and request-processing metadata agree. For example, two `api.example.com:443` REST entries can contribute different allow and deny rules when they use the same TLS, destination, credential, parser, and enforcement settings. A plain L4 endpoint may overlap an L7 endpoint because it authorizes the destination without contributing request-processing metadata. A more-specific path endpoint may override request-processing metadata from a broader endpoint, such as a `/graphql` GraphQL endpoint alongside a general REST endpoint for the same host. OpenShell rejects the candidate when overlapping exact or wildcard host selectors can both contribute equally specific endpoint configuration and disagree on those fields. diff --git a/proto/openshell.proto b/proto/openshell.proto index f664a2faa1..7b009d7430 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -1118,6 +1118,8 @@ message SandboxStatus { repeated EndpointStatus endpoint_statuses = 10; // Independent of infrastructure phase; retained across driver observations. SandboxConfigurationAdmission configuration_admission = 11; + // Durable first-acceptance marker. Absent on legacy records; never reset by restart. + optional bool configuration_activated = 12; } // User-facing sandbox condition derived from platform or gateway observations. diff --git a/sdk/go/openshell/v1/internal/converter/coverage_test.go b/sdk/go/openshell/v1/internal/converter/coverage_test.go index c09454add6..bab68ecb84 100644 --- a/sdk/go/openshell/v1/internal/converter/coverage_test.go +++ b/sdk/go/openshell/v1/internal/converter/coverage_test.go @@ -126,12 +126,10 @@ func TestConverterCoversAllProtoFields_SandboxStatus(t *testing.T) { "exit_code": true, "configuration_admission": true, } - // These fields coordinate internal gateway/supervisor lifecycle fencing - // and idempotent status reconciliation. They remain available only through - // the raw protobuf API. - skipped := fieldSet{ - "main_process_instance_id": true, - } + // The instance ID coordinates internal gateway/supervisor lifecycle + // fencing. The first-activation marker governs static policy repair. + // Both are exposed only through the raw protobuf API. + skipped := fieldSet{"main_process_instance_id": true, "configuration_activated": true} assertAllFieldsCovered(t, (&pb.SandboxStatus{}).ProtoReflect().Descriptor(), handled, skipped) } diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index 0710aff65c..d2228f803b 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -194,6 +194,9 @@ See [policy validation and repair](https://docs.nvidia.com/openshell/latest/sand In sidecar topology, the process supervisor sends image-policy discovery over the authenticated control socket and waits for an accepted bootstrap. A process container waiting there can be expected during repair, rather than a crash loop. +A missing process-sidecar connection times out during discovery. Permanent +gateway errors and exhausted transient retries terminate startup; inspect those +errors as connectivity, authorization, or lifecycle failures. ### Step 4: Check Docker-Backed Gateways diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index 7926263e57..13d78cfeac 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -451,7 +451,9 @@ This is the most important multi-step workflow. It enables a tight feedback cycl If startup reports `ConfigurationInvalid`, inspect `openshell sandbox get` and repair the complete policy or provider set through the gateway. The workload -has not started, so static fields can also be replaced during this repair. +has not started on its first activation, so static fields can also be replaced +during this initial repair. A previously activated sandbox retains static-field +restrictions while restart admission is pending or rejected. After validation succeeds, the supervisor completes startup. Follow the published [policy repair guidance](https://docs.nvidia.com/openshell/latest/sandboxes/policies.md) and confirm current replacement/detach syntax with installed CLI help. From f7922bfe77c70c03613009258aed78527ec06862 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:25:09 -0700 Subject: [PATCH 03/23] fix(sandbox): enforce deadlines on startup RPC attempts Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- architecture/sandbox.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/architecture/sandbox.md b/architecture/sandbox.md index 9f3c274922..d01d2e3a81 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -640,7 +640,8 @@ the matching provider environment and constructs the runtime before reporting acceptance. Live reconciliation begins only after the main process has spawned, so it cannot replace the configuration captured for that launch. Restart resets admission and requires a fresh accepted configuration. Permanent gateway errors -and exhausted transient retries terminate startup; only acknowledged configuration +and exhausted transient retries terminate startup; each RPC attempt has a +10-second deadline, including acceptance reports; only acknowledged configuration rejections wait indefinitely for repair. Process-sidecar discovery uses the existing sidecar readiness timeout. From a7ee151b306dd955b899cca94bca7ab294104b56 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:59:14 -0700 Subject: [PATCH 04/23] test(sandbox): isolate provider auto-create policy fixture Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- e2e/rust/tests/provider_auto_create.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/e2e/rust/tests/provider_auto_create.rs b/e2e/rust/tests/provider_auto_create.rs index 80e6e75fc3..2c7b3a31ff 100644 --- a/e2e/rust/tests/provider_auto_create.rs +++ b/e2e/rust/tests/provider_auto_create.rs @@ -84,12 +84,35 @@ async fn auto_created_provider_credential_available_in_sandbox() { // Clean up any leftover from a previous run. delete_provider("claude-code").await; + // This test only reads the injected environment placeholder. Do not inherit + // the published image's network rules, which may be incompatible with the + // attached credential provider's startup validation. + let policy = tempfile::NamedTempFile::new().expect("create provider test policy"); + std::fs::write( + policy.path(), + r#"version: 1 +filesystem_policy: + include_workdir: true + read_only: [/usr, /lib, /etc, /proc] + read_write: [/sandbox, /tmp, /dev/null] +landlock: + compatibility: best_effort +process: + run_as_user: sandbox + run_as_group: sandbox +network_policies: {} +"#, + ) + .expect("write provider test policy"); + // Create a sandbox that prints the ANTHROPIC_API_KEY env var. // --auto-providers skips the interactive prompt. let mut cmd = openshell_cmd(); cmd.arg("sandbox") .arg("create") .arg("--detach") + .arg("--policy") + .arg(policy.path()) .arg("--provider") .arg("claude-code") .arg("--auto-providers") From 003826de6d0e6ac5ee11281a1e17f415b7289484 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:59:37 -0700 Subject: [PATCH 05/23] test(sandbox): remove unnecessary fixture string delimiters Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- e2e/rust/tests/provider_auto_create.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/rust/tests/provider_auto_create.rs b/e2e/rust/tests/provider_auto_create.rs index 2c7b3a31ff..86d5d336c2 100644 --- a/e2e/rust/tests/provider_auto_create.rs +++ b/e2e/rust/tests/provider_auto_create.rs @@ -90,7 +90,7 @@ async fn auto_created_provider_credential_available_in_sandbox() { let policy = tempfile::NamedTempFile::new().expect("create provider test policy"); std::fs::write( policy.path(), - r#"version: 1 + r"version: 1 filesystem_policy: include_workdir: true read_only: [/usr, /lib, /etc, /proc] @@ -101,7 +101,7 @@ process: run_as_user: sandbox run_as_group: sandbox network_policies: {} -"#, +", ) .expect("write provider test policy"); From 3f3a955645f4c402b921a31e0b259ec7b3de7977 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:29:59 -0700 Subject: [PATCH 06/23] test(sandbox): capture startup logs before ephemeral cleanup Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- e2e/rust/tests/sandbox_lifecycle.rs | 61 ++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/e2e/rust/tests/sandbox_lifecycle.rs b/e2e/rust/tests/sandbox_lifecycle.rs index 279b5fdd47..e82a269812 100644 --- a/e2e/rust/tests/sandbox_lifecycle.rs +++ b/e2e/rust/tests/sandbox_lifecycle.rs @@ -680,7 +680,54 @@ async fn canonical_main_disconnect_reconnect_replays_history_for_same_process() #[tokio::test] async fn sandbox_create_with_no_keep_cleans_up_after_tty_command() { - let mut cmd = openshell_tty_cmd(&["sandbox", "create", "--no-keep", "--", "echo", "OK"]); + let name = format!("tty-{:015x}", rand::random::() & 0x0fff_ffff_ffff_ffff); + // Capture startup diagnostics before --no-keep removes a failed container. + // This is best-effort: the lifecycle assertions also run on other drivers. + let log_name = name.clone(); + let diagnostics = tokio::spawn(async move { + tokio::time::timeout(Duration::from_secs(30), async { + loop { + let containers = tokio::process::Command::new("docker") + .args(["ps", "--all", "--quiet", "--filter"]) + .arg(format!("label=openshell.ai/sandbox-name={log_name}")) + .kill_on_drop(true) + .output() + .await + .ok()?; + if !containers.status.success() { + return None; + } + let ids = String::from_utf8_lossy(&containers.stdout); + if let Some(id) = ids.split_whitespace().next() { + let logs = tokio::process::Command::new("docker") + .args(["logs", "--follow", id]) + .kill_on_drop(true) + .output() + .await + .ok()?; + return Some(normalize_output(&format!( + "{}{}", + String::from_utf8_lossy(&logs.stdout), + String::from_utf8_lossy(&logs.stderr) + ))); + } + sleep(Duration::from_millis(100)).await; + } + }) + .await + .ok() + .flatten() + }); + let mut cmd = openshell_tty_cmd(&[ + "sandbox", + "create", + "--name", + &name, + "--no-keep", + "--", + "echo", + "OK", + ]); cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); let output = cmd.output().await.expect("spawn openshell sandbox create"); @@ -688,7 +735,17 @@ async fn sandbox_create_with_no_keep_cleans_up_after_tty_command() { let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let combined = normalize_output(&format!("{stdout}{stderr}")); - assert!(output.status.success(), "create failed:\n{combined}"); + let startup_logs = if output.status.success() { + diagnostics.abort(); + None + } else { + diagnostics.await.ok().flatten() + }; + assert!( + output.status.success(), + "create failed:\n{combined}\nsupervisor logs:\n{}", + startup_logs.as_deref().unwrap_or("unavailable") + ); assert!( combined.contains("OK"), "main output was not streamed:\n{combined}" From 62802a9ab0f7008ff5d46efe7879ad8d441252ed Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Tue, 15 Sep 2026 07:26:01 -0700 Subject: [PATCH 07/23] fix(sandbox): preserve credential revocation after rebase Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- .../src/provider_credentials.rs | 32 +++++++++++++++++++ crates/openshell-server/src/storage_proto.rs | 6 ++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/openshell-core/src/provider_credentials.rs b/crates/openshell-core/src/provider_credentials.rs index f5456f4eaa..fee82e0d3f 100644 --- a/crates/openshell-core/src/provider_credentials.rs +++ b/crates/openshell-core/src/provider_credentials.rs @@ -781,6 +781,10 @@ impl ProviderCredentialState { ); inner.static_credential_bindings = bindings; inner.non_secret_environment_keys = non_secret_keys; + inner.body_inventory_available = true; + inner + .known_body_keys + .extend(snapshot.child_env.keys().cloned()); inner.current = Arc::new(snapshot); inner.current.child_env.len() } @@ -2457,6 +2461,34 @@ mod tests { ); } + #[test] + fn prepared_install_restores_body_inventory_after_revocation() { + let live = ProviderCredentialState::from_environment( + 1, + HashMap::new(), + HashMap::new(), + HashMap::new(), + ); + live.revoke_static_provider_environment(2); + assert!(!live.inner.read().unwrap().body_inventory_available); + let candidate = ProviderCredentialState::from_bound_environment( + 3, + HashMap::from([("NEW_KEY".to_string(), "secret".to_string())]), + HashMap::new(), + HashMap::new(), + HashMap::from([( + "NEW_KEY".to_string(), + binding("api.example.com", 443, "/**"), + )]), + Vec::new(), + ) + .unwrap(); + live.install_prepared(&candidate); + let inner = live.inner.read().unwrap(); + assert!(inner.body_inventory_available); + assert!(inner.known_body_keys.contains("NEW_KEY")); + } + #[test] fn prepared_install_preserves_suppression_and_rejects_replaced_identity() { let make_state = |revision, identity: &str, secret: &str| { diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index a0dcc3521d..a0a35ec22e 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -513,12 +513,12 @@ mod tests { } assert_eq!( compiled_method_count, - 101 + PROVIDER_READINESS_RPC_SIGNATURES.len(), + 102 + PROVIDER_READINESS_RPC_SIGNATURES.len(), "classify every compiled RPC" ); assert_eq!( methods.len(), - 75 + PROVIDER_READINESS_RPC_SIGNATURES.len(), + 76 + PROVIDER_READINESS_RPC_SIGNATURES.len(), "inventory every public gateway RPC" ); assert_eq!( @@ -526,7 +526,7 @@ mod tests { .iter() .filter(|method| method.starts_with("openshell.v1.OpenShell/")) .count(), - 75 + PROVIDER_READINESS_RPC_SIGNATURES.len() + 76 + PROVIDER_READINESS_RPC_SIGNATURES.len() ); assert!(methods.iter().all(|method| !method.contains(".storage."))); From 766b6ecd0755b93bb83c1ccfa693a7dbb8322c4d Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Tue, 15 Sep 2026 07:32:55 -0700 Subject: [PATCH 08/23] fix(server): retain provider revision helper for endpoint reports Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- crates/openshell-server/src/grpc/policy.rs | 1 - .../v1/internal/converter/coverage_test.go | 18 +++++++++--------- sdk/go/openshell/v1/types/sandbox.go | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 63e0347127..77f6ef49b9 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -2858,7 +2858,6 @@ pub(super) async fn compute_provider_env_revision_with_catalog( .await } -#[cfg(test)] async fn compute_provider_env_revision_with_catalog_and_policy_bindings( store: &Store, catalog: &EffectiveProviderProfileCatalog, diff --git a/sdk/go/openshell/v1/internal/converter/coverage_test.go b/sdk/go/openshell/v1/internal/converter/coverage_test.go index bab68ecb84..336ebdbe49 100644 --- a/sdk/go/openshell/v1/internal/converter/coverage_test.go +++ b/sdk/go/openshell/v1/internal/converter/coverage_test.go @@ -115,15 +115,15 @@ func TestConverterCoversAllProtoFields_SandboxStartup(t *testing.T) { func TestConverterCoversAllProtoFields_SandboxStatus(t *testing.T) { handled := fieldSet{ - "sandbox_name": true, - "agent_pod": true, - "agent_fd": true, - "sandbox_fd": true, - "phase": true, - "conditions": true, - "endpoint_statuses": true, - "current_policy_version": true, - "exit_code": true, + "sandbox_name": true, + "agent_pod": true, + "agent_fd": true, + "sandbox_fd": true, + "phase": true, + "conditions": true, + "endpoint_statuses": true, + "current_policy_version": true, + "exit_code": true, "configuration_admission": true, } // The instance ID coordinates internal gateway/supervisor lifecycle diff --git a/sdk/go/openshell/v1/types/sandbox.go b/sdk/go/openshell/v1/types/sandbox.go index 25db870811..8d9436979d 100644 --- a/sdk/go/openshell/v1/types/sandbox.go +++ b/sdk/go/openshell/v1/types/sandbox.go @@ -119,7 +119,7 @@ type SandboxStatus struct { ExitCode *int32 // EndpointStatuses describes configured external tool endpoints and their // last accepted network results, independently of sandbox readiness. - EndpointStatuses []EndpointStatus + EndpointStatuses []EndpointStatus ConfigurationAdmission *SandboxConfigurationAdmission } From 58a72c58f1b7e07dbd751984b54e46b7a0c995e0 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Tue, 15 Sep 2026 07:40:25 -0700 Subject: [PATCH 09/23] fix(server): import provider object trait in production Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- crates/openshell-server/src/grpc/policy.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 77f6ef49b9..ba6ffcc621 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -25,10 +25,9 @@ use crate::auth::workspace_authz::{ require_platform_admin, selected_workspace_name, }; use crate::pagination::Pagination; -#[cfg(test)] -use crate::persistence::ObjectType; use crate::persistence::{ - DraftChunkRecord, ObjectId, ObjectListQuery, ObjectName, ObjectWorkspace, PolicyRecord, Store, + DraftChunkRecord, ObjectId, ObjectListQuery, ObjectName, ObjectType, ObjectWorkspace, + PolicyRecord, Store, }; use crate::policy_store::{AtomicPolicyRevisionWrite, PolicyStoreExt}; use crate::provider_profile_sources::EffectiveProviderProfileCatalog; From eb39c4e9891a524de11492e97cca3d258bb84603 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Wed, 16 Sep 2026 09:54:33 -0700 Subject: [PATCH 10/23] fix(supervisor): preserve admission across boundary extraction Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- architecture/sandbox.md | 16 +- .../src/boundary_protocol.rs | 7 + .../openshell-sandbox-backend/src/runtime.rs | 20 +- .../openshell-sandbox/src/boundary_server.rs | 84 +- crates/openshell-supervisor/Cargo.toml | 1 + crates/openshell-supervisor/src/lib.rs | 1465 ++++++++++++----- docs/sandboxes/policies.mdx | 2 +- skills/debug-openshell-cluster/SKILL.md | 8 +- 8 files changed, 1191 insertions(+), 412 deletions(-) diff --git a/architecture/sandbox.md b/architecture/sandbox.md index d01d2e3a81..a7af39d59b 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -642,8 +642,8 @@ so it cannot replace the configuration captured for that launch. Restart resets admission and requires a fresh accepted configuration. Permanent gateway errors and exhausted transient retries terminate startup; each RPC attempt has a 10-second deadline, including acceptance reports; only acknowledged configuration -rejections wait indefinitely for repair. Process-sidecar discovery uses the -existing sidecar readiness timeout. +rejections wait indefinitely for repair. Image discovery uses the authenticated +sandbox boundary control request deadline. Policy and provider refreshes are prepared before publication. Publication invalidates prior policy guards before exposing new provider material and swaps @@ -651,11 +651,13 @@ the policy under the same publication locks. Rejected candidates cannot install their credentials alongside the previous policy. Existing runtime fail-closed checks remain necessary for in-flight traffic and invalid live updates. -In sidecar topology, the authenticated process supervisor supplies discovery -from the workload image over the existing control socket. The network supervisor -withholds bootstrap until admission succeeds, then sends the accepted policy and -child environment together. Subsequent configuration messages carry both parts -and an ordered generation; older messages cannot restore stale child credentials. +The supervisor reads the workload image policy through an authenticated, +read-only `DiscoverPolicy` boundary request before attaching or launching the +workload. The boundary reads only the well-known policy paths, bounds the response, +and distinguishes missing policy from unreadable or invalid content. The supervisor +validates this candidate with gateway provider composition and obtains admission +before `Attach`, `Confirm`, networking startup, and `StartAgent`. Workload image +environment variables cannot configure the isolated supervisor. ## Policy Revision Acknowledgement diff --git a/crates/openshell-sandbox-backend/src/boundary_protocol.rs b/crates/openshell-sandbox-backend/src/boundary_protocol.rs index 73668daae1..05542fb2bc 100644 --- a/crates/openshell-sandbox-backend/src/boundary_protocol.rs +++ b/crates/openshell-sandbox-backend/src/boundary_protocol.rs @@ -520,6 +520,8 @@ impl fmt::Debug for RequestEnvelope { #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "operation", rename_all = "snake_case")] pub enum Request { + /// Read image policy without attaching or launching a workload. + DiscoverPolicy, Attach { supervisor_instance_id: SupervisorInstanceId, policy: Box, @@ -603,6 +605,7 @@ impl Request { impl fmt::Debug for Request { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::DiscoverPolicy => formatter.write_str("DiscoverPolicy"), Self::Attach { supervisor_instance_id: _, policy: _, @@ -701,6 +704,10 @@ pub struct ResponseEnvelope { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "result", rename_all = "snake_case")] pub enum Response { + ImagePolicy { + yaml: Option, + invalid: bool, + }, Attached { snapshot: SessionSnapshotWire, }, diff --git a/crates/openshell-sandbox-backend/src/runtime.rs b/crates/openshell-sandbox-backend/src/runtime.rs index 96085e0940..1b56d6df9d 100644 --- a/crates/openshell-sandbox-backend/src/runtime.rs +++ b/crates/openshell-sandbox-backend/src/runtime.rs @@ -69,6 +69,20 @@ pub struct OpenShellRuntimeBackend { } impl OpenShellRuntimeBackend { + /// Read the workload image policy over the authenticated boundary before admission. + pub async fn discover_policy( + descriptor: SandboxRuntimeDescriptor, + bearer: openshell_core::jwt::SessionBearerTokenSlot, + ) -> Result<(Option, bool), BackendError> { + let client = BoundaryClient::new(descriptor, bearer); + match client.call_idempotent(Request::DiscoverPolicy).await? { + Response::ImagePolicy { yaml, invalid } => Ok((yaml, invalid)), + _ => Err(BackendError::Descriptor( + "expected image policy discovery response".to_string(), + )), + } + } + pub fn new( ca_file_paths: Arc>>, provider_credentials: openshell_core::provider_credentials::ProviderCredentialState, @@ -1074,7 +1088,7 @@ impl BoundaryClient { async fn call_idempotent(&self, request: Request) -> Result { let remember_attach = matches!(request, Request::Attach { .. }); let remember_confirm = matches!(request, Request::Confirm); - let timeout = if remember_attach { + let timeout = if remember_attach || matches!(request, Request::DiscoverPolicy) { ATTACH_REQUEST_TIMEOUT } else { REQUEST_TIMEOUT @@ -1999,6 +2013,10 @@ mod tests { match encode_frame(&ResponseEnvelope { request_id: envelope.request_id, response: match envelope.request { + Request::DiscoverPolicy => Response::ImagePolicy { + yaml: None, + invalid: false, + }, Request::Attach { .. } => Response::Attached { snapshot: crate::boundary_protocol::SessionSnapshotWire { generation: "test-generation".to_string(), diff --git a/crates/openshell-sandbox/src/boundary_server.rs b/crates/openshell-sandbox/src/boundary_server.rs index a926074f95..0890050cdc 100644 --- a/crates/openshell-sandbox/src/boundary_server.rs +++ b/crates/openshell-sandbox/src/boundary_server.rs @@ -1537,12 +1537,53 @@ mod linux { .map_err(|error| tonic::Status::unauthenticated(error.to_string())) } + fn discover_image_policy() -> Response { + Self::discover_image_policy_from_paths(&[ + openshell_policy::CONTAINER_POLICY_PATH, + openshell_policy::LEGACY_CONTAINER_POLICY_PATH, + ]) + } + + fn discover_image_policy_from_paths(paths: &[&str]) -> Response { + use std::io::Read as _; + for path in paths { + match std::fs::File::open(path) { + Ok(file) => { + let mut yaml = String::new(); + if file.take(1_048_577).read_to_string(&mut yaml).is_err() + || yaml.len() > 1_048_576 + { + return Response::ImagePolicy { + yaml: None, + invalid: true, + }; + } + return Response::ImagePolicy { + yaml: Some(yaml), + invalid: false, + }; + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(_) => { + return Response::ImagePolicy { + yaml: None, + invalid: true, + }; + } + } + } + Response::ImagePolicy { + yaml: None, + invalid: false, + } + } + fn authorize_request( &self, principal: &SandboxProtocolPrincipal, request: &Request, ) -> Result<(), String> { - if matches!(request, Request::Attach { .. }) { + if matches!(request, Request::Attach { .. } | Request::DiscoverPolicy) { return Ok(()); } if matches!(request, Request::Confirm) { @@ -1830,6 +1871,7 @@ mod linux { let request_id = envelope.request_id; let payload_digest = envelope.payload_digest; let response = match envelope.request { + Request::DiscoverPolicy => Self::discover_image_policy(), Request::Attach { supervisor_instance_id: _, policy, @@ -4258,6 +4300,46 @@ mod linux { ) } + #[test] + fn image_discovery_preserves_invalid_content_and_bounds_reads() { + let directory = tempfile::tempdir().unwrap(); + let primary = directory.path().join("policy.yaml"); + let legacy = directory.path().join("legacy.yaml"); + let paths = [primary.to_str().unwrap(), legacy.to_str().unwrap()]; + assert_eq!( + BoundaryRuntime::discover_image_policy_from_paths(&paths), + Response::ImagePolicy { + yaml: None, + invalid: false + } + ); + std::fs::write(&legacy, "legacy policy").unwrap(); + assert_eq!( + BoundaryRuntime::discover_image_policy_from_paths(&paths), + Response::ImagePolicy { + yaml: Some("legacy policy".to_string()), + invalid: false + } + ); + // Parsing belongs to admission; malformed primary content must never fall back. + std::fs::write(&primary, "not: [valid yaml").unwrap(); + assert_eq!( + BoundaryRuntime::discover_image_policy_from_paths(&paths), + Response::ImagePolicy { + yaml: Some("not: [valid yaml".to_string()), + invalid: false + } + ); + std::fs::write(&primary, vec![b'x'; 1_048_577]).unwrap(); + assert_eq!( + BoundaryRuntime::discover_image_policy_from_paths(&paths), + Response::ImagePolicy { + yaml: None, + invalid: true + } + ); + } + #[test] fn unix_listener_allows_authenticated_cross_uid_control() { use std::os::unix::fs::PermissionsExt as _; diff --git a/crates/openshell-supervisor/Cargo.toml b/crates/openshell-supervisor/Cargo.toml index 2b7197a596..d4fb9cee68 100644 --- a/crates/openshell-supervisor/Cargo.toml +++ b/crates/openshell-supervisor/Cargo.toml @@ -51,6 +51,7 @@ telemetry = ["openshell-core/telemetry"] bundled-ca-roots = ["openshell-supervisor-network/bundled-ca-roots"] [dev-dependencies] +tokio = { workspace = true, features = ["test-util"] } futures = { workspace = true } temp-env = "0.3" tokio-tungstenite = { workspace = true } diff --git a/crates/openshell-supervisor/src/lib.rs b/crates/openshell-supervisor/src/lib.rs index 3443b8fae6..0ea1fcc7b0 100644 --- a/crates/openshell-supervisor/src/lib.rs +++ b/crates/openshell-supervisor/src/lib.rs @@ -465,7 +465,7 @@ pub async fn run_network_proxy( } let extension_credentials = openshell_extension_core::ExtensionCredentialStore::new(); - let (mut policy, opa_engine, _, _, _, initial_agent_proposals_enabled, _) = load_policy( + let (mut policy, opa_engine, _, _, _, initial_agent_proposals_enabled, _, _) = load_policy( None, None, None, @@ -611,6 +611,33 @@ pub async fn run_sandbox( // and the policy poll loop that rotates them stay the same objects. let extension_credentials = openshell_extension_core::ExtensionCredentialStore::new(); + let runtime_descriptor: openshell_sandbox_backend::boundary_protocol::SandboxRuntimeDescriptor = + serde_json::from_slice(&backend_descriptor.payload) + .map_err(|error| miette::miette!("decode sandbox runtime descriptor: {error}"))?; + if auth_bundle.runtime_generation.as_str() != runtime_descriptor.generation { + return Err(miette::miette!( + "supervisor authentication bundle does not match runtime generation" + )); + } + let sandbox_bearer = openshell_core::grpc_client::install_supervisor_auth_bundle(&auth_bundle)?; + let (image_yaml, invalid_image) = + openshell_sandbox_backend::OpenShellRuntimeBackend::discover_policy( + runtime_descriptor.clone(), + sandbox_bearer.clone(), + ) + .await + .map_err(|error| miette::miette!("discover workload image policy: {error}"))?; + let image_discovery = if invalid_image { + ImagePolicyDiscovery::Invalid + } else if let Some(yaml) = image_yaml { + openshell_policy::parse_sandbox_policy(&yaml) + .map_or(ImagePolicyDiscovery::Invalid, |policy| { + ImagePolicyDiscovery::Policy(Box::new(policy)) + }) + } else { + ImagePolicyDiscovery::Missing + }; + // Load policy and initialize OPA engine let openshell_endpoint_for_proxy = openshell_endpoint.clone(); let sandbox_name_for_agg = sandbox.clone(); @@ -622,7 +649,8 @@ pub async fn run_sandbox( loaded_policy_origin, initial_agent_proposals_enabled, initial_extension_authentication_enabled, - ) = load_policy( + captured_provider_credentials, + ) = load_policy_with_gateway( sandbox_id.clone(), sandbox, openshell_endpoint.clone(), @@ -630,6 +658,10 @@ pub async fn run_sandbox( policy_data, &extension_credentials, LocalPolicyIdentity::Required, + Some(image_discovery), + &RemoteStartupGateway { + endpoint: openshell_endpoint.clone().unwrap_or_default(), + }, ) .await?; @@ -642,7 +674,9 @@ pub async fn run_sandbox( let workspace = workdir; let provider_readiness = ProviderReadinessTracker::new(); - let provider_credentials = { + let provider_credentials = if let Some(credentials) = captured_provider_credentials { + credentials + } else { // 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). @@ -709,6 +743,9 @@ pub async fn run_sandbox( // snapshot that produced the policy so networking and process setup agree // before the poll loop starts reconciling later changes. let agent_proposals = AgentProposals::new(initial_agent_proposals_enabled); + // Keep the accepted launch generation fixed until the child has actually + // spawned. Live reconciliation must not race its captured policy/env. + let (workload_started_tx, workload_started_rx) = tokio::sync::watch::channel(false); // Shared PID: set after process spawn so the proxy can look up // the entrypoint process's /proc/net/tcp for identity binding. @@ -720,15 +757,6 @@ pub async fn run_sandbox( let admitted_backend_name = admitted_isolation_backend.ok_or_else(|| { miette::miette!("runtime descriptor supplied without an admitted isolation backend") })?; - let runtime_descriptor: openshell_sandbox_backend::boundary_protocol::SandboxRuntimeDescriptor = - serde_json::from_slice(&backend_descriptor.payload) - .map_err(|error| miette::miette!("decode sandbox runtime descriptor: {error}"))?; - if auth_bundle.runtime_generation.as_str() != runtime_descriptor.generation { - return Err(miette::miette!( - "supervisor authentication bundle does not match runtime generation" - )); - } - let sandbox_bearer = openshell_core::grpc_client::install_supervisor_auth_bundle(&auth_bundle)?; let session_id = runtime_descriptor.session_id; let ca_file_paths = Arc::new(std::sync::Mutex::new(None)); let backend: Arc = @@ -1020,6 +1048,10 @@ pub async fn run_sandbox( }; tokio::spawn(async move { + let mut workload_started = workload_started_rx; + if workload_started.wait_for(|started| *started).await.is_err() { + return; + } if let Err(e) = run_policy_poll_loop(poll_ctx).await { ocsf_emit!( AppLifecycleBuilder::new(ocsf_ctx()) @@ -1053,6 +1085,7 @@ pub async fn run_sandbox( .start_agent() .await .map_err(|error| miette::miette!(error.to_string()))?; + workload_started_tx.send_replace(true); info!(backend = %backend_name, "Isolation boundary agent started"); let agent = running.agent(); let boundary_access = openshell_supervisor_process::delegated::start_boundary_access( @@ -2010,6 +2043,18 @@ fn is_retryable_error(err: &miette::Report) -> bool { true } +/// Bound the complete attempt, including connection setup and a pending unary +/// response. Dropping a timed-out future prevents it from stalling startup. +async fn grpc_attempt(op_name: &str, operation: impl Future>) -> Result { + timeout(Duration::from_secs(10), operation) + .await + .map_err(|_| { + openshell_core::grpc_client::grpc_status_error(tonic::Status::deadline_exceeded( + format!("{op_name} timed out after 10 seconds"), + )) + })? +} + /// Retry a gRPC operation with exponential backoff (capped at 4 s). /// /// Non-transient gRPC errors (e.g. `NOT_FOUND`, `INVALID_ARGUMENT`, @@ -2021,7 +2066,7 @@ where { let mut last_err = None; for attempt in 1..=5u32 { - match f().await { + match grpc_attempt(op_name, f()).await { Ok(val) => return Ok(val), Err(e) => { if !is_retryable_error(&e) { @@ -2047,6 +2092,81 @@ where )) } +#[tonic::async_trait] +trait StartupGateway: Send + Sync { + async fn snapshot(&self, id: &str) -> Result; + async fn provider( + &self, + id: &str, + ) -> Result; + async fn sync( + &self, + id: &str, + sandbox: &str, + policy: &openshell_core::proto::SandboxPolicy, + workspace: &str, + ) -> Result; + async fn report( + &self, + id: &str, + instance_id: &str, + snapshot: Option<&openshell_core::grpc_client::SettingsPollResult>, + state: openshell_core::proto::ConfigurationAdmissionState, + error: &str, + ) -> Result<()>; +} + +struct RemoteStartupGateway { + endpoint: String, +} + +#[tonic::async_trait] +impl StartupGateway for RemoteStartupGateway { + async fn snapshot(&self, id: &str) -> Result { + openshell_core::grpc_client::fetch_settings_snapshot(&self.endpoint, id).await + } + async fn provider( + &self, + id: &str, + ) -> Result { + openshell_core::grpc_client::fetch_provider_environment(&self.endpoint, id).await + } + async fn sync( + &self, + id: &str, + sandbox: &str, + policy: &openshell_core::proto::SandboxPolicy, + workspace: &str, + ) -> Result { + openshell_core::grpc_client::sync_policy_and_fetch_snapshot( + &self.endpoint, + id, + sandbox, + policy, + workspace, + ) + .await + } + async fn report( + &self, + id: &str, + instance_id: &str, + snapshot: Option<&openshell_core::grpc_client::SettingsPollResult>, + state: openshell_core::proto::ConfigurationAdmissionState, + error: &str, + ) -> Result<()> { + openshell_core::grpc_client::report_sandbox_configuration( + &self.endpoint, + id, + instance_id, + snapshot, + state, + error, + ) + .await + } +} + /// Load sandbox policy from local files or gRPC. /// /// Priority: @@ -2080,9 +2200,56 @@ async fn load_policy( LoadedPolicyOrigin, bool, bool, + Option, +)> { + load_policy_with_gateway( + sandbox_id, + sandbox, + openshell_endpoint.clone(), + policy_rules, + policy_data, + extension_credentials, + local_policy_identity, + None, + &RemoteStartupGateway { + endpoint: openshell_endpoint.unwrap_or_default(), + }, + ) + .await +} + +#[allow( + clippy::too_many_arguments, + reason = "Startup gateway injection preserves the production policy-loading inputs" +)] +async fn load_policy_with_gateway( + sandbox_id: Option, + sandbox: Option, + openshell_endpoint: Option, + policy_rules: Option, + policy_data: Option, + extension_credentials: &openshell_extension_core::ExtensionCredentialStore, + local_policy_identity: LocalPolicyIdentity, + image_discovery: Option, + gateway: &impl StartupGateway, +) -> Result<( + SandboxPolicy, + Option>, + Option, + MiddlewareRegistryStatus, + LoadedPolicyOrigin, + bool, + bool, + Option, )> { + use openshell_core::proto::ConfigurationAdmissionState; // File mode: load OPA engine from rego rules + YAML data (dev override) if let (Some(policy_file), Some(data_file)) = (&policy_rules, &policy_data) { + if sandbox_id.is_some() && openshell_endpoint.is_some() { + return Err(miette::miette!( + "Local policy overrides cannot be combined with gateway-managed activation; replace the sandbox policy through the gateway" + )); + } ocsf_emit!(ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::Informational) .status(StatusId::Success) @@ -2139,6 +2306,7 @@ async fn load_policy( LoadedPolicyOrigin::LocalOverride, false, false, + None, )); } @@ -2149,156 +2317,207 @@ async fn load_policy( endpoint = %endpoint, "Fetching sandbox policy via gRPC" ); - let mut snapshot = grpc_retry("Policy fetch", || { - openshell_core::grpc_client::fetch_settings_snapshot(endpoint, id) + let instance_id = uuid::Uuid::new_v4().to_string(); + // Capture the previous instance once. Registration retries must never + // rebase this fence and displace a newer supervisor instance. + let registration_snapshot = + grpc_retry("Startup configuration fetch", || gateway.snapshot(id)).await?; + grpc_retry("Supervisor registration", || { + gateway.report( + id, + &instance_id, + Some(®istration_snapshot), + ConfigurationAdmissionState::Pending, + "", + ) }) .await?; + let discovery = image_discovery.unwrap_or_else(discover_image_policy); + let mut reconciliation_attempts = 0u32; + loop { + if reconciliation_attempts == 5 { + return Err(miette::miette!( + "Startup configuration did not stabilize after 5 attempts" + )); + } + reconciliation_attempts += 1; + let mut snapshot = + grpc_retry("Startup configuration fetch", || gateway.snapshot(id)).await?; - let mut proto_policy = if let Some(p) = snapshot.policy.clone() { - p - } else { - // No policy configured on the server. Discover from disk or - // fall back to the restrictive default, then sync to the - // gateway so it becomes the authoritative baseline. - ocsf_emit!( - ConfigStateChangeBuilder::new(ocsf_ctx()) - .severity(SeverityId::Informational) - .status(StatusId::Success) - .state(StateId::Other, "discovery") - .message("Server returned no policy; attempting local discovery") - .build() - ); - let mut discovered = discover_policy_from_disk_or_default(); - // Enrich before syncing so the gateway baseline includes - // baseline paths from the start. - enrich_proto_baseline_paths(&mut discovered); - strip_proto_provider_policy_entries(&mut discovered); - let sandbox = sandbox.as_deref().ok_or_else(|| { - miette::miette!( - "Cannot sync discovered policy: sandbox not available.\n\ - Set OPENSHELL_SANDBOX or --sandbox to enable policy sync." - ) - })?; - - // Sync and re-fetch over a single connection to avoid extra - // TLS handshakes. - let ws = snapshot.workspace.clone(); - snapshot = grpc_retry("Policy discovery sync", || { - openshell_core::grpc_client::sync_policy_and_fetch_snapshot( + if snapshot.policy.is_none() && !snapshot.configuration_error.is_empty() { + reject_startup_configuration( + gateway, endpoint, id, - sandbox, - &discovered, - &ws, + &instance_id, + &snapshot, + &snapshot.configuration_error, ) - }) - .await?; - snapshot.policy.clone().ok_or_else(|| { - miette::miette!("Server still returned no policy after sync — this is a bug") - })? - }; + .await?; + reconciliation_attempts = 0; + continue; + } - // True only while `snapshot` describes the exact policy that will be - // constructed below. If enrichment cannot be synced and re-fetched, - // the policy remains enforceable but cannot be acknowledged by - // inferred structural equality. - let mut policy_bound_to_snapshot = true; - - // Ensure baseline filesystem paths are present for proxy-mode - // sandboxes. If the policy was enriched, sync the updated version - // back to the gateway so users can see the effective policy. - let enriched = enrich_proto_baseline_paths(&mut proto_policy); - let sync_policy = proto_sync_payload_for_enriched_policy(&proto_policy, enriched); - if let Some(sync_policy) = sync_policy { - if let Some(sandbox_name) = sandbox.as_deref() { - match openshell_core::grpc_client::sync_policy_and_fetch_snapshot( - endpoint, - id, - sandbox_name, - &sync_policy, - &snapshot.workspace, - ) - .await - { - Ok(canonical) => { - if let Some(policy) = canonical.policy.clone() { - proto_policy = policy; - snapshot = canonical; - } else { - policy_bound_to_snapshot = false; - warn!( - "Gateway returned no policy after enrichment sync; initial revision will be reconciled" - ); - } + let mut proto_policy = if let Some(p) = snapshot.policy.clone() { + p + } else { + // No policy configured on the server. Discover from disk or + // fall back to the restrictive default, then sync to the + // gateway so it becomes the authoritative baseline. + ocsf_emit!( + ConfigStateChangeBuilder::new(ocsf_ctx()) + .severity(SeverityId::Informational) + .status(StatusId::Success) + .state(StateId::Other, "discovery") + .message("Server returned no policy; attempting local discovery") + .build() + ); + let mut discovered = match &discovery { + ImagePolicyDiscovery::Policy(policy) => *policy.clone(), + ImagePolicyDiscovery::Missing => openshell_policy::restrictive_default_policy(), + ImagePolicyDiscovery::Invalid => { + reject_startup_configuration(gateway, endpoint, id, &instance_id, &snapshot, "Image policy is invalid; replace the sandbox policy to repair configuration").await?; + reconciliation_attempts = 0; + continue; } - Err(e) => { - policy_bound_to_snapshot = false; - warn!( - error = %e, - "Failed to sync enriched policy back to gateway; initial revision will be reconciled" - ); + }; + // Enrich before syncing so the gateway baseline includes + // baseline paths from the start. + enrich_proto_baseline_paths(&mut discovered); + strip_proto_provider_policy_entries(&mut discovered); + let sandbox = sandbox.as_deref().ok_or_else(|| { + miette::miette!( + "Cannot sync discovered policy: sandbox not available.\n\ + Set OPENSHELL_SANDBOX or --sandbox to enable policy sync." + ) + })?; + + // Sync and re-fetch over a single connection to avoid extra + // TLS handshakes. + let ws = snapshot.workspace.clone(); + snapshot = grpc_retry("Image policy synchronization", || { + gateway.sync(id, sandbox, &discovered, &ws) + }) + .await?; + if let Some(policy) = snapshot.policy.clone() { + policy + } else { + if snapshot.configuration_error.is_empty() { + return Err(miette::miette!( + "Gateway returned no effective policy after image discovery" + )); } + reject_startup_configuration( + gateway, + endpoint, + id, + &instance_id, + &snapshot, + "Effective policy is unavailable after image discovery", + ) + .await?; + reconciliation_attempts = 0; + continue; + } + }; + + // Ensure baseline filesystem paths are present for proxy-mode + // sandboxes. If the policy was enriched, sync the updated version + // back to the gateway so users can see the effective policy. + let enriched = enrich_proto_baseline_paths(&mut proto_policy); + let sync_policy = proto_sync_payload_for_enriched_policy(&proto_policy, enriched); + if let Some(sync_policy) = sync_policy { + if let Some(sandbox_name) = sandbox.as_deref() { + let canonical = grpc_retry("Enriched policy synchronization", || { + gateway.sync(id, sandbox_name, &sync_policy, &snapshot.workspace) + }) + .await?; + proto_policy = canonical.policy.clone().ok_or_else(|| { + miette::miette!("Gateway returned no effective policy after enrichment") + })?; + snapshot = canonical; + } else { + return Err(miette::miette!( + "Cannot sync enriched policy: sandbox name is unavailable" + )); } - } else { - policy_bound_to_snapshot = false; } - } - let mut loaded_policy_revision = - policy_bound_to_snapshot.then(|| LoadedPolicyRevision::from_snapshot(&snapshot)); - - // Build OPA engine from baked-in rules + typed proto data. - // In cluster mode, proxy networking is always enabled so OPA is - // always required for allow/deny decisions. - // The initial load uses pid=0 (no symlink resolution) because the - // container hasn't started yet. After the entrypoint spawns, the - // engine is rebuilt with the real PID for symlink resolution. - info!("Creating OPA engine from proto policy data"); - let mut has_last_valid_policy = true; - let engine = match OpaEngine::from_proto(&proto_policy) { - Ok(engine) => Arc::new(engine), - Err(e) => { - report_initial_policy_failure(endpoint, id, loaded_policy_revision.as_ref(), &e) + let loaded_policy_revision = Some({ + let mut revision = LoadedPolicyRevision::from_snapshot(&snapshot); + revision.admission_instance_id = Some(instance_id.clone()); + revision + }); + + // Build OPA engine from baked-in rules + typed proto data. + // In cluster mode, proxy networking is always enabled so OPA is + // always required for allow/deny decisions. + // The initial load uses pid=0 (no symlink resolution) because the + // container hasn't started yet. After the entrypoint spawns, the + // engine is rebuilt with the real PID for symlink resolution. + info!("Creating OPA engine from proto policy data"); + let has_last_valid_policy = true; + if !snapshot.configuration_admitted { + reject_startup_configuration( + gateway, + endpoint, + id, + &instance_id, + &snapshot, + if snapshot.configuration_error.is_empty() { + "Effective configuration was rejected by admission" + } else { + &snapshot.configuration_error + }, + ) + .await?; + reconciliation_attempts = 0; + continue; + } + let provider = + grpc_retry("Startup provider environment", || gateway.provider(id)).await?; + if provider.provider_env_revision != snapshot.provider_env_revision { + tokio::time::sleep(Duration::from_secs(1u64 << reconciliation_attempts.min(2))) .await; - let validation_error = e.to_string(); - let candidate_version = snapshot.version; - let candidate_hash = snapshot.policy_hash.clone(); - // There is no in-memory last-known-good generation during - // startup, so both configured modes necessarily fail closed. - // Load the restrictive default atomically and keep the - // rejected revision unacknowledged for poll reconciliation. - has_last_valid_policy = false; - proto_policy = openshell_policy::restrictive_default_policy(); - let engine = Arc::new(OpaEngine::from_proto(&proto_policy)?); - let disposition = apply_policy_validation_failure( - &engine, - snapshot.policy_validation_failure_mode, - has_last_valid_policy, - candidate_version, - &validation_error, - )?; - emit_policy_validation_failure( - &disposition, - candidate_version, - &candidate_hash, - &validation_error, - ); - loaded_policy_revision = None; - engine + continue; } - }; - - // Install the in-process catalog before any external connection can - // fail. A newly started sandbox must always be able to resolve built-in - // bindings, even while operator-run services are unavailable. - install_builtin_middleware_registry(&engine).await?; - - // Connect operator-registered middleware services. A connect/describe - // failure keeps the built-in registry active so each request's - // `on_error` policy governs matched traffic. The policy poll loop - // retries the install without waiting for a config change. - let middleware_services = snapshot.supervisor_middleware_services.clone(); - let middleware_registry_status = if middleware_services.is_empty() { + let (engine, policy, captured_provider_credentials) = + match prepare_startup_configuration(&snapshot, &proto_policy, &provider) { + Ok(prepared) => prepared, + Err(error) => { + report_initial_policy_failure( + endpoint, + id, + loaded_policy_revision.as_ref(), + &error, + ) + .await; + reject_startup_configuration( + gateway, + endpoint, + id, + &instance_id, + &snapshot, + "Policy or provider environment failed runtime validation", + ) + .await?; + reconciliation_attempts = 0; + continue; + } + }; + let engine = Arc::new(engine); + + // Install the in-process catalog before any external connection can + // fail. A newly started sandbox must always be able to resolve built-in + // bindings, even while operator-run services are unavailable. + install_builtin_middleware_registry(&engine).await?; + + // Connect operator-registered middleware services. A connect/describe + // failure keeps the built-in registry active so each request's + // `on_error` policy governs matched traffic. The policy poll loop + // retries the install without waiting for a config change. + let middleware_services = snapshot.supervisor_middleware_services.clone(); + let middleware_registry_status = if middleware_services.is_empty() { MiddlewareRegistryStatus::Synchronized } else if let Err(error) = grpc_retry("Middleware connect", || { let middleware_services = middleware_services.clone(); @@ -2349,28 +2568,44 @@ async fn load_policy( } else { MiddlewareRegistryStatus::Synchronized }; - let opa_engine = Some(engine); - - let policy = match SandboxPolicy::try_from(proto_policy.clone()) { - Ok(policy) => policy, - Err(e) => { - report_initial_policy_failure(endpoint, id, loaded_policy_revision.as_ref(), &e) + let opa_engine = Some(engine); + + // The gateway compares the entire tuple again. A concurrent repair or + // provider rotation invalidates this candidate before any workload + // identity, child environment or services are captured. + if let Err(error) = grpc_attempt( + "Startup acceptance report", + gateway.report( + id, + &instance_id, + Some(&snapshot), + ConfigurationAdmissionState::Accepted, + "", + ), + ) + .await + { + if !is_retryable_error(&error) { + return Err(error); + } + tokio::time::sleep(Duration::from_secs(1u64 << reconciliation_attempts.min(2))) .await; - return Err(e); + continue; } - }; - return Ok(( - policy, - opa_engine, - Some(proto_policy), - middleware_registry_status, - LoadedPolicyOrigin::Gateway { - revision: loaded_policy_revision, - has_last_valid_policy, - }, - agent_proposals_enabled_from_settings(&snapshot.settings), - snapshot.extension_authentication_enabled, - )); + return Ok(( + policy, + opa_engine, + Some(proto_policy), + middleware_registry_status, + LoadedPolicyOrigin::Gateway { + revision: loaded_policy_revision, + has_last_valid_policy, + }, + agent_proposals_enabled_from_settings(&snapshot.settings), + snapshot.extension_authentication_enabled, + Some(captured_provider_credentials), + )); + } } // No policy source available @@ -2381,112 +2616,103 @@ async fn load_policy( )) } -/// Try to discover a sandbox policy from the well-known disk path, falling -/// back to the legacy path, then to the hardcoded restrictive default. -fn discover_policy_from_disk_or_default() -> openshell_core::proto::SandboxPolicy { - let primary = std::path::Path::new(openshell_policy::CONTAINER_POLICY_PATH); - if primary.exists() { - return discover_policy_from_path(primary); - } - let legacy = std::path::Path::new(openshell_policy::LEGACY_CONTAINER_POLICY_PATH); - if legacy.exists() { - ocsf_emit!( - ConfigStateChangeBuilder::new(ocsf_ctx()) - .severity(SeverityId::Informational) - .status(StatusId::Success) - .state(StateId::Enabled, "loaded") - .unmapped( - "legacy_path", - serde_json::json!(legacy.display().to_string()) - ) - .unmapped("new_path", serde_json::json!(primary.display().to_string())) - .message(format!( - "Policy found at legacy path; consider moving [legacy_path:{} new_path:{}]", - legacy.display(), - primary.display() - )) - .build() - ); - return discover_policy_from_path(legacy); +/// Capture the policy from the workload filesystem, preserving invalid input +/// as a repairable error instead of silently substituting another policy. +#[derive(Clone, Debug)] +enum ImagePolicyDiscovery { + Missing, + Invalid, + Policy(Box), +} + +fn discover_image_policy() -> ImagePolicyDiscovery { + for path in [ + openshell_policy::CONTAINER_POLICY_PATH, + openshell_policy::LEGACY_CONTAINER_POLICY_PATH, + ] { + match discover_image_policy_from_path(std::path::Path::new(path)) { + ImagePolicyDiscovery::Missing => {} + discovered => return discovered, + } } - discover_policy_from_path(primary) + ImagePolicyDiscovery::Missing } -/// Try to read a sandbox policy YAML from `path`, falling back to the -/// hardcoded restrictive default if the file is missing or invalid. -fn discover_policy_from_path(path: &std::path::Path) -> openshell_core::proto::SandboxPolicy { - use openshell_policy::{ - parse_sandbox_policy_file, restrictive_default_policy, validate_sandbox_policy, - }; +fn discover_image_policy_from_path(path: &std::path::Path) -> ImagePolicyDiscovery { + if matches!(path.try_exists(), Ok(false)) { + return ImagePolicyDiscovery::Missing; + } + openshell_policy::parse_sandbox_policy_file(path) + .map_or(ImagePolicyDiscovery::Invalid, |policy| { + ImagePolicyDiscovery::Policy(Box::new(policy)) + }) +} - if !path.exists() { - ocsf_emit!( - ConfigStateChangeBuilder::new(ocsf_ctx()) - .severity(SeverityId::Informational) - .status(StatusId::Success) - .state(StateId::Enabled, "default") - .message(format!( - "No policy file on disk, using restrictive default [path:{}]", - path.display() - )) - .build() - ); - return restrictive_default_policy(); +/// Everything here is preparation: it cannot mutate an accepted generation. +fn prepare_startup_configuration( + snapshot: &openshell_core::grpc_client::SettingsPollResult, + policy: &openshell_core::proto::SandboxPolicy, + provider: &openshell_core::grpc_client::ProviderEnvironmentResult, +) -> Result<(OpaEngine, SandboxPolicy, ProviderCredentialState)> { + if !snapshot.configuration_admitted { + return Err(miette::miette!( + "Effective configuration admission rejected" + )); } - match parse_sandbox_policy_file(path) { - Ok(policy) => { - ocsf_emit!( - ConfigStateChangeBuilder::new(ocsf_ctx()) - .severity(SeverityId::Informational) - .status(StatusId::Success) - .state(StateId::Enabled, "loaded") - .message(format!( - "Loaded sandbox policy from container disk [path:{}]", - path.display() - )) - .build() - ); - // Validate the disk-loaded policy for safety. - if let Err(violations) = validate_sandbox_policy(&policy) { - let messages: Vec = violations.iter().map(ToString::to_string).collect(); - ocsf_emit!(DetectionFindingBuilder::new(ocsf_ctx()) - .activity(ActivityId::Open) - .severity(SeverityId::Medium) - .action(ActionId::Denied) - .disposition(DispositionId::Blocked) - .finding_info( - FindingInfo::new( - "unsafe-disk-policy", - "Unsafe Disk Policy Content", - ) - .with_desc(&format!( - "Disk policy at {} contains unsafe content: {}", - path.display(), - messages.join("; "), - )), - ) - .message(format!( - "Disk policy contains unsafe content, using restrictive default [path:{}]", - path.display() - )) - .build()); - return restrictive_default_policy(); - } - policy - } - Err(e) => { - ocsf_emit!(ConfigStateChangeBuilder::new(ocsf_ctx()) - .severity(SeverityId::Medium) - .status(StatusId::Failure) - .state(StateId::Other, "fallback") - .message(format!( - "Failed to parse disk policy, using restrictive default [path:{} error:{e}]", - path.display() - )) - .build()); - restrictive_default_policy() - } + if snapshot.provider_env_revision != provider.provider_env_revision { + return Err(miette::miette!( + "Provider environment revision changed during configuration preparation" + )); } + let engine = OpaEngine::from_proto(policy)?; + let process_policy = SandboxPolicy::try_from(policy.clone())?; + let credentials = prepare_provider_environment(provider)?; + Ok((engine, process_policy, credentials)) +} + +fn prepare_provider_environment( + provider: &openshell_core::grpc_client::ProviderEnvironmentResult, +) -> Result { + ProviderCredentialState::from_bound_environment( + provider.provider_env_revision, + provider.environment.clone(), + provider.credential_expires_at_ms.clone(), + provider.dynamic_credentials.clone(), + provider.static_credential_bindings.clone(), + provider.non_secret_environment_keys.clone(), + ) + .map_err(|_| miette::miette!("Provider credential bindings are invalid")) +} + +async fn reject_startup_configuration( + gateway: &impl StartupGateway, + _endpoint: &str, + sandbox_id: &str, + instance_id: &str, + snapshot: &openshell_core::grpc_client::SettingsPollResult, + error: &str, +) -> Result<()> { + grpc_retry("Startup rejection report", || { + gateway.report( + sandbox_id, + instance_id, + Some(snapshot), + openshell_core::proto::ConfigurationAdmissionState::Rejected, + error, + ) + }) + .await?; + // Fixed, bounded diagnostics deliberately omit the candidate and credentials. + ocsf_emit!( + ConfigStateChangeBuilder::new(ocsf_ctx()) + .severity(SeverityId::High) + .status(StatusId::Failure) + .state(StateId::Disabled, "configuration_error") + .message(error) + .build() + ); + tokio::time::sleep(Duration::from_secs(2)).await; + Ok(()) } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -2545,12 +2771,32 @@ struct MiddlewareReloadContext<'a> { connector: &'a MiddlewareConnector, } +#[cfg(test)] async fn reload_gateway_policy_runtime( engine: &OpaEngine, policy: Option<&openshell_core::proto::SandboxPolicy>, entrypoint_pid: u32, middleware: MiddlewareReloadContext<'_>, transparent_tcp: TransparentTcpReloadState, +) -> std::result::Result { + reload_gateway_configuration_runtime( + engine, + policy, + entrypoint_pid, + middleware, + transparent_tcp, + || {}, + ) + .await +} + +async fn reload_gateway_configuration_runtime( + engine: &OpaEngine, + policy: Option<&openshell_core::proto::SandboxPolicy>, + entrypoint_pid: u32, + middleware: MiddlewareReloadContext<'_>, + transparent_tcp: TransparentTcpReloadState, + commit_credentials: impl FnOnce(), ) -> std::result::Result { if let Some(policy) = policy && policy_contains_explicit_tcp(policy) @@ -2579,14 +2825,24 @@ async fn reload_gateway_policy_runtime( .await .map_err(GatewayRuntimeReloadError::MiddlewareRegistry)?; engine - .reload_policy_and_middleware_from_proto_with_pid(policy, entrypoint_pid, registry) + .reload_configuration_from_proto_with_pid( + policy, + entrypoint_pid, + Some(registry), + commit_credentials, + ) .map_err(GatewayRuntimeReloadError::PolicyValidation) } // Policy-only change: the installed registry already matches the // delivered service set, so swap the engine alone. This must not // require middleware reachability. Some(policy) => engine - .reload_from_proto_with_pid(policy, entrypoint_pid) + .reload_configuration_from_proto_with_pid( + policy, + entrypoint_pid, + None, + commit_credentials, + ) .map_err(GatewayRuntimeReloadError::PolicyValidation), None => Err(GatewayRuntimeReloadError::PolicyValidation( miette::miette!("runtime reload requires a policy payload but none was returned"), @@ -2648,6 +2904,8 @@ struct LoadedPolicyRevision { policy_hash: String, config_revision: u64, policy_source: openshell_core::proto::PolicySource, + admission_instance_id: Option, + provider_env_revision: u64, } /// Identifies where the policy currently loaded into OPA came from. @@ -2690,6 +2948,8 @@ impl LoadedPolicyRevision { policy_hash: snapshot.policy_hash.clone(), config_revision: snapshot.config_revision, policy_source: snapshot.policy_source, + admission_instance_id: None, + provider_env_revision: snapshot.provider_env_revision, } } } @@ -2777,6 +3037,11 @@ fn initial_policy_ack_candidate( canonical: &openshell_core::grpc_client::SettingsPollResult, ) -> Option { let loaded = loaded?; + if !canonical.configuration_admitted + || canonical.provider_env_revision != loaded.provider_env_revision + { + return None; + } if loaded.policy_source != openshell_core::proto::PolicySource::Sandbox || canonical.policy_source != openshell_core::proto::PolicySource::Sandbox { @@ -3595,6 +3860,49 @@ fn emit_policy_validation_failure( } } +async fn report_runtime_configuration( + ctx: &PolicyPollLoopContext, + snapshot: &openshell_core::grpc_client::SettingsPollResult, + accepted: bool, + error: &str, +) -> bool { + let LoadedPolicyOrigin::Gateway { + revision: Some(revision), + .. + } = &ctx.loaded_policy_origin + else { + return true; + }; + let Some(instance_id) = revision.admission_instance_id.as_deref() else { + return true; + }; + let state = if accepted { + openshell_core::proto::ConfigurationAdmissionState::Accepted + } else { + openshell_core::proto::ConfigurationAdmissionState::Rejected + }; + if !accepted { + ocsf_emit!( + ConfigStateChangeBuilder::new(ocsf_ctx()) + .severity(SeverityId::High) + .status(StatusId::Failure) + .state(StateId::Other, "configuration_error") + .message(error) + .build() + ); + } + openshell_core::grpc_client::report_sandbox_configuration( + &ctx.endpoint, + &ctx.sandbox_id, + instance_id, + Some(snapshot), + state, + error, + ) + .await + .is_ok() +} + async fn run_policy_poll_loop(ctx: PolicyPollLoopContext) -> Result<()> { let client = openshell_core::grpc_client::CachedOpenShellClient::connect_with_credentials( &ctx.endpoint, @@ -3785,6 +4093,30 @@ async fn run_policy_poll_loop_with_client( std::collections::HashMap::new() }; + if reloads_gateway_policy && !result.configuration_admitted { + let disposition = apply_policy_validation_failure( + &ctx.opa_engine, + result.policy_validation_failure_mode, + has_last_valid_policy, + result.version, + &result.configuration_error, + )?; + emit_policy_validation_failure( + &disposition, + result.version, + &result.policy_hash, + &result.configuration_error, + ); + rejected_policy_generation = Some(RejectedPolicyGeneration { + version: result.version, + policy_hash: result.policy_hash.clone(), + validation_error: result.configuration_error.clone(), + configured_mode: result.policy_validation_failure_mode, + }); + report_runtime_configuration(&ctx, &result, false, &result.configuration_error).await; + continue; + } + let config_changed = result.config_revision != current_config_revision; let desired_identity = EnvironmentIdentity::from_settings(&result); let provider_env_changed = result.provider_env_revision != current_provider_env_revision @@ -3803,14 +4135,11 @@ async fn run_policy_poll_loop_with_client( // equals `current_policy_hash`, but the runtime is still quarantined // and must reload (or it would remain deny-all indefinitely). let recovering_rejected_policy = reloads_gateway_policy - && rejected_policy_generation - .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)) + && rejected_policy_generation.is_some() + && result.configuration_admitted; + let policy_runtime_changed = (reloads_gateway_policy && current_policy_generation.as_ref().is_some_and(PolicyGenerationGuard::is_stale)) + || (reloads_gateway_policy && provider_env_changed) + || recovering_rejected_policy || extension_authentication_changed || gateway_policy_runtime_needs_reconciliation( reloads_gateway_policy, @@ -3909,108 +4238,86 @@ async fn run_policy_poll_loop_with_client( .build()); } - if provider_env_changed { - ctx.provider_readiness.credentials_failed( - desired_identity.clone(), - ProviderReadinessReason::WaitingForCredentials, - ); - match client - .fetch_provider_environment(&ctx.endpoint, &ctx.sandbox_id) - .await + // Prepare the matching environment before activation. Failed refreshes + // revoke static credentials while preserving independently bound dynamic grants. + let prepared_provider = if provider_env_changed { + let provider = match openshell_core::grpc_client::fetch_provider_environment( + &ctx.endpoint, + &ctx.sandbox_id, + ) + .await { - Ok(env_result) => { - 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) - .state(StateId::Disabled, "fail_closed") - .message(format!( - "Rejected provider environment refresh; static provider credentials were revoked; fetched dynamic token grants remain active: {error}" - )) - .build() - ); - } 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) - .state(StateId::Enabled, "loaded") - .unmapped( - "provider_env_revision", - serde_json::json!(provider_env_revision) - ) - .message(format!( - "Provider environment refreshed [revision:{provider_env_revision} env_count:{env_count}]" - )) - .build() - ); - } - } else { - ctx.provider_credentials - .revoke_static_provider_environment(env_result.provider_env_revision); - ctx.provider_readiness - .credentials_failed(identity, env_result.readiness_reason); - } + Ok(provider) if provider.provider_env_revision == result.provider_env_revision => { + provider } - Err(e) => { - ctx.provider_readiness.credentials_failed( - desired_identity.clone(), - ProviderReadinessReason::CredentialInstallFailed, - ); + _ => { + ocsf_emit!(ConfigStateChangeBuilder::new(ocsf_ctx()) + .severity(SeverityId::High) + .status(StatusId::Failure) + .state(StateId::Disabled, "fail_closed") + .message("Provider environment refresh failed; static credentials were revoked and previous dynamic grants remain active") + .build()); ctx.provider_credentials .revoke_static_provider_environment(result.provider_env_revision); - warn!( - error = %e, - provider_env_revision = result.provider_env_revision, - "Settings poll: failed to refresh provider environment; static provider credentials were revoked; previous dynamic token grants remain active" - ); - ocsf_emit!( - ConfigStateChangeBuilder::new(ocsf_ctx()) - .severity(SeverityId::High) - .status(StatusId::Failure) - .state(StateId::Disabled, "fail_closed") - .message( - "Provider environment refresh failed; static provider credentials were revoked; previous dynamic token grants remain active" - ) - .build() - ); + endpoint_status::reset( + ctx.endpoint_observation_tx.as_ref(), + current_endpoint_policy.as_ref(), + ¤t_policy_hash, + result.provider_env_revision, + ) + .await; + report_runtime_configuration( + &ctx, + &result, + false, + "Provider environment is unavailable or changed during preparation", + ) + .await; + continue; } + }; + if let Ok(prepared) = prepare_provider_environment(&provider) { + Some(prepared) + } else { + ocsf_emit!(ConfigStateChangeBuilder::new(ocsf_ctx()) + .severity(SeverityId::High) + .status(StatusId::Failure) + .state(StateId::Disabled, "fail_closed") + .message("Provider environment bindings failed validation; static credentials were revoked and fetched dynamic grants remain active") + .build()); + // Repeat the rejected binding validation on the live state to + // revoke static material and retain the fetched dynamic grants. + let _ = ctx.provider_credentials.install_bound_environment( + provider.provider_env_revision, + provider.environment, + provider.credential_expires_at_ms, + provider.dynamic_credentials, + provider.static_credential_bindings, + provider.non_secret_environment_keys, + ); + endpoint_status::reset( + ctx.endpoint_observation_tx.as_ref(), + current_endpoint_policy.as_ref(), + ¤t_policy_hash, + result.provider_env_revision, + ) + .await; + report_runtime_configuration( + &ctx, + &result, + false, + "Provider environment bindings failed validation", + ) + .await; + continue; } - } + } else { + None + }; if policy_runtime_changed { let pid = ctx.entrypoint_pid.load(Ordering::Acquire); - let runtime_result = reload_gateway_policy_runtime( + let runtime_result = reload_gateway_configuration_runtime( &ctx.opa_engine, result.policy.as_ref(), pid, @@ -4024,6 +4331,11 @@ async fn run_policy_poll_loop_with_client( connector: &ctx.middleware_connector, }, ctx.transparent_tcp, + || { + if let Some(prepared) = prepared_provider.as_ref() { + ctx.provider_credentials.install_prepared(prepared); + } + }, ) .await; @@ -4220,6 +4532,17 @@ async fn run_policy_poll_loop_with_client( } } + if policy_runtime_changed && !policy_runtime_reconciled { + report_runtime_configuration(&ctx, &result, false, "Effective configuration failed runtime preparation; prior credentials remain installed").await; + continue; + } + if !reloads_gateway_policy && let Some(prepared) = prepared_provider.as_ref() { + ctx.provider_credentials.install_prepared(prepared); + } + if provider_env_changed || policy_runtime_reconciled { + current_provider_env_revision = result.provider_env_revision; + } + if let Some(version) = unchanged_policy_revision_ready_to_ack( unchanged_policy_revision, policy_runtime_changed, @@ -4275,6 +4598,11 @@ async fn run_policy_poll_loop_with_client( skills::install_static_skills, ); + if !report_runtime_configuration(&ctx, &result, true, "").await { + // Retry the exact status tuple on the next poll before advancing + // the observed revision; an old instance cannot claim readiness. + continue; + } current_config_revision = result.config_revision; if !reloads_gateway_policy { current_policy_hash = result.policy_hash; @@ -4656,15 +4984,12 @@ mod tests { // ---- Policy disk discovery tests ---- #[test] - fn discover_policy_from_nonexistent_path_returns_restrictive_default() { + fn discover_policy_from_nonexistent_path_is_missing() { let path = std::path::Path::new("/nonexistent/policy.yaml"); - let policy = discover_policy_from_path(path); - // Restrictive default has no network policies. - assert!(policy.network_policies.is_empty()); - // It keeps filesystem restrictions while leaving identity to the - // active compute driver. - assert!(policy.filesystem.is_some()); - assert!(policy.process.is_none()); + assert!(matches!( + discover_image_policy_from_path(path), + ImagePolicyDiscovery::Missing + )); } #[test] @@ -4692,7 +5017,9 @@ network_policies: ) .unwrap(); - let policy = discover_policy_from_path(&path); + let ImagePolicyDiscovery::Policy(policy) = discover_image_policy_from_path(&path) else { + panic!("expected parsed policy") + }; assert_eq!(policy.network_policies.len(), 1); assert!(policy.network_policies.contains_key("test")); let fs = policy.filesystem.unwrap(); @@ -4700,34 +5027,31 @@ network_policies: } #[test] - fn discover_policy_from_invalid_yaml_returns_restrictive_default() { + fn discover_policy_from_invalid_yaml_remains_invalid() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("policy.yaml"); std::fs::write(&path, "this is not valid yaml: [[[").unwrap(); - let policy = discover_policy_from_path(&path); - // Falls back to restrictive default. - assert!(policy.network_policies.is_empty()); - assert!(policy.filesystem.is_some()); + assert!(matches!( + discover_image_policy_from_path(&path), + ImagePolicyDiscovery::Invalid + )); } #[test] - fn discover_policy_from_oversized_file_returns_restrictive_default() { + fn discover_policy_from_oversized_file_remains_invalid() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("policy.yaml"); let oversized = format!("version: 1\n{}", " ".repeat(4 * 1024 * 1024)); std::fs::write(&path, oversized).unwrap(); - - let policy = discover_policy_from_path(&path); - assert!(policy.network_policies.is_empty()); - assert!( - policy.filesystem.is_some(), - "an oversized otherwise-valid policy must fall back instead of parsing" - ); + assert!(matches!( + discover_image_policy_from_path(&path), + ImagePolicyDiscovery::Invalid + )); } #[test] - fn discover_policy_from_unsafe_yaml_falls_back_to_default() { + fn discover_policy_from_unsafe_yaml_preserves_candidate_for_admission() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("policy.yaml"); std::fs::write( @@ -4747,9 +5071,10 @@ filesystem_policy: ) .unwrap(); - let policy = discover_policy_from_path(&path); - // Falls back to restrictive default because of root user. - assert!(policy.process.is_none()); + let ImagePolicyDiscovery::Policy(policy) = discover_image_policy_from_path(&path) else { + panic!("expected parsed policy") + }; + assert!(openshell_policy::validate_sandbox_policy(&policy).is_err()); } #[test] @@ -4804,9 +5129,353 @@ network_policies: workspace: String::new(), policy_validation_failure_mode: PolicyValidationFailureMode::default(), extension_authentication_enabled: false, + configuration_admitted: true, + configuration_error: String::new(), + configuration_instance_id: String::new(), + } + } + + #[derive(Clone)] + struct TestStartupGateway { + desired: Arc>, + reports: UnboundedSender, + reject_next_accept: Arc, + snapshot_error: Option, + report_error: Option, + pending_snapshot: bool, + pending_acceptance: bool, + } + + #[tonic::async_trait] + impl StartupGateway for TestStartupGateway { + async fn snapshot( + &self, + _id: &str, + ) -> Result { + if self.pending_snapshot { + return std::future::pending().await; + } + if let Some(code) = self.snapshot_error { + return Err(openshell_core::grpc_client::grpc_status_error( + tonic::Status::new(code, "snapshot unavailable"), + )); + } + Ok(self.desired.lock().unwrap().clone()) + } + async fn provider( + &self, + _id: &str, + ) -> Result { + Ok(startup_provider( + self.desired.lock().unwrap().provider_env_revision, + )) + } + async fn sync( + &self, + _id: &str, + _sandbox: &str, + _policy: &openshell_core::proto::SandboxPolicy, + _workspace: &str, + ) -> Result { + self.snapshot("").await + } + async fn report( + &self, + _id: &str, + _instance_id: &str, + snapshot: Option<&openshell_core::grpc_client::SettingsPollResult>, + state: openshell_core::proto::ConfigurationAdmissionState, + _error: &str, + ) -> Result<()> { + use openshell_core::proto::ConfigurationAdmissionState; + if let Some(code) = self.report_error { + return Err(openshell_core::grpc_client::grpc_status_error( + tonic::Status::new(code, "registration fence changed"), + )); + } + self.reports.send(state).unwrap(); + if state == ConfigurationAdmissionState::Accepted { + if self.pending_acceptance { + return std::future::pending().await; + } + if self.reject_next_accept.swap(false, Ordering::SeqCst) { + return Err(miette::miette!( + "desired generation changed before activation" + )); + } + assert_eq!( + snapshot.unwrap().config_revision, + self.desired.lock().unwrap().config_revision + ); + } + Ok(()) + } + } + + #[tokio::test(start_paused = true)] + async fn startup_pending_gateway_calls_exhaust_their_budgets() { + for pending_snapshot in [true, false] { + let mut policy = proto_policy_fixture(); + enrich_proto_baseline_paths(&mut policy); + let (reports, _reported) = tokio::sync::mpsc::unbounded_channel(); + let gateway = TestStartupGateway { + desired: Arc::new(std::sync::Mutex::new(settings_poll_result( + Some(policy), + 1, + openshell_core::proto::PolicySource::Sandbox, + ))), + reports, + reject_next_accept: Arc::new(AtomicBool::new(false)), + snapshot_error: None, + report_error: None, + pending_snapshot, + pending_acceptance: !pending_snapshot, + }; + let result = timeout( + Duration::from_mins(2), + load_policy_with_gateway( + Some("sandbox-id".to_string()), + Some("sandbox".to_string()), + Some("http://unused.invalid".to_string()), + None, + None, + &openshell_extension_core::ExtensionCredentialStore::new(), + LocalPolicyIdentity::Required, + Some(ImagePolicyDiscovery::Missing), + &gateway, + ), + ) + .await + .expect("pending RPC must not hang startup"); + let Err(error) = result else { + panic!("pending gateway unexpectedly admitted startup") + }; + assert!( + error.to_string().contains(if pending_snapshot { + "failed after 5 attempts" + } else { + "did not stabilize after 5 attempts" + }), + "{error}" + ); + } + } + + #[tokio::test] + async fn startup_transient_gateway_errors_exhaust_retry_budget() { + let calls = AtomicUsize::new(0); + let result: Result<()> = timeout( + Duration::from_secs(15), + grpc_retry("Startup configuration fetch", || { + calls.fetch_add(1, Ordering::SeqCst); + async { + Err(openshell_core::grpc_client::grpc_status_error( + tonic::Status::unavailable("gateway down"), + )) + } + }), + ) + .await + .expect("transient failure must have a bounded retry budget"); + assert!( + result + .unwrap_err() + .to_string() + .contains("failed after 5 attempts") + ); + assert_eq!(calls.load(Ordering::SeqCst), 5); + } + + #[tokio::test] + async fn startup_returns_permanent_gateway_errors_without_waiting_for_policy_repair() { + for (snapshot_error, report_error) in [ + (Some(tonic::Code::PermissionDenied), None), + (Some(tonic::Code::NotFound), None), + (None, Some(tonic::Code::FailedPrecondition)), + ] { + let (reports, _reported) = tokio::sync::mpsc::unbounded_channel(); + let gateway = TestStartupGateway { + desired: Arc::new(std::sync::Mutex::new(settings_poll_result( + None, + 1, + openshell_core::proto::PolicySource::Sandbox, + ))), + reports, + reject_next_accept: Arc::new(AtomicBool::new(false)), + snapshot_error, + report_error, + pending_snapshot: false, + pending_acceptance: false, + }; + let result = timeout( + Duration::from_secs(1), + load_policy_with_gateway( + Some("sandbox-id".to_string()), + Some("sandbox".to_string()), + Some("http://unused.invalid".to_string()), + None, + None, + &openshell_extension_core::ExtensionCredentialStore::new(), + LocalPolicyIdentity::Required, + Some(ImagePolicyDiscovery::Missing), + &gateway, + ), + ) + .await + .expect("permanent errors must terminate startup"); + let Err(error) = result else { + panic!("startup unexpectedly succeeded") + }; + assert!(!is_retryable_error(&error)); + assert!(error.to_string().contains(if report_error.is_some() { + "registration fence changed" + } else { + "snapshot unavailable" + })); + } + } + + #[tokio::test] + async fn startup_waits_for_repair_and_retries_stale_activation_before_returning() { + use openshell_core::proto::{ConfigurationAdmissionState, PolicySource}; + let mut policy = proto_policy_fixture(); + enrich_proto_baseline_paths(&mut policy); + let mut rejected = settings_poll_result(Some(policy), 1, PolicySource::Sandbox); + rejected.configuration_admitted = false; + let (reports, mut reported) = tokio::sync::mpsc::unbounded_channel(); + let gateway = TestStartupGateway { + desired: Arc::new(std::sync::Mutex::new(rejected)), + reports, + reject_next_accept: Arc::new(AtomicBool::new(true)), + snapshot_error: None, + report_error: None, + pending_snapshot: false, + pending_acceptance: false, + }; + let active_gateway = gateway.clone(); + let handle = tokio::spawn(async move { + load_policy_with_gateway( + Some("sandbox-id".to_string()), + Some("sandbox".to_string()), + Some("http://unused.invalid".to_string()), + None, + None, + &openshell_extension_core::ExtensionCredentialStore::new(), + LocalPolicyIdentity::Required, + Some(ImagePolicyDiscovery::Missing), + &active_gateway, + ) + .await + }); + assert_eq!( + reported.recv().await, + Some(ConfigurationAdmissionState::Pending) + ); + assert_eq!( + reported.recv().await, + Some(ConfigurationAdmissionState::Rejected) + ); + assert!( + !handle.is_finished(), + "rejected configuration must not return a launch bundle" + ); + { + let mut desired = gateway.desired.lock().unwrap(); + desired.configuration_admitted = true; + desired.config_revision += 1; + } + assert_eq!( + timeout(Duration::from_secs(5), reported.recv()) + .await + .unwrap(), + Some(ConfigurationAdmissionState::Accepted) + ); + assert!( + !handle.is_finished(), + "a stale activation acknowledgement must not return a launch bundle" + ); + assert_eq!( + timeout(Duration::from_secs(5), reported.recv()) + .await + .unwrap(), + Some(ConfigurationAdmissionState::Accepted) + ); + let bundle = timeout(Duration::from_secs(5), handle) + .await + .unwrap() + .unwrap() + .expect("repair returns one launch bundle"); + assert!( + bundle.7.is_some(), + "launch bundle retains matching provider state" + ); + } + + fn startup_provider(revision: u64) -> openshell_core::grpc_client::ProviderEnvironmentResult { + openshell_core::grpc_client::ProviderEnvironmentResult { + provider_env_revision: revision, + environment: std::collections::HashMap::new(), + credential_expires_at_ms: std::collections::HashMap::new(), + dynamic_credentials: std::collections::HashMap::new(), + static_credential_bindings: std::collections::HashMap::new(), + non_secret_environment_keys: Vec::new(), + } + } + + #[test] + fn startup_configuration_rejects_mixed_provider_revision() { + let policy = proto_policy_fixture(); + let mut snapshot = settings_poll_result( + Some(policy.clone()), + 1, + openshell_core::proto::PolicySource::Sandbox, + ); + snapshot.provider_env_revision = 10; + assert!(prepare_startup_configuration(&snapshot, &policy, &startup_provider(11)).is_err()); + let (_, _, credentials) = + prepare_startup_configuration(&snapshot, &policy, &startup_provider(10)) + .expect("matching generation is admitted"); + assert_eq!(credentials.revision(), 10); + } + + #[test] + fn startup_configuration_revalidates_on_restart_and_accepts_repair() { + let policy = proto_policy_fixture(); + let mut snapshot = settings_poll_result( + Some(policy.clone()), + 1, + openshell_core::proto::PolicySource::Sandbox, + ); + for _restart in 0..2 { + snapshot.configuration_admitted = false; + assert!( + prepare_startup_configuration(&snapshot, &policy, &startup_provider(0)).is_err() + ); + snapshot.configuration_admitted = true; + assert!( + prepare_startup_configuration(&snapshot, &policy, &startup_provider(0)).is_ok() + ); } } + #[test] + fn startup_configuration_does_not_substitute_invalid_opa_policy() { + let mut policy = proto_tcp_policy_fixture(); + policy + .network_policies + .values_mut() + .next() + .expect("fixture has policy") + .endpoints[0] + .protocol = "invalid-protocol".to_string(); + let snapshot = settings_poll_result( + Some(policy.clone()), + 1, + openshell_core::proto::PolicySource::Sandbox, + ); + assert!(prepare_startup_configuration(&snapshot, &policy, &startup_provider(0)).is_err()); + } + #[derive(Clone)] struct ScriptedPolicyGateway { polls: Arc< diff --git a/docs/sandboxes/policies.mdx b/docs/sandboxes/policies.mdx index c404d5acc4..2edbb0a222 100644 --- a/docs/sandboxes/policies.mdx +++ b/docs/sandboxes/policies.mdx @@ -292,7 +292,7 @@ apply permanently, including while a restart is pending or rejected. Images with embedded policy use the restrictive baseline and gain network access only from operator-selected configuration. Explicit user/global policy precedence remains unchanged. Gateway authorization or compatibility errors, exhausted connection -retries, and missing process-sidecar connections terminate startup rather than +retries, and unavailable sandbox boundary connections terminate startup rather than waiting for policy repair. OpenShell validates a complete candidate policy before activating any part of it. Endpoints may overlap when their connection and request-processing metadata agree. For example, two `api.example.com:443` REST entries can contribute different allow and deny rules when they use the same TLS, destination, credential, parser, and enforcement settings. A plain L4 endpoint may overlap an L7 endpoint because it authorizes the destination without contributing request-processing metadata. A more-specific path endpoint may override request-processing metadata from a broader endpoint, such as a `/graphql` GraphQL endpoint alongside a general REST endpoint for the same host. OpenShell rejects the candidate when overlapping exact or wildcard host selectors can both contribute equally specific endpoint configuration and disagree on those fields. diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index d2228f803b..322ddfeff9 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -191,10 +191,10 @@ alive while the workload stays unstarted. Inspect `openshell sandbox get` and repair the desired configuration with a complete policy replacement or provider change; do not treat a healthy container as proof that the workload is ready. See [policy validation and repair](https://docs.nvidia.com/openshell/latest/sandboxes/policies.md). -In sidecar topology, the process supervisor sends image-policy discovery over -the authenticated control socket and waits for an accepted bootstrap. A process -container waiting there can be expected during repair, rather than a crash loop. -A missing process-sidecar connection times out during discovery. Permanent +The isolated supervisor requests image-policy discovery through the authenticated +sandbox boundary before admission. The workload boundary can remain alive without +launching the workload while configuration is repaired. An unavailable boundary +fails discovery within its control-request deadline. Permanent gateway errors and exhausted transient retries terminate startup; inspect those errors as connectivity, authorization, or lifecycle failures. From 53907d7a01908ef3059cb3eec5e2c4826dba46e2 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Wed, 16 Sep 2026 10:02:26 -0700 Subject: [PATCH 11/23] fix(sandbox): use existing boundary discovery imports Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- crates/openshell-sandbox/src/boundary_server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/openshell-sandbox/src/boundary_server.rs b/crates/openshell-sandbox/src/boundary_server.rs index 0890050cdc..5452ea6efa 100644 --- a/crates/openshell-sandbox/src/boundary_server.rs +++ b/crates/openshell-sandbox/src/boundary_server.rs @@ -1547,7 +1547,7 @@ mod linux { fn discover_image_policy_from_paths(paths: &[&str]) -> Response { use std::io::Read as _; for path in paths { - match std::fs::File::open(path) { + match File::open(path) { Ok(file) => { let mut yaml = String::new(); if file.take(1_048_577).read_to_string(&mut yaml).is_err() @@ -1563,7 +1563,7 @@ mod linux { invalid: false, }; } - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) if error.kind() == io::ErrorKind::NotFound => {} Err(_) => { return Response::ImagePolicy { yaml: None, From bb65bad9261d8de588ab587c814f8ca12c160bfe Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Wed, 16 Sep 2026 10:31:19 -0700 Subject: [PATCH 12/23] test(sandbox): distinguish workload and supervisor containers Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- e2e/rust/tests/policy_activation.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/e2e/rust/tests/policy_activation.rs b/e2e/rust/tests/policy_activation.rs index 45c9019a63..b2e9a3056f 100644 --- a/e2e/rust/tests/policy_activation.rs +++ b/e2e/rust/tests/policy_activation.rs @@ -79,6 +79,10 @@ async fn cli_ok(args: &[&str]) { } fn container_id(engine: &ContainerEngine, name: &str) -> String { + role_container_id(engine, name, "sandbox") +} + +fn role_container_id(engine: &ContainerEngine, name: &str, role: &str) -> String { let output = engine .command() .args([ @@ -86,6 +90,8 @@ fn container_id(engine: &ContainerEngine, name: &str) -> String { "--quiet", "--filter", &format!("label=openshell.ai/sandbox-name={name}"), + "--filter", + &format!("label=openshell.ai/isolation-role={role}"), ]) .output() .expect("find sandbox container"); @@ -95,7 +101,7 @@ fn container_id(engine: &ContainerEngine, name: &str) -> String { assert_eq!( ids.len(), 1, - "expected one running sandbox container: {ids:?}" + "expected one running {role} container: {ids:?}" ); ids[0].to_string() } @@ -278,13 +284,18 @@ binaries: tokio::time::sleep(Duration::from_millis(500)).await; } let container = container_id(&engine, &resources.sandbox); + let supervisor = role_container_id(&engine, &resources.sandbox, "supervisor"); assert_marker(&engine, &container, false); // Repeated observation distinguishes a stable gate from a crash/relaunch loop. tokio::time::sleep(Duration::from_secs(3)).await; assert_eq!(container_id(&engine, &resources.sandbox), container); + assert_eq!( + role_container_id(&engine, &resources.sandbox, "supervisor"), + supervisor + ); let restarts = engine .command() - .args(["inspect", "--format", "{{.RestartCount}}", &container]) + .args(["inspect", "--format", "{{.RestartCount}}", &supervisor]) .output() .expect("inspect supervisor restart count"); assert!(restarts.status.success()); From 1d651d6cadebd093cfb6d2e52bece93d3f52411b Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Wed, 16 Sep 2026 13:21:30 -0700 Subject: [PATCH 13/23] test(server): reconcile admission schema with timestamp migration Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- architecture/gateway.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/architecture/gateway.md b/architecture/gateway.md index 0b7fbc643a..d116999f8b 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -443,6 +443,9 @@ acceptance stores `true` permanently, including across restart. Legacy rows have neither field and conservatively retain static-policy restrictions. No database rewrite is required. A pre-admission byte fixture verifies that legacy phase and policy-version fields survive without fabricated admission or activation. +With the timestamp migration, the admission contract brings the public closure +to 286 messages and 15 enums, the durable closure to 84 messages and 10 enums, +and their overlap to 74 messages and 10 enums. | Dual-purpose encoded root | Current decision | |---|---| From 64c39c38af44d73e49175af584b8e0cdbf1a6837 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:43:58 -0700 Subject: [PATCH 14/23] test(server): reconcile admission with typed deletion schema Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- architecture/gateway.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/architecture/gateway.md b/architecture/gateway.md index d116999f8b..80fc2d3665 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -443,7 +443,7 @@ acceptance stores `true` permanently, including across restart. Legacy rows have neither field and conservatively retain static-policy restrictions. No database rewrite is required. A pre-admission byte fixture verifies that legacy phase and policy-version fields survive without fabricated admission or activation. -With the timestamp migration, the admission contract brings the public closure +With timestamp types and deletion outcomes, the admission contract brings the public closure to 286 messages and 15 enums, the durable closure to 84 messages and 10 enums, and their overlap to 74 messages and 10 enums. From bc8575de582dc692f52fe4754fa4ba2eead52c57 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Thu, 17 Sep 2026 08:51:06 -0700 Subject: [PATCH 15/23] test(server): reconcile admission with mutation request IDs Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- architecture/gateway.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/architecture/gateway.md b/architecture/gateway.md index 80fc2d3665..da3b8d790c 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -443,9 +443,11 @@ acceptance stores `true` permanently, including across restart. Legacy rows have neither field and conservatively retain static-policy restrictions. No database rewrite is required. A pre-admission byte fixture verifies that legacy phase and policy-version fields survive without fabricated admission or activation. -With timestamp types and deletion outcomes, the admission contract brings the public closure -to 286 messages and 15 enums, the durable closure to 84 messages and 10 enums, -and their overlap to 74 messages and 10 enums. +With timestamp types, deletion outcomes, and optional mutation request IDs, the +admission contract brings the public closure to 286 messages and 15 enums, the +durable closure to 84 messages and 10 enums, and their overlap to 74 messages +and 10 enums. Mutation request IDs extend public request fields without adding +messages to these closures or changing the durable protobuf schema. | Dual-purpose encoded root | Current decision | |---|---| From d6b9f7e337a87e513a0dc3b2182f46dcd8a83472 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Thu, 17 Sep 2026 08:58:58 -0700 Subject: [PATCH 16/23] test(supervisor): adapt local startup fixture to admission state Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- crates/openshell-supervisor/src/lib.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/openshell-supervisor/src/lib.rs b/crates/openshell-supervisor/src/lib.rs index 0ea1fcc7b0..4a350b6000 100644 --- a/crates/openshell-supervisor/src/lib.rs +++ b/crates/openshell-supervisor/src/lib.rs @@ -6754,8 +6754,17 @@ network_policies: LocalPolicyIdentity::Required, ) }; - let (_, engine, proto, registry, origin, proposals, extension_authentication_enabled) = - startup().await.expect("load valid local policy"); + let ( + _, + engine, + proto, + registry, + origin, + proposals, + extension_authentication_enabled, + provider_credentials, + ) = startup().await.expect("load valid local policy"); + assert!(provider_credentials.is_none()); assert!(proto.is_none()); assert!(matches!(origin, LoadedPolicyOrigin::LocalOverride)); assert!(matches!(registry, MiddlewareRegistryStatus::Synchronized)); From 6cd13d107ea0eb80fc4c6c94b0bd7a6578a70347 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Thu, 17 Sep 2026 10:02:14 -0700 Subject: [PATCH 17/23] fix(supervisor): deduplicate startup quarantine diagnostics Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- architecture/sandbox.md | 3 + crates/openshell-supervisor/src/lib.rs | 98 ++++++++++++++++++++++---- e2e/rust/tests/policy_activation.rs | 18 +++++ 3 files changed, 104 insertions(+), 15 deletions(-) diff --git a/architecture/sandbox.md b/architecture/sandbox.md index a7af39d59b..20ed5759b1 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -626,6 +626,9 @@ A blocked startup remains `Provisioning` with a `ConfigurationInvalid` readiness condition, even when the container backend reports readiness. Gateway management operations remain available. Replacing the policy or repairing providers allows the same supervisor to reconcile and launch; it does not recreate the sandbox. +Startup retries continue reporting readiness, but unchanged configuration rejections +produce only one log event. A changed configuration or diagnostic emits a new +rejection event; successful repair emits a recovery event. Static policy fields can be replaced before the first accepted activation. A durable first-activation marker closes this repair window permanently, including across stop/start and later rejected configurations. Legacy records without the diff --git a/crates/openshell-supervisor/src/lib.rs b/crates/openshell-supervisor/src/lib.rs index 4a350b6000..2ef24a63e0 100644 --- a/crates/openshell-supervisor/src/lib.rs +++ b/crates/openshell-supervisor/src/lib.rs @@ -2334,6 +2334,7 @@ async fn load_policy_with_gateway( .await?; let discovery = image_discovery.unwrap_or_else(discover_image_policy); let mut reconciliation_attempts = 0u32; + let mut rejection_log = StartupRejectionLog::default(); loop { if reconciliation_attempts == 5 { return Err(miette::miette!( @@ -2347,7 +2348,7 @@ async fn load_policy_with_gateway( if snapshot.policy.is_none() && !snapshot.configuration_error.is_empty() { reject_startup_configuration( gateway, - endpoint, + &mut rejection_log, id, &instance_id, &snapshot, @@ -2376,7 +2377,7 @@ async fn load_policy_with_gateway( ImagePolicyDiscovery::Policy(policy) => *policy.clone(), ImagePolicyDiscovery::Missing => openshell_policy::restrictive_default_policy(), ImagePolicyDiscovery::Invalid => { - reject_startup_configuration(gateway, endpoint, id, &instance_id, &snapshot, "Image policy is invalid; replace the sandbox policy to repair configuration").await?; + reject_startup_configuration(gateway, &mut rejection_log, id, &instance_id, &snapshot, "Image policy is invalid; replace the sandbox policy to repair configuration").await?; reconciliation_attempts = 0; continue; } @@ -2409,7 +2410,7 @@ async fn load_policy_with_gateway( } reject_startup_configuration( gateway, - endpoint, + &mut rejection_log, id, &instance_id, &snapshot, @@ -2455,12 +2456,11 @@ async fn load_policy_with_gateway( // The initial load uses pid=0 (no symlink resolution) because the // container hasn't started yet. After the entrypoint spawns, the // engine is rebuilt with the real PID for symlink resolution. - info!("Creating OPA engine from proto policy data"); let has_last_valid_policy = true; if !snapshot.configuration_admitted { reject_startup_configuration( gateway, - endpoint, + &mut rejection_log, id, &instance_id, &snapshot, @@ -2481,6 +2481,7 @@ async fn load_policy_with_gateway( .await; continue; } + debug!("Creating OPA engine from proto policy data"); let (engine, policy, captured_provider_credentials) = match prepare_startup_configuration(&snapshot, &proto_policy, &provider) { Ok(prepared) => prepared, @@ -2494,7 +2495,7 @@ async fn load_policy_with_gateway( .await; reject_startup_configuration( gateway, - endpoint, + &mut rejection_log, id, &instance_id, &snapshot, @@ -2592,6 +2593,16 @@ async fn load_policy_with_gateway( .await; continue; } + if rejection_log.0.is_some() { + ocsf_emit!( + ConfigStateChangeBuilder::new(ocsf_ctx()) + .severity(SeverityId::Informational) + .status(StatusId::Success) + .state(StateId::Other, "configuration_recovered") + .message("Startup configuration repaired and accepted") + .build() + ); + } return Ok(( policy, opa_engine, @@ -2684,9 +2695,31 @@ fn prepare_provider_environment( .map_err(|_| miette::miette!("Provider credential bindings are invalid")) } +// Retain only the most recent rejection, so A -> B -> A emits all transitions. +#[derive(Default)] +struct StartupRejectionLog(Option<(LoadedPolicyRevision, String)>); + +impl StartupRejectionLog { + fn changed( + &mut self, + snapshot: &openshell_core::grpc_client::SettingsPollResult, + error: &str, + ) -> bool { + let rejection = ( + LoadedPolicyRevision::from_snapshot(snapshot), + error.to_owned(), + ); + if self.0.as_ref() == Some(&rejection) { + return false; + } + self.0 = Some(rejection); + true + } +} + async fn reject_startup_configuration( gateway: &impl StartupGateway, - _endpoint: &str, + rejection_log: &mut StartupRejectionLog, sandbox_id: &str, instance_id: &str, snapshot: &openshell_core::grpc_client::SettingsPollResult, @@ -2702,15 +2735,18 @@ async fn reject_startup_configuration( ) }) .await?; + // Keep reporting readiness on every retry, but log only changed rejections. // Fixed, bounded diagnostics deliberately omit the candidate and credentials. - ocsf_emit!( - ConfigStateChangeBuilder::new(ocsf_ctx()) - .severity(SeverityId::High) - .status(StatusId::Failure) - .state(StateId::Disabled, "configuration_error") - .message(error) - .build() - ); + if rejection_log.changed(snapshot, error) { + ocsf_emit!( + ConfigStateChangeBuilder::new(ocsf_ctx()) + .severity(SeverityId::High) + .status(StatusId::Failure) + .state(StateId::Disabled, "configuration_error") + .message(error) + .build() + ); + } tokio::time::sleep(Duration::from_secs(2)).await; Ok(()) } @@ -5212,6 +5248,31 @@ network_policies: } } + #[test] + fn startup_rejection_logs_only_changed_configuration_or_error() { + let mut snapshot = settings_poll_result( + Some(proto_policy_fixture()), + 1, + openshell_core::proto::PolicySource::Sandbox, + ); + let mut log = StartupRejectionLog::default(); + assert!(log.changed(&snapshot, "invalid policy")); + for _ in 0..10 { + assert!(!log.changed(&snapshot, "invalid policy")); + } + snapshot.config_revision += 1; + assert!(log.changed(&snapshot, "invalid policy")); + snapshot.provider_env_revision += 1; + assert!(log.changed(&snapshot, "invalid policy")); + snapshot.policy_hash.push_str("changed"); + assert!(log.changed(&snapshot, "invalid policy")); + snapshot.version += 1; + assert!(log.changed(&snapshot, "invalid policy")); + assert!(log.changed(&snapshot, "invalid provider")); + assert!(!log.changed(&snapshot, "invalid provider")); + assert!(log.changed(&snapshot, "invalid policy")); + } + #[tokio::test(start_paused = true)] async fn startup_pending_gateway_calls_exhaust_their_budgets() { for pending_snapshot in [true, false] { @@ -5379,6 +5440,13 @@ network_policies: !handle.is_finished(), "rejected configuration must not return a launch bundle" ); + assert_eq!( + timeout(Duration::from_secs(5), reported.recv()) + .await + .unwrap(), + Some(ConfigurationAdmissionState::Rejected), + "unchanged rejection must still report readiness on subsequent polls" + ); { let mut desired = gateway.desired.lock().unwrap(); desired.configuration_admitted = true; diff --git a/e2e/rust/tests/policy_activation.rs b/e2e/rust/tests/policy_activation.rs index b2e9a3056f..0491e80c3f 100644 --- a/e2e/rust/tests/policy_activation.rs +++ b/e2e/rust/tests/policy_activation.rs @@ -305,6 +305,24 @@ binaries: "invalid configuration must not crash-loop" ); assert_marker(&engine, &container, false); + let logs = engine + .command() + .args(["logs", &supervisor]) + .output() + .expect("read quarantined supervisor logs"); + assert!(logs.status.success()); + let logs = format!( + "{}{}", + String::from_utf8_lossy(&logs.stdout), + String::from_utf8_lossy(&logs.stderr) + ); + assert_eq!( + logs.matches("credentialed endpoint 'api.example.com:443'") + .count(), + 1, + "unchanged startup rejection must be logged only once: {logs}" + ); + assert!(!logs.contains("Creating OPA engine from proto policy data")); let repaired = context.path().join("repaired.yaml"); std::fs::write( &repaired, From 9548260cc84f6f6687aa7caf31dcb07215ffc971 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Thu, 17 Sep 2026 10:06:34 -0700 Subject: [PATCH 18/23] fix(tui): show configuration blockers in sandbox notes Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- .agents/skills/tui-development/SKILL.md | 2 +- architecture/sandbox.md | 3 +- crates/openshell-tui/src/lib.rs | 82 ++++++++++++++++++- crates/openshell-tui/src/ui/sandbox_detail.rs | 8 +- docs/sandboxes/manage-sandboxes.mdx | 2 + 5 files changed, 89 insertions(+), 8 deletions(-) diff --git a/.agents/skills/tui-development/SKILL.md b/.agents/skills/tui-development/SKILL.md index 0de6246c9f..79fd5058fc 100644 --- a/.agents/skills/tui-development/SKILL.md +++ b/.agents/skills/tui-development/SKILL.md @@ -170,7 +170,7 @@ Phase 1: GetSandboxLogs → 500 initial lines → send via Event::LogLines Phase 2: WatchSandbox(follow_logs: true) → live tail → send via Event::LogLines ``` -**Sandboxes**: Fetched via `ListSandboxes` in a background collection-refresh task scheduled from the 2-second tick, scoped to the current workspace (or all workspaces). Follow `next_page_token` until empty so the dashboard reflects the complete collection. +**Sandboxes**: Fetched via `ListSandboxes` in a background collection-refresh task scheduled from the 2-second tick, scoped to the current workspace (or all workspaces). Follow `next_page_token` until empty so the dashboard reflects the complete collection. The NOTES column puts active `ConfigurationInvalid` readiness diagnostics before port forwards and clears them on refresh after repair. The sandbox detail pane uses the same Notes field. **Providers**: Fetched via `ListProviders` in the background collection-refresh task. Provider profiles are fetched per-workspace via `ListProviderProfiles` and cached in a `ProviderProfileCache` keyed by `(workspace, profile_id)`. Follow each list RPC's `next_page_token` until empty. diff --git a/architecture/sandbox.md b/architecture/sandbox.md index 20ed5759b1..5232cdd960 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -624,7 +624,8 @@ default. The gateway tracks configuration admission independently of compute health. A blocked startup remains `Provisioning` with a `ConfigurationInvalid` readiness condition, even when the container backend reports readiness. Gateway management -operations remain available. Replacing the policy or repairing providers allows +operations remain available. The TUI exposes configuration rejection conditions +in sandbox NOTES alongside active port forwards. Replacing the policy or repairing providers allows the same supervisor to reconcile and launch; it does not recreate the sandbox. Startup retries continue reporting readiness, but unchanged configuration rejections produce only one log event. A changed configuration or diagnostic emits a new diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 23705a24e9..37532e8416 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -2731,6 +2731,35 @@ async fn fetch_sandboxes( } } +fn sandbox_notes(sandbox: &openshell_core::proto::Sandbox, forwards: String) -> String { + let rejection = sandbox.status.as_ref().and_then(|status| { + status.conditions.iter().find(|condition| { + matches!(condition.r#type.as_str(), "ConfigurationReady" | "Ready") + && condition.status == "False" + && condition.reason == "ConfigurationInvalid" + }) + }); + let Some(rejection) = rejection else { + return forwards; + }; + // Keep the table row on one line even when a diagnostic contains newlines. + let message = rejection + .message + .split_whitespace() + .collect::>() + .join(" "); + let mut notes = "Config invalid".to_string(); + if !message.is_empty() { + notes.push_str(": "); + notes.push_str(&message); + } + if !forwards.is_empty() { + notes.push_str("; "); + notes.push_str(&forwards); + } + notes +} + fn apply_sandbox_refresh(app: &mut App, sandboxes: Vec) { app.sandbox_count = sandboxes.len(); app.sandbox_ids = sandboxes @@ -2780,13 +2809,14 @@ fn apply_sandbox_refresh(app: &mut App, sandboxes: Vec, app: &App, area: Rect) { Span::styled(providers_str, t.text), ]); - // Row 6: Forwarded Ports - let forwards_str = app + // Row 6: Configuration diagnostics and forwarded ports + let notes_str = app .sandbox_notes .get(idx) .filter(|s| !s.is_empty()) .map_or("none", String::as_str); let row6 = Line::from(vec![ - Span::styled(" Forwards: ", t.muted), - Span::styled(forwards_str, t.text), + Span::styled(" Notes: ", t.muted), + Span::styled(notes_str, t.text), ]); let mut lines = vec![row1, row2, row3, row4, row5, row6]; diff --git a/docs/sandboxes/manage-sandboxes.mdx b/docs/sandboxes/manage-sandboxes.mdx index c3e777f3a4..641b46a3c0 100644 --- a/docs/sandboxes/manage-sandboxes.mdx +++ b/docs/sandboxes/manage-sandboxes.mdx @@ -609,6 +609,8 @@ Use the terminal to spot blocked connections marked `action=deny` and provider-r The dashboard has three panels stacked vertically: Gateways, Providers (or Global Settings), and Sandboxes. Navigate within a panel with `Up`/`Down` or `j`/`k`. At a list boundary the cursor overflows into the adjacent panel, skipping empty panels. Use `Tab`/`Shift+Tab` to cycle panels directly. Press `h`/`l` or `Left`/`Right` in the middle panel to switch between the Providers and Global Settings tabs. +The sandbox table’s NOTES column shows `Config invalid` and the rejection reason when policy or provider configuration blocks provisioning. Open the sandbox detail view to see the Notes field. The diagnostic clears after the configuration is repaired; active port forwards remain listed. + ## Port Forwarding Forward a local port to a running sandbox to access services inside it, such as a web server or database: From fc1ea5fd51dcf44e52b269bb74e1c68bf0c550f0 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Thu, 17 Sep 2026 13:17:17 -0700 Subject: [PATCH 19/23] feat(sandbox): expire provisioning repair attempts after five minutes Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- .agents/skills/tui-development/SKILL.md | 2 +- architecture/gateway.md | 14 +- architecture/sandbox.md | 25 +- crates/openshell-cli/src/run.rs | 55 +- crates/openshell-driver-docker/src/lib.rs | 113 ++- crates/openshell-driver-podman/src/driver.rs | 26 +- crates/openshell-server/src/compute/mod.rs | 713 ++++++++++++++-- .../src/compute/provisioning_deadline.rs | 799 ++++++++++++++++++ crates/openshell-server/src/grpc/mod.rs | 10 + crates/openshell-server/src/grpc/policy.rs | 126 ++- .../src/grpc/policy/provisioning_clock.rs | 271 ++++++ crates/openshell-server/src/grpc/sandbox.rs | 20 + crates/openshell-server/src/lib.rs | 8 +- crates/openshell-server/src/storage_proto.rs | 1 + crates/openshell-tui/src/lib.rs | 48 ++ docs/sandboxes/manage-sandboxes.mdx | 25 + docs/sandboxes/policies.mdx | 6 + proto/openshell.proto | 22 + skills/debug-openshell-cluster/SKILL.md | 6 + skills/openshell-cli/SKILL.md | 7 +- 20 files changed, 2136 insertions(+), 161 deletions(-) create mode 100644 crates/openshell-server/src/compute/provisioning_deadline.rs create mode 100644 crates/openshell-server/src/grpc/policy/provisioning_clock.rs diff --git a/.agents/skills/tui-development/SKILL.md b/.agents/skills/tui-development/SKILL.md index 79fd5058fc..f3f821a32a 100644 --- a/.agents/skills/tui-development/SKILL.md +++ b/.agents/skills/tui-development/SKILL.md @@ -170,7 +170,7 @@ Phase 1: GetSandboxLogs → 500 initial lines → send via Event::LogLines Phase 2: WatchSandbox(follow_logs: true) → live tail → send via Event::LogLines ``` -**Sandboxes**: Fetched via `ListSandboxes` in a background collection-refresh task scheduled from the 2-second tick, scoped to the current workspace (or all workspaces). Follow `next_page_token` until empty so the dashboard reflects the complete collection. The NOTES column puts active `ConfigurationInvalid` readiness diagnostics before port forwards and clears them on refresh after repair. The sandbox detail pane uses the same Notes field. +**Sandboxes**: Fetched via `ListSandboxes` in a background collection-refresh task scheduled from the 2-second tick, scoped to the current workspace (or all workspaces). Follow `next_page_token` until empty so the dashboard reflects the complete collection. The NOTES column puts active `ConfigurationInvalid` readiness diagnostics before port forwards and clears them on refresh after repair. Timed-out provisioning attempts show `Provisioning timed out` with cleanup pending or compute reclaimed, preserving port forwards. The sandbox detail pane uses the same Notes field. **Providers**: Fetched via `ListProviders` in the background collection-refresh task. Provider profiles are fetched per-workspace via `ListProviderProfiles` and cached in a `ProviderProfileCache` keyed by `(workspace, profile_id)`. Follow each list RPC's `next_page_token` until empty. diff --git a/architecture/gateway.md b/architecture/gateway.md index da3b8d790c..a98afa2d3e 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -443,9 +443,19 @@ acceptance stores `true` permanently, including across restart. Legacy rows have neither field and conservatively retain static-policy restrictions. No database rewrite is required. A pre-admission byte fixture verifies that legacy phase and policy-version fields survive without fabricated admission or activation. +`SandboxStatus.provisioning` uses field 13 for gateway-owned attempt timing and +compute reclamation progress. Its timestamps survive supervisor reconnects and +ordinary driver status updates. Older records decode with no provisioning +record; timing must be adopted once and persisted, never reconstructed from the +object's frequently changing update timestamp. The additive message requires +no rewrite of existing payloads and leaves the frozen storage-v1 schema intact. +Stored settings JSON also carries per-key change IDs and commit timestamps, +including deletion tombstones. Legacy values acquire stable source identities +on read; a subsequent write preserves them. These clocks distinguish effective +edits from no-op writes without treating status updates as configuration edits. With timestamp types, deletion outcomes, and optional mutation request IDs, the -admission contract brings the public closure to 286 messages and 15 enums, the -durable closure to 84 messages and 10 enums, and their overlap to 74 messages +admission contract brings the public closure to 287 messages and 15 enums, the +durable closure to 85 messages and 10 enums, and their overlap to 75 messages and 10 enums. Mutation request IDs extend public request fields without adding messages to these closures or changing the durable protobuf schema. diff --git a/architecture/sandbox.md b/architecture/sandbox.md index 5232cdd960..eecb5fdf6d 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -630,6 +630,29 @@ the same supervisor to reconcile and launch; it does not recreate the sandbox. Startup retries continue reporting readiness, but unchanged configuration rejections produce only one log event. A changed configuration or diagnostic emits a new rejection event; successful repair emits a recovery event. +The gateway gives each initial provisioning attempt and explicit restart a +300-second repair window. Persisted configuration-source clocks reset the window +from the latest effective stored change, including settings deletion and provider +attachment changes. The first accepted rejection for that generation grants one +full window; repeated reports and reconnects do not extend it. Ready disarms the +timer. Failed desired updates to a running sandbox do not arm it. + +A leader-owned scan runs independently of driver inventory. Expiry records +`Error`/`ProvisioningTimedOut` before reclaiming compute; cleanup progress and +backoff survive restart. Late runtime reports cannot replace that result. The +record and restartable storage survive cleanup, including for ephemeral creates. +Explicit start is blocked while cleanup is pending, then creates a fresh attempt +using the latest configuration. Configuration edits alone never restart an +expired sandbox. Legacy provisioning records receive one persisted rollout +window. Cross-object configuration serialization uses the gateway's existing +single-writer guard; enabling concurrent configuration writers still requires +the database-backed invariant work tracked by #1255. + +Docker startup health remains unready during policy quarantine. A failed probe +does not terminate a live provisioning supervisor; the gateway deadline owns +that decision. Cleanup cancels pending driver startup before stopping compute +so a late startup failure cannot remove retained workload storage. + Static policy fields can be replaced before the first accepted activation. A durable first-activation marker closes this repair window permanently, including across stop/start and later rejected configurations. Legacy records without the @@ -646,7 +669,7 @@ so it cannot replace the configuration captured for that launch. Restart resets admission and requires a fresh accepted configuration. Permanent gateway errors and exhausted transient retries terminate startup; each RPC attempt has a 10-second deadline, including acceptance reports; only acknowledged configuration -rejections wait indefinitely for repair. Image discovery uses the authenticated +rejections wait for repair within the gateway's provisioning deadline. Image discovery uses the authenticated sandbox boundary control request deadline. Policy and provider refreshes are prepared before publication. Publication diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index 0ce00051f2..098c52c1cf 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -1173,7 +1173,16 @@ pub async fn sandbox_create( SandboxPhase::Error => { drop(stream); drop(client); - let create_result = if last_error_reason.is_empty() { + let provisioning_timed_out = last_sandbox + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + .is_some_and(|record| record.timeout_time.is_some()); + let create_result = if provisioning_timed_out { + Err(miette::miette!( + "{last_error_reason}\nSandbox '{sandbox_name}' was retained. Inspect it with `openshell sandbox get {sandbox_name}`; repair its configuration, then run `openshell sandbox start {sandbox_name}` after cleanup completes." + )) + } else if last_error_reason.is_empty() { Err(miette::miette!( "sandbox entered error phase while provisioning" )) @@ -1186,7 +1195,12 @@ pub async fn sandbox_create( finalize_sandbox_create_session( &effective_server, &sandbox_name, - persist, + persist + || last_sandbox + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + .is_some_and(|record| record.timeout_time.is_some()), create_result, workspace, &effective_tls, @@ -2566,6 +2580,18 @@ fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value { "provider_env_revision": admission.provider_env_revision, }) }); + let provisioning = sandbox.status.as_ref().and_then(|status| status.provisioning.as_ref()) + .map(|record| serde_json::json!({ + "attempt_id": record.attempt_id, + "configuration_change_id": record.configuration_change_id, + "configuration_change_time": record.configuration_change_time.as_ref().map(ToString::to_string), + "first_rejection_time": record.first_rejection_time.as_ref().map(ToString::to_string), + "deadline": record.deadline.as_ref().map(ToString::to_string), + "timeout_time": record.timeout_time.as_ref().map(ToString::to_string), + "cleanup_completed_time": record.cleanup_completed_time.as_ref().map(ToString::to_string), + "cleanup_error": record.cleanup_error, + "cleanup_retry_time": record.cleanup_retry_time.as_ref().map(ToString::to_string), + })); serde_json::json!({ "id": sandbox.object_id(), "name": sandbox.object_name(), @@ -2580,6 +2606,7 @@ fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value { "conditions": conditions, "endpoint_statuses": endpoint_statuses, "configuration_admission": admission, + "provisioning": provisioning, "created_from_workload_template": created_from_workload_template, }) } @@ -7425,6 +7452,30 @@ mod tests { assert_eq!(json["resources"]["gpu"], 2); } + #[test] + fn provisioning_json_exposes_deadline_and_cleanup_separately() { + let mut sandbox = Sandbox::default(); + sandbox.set_phase(SandboxPhase::Error.into()); + sandbox.status.as_mut().unwrap().provisioning = + Some(openshell_core::proto::SandboxProvisioning { + attempt_id: "attempt".into(), + timeout_time: openshell_core::time::timestamp_from_millis(300_000).ok(), + cleanup_error: "Compute reclamation is pending; the gateway will retry".into(), + ..Default::default() + }); + let json = super::sandbox_to_json(&sandbox); + assert_eq!(json["provisioning"]["attempt_id"], "attempt"); + assert!(json["provisioning"]["deadline"].is_null()); + assert!(json["provisioning"]["cleanup_completed_time"].is_null()); + assert_eq!(json["provisioning"]["timeout_time"], "1970-01-01T00:05:00Z"); + assert!( + json["provisioning"]["cleanup_error"] + .as_str() + .unwrap() + .contains("pending") + ); + } + #[test] fn sandbox_json_exposes_repair_diagnostic_and_accepted_generation() { use openshell_core::proto::{ConfigurationAdmissionState, SandboxConfigurationAdmission}; diff --git a/crates/openshell-driver-docker/src/lib.rs b/crates/openshell-driver-docker/src/lib.rs index 1bdfac927f..7fa57ed07f 100644 --- a/crates/openshell-driver-docker/src/lib.rs +++ b/crates/openshell-driver-docker/src/lib.rs @@ -89,7 +89,6 @@ use url::Url; const WATCH_BUFFER: usize = 128; const WATCH_POLL_INTERVAL: Duration = Duration::from_secs(2); const WATCH_POLL_MAX_BACKOFF: Duration = Duration::from_secs(30); -const SUPERVISOR_READY_TIMEOUT: Duration = Duration::from_secs(90); // The gateway closes a supervisor session as soon as it commits a sandbox to // Stopping, just before the compute-driver StopSandbox RPC arrives. Give that // request a bounded opportunity to mark the control exit intentional before @@ -2131,27 +2130,36 @@ impl DockerComputeDriver { } async fn stop_sandbox_inner(&self, sandbox_id: &str, sandbox_name: &str) -> Result<(), Status> { - let Some(container) = self + let container = self .find_managed_container_summary(sandbox_id, sandbox_name) - .await? - else { - if let Some(record) = self - .remove_pending_sandbox(sandbox_id, sandbox_name) - .await? - { + .await?; + // Startup can still be waiting for supervisor readiness after the + // workload container exists. Cancel it before removing the supervisor + // so its failure path cannot delete retained workload storage. + let mut pending = self + .remove_pending_sandbox(sandbox_id, sandbox_name) + .await?; + if let Some(task) = pending.as_mut().and_then(|record| record.task.take()) { + task.abort(); + let _ = task.await; + } + let Some(container) = container else { + if let Some(record) = pending { self.stop_control_process(&record.sandbox.id).await; self.remove_auxiliary_containers_for_sandbox(&record.sandbox.id) .await?; self.clear_runtime_failure(&record.sandbox.id).await; - if let Some(task) = record.task { - task.abort(); - } remove_docker_channel_volume_by_id(&self.docker, &record.sandbox.id, &self.config) .await?; cleanup_docker_boundary_state(&record.sandbox, &self.config); self.publish_deleted(record.sandbox.id); return Ok(()); } + if !sandbox_id.is_empty() { + self.stop_control_process(sandbox_id).await; + self.remove_auxiliary_containers_for_sandbox(sandbox_id) + .await?; + } return Err(Status::not_found("sandbox not found")); }; let Some(target) = summary_container_target(&container) else { @@ -5161,64 +5169,43 @@ async fn wait_for_docker_supervisor_ready( supervisor_id: &str, sandbox_id: &str, ) -> Result<(), Status> { - let wait = async { - loop { - let sandbox = docker - .inspect_container(sandbox_id, None) - .await - .map_err(|error| { - Status::internal(format!("inspect Docker sandbox container: {error}")) - })?; - if sandbox.state.unwrap_or_default().running == Some(false) { + // Gateway provisioning deadlines own startup expiration, including policy + // repairs that extend the window. Unhealthy means not ready while the + // supervisor is quarantined; only a stopped process is a startup failure. + loop { + let sandbox = docker + .inspect_container(sandbox_id, None) + .await + .map_err(|error| { + Status::internal(format!("inspect Docker sandbox container: {error}")) + })?; + if sandbox.state.unwrap_or_default().running == Some(false) { + let sandbox_log_tail = docker_container_log_tail(docker, sandbox_id).await; + return Err(Status::unavailable(format!( + "Docker sandbox exited before supervisor became ready{}", + format_named_log_tail("sandbox log tail", &sandbox_log_tail) + ))); + } + let inspected = docker + .inspect_container(supervisor_id, None) + .await + .map_err(|error| { + Status::internal(format!("inspect Docker supervisor container: {error}")) + })?; + let state = inspected.state.unwrap_or_default(); + match state.health.and_then(|health| health.status) { + Some(HealthStatusEnum::HEALTHY) => return Ok(()), + _ if state.running == Some(false) => { + let log_tail = docker_container_log_tail(docker, supervisor_id).await; let sandbox_log_tail = docker_container_log_tail(docker, sandbox_id).await; return Err(Status::unavailable(format!( - "Docker sandbox exited before supervisor became ready{}", + "Docker supervisor exited before becoming ready{}{}", + format_log_tail(&log_tail), format_named_log_tail("sandbox log tail", &sandbox_log_tail) ))); } - let inspected = docker - .inspect_container(supervisor_id, None) - .await - .map_err(|error| { - Status::internal(format!("inspect Docker supervisor container: {error}")) - })?; - let state = inspected.state.unwrap_or_default(); - match state.health.and_then(|health| health.status) { - Some(HealthStatusEnum::HEALTHY) => return Ok(()), - Some(HealthStatusEnum::UNHEALTHY) => { - let log_tail = docker_container_log_tail(docker, supervisor_id).await; - let sandbox_log_tail = docker_container_log_tail(docker, sandbox_id).await; - return Err(Status::unavailable(format!( - "Docker supervisor failed its readiness check{}{}", - format_log_tail(&log_tail), - format_named_log_tail("sandbox log tail", &sandbox_log_tail) - ))); - } - _ if state.running == Some(false) => { - let log_tail = docker_container_log_tail(docker, supervisor_id).await; - let sandbox_log_tail = docker_container_log_tail(docker, sandbox_id).await; - return Err(Status::unavailable(format!( - "Docker supervisor exited before becoming ready{}{}", - format_log_tail(&log_tail), - format_named_log_tail("sandbox log tail", &sandbox_log_tail) - ))); - } - _ => tokio::time::sleep(Duration::from_millis(100)).await, - } + _ => tokio::time::sleep(Duration::from_millis(100)).await, } - }; - - if let Ok(result) = tokio::time::timeout(SUPERVISOR_READY_TIMEOUT, wait).await { - result - } else { - let log_tail = docker_container_log_tail(docker, supervisor_id).await; - let sandbox_log_tail = docker_container_log_tail(docker, sandbox_id).await; - Err(Status::deadline_exceeded(format!( - "Docker supervisor did not become ready within {} seconds{}{}", - SUPERVISOR_READY_TIMEOUT.as_secs(), - format_log_tail(&log_tail), - format_named_log_tail("sandbox log tail", &sandbox_log_tail) - ))) } } diff --git a/crates/openshell-driver-podman/src/driver.rs b/crates/openshell-driver-podman/src/driver.rs index 62142a3157..32419fdf3e 100644 --- a/crates/openshell-driver-podman/src/driver.rs +++ b/crates/openshell-driver-podman/src/driver.rs @@ -1250,11 +1250,7 @@ impl PodmanComputeDriver { )] pub async fn stop_sandbox(&self, sandbox_id: &str) -> Result<(), ComputeDriverError> { let span_status = openshell_otel::ErrorStatusGuard::current(); - let container = self - .find_container(sandbox_id) - .await? - .ok_or(ComputeDriverError::NotFound)?; - let container_id = container.id; + let container = self.find_container(sandbox_id).await?; let supervisor = crate::isolation::supervisor_name(sandbox_id); match self .client @@ -1264,6 +1260,8 @@ impl PodmanComputeDriver { Ok(()) | Err(PodmanApiError::NotFound(_)) => {} Err(error) => return Err(error.into()), } + let container = container.ok_or(ComputeDriverError::NotFound)?; + let container_id = container.id; if container.state == "stopping" { let result = async { let finished_at = self @@ -2013,6 +2011,24 @@ mod tests { assert!(matches!(err, ComputeDriverError::NotFound)); } + #[tokio::test] + async fn stop_missing_workload_still_reclaims_supervisor() { + let (socket, requests, handle) = spawn_podman_stub( + "stop-orphan-supervisor", + vec![ + StubResponse::new(StatusCode::OK, "[]"), + StubResponse::new(StatusCode::NO_CONTENT, ""), + ], + ); + let result = test_driver(socket).stop_sandbox("sandbox-1").await; + assert!(matches!(result, Err(ComputeDriverError::NotFound))); + handle.await.unwrap(); + let requests = requests.lock().unwrap(); + assert_eq!(requests.len(), 2); + assert!(requests[1].contains("/stop?timeout=10")); + assert!(requests[1].contains(&crate::isolation::supervisor_name("sandbox-1"))); + } + #[tokio::test] async fn stop_and_start_target_the_existing_container() { let (stop_socket, stop_requests, stop_handle) = spawn_podman_stub( diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 5dbb697175..9732999c31 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -5,6 +5,7 @@ pub mod driver_config; pub mod lease; +pub mod provisioning_deadline; pub mod rootfs_tar; use crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE; @@ -328,6 +329,21 @@ pub struct ComputeDriverInfoSnapshot { pub rootfs_tar_max_bytes: u64, } +// Startup recovery owns persisted desired state before normal observations +// resume. Only deadline enforcement may run while startup is still pending. +async fn wait_for_startup( + mut startup: watch::Receiver, + mut cancel: watch::Receiver, +) -> bool { + if *cancel.borrow() { + return false; + } + tokio::select! { + result = startup.wait_for(|ready| *ready) => result.is_ok(), + _ = cancel.changed() => false, + } +} + /// Interval between store-vs-backend reconciliation sweeps. const RECONCILE_INTERVAL: Duration = Duration::from_mins(1); @@ -1156,15 +1172,14 @@ impl ComputeRuntime { let request_span = tracing::Span::current(); tokio::spawn( async move { - runtime - .complete_sandbox_stop( - sandbox_id, - sandbox_name, - previous, - stopping, - lifecycle_guard, - ) - .await + Box::pin(runtime.complete_sandbox_stop( + sandbox_id, + sandbox_name, + previous, + stopping, + lifecycle_guard, + )) + .await } .instrument(request_span), ) @@ -1269,6 +1284,17 @@ impl ComputeRuntime { .await .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? .ok_or_else(|| Status::not_found("sandbox not found"))?; + if provisioning_deadline::timed_out(&candidate) + && candidate + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + .is_some_and(|record| record.cleanup_completed_time.is_none()) + { + return Err(Status::failed_precondition( + "provisioning timeout cleanup is still pending; retry start after compute is reclaimed", + )); + } let sandbox_id = candidate.object_id().to_string(); let sandbox_name = candidate.object_name().to_string(); let lifecycle_guard = self.lifecycle_gates.lock_for(&sandbox_id).await; @@ -1289,10 +1315,24 @@ impl ComputeRuntime { if phase == SandboxPhase::Ready { return Ok(current); } + let provisioning_timeout = + phase == SandboxPhase::Error && provisioning_deadline::timed_out(¤t); + if provisioning_timeout + && current + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + .is_some_and(|record| record.cleanup_completed_time.is_none()) + { + return Err(Status::failed_precondition( + "provisioning timeout cleanup is still pending; retry start after compute is reclaimed", + )); + } if !matches!( phase, SandboxPhase::Stopped | SandboxPhase::Completed | SandboxPhase::Starting ) && !is_failed_main_process_result(¤t) + && !provisioning_timeout { return Err(Status::failed_precondition(format!( "sandbox must be Stopped, Completed, or a failed main-process Error to start (current phase: {phase:?})" @@ -1331,16 +1371,15 @@ impl ComputeRuntime { let request_span = tracing::Span::current(); tokio::spawn( async move { - runtime - .complete_sandbox_start( - sandbox_id, - sandbox_name, - previous, - starting, - lifecycle_guard, - launch_authentication, - ) - .await + Box::pin(runtime.complete_sandbox_start( + sandbox_id, + sandbox_name, + previous, + starting, + lifecycle_guard, + launch_authentication, + )) + .await } .instrument(request_span), ) @@ -1352,6 +1391,30 @@ impl ComputeRuntime { })? } + /// Release the lifecycle gate after the durable deadline expires, allowing + /// cleanup to cancel partial startup. Configuration repairs can extend this + /// deadline, so a fixed timeout around the driver RPC would expire too soon. + async fn await_provisioning_operation( + &self, + starting: &Sandbox, + operation: impl std::future::Future>, + ) -> Result { + tokio::pin!(operation); + loop { + tokio::select! { + result = &mut operation => return result, + () = tokio::time::sleep(Duration::from_secs(1)) => { + let current = self.store.get_message::(starting.object_id()) + .await.map_err(|error| Status::internal(error.to_string()))? + .ok_or_else(|| Status::not_found("sandbox removed during startup"))?; + if provisioning_deadline::timed_out(¤t) { + return Err(Status::deadline_exceeded("provisioning repair window expired")); + } + } + } + } + } + async fn complete_sandbox_start( &self, sandbox_id: String, @@ -1364,28 +1427,64 @@ impl ComputeRuntime { let generation_id = sandbox_runtime_generation(&starting) .map_err(Status::failed_precondition)? .into_string(); - let result = self - .driver - .call( - openshell_otel::rpc::START_SANDBOX, - Some(&sandbox_id), - |driver| { - let sandbox_id = sandbox_id.clone(); - let sandbox_name = sandbox_name.clone(); - async move { - driver - .start_sandbox(Request::new(StartSandboxRequest { - sandbox_id, - sandbox_name, - launch_authentication, - generation_id, - })) - .await - } - }, + let authentication_for_recreate = launch_authentication.clone(); + let mut result = self + .await_provisioning_operation( + &starting, + self.driver.call( + openshell_otel::rpc::START_SANDBOX, + Some(&sandbox_id), + |driver| { + let sandbox_id = sandbox_id.clone(); + let sandbox_name = sandbox_name.clone(); + async move { + driver + .start_sandbox(Request::new(StartSandboxRequest { + sandbox_id, + sandbox_name, + launch_authentication, + generation_id, + })) + .await + } + }, + ), ) .await; + if provisioning_deadline::timed_out(&previous) + && matches!(&result, Err(error) if error.code() == Code::NotFound) + { + // A partial provisioning attempt may have been canceled before a + // restartable backend object existed. Keep the API identity/spec. + let mut driver_sandbox = driver_sandbox_from_public(&starting, &self.driver_info.name) + .map_err(|status| *status)?; + if let Some(spec) = driver_sandbox.spec.as_mut() { + spec.launch_authentication = authentication_for_recreate; + } + result = self + .await_provisioning_operation( + &starting, + self.driver.call( + openshell_otel::rpc::CREATE_SANDBOX, + Some(&sandbox_id), + |driver| async move { + driver + .create_sandbox(Request::new(CreateSandboxRequest { + sandbox: Some(driver_sandbox), + })) + .await + .map(|_| { + tonic::Response::new( + openshell_core::proto::compute::v1::StartSandboxResponse {}, + ) + }) + }, + ), + ) + .await; + } + match result { Ok(_) => { let _global_guard = self.lock_global_for_lifecycle(&lifecycle_guard).await; @@ -1423,7 +1522,12 @@ impl ComputeRuntime { ) { let sandbox_id = transition.object_id(); let sandbox_name = transition.object_name(); - let observed = self.get_driver_sandbox(sandbox_id, sandbox_name).await; + let observed = tokio::time::timeout( + Duration::from_secs(10), + self.get_driver_sandbox(sandbox_id, sandbox_name), + ) + .await + .unwrap_or_else(|_| Err("compute lifecycle reconciliation timed out".into())); let _global_guard = self.lock_global_for_lifecycle(lifecycle_guard).await; match observed { @@ -1534,6 +1638,9 @@ impl ComputeRuntime { // the restarted supervisor registers its new id. status.exit_code = None; if phase == SandboxPhase::Starting { + status.provisioning = Some(provisioning_deadline::new_record( + openshell_core::time::now_ms(), + )); status.configuration_admission = Some(openshell_core::proto::SandboxConfigurationAdmission { // Fence delayed registrations from the previous runtime. @@ -1632,9 +1739,7 @@ impl ComputeRuntime { let request_span = tracing::Span::current(); tokio::spawn( async move { - runtime - .delete_sandbox_inner(target, delete_guard, global_guard) - .await + Box::pin(runtime.delete_sandbox_inner(target, delete_guard, global_guard)).await } .instrument(request_span), ) @@ -2190,20 +2295,36 @@ impl ComputeRuntime { Ok(sandbox) } - pub fn spawn_watchers(&self, shutdown_rx: watch::Receiver) { + pub fn spawn_watchers( + &self, + shutdown_rx: watch::Receiver, + startup_rx: watch::Receiver, + ) { let runtime = Arc::new(self.clone()); if self.store.is_single_replica() { + let deadline_runtime = runtime.clone(); + let deadline_shutdown = shutdown_rx.clone(); + tokio::spawn(async move { + deadline_runtime.provisioning_loop(deadline_shutdown).await; + }); let watch_runtime = runtime.clone(); let watch_shutdown = shutdown_rx.clone(); + let watch_startup = startup_rx.clone(); tokio::spawn(async move { + if !wait_for_startup(watch_startup, watch_shutdown.clone()).await { + return; + } Box::pin(watch_runtime.watch_loop(watch_shutdown)).await; }); tokio::spawn(async move { + if !wait_for_startup(startup_rx, shutdown_rx.clone()).await { + return; + } runtime.reconcile_loop(shutdown_rx).await; }); } else { tokio::spawn(async move { - runtime.lease_coordinator(shutdown_rx).await; + runtime.lease_coordinator(shutdown_rx, startup_rx).await; }); } } @@ -2440,25 +2561,27 @@ impl ComputeRuntime { } }; match self - .driver - .call( - openshell_otel::rpc::START_SANDBOX, - Some(&sandbox_id), - |driver| { - let sandbox_id = sandbox_id.clone(); - let sandbox_name = sandbox_name.clone(); - let launch_authentication = launch_authentication.clone(); - async move { - driver - .start_sandbox(Request::new(StartSandboxRequest { - sandbox_id, - sandbox_name, - launch_authentication, - generation_id, - })) - .await - } - }, + .await_provisioning_operation( + &sandbox, + self.driver.call( + openshell_otel::rpc::START_SANDBOX, + Some(&sandbox_id), + |driver| { + let sandbox_id = sandbox_id.clone(); + let sandbox_name = sandbox_name.clone(); + let launch_authentication = launch_authentication.clone(); + async move { + driver + .start_sandbox(Request::new(StartSandboxRequest { + sandbox_id, + sandbox_name, + launch_authentication, + generation_id, + })) + .await + } + }, + ), ) .await { @@ -2676,6 +2799,9 @@ impl ComputeRuntime { match self .store .update_message_cas::(&sandbox_id, 0, |s| { + if provisioning_deadline::timed_out(s) { + return; + } s.set_phase(SandboxPhase::Error as i32); let name = s.object_name().to_string(); upsert_ready_condition( @@ -2748,7 +2874,11 @@ impl ComputeRuntime { } } - async fn lease_coordinator(self: Arc, mut shutdown_rx: watch::Receiver) { + async fn lease_coordinator( + self: Arc, + mut shutdown_rx: watch::Receiver, + startup_rx: watch::Receiver, + ) { use lease::{LEASE_ACQUIRE_INTERVAL, LEASE_TTL, ReconcilerLease}; let lease = ReconcilerLease::new(self.store.clone(), self.replica_id.clone(), LEASE_TTL); @@ -2762,7 +2892,8 @@ impl ComputeRuntime { match lease.acquire_or_steal().await { Ok(guard) => { info!(replica = %lease.replica_id(), "acquired reconciler lease"); - self.run_as_holder(&lease, guard, &mut shutdown_rx).await; + self.run_as_holder(&lease, guard, &mut shutdown_rx, startup_rx.clone()) + .await; } Err(e) => { debug!( @@ -2790,6 +2921,7 @@ impl ComputeRuntime { lease: &lease::ReconcilerLease, mut guard: lease::LeaseGuard, shutdown_rx: &mut watch::Receiver, + startup_rx: watch::Receiver, ) { use lease::LEASE_RENEWAL_INTERVAL; @@ -2797,12 +2929,24 @@ impl ComputeRuntime { let runtime = self.clone(); let watch_cancel = cancel_rx.clone(); + let watch_startup = startup_rx.clone(); let watch_handle = tokio::spawn(async move { + if !wait_for_startup(watch_startup, watch_cancel.clone()).await { + return; + } Box::pin(runtime.watch_loop(watch_cancel)).await; }); + let runtime = self.clone(); + let deadline_cancel = cancel_rx.clone(); + let deadline_handle = tokio::spawn(async move { + runtime.provisioning_loop(deadline_cancel).await; + }); let runtime = self.clone(); let reconcile_handle = tokio::spawn(async move { + if !wait_for_startup(startup_rx, cancel_rx.clone()).await { + return; + } runtime.reconcile_loop(cancel_rx).await; }); @@ -2832,6 +2976,7 @@ impl ComputeRuntime { let _ = cancel_tx.send(true); let _ = watch_handle.await; let _ = reconcile_handle.await; + let _ = deadline_handle.await; return; } } @@ -2841,6 +2986,7 @@ impl ComputeRuntime { let _ = cancel_tx.send(true); let _ = watch_handle.await; let _ = reconcile_handle.await; + let _ = deadline_handle.await; info!(replica = %lease.replica_id(), "reconciler lease lost — returning to standby"); } @@ -3240,10 +3386,11 @@ impl ComputeRuntime { let current_phase = SandboxPhase::try_from(current.phase()).unwrap_or(SandboxPhase::Unknown); if connected - && matches!( - current_phase, - SandboxPhase::Deleting | SandboxPhase::Stopping | SandboxPhase::Stopped - ) + && (provisioning_deadline::timed_out(¤t) + || matches!( + current_phase, + SandboxPhase::Deleting | SandboxPhase::Stopping | SandboxPhase::Stopped + )) { return Err(format!( "sandbox is not accepting supervisor sessions while {current_phase:?}" @@ -3362,6 +3509,9 @@ impl ComputeRuntime { else { return Ok(()); }; + if provisioning_deadline::timed_out(&existing) { + return Ok(()); + } let phase = SandboxPhase::try_from(existing.phase()).unwrap_or(SandboxPhase::Unknown); if matches!( phase, @@ -3462,6 +3612,9 @@ impl ComputeRuntime { } fn schedule_ephemeral_sandbox_delete(&self, sandbox: &Sandbox) { + if provisioning_deadline::timed_out(sandbox) { + return; + } let ephemeral = sandbox.metadata.as_ref().is_some_and(|metadata| { metadata .annotations @@ -3498,6 +3651,11 @@ impl ComputeRuntime { .await .map_err(|e| e.to_string())?; if let Some(sandbox) = sandbox.as_ref() { + if provisioning_deadline::timed_out(sandbox) + && sandbox.phase() == i32::from(SandboxPhase::Error) + { + return Ok(()); + } // The watcher told us this sandbox's compute resource is gone, so // no request-side DeleteSandbox call is coming — release // driver-owned resources in the background. Watch events are @@ -3875,7 +4033,10 @@ impl ComputeRuntime { let sandbox = decode_sandbox_record(¤t_record)?; let phase = SandboxPhase::try_from(sandbox.phase()).unwrap_or(SandboxPhase::Unknown); - if phase == SandboxPhase::Completed || is_failed_main_process_result(&sandbox) { + if phase == SandboxPhase::Completed + || is_failed_main_process_result(&sandbox) + || provisioning_deadline::timed_out(&sandbox) + { // A terminal canonical process may legitimately have removed its // transient compute object. Keep the durable command result. return Ok(()); @@ -4539,6 +4700,7 @@ fn public_status_from_driver( endpoint_statuses: Vec::new(), configuration_admission: None, configuration_activated: None, + provisioning: None, } } @@ -4669,6 +4831,7 @@ fn apply_driver_snapshot( .configuration_admission .clone_from(¤t_status.configuration_admission); status.configuration_activated = current_status.configuration_activated; + status.provisioning.clone_from(¤t_status.provisioning); } if old_phase != phase { info!( @@ -4715,6 +4878,7 @@ pub fn apply_configuration_readiness(sandbox: &mut Sandbox) { return; }; let Some(admission) = status.configuration_admission.as_ref() else { + provisioning_deadline::reconcile_readiness(sandbox, openshell_core::time::now_ms()); return; }; let accepted = admission.state == i32::from(ConfigurationAdmissionState::Accepted); @@ -4770,6 +4934,7 @@ pub fn apply_configuration_readiness(sandbox: &mut Sandbox) { ..Default::default() }); } + provisioning_deadline::reconcile_readiness(sandbox, openshell_core::time::now_ms()); } fn driver_snapshot_confirms_stopped(incoming: &DriverSandbox) -> bool { @@ -12261,4 +12426,412 @@ mod tests { .unwrap(); assert_eq!(stored.object_workspace(), "alpha"); } + async fn seed_provisioning_attempt(runtime: &ComputeRuntime) -> Sandbox { + let mut sandbox = sandbox_record("sb-ttl", "sandbox-ttl", SandboxPhase::Provisioning); + let status = sandbox.status.as_mut().unwrap(); + status.provisioning = Some(provisioning_deadline::new_record(0)); + status.configuration_activated = Some(false); + status.configuration_admission = + Some(openshell_core::proto::SandboxConfigurationAdmission { + instance_id: uuid::Uuid::new_v4().to_string(), + state: openshell_core::proto::ConfigurationAdmissionState::Rejected.into(), + error: "Provider requires an inspected endpoint".into(), + ..Default::default() + }); + runtime.store.put_message(&sandbox).await.unwrap(); + runtime + .store + .get_message::("sb-ttl") + .await + .unwrap() + .unwrap() + } + + #[tokio::test] + async fn provisioning_timeout_persists_error_before_cleanup_and_preserves_record() { + let driver = ControlledDriver::new(); + let runtime = test_runtime(driver.clone()).await; + let sandbox = seed_provisioning_attempt(&runtime).await; + let gate = runtime.lifecycle_gates.lock_for("sb-ttl").await; + let global = runtime.lock_global_for_lifecycle(&gate).await; + assert!( + runtime + .claim_provisioning_timeout(&sandbox, 299_999) + .await + .unwrap() + .is_none() + ); + let expired = runtime + .claim_provisioning_timeout(&sandbox, 300_000) + .await + .unwrap() + .unwrap(); + drop(global); + assert_eq!(expired.phase(), i32::from(SandboxPhase::Error)); + assert!( + ready_condition(&expired) + .unwrap() + .message + .contains("Provider requires an inspected endpoint") + ); + assert_eq!(driver.stop_calls(), 0); + runtime + .report_main_process_exit("sb-ttl", "late-instance", 0) + .await + .unwrap(); + runtime + .reclaim_provisioning_timeout(&expired, &gate) + .await + .unwrap(); + assert_eq!(driver.stop_calls(), 1); + let cleaned = runtime + .store + .get_message::("sb-ttl") + .await + .unwrap() + .unwrap(); + assert!( + cleaned + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap() + .cleanup_completed_time + .is_some() + ); + assert_eq!(cleaned.phase(), i32::from(SandboxPhase::Error)); + runtime.apply_deleted("sb-ttl").await.unwrap(); + assert!( + runtime + .store + .get_message::("sb-ttl") + .await + .unwrap() + .is_some() + ); + drop(gate); + let starting = runtime + .start_sandbox("default", "sandbox-ttl") + .await + .unwrap(); + assert_eq!(starting.phase(), i32::from(SandboxPhase::Starting)); + let retry = starting + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap(); + assert!(retry.deadline.is_some()); + assert!(retry.timeout_time.is_none()); + assert_ne!( + retry.attempt_id, + expired + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap() + .attempt_id + ); + assert_eq!( + starting.status.as_ref().unwrap().configuration_activated, + Some(false) + ); + let gate = runtime.lifecycle_gates.lock_for("sb-ttl").await; + runtime + .reclaim_provisioning_timeout(&expired, &gate) + .await + .unwrap(); + assert_eq!( + driver.stop_calls(), + 1, + "stale cleanup must not stop a new attempt" + ); + } + + #[tokio::test] + async fn provisioning_timeout_cleanup_failure_is_durable_and_blocks_start() { + let driver = ControlledDriver::new(); + driver.set_stop_outcome(ControlledLifecycleOutcome::Error("private driver payload")); + let runtime = test_runtime(driver.clone()).await; + let sandbox = seed_provisioning_attempt(&runtime).await; + let gate = runtime.lifecycle_gates.lock_for("sb-ttl").await; + let global = runtime.lock_global_for_lifecycle(&gate).await; + let expired = runtime + .claim_provisioning_timeout(&sandbox, 300_000) + .await + .unwrap() + .unwrap(); + drop(global); + runtime + .reclaim_provisioning_timeout(&expired, &gate) + .await + .unwrap(); + drop(gate); + let failed = runtime + .store + .get_message::("sb-ttl") + .await + .unwrap() + .unwrap(); + let record = failed + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap(); + assert!(record.cleanup_completed_time.is_none()); + assert!(record.cleanup_retry_time.is_some()); + assert!(!record.cleanup_error.contains("private driver payload")); + let error = runtime + .start_sandbox("default", "sandbox-ttl") + .await + .unwrap_err(); + assert_eq!(error.code(), Code::FailedPrecondition); + assert_eq!(driver.start_calls(), 0); + let gate = runtime.lifecycle_gates.lock_for("sb-ttl").await; + runtime + .reclaim_provisioning_timeout(&failed, &gate) + .await + .unwrap(); + assert_eq!( + driver.stop_calls(), + 1, + "durable backoff must suppress immediate retries" + ); + } + #[tokio::test] + async fn provisioning_worker_adopts_legacy_once_and_reclaims_without_inventory() { + let driver = ControlledDriver::new(); + let runtime = test_runtime(driver.clone()).await; + let sandbox = sandbox_record("sb-adopt", "adopt", SandboxPhase::Provisioning); + runtime.store.put_message(&sandbox).await.unwrap(); + let now = openshell_core::time::now_ms(); + runtime.reconcile_provisioning_deadlines(now).await.unwrap(); + let first = runtime + .store + .get_message::("sb-adopt") + .await + .unwrap() + .unwrap(); + let record = first + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap(); + assert_eq!( + openshell_core::time::timestamp_to_millis(record.deadline.as_ref().unwrap()).unwrap(), + now + 300_000 + ); + runtime + .reconcile_provisioning_deadlines(now + 100_000) + .await + .unwrap(); + let second = runtime + .store + .get_message::("sb-adopt") + .await + .unwrap() + .unwrap(); + assert_eq!( + second.status.as_ref().unwrap().provisioning, + first.status.as_ref().unwrap().provisioning + ); + runtime + .reconcile_provisioning_deadlines(now + 300_000) + .await + .unwrap(); + tokio::time::timeout(Duration::from_secs(2), async { + loop { + let current = runtime + .store + .get_message::("sb-adopt") + .await + .unwrap() + .unwrap(); + if current + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap() + .cleanup_completed_time + .is_some() + { + assert_eq!(current.phase(), i32::from(SandboxPhase::Error)); + break; + } + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + assert_eq!(driver.stop_calls(), 1); + } + + #[tokio::test] + async fn provisioning_worker_uses_policy_commit_time_not_scan_time() { + use crate::policy_store::PolicyStoreExt; + let driver = ControlledDriver::new(); + let runtime = test_runtime(driver).await; + let now = openshell_core::time::now_ms(); + let mut sandbox = sandbox_record("sb-reset", "reset", SandboxPhase::Provisioning); + sandbox.status.as_mut().unwrap().provisioning = + Some(provisioning_deadline::new_record(now - 100_000)); + provisioning_deadline::refresh_configuration(&runtime.store, &mut sandbox, now - 100_000) + .await + .unwrap(); + runtime.store.put_message(&sandbox).await.unwrap(); + let policy = openshell_policy::restrictive_default_policy(); + let hash = openshell_core::policy_identity::deterministic_policy_hash(&policy); + runtime + .store + .put_policy_revision( + "new-policy", + "sb-reset", + "default", + 2, + &policy.encode_to_vec(), + &hash, + ) + .await + .unwrap(); + let committed = runtime + .store + .get_latest_policy("sb-reset") + .await + .unwrap() + .unwrap() + .created_at_ms; + runtime + .reconcile_provisioning_deadlines(committed + 60_000) + .await + .unwrap(); + let updated = runtime + .store + .get_message::("sb-reset") + .await + .unwrap() + .unwrap(); + let record = updated + .status + .as_ref() + .unwrap() + .provisioning + .as_ref() + .unwrap(); + assert_eq!( + openshell_core::time::timestamp_to_millis(record.deadline.as_ref().unwrap()).unwrap(), + committed + 300_000 + ); + assert_eq!(updated.phase(), i32::from(SandboxPhase::Provisioning)); + } + #[tokio::test] + async fn provisioning_expiry_does_not_wait_for_busy_lifecycle_driver() { + let driver = ControlledDriver::new(); + let runtime = test_runtime(driver.clone()).await; + let now = openshell_core::time::now_ms(); + let mut sandbox = sandbox_record("sb-busy-ttl", "busy-ttl", SandboxPhase::Starting); + sandbox.status.as_mut().unwrap().provisioning = + Some(provisioning_deadline::new_record(now)); + runtime.store.put_message(&sandbox).await.unwrap(); + let gate = runtime.lifecycle_gates.lock_for("sb-busy-ttl").await; + runtime + .reconcile_provisioning_deadlines(now + 300_000) + .await + .unwrap(); + let expired = runtime + .store + .get_message::("sb-busy-ttl") + .await + .unwrap() + .unwrap(); + assert_eq!(expired.phase(), i32::from(SandboxPhase::Error)); + let interrupted = tokio::time::timeout( + Duration::from_secs(3), + runtime.await_provisioning_operation( + &sandbox, + std::future::pending::>(), + ), + ) + .await + .expect("expired startup releases its lifecycle gate") + .unwrap_err(); + assert_eq!(interrupted.code(), Code::DeadlineExceeded); + runtime + .mark_sandbox_error(&sandbox, "StartFailed", "late startup failure") + .await; + let retained = runtime + .store + .get_message::("sb-busy-ttl") + .await + .unwrap() + .unwrap(); + assert!(provisioning_deadline::timed_out(&retained)); + assert!( + retained + .status + .as_ref() + .unwrap() + .conditions + .iter() + .any(|condition| condition.reason == "ProvisioningTimedOut") + ); + assert_eq!( + driver.stop_calls(), + 0, + "cleanup waits for the in-flight lifecycle operation" + ); + assert_eq!( + runtime + .start_sandbox("default", "busy-ttl") + .await + .unwrap_err() + .code(), + Code::FailedPrecondition + ); + drop(gate); + runtime + .reconcile_provisioning_deadlines(now + 300_001) + .await + .unwrap(); + tokio::time::timeout(Duration::from_secs(1), driver.stop_finished.notified()) + .await + .unwrap(); + } + + #[tokio::test] + async fn provisioning_retry_recreates_missing_partial_compute() { + let driver = ControlledDriver::new(); + let runtime = test_runtime(driver.clone()).await; + let sandbox = seed_provisioning_attempt(&runtime).await; + let gate = runtime.lifecycle_gates.lock_for("sb-ttl").await; + let global = runtime.lock_global_for_lifecycle(&gate).await; + let expired = runtime + .claim_provisioning_timeout(&sandbox, 300_000) + .await + .unwrap() + .unwrap(); + drop(global); + runtime + .reclaim_provisioning_timeout(&expired, &gate) + .await + .unwrap(); + drop(gate); + driver.set_start_outcome(ControlledLifecycleOutcome::NotFound); + let starting = runtime + .start_sandbox("default", "sandbox-ttl") + .await + .unwrap(); + assert_eq!(starting.object_id(), "sb-ttl"); + assert_eq!(starting.phase(), i32::from(SandboxPhase::Starting)); + } } diff --git a/crates/openshell-server/src/compute/provisioning_deadline.rs b/crates/openshell-server/src/compute/provisioning_deadline.rs new file mode 100644 index 0000000000..1b0bf3b296 --- /dev/null +++ b/crates/openshell-server/src/compute/provisioning_deadline.rs @@ -0,0 +1,799 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Gateway-owned provisioning repair-window transitions. +//! +//! Callers must persist each transition under the sandbox lifecycle fence. Times +//! are gateway-assigned Unix milliseconds, never supervisor-supplied values. +//! The timer remains armed after admission acceptance until compute becomes Ready. + +use openshell_core::proto::SandboxProvisioning; +use openshell_core::time::{timestamp_from_millis, timestamp_to_millis}; +use serde::{Deserialize, Serialize}; + +const REPAIR_WINDOW_MS: i64 = 300_000; + +/// Opaque identity of a committed effective configuration change. A -> B -> A +/// must use three different IDs even if the first and last content hashes match. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ConfigurationChange { + pub id: String, + pub committed_at_ms: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub(super) struct ProvisioningDeadline { + attempt_id: String, + change: ConfigurationChange, + first_rejection_at_ms: Option, + state: DeadlineState, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +enum DeadlineState { + Armed { deadline_at_ms: i64 }, + Expired { expired_at_ms: i64 }, + Ready, +} + +impl ProvisioningDeadline { + /// Decode persisted timing strictly. Invalid timing must not silently grant + /// another repair window on every gateway restart. + pub fn from_record(record: &SandboxProvisioning) -> Result { + fn millis(value: Option<&prost_types::Timestamp>, field: &str) -> Result { + let value = value.ok_or_else(|| format!("missing provisioning {field}"))?; + timestamp_to_millis(value).map_err(|_| format!("invalid provisioning {field}")) + } + if record.attempt_id.is_empty() || record.configuration_change_id.is_empty() { + return Err("missing provisioning identity".into()); + } + if record.deadline.is_some() && record.timeout_time.is_some() { + return Err("provisioning cannot be armed and expired".into()); + } + let state = if record.timeout_time.is_some() { + DeadlineState::Expired { + expired_at_ms: millis(record.timeout_time.as_ref(), "timeout time")?, + } + } else if record.deadline.is_some() { + DeadlineState::Armed { + deadline_at_ms: millis(record.deadline.as_ref(), "deadline")?, + } + } else { + DeadlineState::Ready + }; + Ok(Self { + attempt_id: record.attempt_id.clone(), + change: ConfigurationChange { + id: record.configuration_change_id.clone(), + committed_at_ms: millis(record.configuration_change_time.as_ref(), "change time")?, + }, + first_rejection_at_ms: record + .first_rejection_time + .as_ref() + .map(|value| millis(Some(value), "rejection time")) + .transpose()?, + state, + }) + } + + /// Replace timing only, preserving cleanup progress written by the worker. + pub fn write_record(&self, record: &mut SandboxProvisioning) { + record.attempt_id.clone_from(&self.attempt_id); + record.configuration_change_id.clone_from(&self.change.id); + record.configuration_change_time = timestamp_from_millis(self.change.committed_at_ms).ok(); + record.first_rejection_time = self + .first_rejection_at_ms + .and_then(|value| timestamp_from_millis(value).ok()); + record.deadline = self + .deadline_at_ms() + .and_then(|value| timestamp_from_millis(value).ok()); + record.timeout_time = match self.state { + DeadlineState::Expired { expired_at_ms } => timestamp_from_millis(expired_at_ms).ok(), + _ => None, + }; + } + + pub fn new(attempt_id: String, change: ConfigurationChange, now_ms: i64) -> Self { + Self { + attempt_id, + change, + first_rejection_at_ms: None, + state: DeadlineState::Armed { + deadline_at_ms: now_ms.saturating_add(REPAIR_WINDOW_MS), + }, + } + } + + pub fn deadline_at_ms(&self) -> Option { + match self.state { + DeadlineState::Armed { deadline_at_ms } => Some(deadline_at_ms), + DeadlineState::Expired { .. } | DeadlineState::Ready => None, + } + } + + /// Apply a newer, committed change to an active attempt. Delayed observation + /// uses commit time, not poll time. A change after expiry cannot revive it. + pub fn configuration_changed(&mut self, attempt_id: &str, change: ConfigurationChange) -> bool { + let Some(deadline_at_ms) = self.deadline_at_ms() else { + return false; + }; + if attempt_id != self.attempt_id + || change.id == self.change.id + || change.committed_at_ms < self.change.committed_at_ms + || change.committed_at_ms >= deadline_at_ms + { + return false; + } + self.state = DeadlineState::Armed { + deadline_at_ms: deadline_at_ms + .max(change.committed_at_ms.saturating_add(REPAIR_WINDOW_MS)), + }; + self.change = change; + self.first_rejection_at_ms = None; + true + } + + /// Only the first accepted rejection for the current change grants a repair + /// window. Retrying, reconnecting, or changing diagnostic text does not. + pub fn rejected(&mut self, attempt_id: &str, change_id: &str, now_ms: i64) -> bool { + let Some(deadline_at_ms) = self.deadline_at_ms() else { + return false; + }; + if !self.matches(attempt_id, change_id) + || self.first_rejection_at_ms.is_some() + || now_ms < self.change.committed_at_ms + || now_ms >= deadline_at_ms + { + return false; + } + self.first_rejection_at_ms = Some(now_ms); + self.state = DeadlineState::Armed { + deadline_at_ms: deadline_at_ms.max(now_ms.saturating_add(REPAIR_WINDOW_MS)), + }; + true + } + + /// Claim expiry after rechecking authoritative configuration under the same + /// lifecycle transaction. The caller must persist cleanup intent atomically. + pub fn expire(&mut self, attempt_id: &str, change_id: &str, now_ms: i64) -> bool { + if !self.matches(attempt_id, change_id) + || self + .deadline_at_ms() + .is_none_or(|deadline| now_ms < deadline) + { + return false; + } + self.state = DeadlineState::Expired { + expired_at_ms: now_ms, + }; + true + } + + /// Ready is the successful end of provisioning; admission acceptance alone + /// must not call this method. Late readiness requires an explicit retry. + pub fn ready(&mut self, attempt_id: &str, change_id: &str, now_ms: i64) -> bool { + if !self.matches(attempt_id, change_id) + || self + .deadline_at_ms() + .is_none_or(|deadline| now_ms >= deadline) + { + return false; + } + self.state = DeadlineState::Ready; + true + } + + fn matches(&self, attempt_id: &str, change_id: &str) -> bool { + self.attempt_id == attempt_id && self.change.id == change_id + } +} + +/// Whether compute reclamation belongs to a timed-out provisioning attempt. +pub fn timed_out(sandbox: &openshell_core::proto::Sandbox) -> bool { + sandbox + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + .is_some_and(|record| record.timeout_time.is_some()) +} + +/// Create an independent attempt. Supervisor reconnects must never call this. +pub fn new_record(now_ms: i64) -> SandboxProvisioning { + let mut record = SandboxProvisioning::default(); + ProvisioningDeadline::new( + uuid::Uuid::new_v4().to_string(), + ConfigurationChange { + id: format!("initial:{}", uuid::Uuid::new_v4()), + committed_at_ms: now_ms, + }, + now_ms, + ) + .write_record(&mut record); + record +} + +/// Apply the first accepted rejection under the same CAS as admission evidence. +/// The report's generation and supervisor instance must already be validated. +pub fn record_rejection(record: &mut SandboxProvisioning, now_ms: i64) -> Result<(), String> { + let mut deadline = ProvisioningDeadline::from_record(record)?; + deadline.rejected(&record.attempt_id, &record.configuration_change_id, now_ms); + deadline.write_record(record); + Ok(()) +} + +pub fn allows_admission(record: &SandboxProvisioning, now_ms: i64) -> bool { + ProvisioningDeadline::from_record(record).is_ok_and(|deadline| { + !matches!(deadline.state, DeadlineState::Expired { .. }) + && deadline.deadline_at_ms().is_none_or(|value| now_ms < value) + }) +} + +/// Readiness, not admission acceptance, ends the repair window. A late Ready +/// observation cannot win merely because the deadline scanner has not run yet. +pub(super) fn reconcile_readiness(sandbox: &mut openshell_core::proto::Sandbox, now_ms: i64) { + use openshell_core::proto::{SandboxCondition, SandboxPhase}; + let Some(status) = sandbox.status.as_mut() else { + return; + }; + if status.phase != i32::from(SandboxPhase::Ready) { + return; + } + let Some(record) = status.provisioning.as_mut() else { + return; + }; + let Ok(mut deadline) = ProvisioningDeadline::from_record(record) else { + return; + }; + if deadline.deadline_at_ms().is_none() { + return; + } + if deadline.ready(&record.attempt_id, &record.configuration_change_id, now_ms) { + deadline.write_record(record); + } else { + status.phase = SandboxPhase::Provisioning.into(); + status + .conditions + .retain(|condition| condition.r#type != "Ready"); + status.conditions.push(SandboxCondition { + r#type: "Ready".into(), + status: "False".into(), + reason: "ProvisioningDeadlineElapsed".into(), + message: "Provisioning deadline elapsed; awaiting compute reclamation".into(), + ..Default::default() + }); + } +} + +/// Attachment edits are stamped in the same sandbox CAS as the spec change. +pub fn attachments_changed(sandbox: &mut openshell_core::proto::Sandbox, now_ms: i64) { + if let Some(record) = sandbox + .status + .as_mut() + .and_then(|status| status.provisioning.as_mut()) + && record.deadline.is_some() + { + record.attachment_change_id = uuid::Uuid::new_v4().to_string(); + record.attachment_change_time = timestamp_from_millis(now_ms).ok(); + } +} + +/// Adopt legacy attempts once and reconcile committed source clocks before any +/// rejection/expiry decision. Persist the returned record with the owning CAS. +pub async fn refresh_configuration( + store: &crate::persistence::Store, + sandbox: &mut openshell_core::proto::Sandbox, + now_ms: i64, +) -> Result<(), String> { + use openshell_core::proto::SandboxPhase; + if !matches!( + SandboxPhase::try_from(sandbox.phase()), + Ok(SandboxPhase::Provisioning | SandboxPhase::Starting) + ) { + return Ok(()); + } + let status = sandbox.status.get_or_insert_with(Default::default); + let record = status + .provisioning + .get_or_insert_with(|| new_record(now_ms)); + if record.deadline.is_none() { + return Ok(()); + } + let change = crate::grpc::policy::configuration_change(store, sandbox).await?; + let record = sandbox + .status + .as_mut() + .and_then(|status| status.provisioning.as_mut()) + .expect("record initialized"); + let mut deadline = ProvisioningDeadline::from_record(record)?; + if record.configuration_change_id.starts_with("initial:") { + // Taking the first source snapshot does not grant a second initial window. + deadline.change.id = change.id; + } else { + deadline.configuration_changed(&record.attempt_id, change); + } + deadline.write_record(record); + Ok(()) +} + +impl super::ComputeRuntime { + pub(super) async fn provisioning_loop( + self: std::sync::Arc, + mut cancel: tokio::sync::watch::Receiver, + ) { + loop { + tokio::select! { + _ = cancel.changed() => return, + result = self.reconcile_provisioning_deadlines(openshell_core::time::now_ms()) => { + if let Err(error) = result { + tracing::warn!(%error, "Provisioning deadline reconciliation failed"); + } + } + } + tokio::select! { + _ = cancel.changed() => return, + () = tokio::time::sleep(std::time::Duration::from_secs(1)) => {} + } + } + } + + pub(super) async fn reconcile_provisioning_deadlines(&self, now_ms: i64) -> Result<(), String> { + use crate::persistence::{ObjectListQuery, ObjectType}; + use openshell_core::{ + ObjectId, + proto::{Sandbox, SandboxPhase}, + }; + use prost::Message; + let records = self + .store + .collect_records(Sandbox::object_type(), ObjectListQuery::AllWorkspaces) + .await + .map_err(|error| error.to_string())?; + for record in records { + let candidate = + Sandbox::decode(record.payload.as_slice()).map_err(|error| error.to_string())?; + if !matches!( + SandboxPhase::try_from(candidate.phase()), + Ok(SandboxPhase::Provisioning | SandboxPhase::Starting) + ) && !timed_out(&candidate) + { + continue; + } + // Expiration can fence Starting while its driver RPC owns the + // lifecycle gate. Cleanup waits for that gate; Error never waits + // for compute I/O, matching the existing driver-observation fence. + let global = self.sync_lock.clone().lock_owned().await; + let Some(mut current) = self + .store + .get_message::(&record.id) + .await + .map_err(|e| e.to_string())? + else { + continue; + }; + let previous = current.clone(); + refresh_configuration(&self.store, &mut current, now_ms).await?; + if current != previous { + current = self + .store + .update_message_cas::( + &record.id, + super::sandbox_resource_version(&previous), + |sandbox| sandbox.status.clone_from(¤t.status), + ) + .await + .map_err(|e| e.to_string())?; + self.sandbox_watch_bus.notify(&record.id); + } + if let Some(expired) = self.claim_provisioning_timeout(¤t, now_ms).await? { + current = expired; + } + drop(global); + if timed_out(¤t) + && current + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + .is_some_and(|record| { + record.cleanup_completed_time.is_none() + && record + .cleanup_retry_time + .as_ref() + .and_then(|t| timestamp_to_millis(t).ok()) + .is_none_or(|t| t <= now_ms) + }) + { + let Ok(guard) = self.lifecycle_gates.gate_for(&record.id).try_lock_owned() else { + continue; + }; + let gate = super::SandboxLifecycleGuard { _guard: guard }; + let runtime = self.clone(); + tokio::spawn(async move { + if let Err(error) = runtime.reclaim_provisioning_timeout(¤t, &gate).await + { + tracing::warn!(sandbox_id = current.object_id(), %error, "Provisioning cleanup will retry"); + } + }); + } + } + Ok(()) + } + + /// Claim expiration durably before touching the backend. The caller owns the + /// global configuration guard; CAS fences concurrent lifecycle operations. + /// The separate cleanup step also requires the per-sandbox lifecycle gate. + pub(crate) async fn claim_provisioning_timeout( + &self, + current: &openshell_core::proto::Sandbox, + now_ms: i64, + ) -> Result, String> { + use openshell_core::ObjectId; + use openshell_core::proto::{Sandbox, SandboxCondition, SandboxPhase}; + if !matches!( + SandboxPhase::try_from(current.phase()), + Ok(SandboxPhase::Provisioning | SandboxPhase::Starting) + ) { + return Ok(None); + } + let Some(record) = current + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + else { + return Ok(None); + }; + let mut deadline = ProvisioningDeadline::from_record(record)?; + if !deadline.expire(&record.attempt_id, &record.configuration_change_id, now_ms) { + return Ok(None); + } + let updated = self + .store + .update_message_cas::( + current.object_id(), + super::sandbox_resource_version(current), + |sandbox| { + let status = sandbox.status.as_mut().expect("provisioning status exists"); + deadline.write_record( + status + .provisioning + .as_mut() + .expect("provisioning record exists"), + ); + status.phase = SandboxPhase::Error.into(); + let diagnostic = status + .configuration_admission + .as_ref() + .map_or("", |admission| admission.error.as_str()); + let message = if diagnostic.is_empty() { + "Provisioning repair window expired after 300 seconds".to_string() + } else { + format!( + "Provisioning repair window expired after 300 seconds: {diagnostic}" + ) + }; + status + .conditions + .retain(|condition| condition.r#type != "Ready"); + status.conditions.push(SandboxCondition { + r#type: "Ready".into(), + status: "False".into(), + reason: "ProvisioningTimedOut".into(), + message, + transition_time: timestamp_from_millis(now_ms).ok(), + }); + }, + ) + .await + .map_err(|error| error.to_string())?; + self.sandbox_index.update_from_sandbox(&updated); + self.sandbox_watch_bus.notify(current.object_id()); + tracing::warn!( + sandbox_id = current.object_id(), + "Sandbox provisioning repair window expired" + ); + Ok(Some(updated)) + } + + /// The lifecycle gate remains owned across the bounded driver call, so an + /// explicit start cannot race an old cleanup worker. Retry intent survives + /// cancellation, gateway restart, and a lost leader lease. + pub(super) async fn reclaim_provisioning_timeout( + &self, + expired: &openshell_core::proto::Sandbox, + lifecycle_guard: &super::SandboxLifecycleGuard, + ) -> Result<(), String> { + use openshell_core::proto::Sandbox; + use openshell_core::proto::compute::v1::StopSandboxRequest; + use openshell_core::{ObjectId, ObjectName}; + let current = { + let _global_guard = self.lock_global_for_lifecycle(lifecycle_guard).await; + self.store + .get_message::(expired.object_id()) + .await + .map_err(|error| error.to_string())? + }; + let Some(current) = current else { + return Ok(()); + }; + let expected_attempt = expired + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + .map(|record| record.attempt_id.as_str()); + if current.phase() != i32::from(openshell_core::proto::SandboxPhase::Error) + || current + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + .map(|record| record.attempt_id.as_str()) + != expected_attempt + { + return Ok(()); + } + let expired = ¤t; + let Some(record) = expired + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + else { + return Ok(()); + }; + if record.timeout_time.is_none() || record.cleanup_completed_time.is_some() { + return Ok(()); + } + let now_ms = openshell_core::time::now_ms(); + if record + .cleanup_retry_time + .as_ref() + .map(timestamp_to_millis) + .transpose() + .map_err(|error| error.to_string())? + .is_some_and(|retry| retry > now_ms) + { + return Ok(()); + } + // Cross-replica cleanup claim. A replacement leader waits longer than + // the bounded driver call before retrying an interrupted reclamation. + { + let _global_guard = self.lock_global_for_lifecycle(lifecycle_guard).await; + self.store + .update_message_cas::( + expired.object_id(), + super::sandbox_resource_version(expired), + |sandbox| { + sandbox + .status + .as_mut() + .and_then(|status| status.provisioning.as_mut()) + .expect("timeout record exists") + .cleanup_retry_time = + timestamp_from_millis(now_ms.saturating_add(35_000)).ok(); + }, + ) + .await + .map_err(|error| error.to_string())?; + } + let sandbox_id = expired.object_id().to_string(); + let sandbox_name = expired.object_name().to_string(); + let result = tokio::time::timeout( + std::time::Duration::from_secs(30), + self.driver.call( + openshell_otel::rpc::STOP_SANDBOX, + Some(&sandbox_id), + |driver| { + let sandbox_id = sandbox_id.clone(); + async move { + driver + .stop_sandbox(tonic::Request::new(StopSandboxRequest { + sandbox_id, + sandbox_name, + })) + .await + } + }, + ), + ) + .await; + let reclaimed = matches!(&result, Ok(Ok(_))) + || matches!(&result, Ok(Err(error)) if error.code() == tonic::Code::NotFound); + let _global_guard = self.lock_global_for_lifecycle(lifecycle_guard).await; + let Some(current) = self + .store + .get_message::(&sandbox_id) + .await + .map_err(|error| error.to_string())? + else { + return Ok(()); + }; + if !current + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + .is_some_and(|latest| { + latest.attempt_id == record.attempt_id && latest.timeout_time.is_some() + }) + { + return Ok(()); + } + let completed_at_ms = openshell_core::time::now_ms(); + let updated = self + .store + .update_message_cas::( + &sandbox_id, + super::sandbox_resource_version(¤t), + |sandbox| { + let record = sandbox + .status + .as_mut() + .and_then(|status| status.provisioning.as_mut()) + .expect("timeout attempt was checked under the lifecycle gate"); + if reclaimed { + record.cleanup_completed_time = timestamp_from_millis(completed_at_ms).ok(); + record.cleanup_error.clear(); + record.cleanup_retry_time = None; + } else { + record.cleanup_error = + "Compute reclamation is pending; the gateway will retry".into(); + record.cleanup_retry_time = + timestamp_from_millis(completed_at_ms.saturating_add(5_000)).ok(); + } + }, + ) + .await + .map_err(|error| error.to_string())?; + if reclaimed { + self.cleanup_stopped_sandbox_sessions(&updated).await?; + tracing::info!(sandbox_id, "Reclaimed timed-out provisioning compute"); + } + self.sandbox_index.update_from_sandbox(&updated); + self.sandbox_watch_bus.notify(&sandbox_id); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn protobuf_roundtrip_retains_deadline_and_cleanup_progress() { + use prost::Message; + let mut deadline = ProvisioningDeadline::new("attempt".into(), change("change", 0), 0); + assert!(deadline.rejected("attempt", "change", 1_000)); + let mut record = SandboxProvisioning::default(); + deadline.write_record(&mut record); + let bytes = record.encode_to_vec(); + let mut restored = SandboxProvisioning::decode(bytes.as_slice()).unwrap(); + assert_eq!( + ProvisioningDeadline::from_record(&restored).unwrap(), + deadline + ); + assert!(deadline.expire("attempt", "change", 301_000)); + deadline.write_record(&mut restored); + restored.cleanup_error = "retry pending".into(); + restored.cleanup_retry_time = timestamp_from_millis(306_000).ok(); + let restored_deadline = ProvisioningDeadline::from_record(&restored).unwrap(); + restored_deadline.write_record(&mut restored); + assert_eq!(restored.cleanup_error, "retry pending"); + assert!(restored.cleanup_retry_time.is_some()); + assert!(restored.deadline.is_none()); + assert_eq!(restored_deadline, deadline); + } + + #[test] + fn malformed_persisted_timing_cannot_grant_another_window() { + assert!(ProvisioningDeadline::from_record(&SandboxProvisioning::default()).is_err()); + let mut record = new_record(0); + record.timeout_time = timestamp_from_millis(300_000).ok(); + assert!(ProvisioningDeadline::from_record(&record).is_err()); + record.timeout_time = None; + record.configuration_change_time = None; + assert!(ProvisioningDeadline::from_record(&record).is_err()); + } + + fn change(id: &str, committed_at_ms: i64) -> ConfigurationChange { + ConfigurationChange { + id: id.into(), + committed_at_ms, + } + } + + fn initial() -> ProvisioningDeadline { + ProvisioningDeadline::new("attempt-1".into(), change("change-1", 0), 0) + } + + #[test] + fn expires_at_exactly_300_seconds_without_a_report() { + let mut timer = initial(); + assert!(!timer.expire("attempt-1", "change-1", 299_999)); + assert!(timer.expire("attempt-1", "change-1", 300_000)); + assert!(!timer.expire("attempt-1", "change-1", 300_001)); + } + + #[test] + fn repeated_failures_do_not_extend_the_repair_window() { + let mut timer = initial(); + assert!(timer.rejected("attempt-1", "change-1", 5_000)); + for now in (7_000..305_000).step_by(2_000) { + assert!(!timer.rejected("attempt-1", "change-1", now)); + } + assert_eq!(timer.deadline_at_ms(), Some(305_000)); + assert!(timer.expire("attempt-1", "change-1", 305_000)); + } + + #[test] + fn change_and_first_failed_load_each_reset_once() { + let mut timer = initial(); + assert!(timer.configuration_changed("attempt-1", change("change-2", 100_000))); + assert_eq!(timer.deadline_at_ms(), Some(400_000)); + assert!(timer.rejected("attempt-1", "change-2", 105_000)); + assert_eq!(timer.deadline_at_ms(), Some(405_000)); + assert!(!timer.configuration_changed("attempt-1", change("change-2", 110_000))); + assert!(!timer.rejected("attempt-1", "change-2", 110_000)); + assert_eq!(timer.deadline_at_ms(), Some(405_000)); + } + + #[test] + fn delayed_observation_uses_commit_time() { + let mut timer = initial(); + // Caller can observe this later, but it committed before the deadline. + assert!(timer.configuration_changed("attempt-1", change("change-2", 100_000))); + assert_eq!(timer.deadline_at_ms(), Some(400_000)); + assert!(!timer.configuration_changed("attempt-1", change("older", 50_000))); + } + + #[test] + fn stale_attempts_and_changes_cannot_extend_or_expire() { + let mut timer = initial(); + assert!(!timer.rejected("old-attempt", "change-1", 1_000)); + assert!(!timer.rejected("attempt-1", "old-change", 1_000)); + assert!(!timer.configuration_changed("old-attempt", change("change-2", 1_000))); + assert!(!timer.expire("old-attempt", "change-1", 400_000)); + assert!(!timer.expire("attempt-1", "old-change", 400_000)); + assert_eq!(timer.deadline_at_ms(), Some(300_000)); + } + + #[test] + fn late_change_failure_or_ready_cannot_escape_expiry() { + let mut timer = initial(); + assert!(!timer.configuration_changed("attempt-1", change("change-2", 300_000))); + assert!(!timer.rejected("attempt-1", "change-1", 300_000)); + assert!(!timer.ready("attempt-1", "change-1", 300_000)); + assert!(timer.expire("attempt-1", "change-1", 300_000)); + assert!(!timer.ready("attempt-1", "change-1", 300_001)); + assert!(!timer.configuration_changed("attempt-1", change("change-2", 100_000))); + } + + #[test] + fn ready_disarms_and_cannot_rearm_for_a_failed_live_update() { + let mut timer = initial(); + assert!(timer.ready("attempt-1", "change-1", 50_000)); + assert!(!timer.rejected("attempt-1", "change-1", 60_000)); + assert!(!timer.configuration_changed("attempt-1", change("change-2", 60_000))); + assert!(!timer.expire("attempt-1", "change-1", 400_000)); + } + + #[test] + fn restart_roundtrip_preserves_first_failure_and_deadline() { + let mut timer = initial(); + assert!(timer.rejected("attempt-1", "change-1", 5_000)); + let encoded = serde_json::to_vec(&timer).unwrap(); + let mut restored: ProvisioningDeadline = serde_json::from_slice(&encoded).unwrap(); + assert_eq!(restored, timer); + assert!(!restored.rejected("attempt-1", "change-1", 100_000)); + assert_eq!(restored.deadline_at_ms(), Some(305_000)); + } + + #[test] + fn explicit_retry_is_a_new_attempt_and_old_cleanup_cannot_expire_it() { + let mut timer = + ProvisioningDeadline::new("attempt-2".into(), change("change-2", 350_000), 400_000); + assert_eq!(timer.deadline_at_ms(), Some(700_000)); + assert!(!timer.expire("attempt-1", "change-2", 800_000)); + } + + #[test] + fn legacy_initialization_grants_one_window_from_adoption() { + let timer = ProvisioningDeadline::new("adopted".into(), change("legacy", 0), 1_000_000); + assert_eq!(timer.deadline_at_ms(), Some(1_300_000)); + } +} diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index 1381faa583..9994208c3e 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -173,12 +173,22 @@ const MAX_LABEL_SELECTOR_PAIRS: usize = 64; struct StoredSettings { revision: u64, settings: BTreeMap, + /// Per-key commit clocks, including deletion tombstones. Persisted in the + /// same CAS payload as values so polling and restart cannot refresh them. + #[serde(default)] + change_clocks: BTreeMap, /// Database `resource_version` for CAS. Not persisted in the JSON payload; /// loaded from `ObjectRecord` and used for optimistic concurrency control. #[serde(skip)] resource_version: u64, } +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SettingChangeClock { + id: String, + committed_at_ms: i64, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(tag = "type", content = "value")] enum StoredSettingValue { diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index ba6ffcc621..a3a9b5cc3b 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -11,6 +11,8 @@ #![allow(clippy::items_after_statements)] // DB_PORTS const inside function mod endpoint_status; +mod provisioning_clock; +pub use provisioning_clock::configuration_change; pub(super) use endpoint_status::handle_report_endpoint_status; pub use endpoint_status::{ @@ -3574,6 +3576,7 @@ async fn handle_update_config_inner( )); } let _settings_guard = state.settings_mutex.lock().await; + let _sandbox_sync_guard = state.compute.sandbox_sync_guard().await; if has_merge_ops { return Err(Status::invalid_argument( @@ -3609,7 +3612,6 @@ async fn handle_update_config_inner( // Global policy determines the report's effective configuration. // Serialize its writes after validation so a report cannot commit // evidence derived from the policy this update has replaced. - let _sandbox_sync_guard = state.compute.sandbox_sync_guard().await; let latest = state .store .get_latest_policy(GLOBAL_POLICY_SANDBOX_ID) @@ -3709,11 +3711,6 @@ async fn handle_update_config_inner( // Deleting global policy changes the report's effective configuration. // Keep settings -> sandbox lock order for all global policy mutations. - let _sandbox_sync_guard = if key == POLICY_SETTING_KEY && req.delete_setting { - Some(state.compute.sandbox_sync_guard().await) - } else { - None - }; let mut global_settings = load_global_settings(state.store.as_ref()).await?; let provider_composition_was_enabled = provider_policy_composition_enabled_in(&global_settings)?; @@ -3780,6 +3777,7 @@ async fn handle_update_config_inner( if has_setting { let _settings_guard = state.settings_mutex.lock().await; + let _sandbox_sync_guard = state.compute.sandbox_sync_guard().await; if key == POLICY_SETTING_KEY { return Err(Status::invalid_argument( @@ -3874,6 +3872,7 @@ async fn handle_update_config_inner( )); } + let _sandbox_sync_guard = state.compute.sandbox_sync_guard().await; if has_merge_ops { let global_settings = load_global_settings(state.store.as_ref()).await?; if global_settings.settings.contains_key(POLICY_SETTING_KEY) { @@ -4047,12 +4046,6 @@ async fn handle_update_config_inner( .await?; } - let _sandbox_sync_guard = if backfill_policy.is_some() { - Some(state.compute.sandbox_sync_guard().await) - } else { - None - }; - let payload = new_policy.encode_to_vec(); let hash = deterministic_policy_hash(&new_policy); let (_next_version, committed_annotations) = { @@ -4423,8 +4416,21 @@ pub(super) async fn handle_report_sandbox_configuration( if reported == ConfigurationAdmissionState::Unspecified { return Err(Status::invalid_argument("admission state is required")); } - let sandbox = + let _guard = state.compute.sandbox_sync_guard().await; + let mut sandbox = super::sandbox::fetch_and_authorize_sandbox(state, &principal, &sandbox_id).await?; + crate::compute::provisioning_deadline::refresh_configuration( + &state.store, + &mut sandbox, + current_time_ms(), + ) + .await + .map_err(Status::internal)?; + if crate::compute::provisioning_deadline::timed_out(&sandbox) { + return Err(Status::failed_precondition( + "provisioning repair window expired; explicitly start the sandbox after cleanup", + )); + } let current = sandbox .status .as_ref() @@ -4494,10 +4500,35 @@ pub(super) async fn handle_report_sandbox_configuration( .metadata .as_ref() .map_or(0, |metadata| metadata.resource_version); - let _guard = state.compute.sandbox_sync_guard().await; + let now_ms = current_time_ms(); + let mut provisioning = sandbox + .status + .as_ref() + .and_then(|status| status.provisioning.clone()); + if let Some(record) = provisioning.as_mut() { + if !crate::compute::provisioning_deadline::allows_admission(record, now_ms) { + state + .compute + .claim_provisioning_timeout(&sandbox, now_ms) + .await + .map_err(Status::internal)?; + return Err(Status::failed_precondition( + "provisioning repair window expired", + )); + } + if reported == ConfigurationAdmissionState::Rejected { + crate::compute::provisioning_deadline::record_rejection(record, now_ms) + .map_err(Status::internal)?; + } + } let updated = state .store .update_message_cas::(&sandbox_id, expected_version, |sandbox| { + sandbox + .status + .get_or_insert_with(Default::default) + .provisioning + .clone_from(&provisioning); sandbox .status .get_or_insert_with(Default::default) @@ -7365,6 +7396,15 @@ async fn load_settings_record( let mut settings = serde_json::from_slice::(&record.payload) .map_err(|e| Status::internal(format!("decode settings payload failed: {e}")))?; settings.resource_version = record.resource_version; + for key in settings.settings.keys() { + settings + .change_clocks + .entry(key.clone()) + .or_insert_with(|| super::SettingChangeClock { + id: format!("{}:{}:{key}", record.id, record.resource_version), + committed_at_ms: record.updated_at_ms, + }); + } Ok(settings) } else { Ok(StoredSettings::default()) @@ -7380,7 +7420,22 @@ async fn save_settings_record( ) -> Result<(), Status> { use crate::persistence::WriteCondition; - let payload = serde_json::to_vec(settings) + let previous = load_settings_record(store, object_type, workspace, name).await?; + let mut persisted = settings.clone(); + persisted.change_clocks = previous.change_clocks.clone(); + let now_ms = current_time_ms(); + for key in previous.settings.keys().chain(settings.settings.keys()) { + if previous.settings.get(key) != settings.settings.get(key) { + persisted.change_clocks.insert( + key.clone(), + super::SettingChangeClock { + id: uuid::Uuid::new_v4().to_string(), + committed_at_ms: now_ms, + }, + ); + } + } + let payload = serde_json::to_vec(&persisted) .map_err(|e| Status::internal(format!("encode settings payload failed: {e}")))?; let (id, condition) = if settings.resource_version == 0 { @@ -7766,6 +7821,47 @@ mod tests { request } + #[tokio::test] + async fn provisioning_timeout_rejects_supervisor_registration() { + use openshell_core::proto::{ + ConfigurationAdmissionState, ReportSandboxConfigurationRequest, + SandboxConfigurationAdmission, SandboxPhase, SandboxProvisioning, + }; + let state = test_server_state().await; + let sandbox_id = "sb-timeout-registration"; + let mut sandbox = test_sandbox( + sandbox_id, + "timeout-registration", + openshell_policy::restrictive_default_policy(), + Vec::new(), + ); + sandbox.set_phase(SandboxPhase::Error.into()); + sandbox.status.as_mut().unwrap().provisioning = Some(SandboxProvisioning { + timeout_time: openshell_core::time::timestamp_from_millis(300_000).ok(), + ..Default::default() + }); + state.store.put_message(&sandbox).await.unwrap(); + let error = handle_report_sandbox_configuration( + &state, + with_sandbox( + Request::new(ReportSandboxConfigurationRequest { + sandbox_id: sandbox_id.into(), + admission: Some(SandboxConfigurationAdmission { + instance_id: uuid::Uuid::new_v4().to_string(), + state: ConfigurationAdmissionState::Pending.into(), + ..Default::default() + }), + ..Default::default() + }), + sandbox_id, + ), + ) + .await + .unwrap_err(); + assert_eq!(error.code(), Code::FailedPrecondition); + assert!(error.message().contains("repair window expired")); + } + #[tokio::test] async fn configuration_admission_rejects_stale_generation_and_instance() { use openshell_core::proto::{ diff --git a/crates/openshell-server/src/grpc/policy/provisioning_clock.rs b/crates/openshell-server/src/grpc/policy/provisioning_clock.rs new file mode 100644 index 0000000000..2097bf37bd --- /dev/null +++ b/crates/openshell-server/src/grpc/policy/provisioning_clock.rs @@ -0,0 +1,271 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Durable source clocks for the provisioning repair window. Status observations +//! never contribute. Source identities distinguish real A -> B -> A edits. + +use super::{POLICY_SETTING_KEY, load_global_settings, load_sandbox_settings}; +use crate::compute::provisioning_deadline::ConfigurationChange; +use crate::persistence::{ObjectId, ObjectName, ObjectType, ObjectWorkspace, Store}; +use crate::policy_store::PolicyStoreExt; +use crate::storage_proto::StoredProviderProfile; +use openshell_core::proto::{Provider, Sandbox}; +use openshell_core::time::timestamp_to_millis; +use prost::Message; +use sha2::{Digest, Sha256}; +use std::collections::BTreeSet; + +/// Read only committed configuration sources, never sandbox `updated_at`. +/// The caller serializes this read with configuration mutations and expiry. +pub async fn configuration_change( + store: &Store, + sandbox: &Sandbox, +) -> Result { + let global = load_global_settings(store) + .await + .map_err(|e| e.to_string())?; + let local = load_sandbox_settings(store, sandbox.object_workspace(), sandbox.object_name()) + .await + .map_err(|e| e.to_string())?; + let created = sandbox + .metadata + .as_ref() + .and_then(|meta| meta.created_time.as_ref()) + .and_then(|time| timestamp_to_millis(time).ok()) + .unwrap_or(0); + let mut committed_at_ms = created; + let mut sources = Vec::<(String, String)>::new(); + let keys: BTreeSet<_> = global + .change_clocks + .keys() + .chain(local.change_clocks.keys()) + .collect(); + for key in keys { + if key != POLICY_SETTING_KEY && openshell_core::settings::setting_for_key(key).is_none() { + continue; + } + // Global settings override sandbox values. Keep global deletion clocks + // because deleting an override reveals the sandbox value again. + if let Some(clock) = global.change_clocks.get(key) { + sources.push((format!("global:{key}"), clock.id.clone())); + committed_at_ms = committed_at_ms.max(clock.committed_at_ms); + } + if key != POLICY_SETTING_KEY + && !global.settings.contains_key(key) + && let Some(clock) = local.change_clocks.get(key) + { + sources.push((format!("sandbox:{key}"), clock.id.clone())); + committed_at_ms = committed_at_ms.max(clock.committed_at_ms); + } + } + if !global.settings.contains_key(POLICY_SETTING_KEY) { + if let Some(policy) = store + .get_latest_policy(sandbox.object_id()) + .await + .map_err(|e| e.to_string())? + { + sources.push(( + "policy".into(), + format!("{}:{}", policy.version, policy.policy_hash), + )); + // Lazy version-one backfill has the same source identity as the + // original spec, so its newer timestamp alone cannot reset the TTL. + committed_at_ms = committed_at_ms.max(policy.created_at_ms); + } else { + let hash = sandbox + .spec + .as_ref() + .and_then(|spec| spec.policy.as_ref()) + .map(openshell_core::policy_identity::deterministic_policy_hash) + .unwrap_or_default(); + sources.push(("policy".into(), format!("1:{hash}"))); + } + } + if let Some(record) = sandbox + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + { + sources.push(("attachments".into(), record.attachment_change_id.clone())); + if let Some(time) = &record.attachment_change_time { + committed_at_ms = + committed_at_ms.max(timestamp_to_millis(time).map_err(|e| e.to_string())?); + } + } + if let Some(spec) = &sandbox.spec { + for name in &spec.providers { + let Some(record) = store + .get_by_name(Provider::object_type(), sandbox.object_workspace(), name) + .await + .map_err(|e| e.to_string())? + else { + sources.push((format!("provider:{name}"), "missing".into())); + continue; + }; + sources.push(( + format!("provider:{name}"), + format!("{}:{}", record.id, record.resource_version), + )); + committed_at_ms = committed_at_ms.max(record.updated_at_ms); + let provider = + Provider::decode(record.payload.as_slice()).map_err(|e| e.to_string())?; + let mut profile = store + .get_by_name( + StoredProviderProfile::object_type(), + &provider.profile_workspace, + &provider.r#type, + ) + .await + .map_err(|e| e.to_string())?; + if profile.is_none() && !provider.profile_workspace.is_empty() { + profile = store + .get_by_name(StoredProviderProfile::object_type(), "", &provider.r#type) + .await + .map_err(|e| e.to_string())?; + } + if let Some(profile) = profile { + sources.push(( + format!("profile:{}:{}", provider.profile_workspace, provider.r#type), + format!("{}:{}", profile.id, profile.resource_version), + )); + committed_at_ms = committed_at_ms.max(profile.updated_at_ms); + } + } + } + sources.sort(); + let encoded = serde_json::to_vec(&sources).map_err(|e| e.to_string())?; + Ok(ConfigurationChange { + id: format!("{:x}", Sha256::digest(encoded)), + committed_at_ms, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::grpc::StoredSettingValue; + use crate::grpc::policy::{save_global_settings, save_sandbox_settings}; + use openshell_core::proto::ObjectMeta; + + fn sandbox() -> Sandbox { + Sandbox { + metadata: Some(ObjectMeta { + id: "sb-clock".into(), + name: "clock".into(), + workspace: "default".into(), + ..Default::default() + }), + ..Default::default() + } + } + + #[tokio::test] + async fn provisioning_settings_clock_survives_noop_and_a_b_a() { + let store = Store::connect("sqlite::memory:").await.unwrap(); + let sandbox = sandbox(); + let mut settings = load_global_settings(&store).await.unwrap(); + settings + .settings + .insert("ocsf_json_enabled".into(), StoredSettingValue::Bool(false)); + save_global_settings(&store, &settings).await.unwrap(); + let first = configuration_change(&store, &sandbox).await.unwrap(); + let settings = load_global_settings(&store).await.unwrap(); + save_global_settings(&store, &settings).await.unwrap(); + assert_eq!(configuration_change(&store, &sandbox).await.unwrap(), first); + let mut settings = load_global_settings(&store).await.unwrap(); + settings + .settings + .insert("ocsf_json_enabled".into(), StoredSettingValue::Bool(true)); + save_global_settings(&store, &settings).await.unwrap(); + let mut settings = load_global_settings(&store).await.unwrap(); + settings + .settings + .insert("ocsf_json_enabled".into(), StoredSettingValue::Bool(false)); + save_global_settings(&store, &settings).await.unwrap(); + let last = configuration_change(&store, &sandbox).await.unwrap(); + assert_ne!( + last.id, first.id, + "a missed intermediate generation is still a real edit" + ); + assert!(last.committed_at_ms >= first.committed_at_ms); + assert_eq!( + configuration_change(&store, &sandbox).await.unwrap(), + last, + "reads cannot refresh commit time" + ); + } + + #[tokio::test] + async fn provisioning_global_override_ignores_shadowed_changes_and_tracks_deletion() { + let store = Store::connect("sqlite::memory:").await.unwrap(); + let sandbox = sandbox(); + let mut global = load_global_settings(&store).await.unwrap(); + global + .settings + .insert("ocsf_json_enabled".into(), StoredSettingValue::Bool(false)); + save_global_settings(&store, &global).await.unwrap(); + let first = configuration_change(&store, &sandbox).await.unwrap(); + let mut local = load_sandbox_settings(&store, "default", "clock") + .await + .unwrap(); + local + .settings + .insert("ocsf_json_enabled".into(), StoredSettingValue::Bool(true)); + save_sandbox_settings(&store, "default", "clock", &local) + .await + .unwrap(); + assert_eq!(configuration_change(&store, &sandbox).await.unwrap(), first); + let mut global = load_global_settings(&store).await.unwrap(); + global.settings.remove("ocsf_json_enabled"); + save_global_settings(&store, &global).await.unwrap(); + let deleted = configuration_change(&store, &sandbox).await.unwrap(); + assert_ne!(deleted.id, first.id); + let loaded = load_global_settings(&store).await.unwrap(); + assert!(loaded.change_clocks.contains_key("ocsf_json_enabled")); + assert_eq!( + deleted.committed_at_ms, + loaded.change_clocks["ocsf_json_enabled"].committed_at_ms + ); + } + + #[tokio::test] + async fn provisioning_lazy_policy_backfill_does_not_change_generation() { + let store = Store::connect("sqlite::memory:").await.unwrap(); + let mut sandbox = sandbox(); + let policy = openshell_policy::restrictive_default_policy(); + let hash = openshell_core::policy_identity::deterministic_policy_hash(&policy); + sandbox.spec = Some(openshell_core::proto::SandboxSpec { + policy: Some(policy.clone()), + ..Default::default() + }); + let first = configuration_change(&store, &sandbox).await.unwrap(); + store + .put_policy_revision( + "policy-1", + "sb-clock", + "default", + 1, + &policy.encode_to_vec(), + &hash, + ) + .await + .unwrap(); + let projected = configuration_change(&store, &sandbox).await.unwrap(); + assert_eq!(projected.id, first.id); + store + .put_policy_revision( + "policy-2", + "sb-clock", + "default", + 2, + &policy.encode_to_vec(), + &hash, + ) + .await + .unwrap(); + assert_ne!( + configuration_change(&store, &sandbox).await.unwrap().id, + first.id + ); + } +} diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 962c42282e..a1342f4575 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -502,6 +502,18 @@ async fn handle_create_sandbox_inner( .as_mut() .expect("status initialized") .configuration_activated = Some(false); + sandbox + .status + .as_mut() + .expect("status initialized") + .provisioning = Some(crate::compute::provisioning_deadline::new_record(now_ms)); + crate::compute::provisioning_deadline::refresh_configuration( + &state.store, + &mut sandbox, + now_ms, + ) + .await + .map_err(Status::internal)?; crate::compute::apply_configuration_readiness(&mut sandbox); // Ensure metadata is valid (defense in depth - should always be true for server-constructed metadata) @@ -1242,6 +1254,10 @@ pub(super) async fn handle_attach_sandbox_provider( spec.providers.push(provider_name.clone()); spec.provider_attachment_epoch.clone_from(&mutation_id); attached_clone.store(true, Ordering::Relaxed); + crate::compute::provisioning_deadline::attachments_changed( + sandbox, + current_time_ms(), + ); } }, ) @@ -1361,6 +1377,10 @@ pub(super) async fn handle_detach_sandbox_provider( detached_clone.store(true, Ordering::Relaxed); // Only dedupe after making a change dedupe_provider_names(&mut spec.providers); + crate::compute::provisioning_deadline::attachments_changed( + sandbox, + current_time_ms(), + ); } }, ) diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 47eefb7704..80cc2acd57 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -813,6 +813,12 @@ pub(crate) async fn run_server( ))); } + // Deadlines must run while restored supervisors wait for policy repair. + let (startup_tx, startup_rx) = watch::channel(false); + state + .compute + .spawn_watchers(shutdown_rx.clone(), startup_rx); + // Restored supervisors need the callback listeners while the compute // driver reconciles persisted sandboxes. Serve them before starting that // reconciliation so policy fetch and supervisor-session registration @@ -841,7 +847,7 @@ pub(crate) async fn run_server( warn!(error = %err, "Failed to start persisted sandboxes during startup"); } - state.compute.spawn_watchers(shutdown_rx.clone()); + startup_tx.send_replace(true); ssh_sessions::spawn_session_reaper(store.clone(), Duration::from_hours(1)); supervisor_session::spawn_relay_reaper(state.clone(), Duration::from_secs(30)); provider_refresh::spawn_refresh_worker(state.clone(), Duration::from_mins(1)); diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index a0a35ec22e..65b44a5a68 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -605,6 +605,7 @@ mod tests { assert_eq!(status.current_policy_version, 7); assert!(status.configuration_admission.is_none()); assert_eq!(status.configuration_activated, None); + assert!(status.provisioning.is_none()); assert!(!crate::policy_store::permits_initial_static_policy_repair( &sandbox )); diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 37532e8416..9e48d95a0a 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -2732,6 +2732,24 @@ async fn fetch_sandboxes( } fn sandbox_notes(sandbox: &openshell_core::proto::Sandbox, forwards: String) -> String { + if let Some(record) = sandbox + .status + .as_ref() + .and_then(|status| status.provisioning.as_ref()) + && record.timeout_time.is_some() + { + let cleanup = if record.cleanup_completed_time.is_some() { + "compute reclaimed" + } else { + "compute cleanup pending" + }; + let mut notes = format!("Provisioning timed out; {cleanup}"); + if !forwards.is_empty() { + notes.push_str("; "); + notes.push_str(&forwards); + } + return notes; + } let rejection = sandbox.status.as_ref().and_then(|status| { status.conditions.iter().find(|condition| { matches!(condition.r#type.as_str(), "ConfigurationReady" | "Ready") @@ -3258,6 +3276,36 @@ mod sandbox_notes_tests { use super::sandbox_notes; use openshell_core::proto::{Sandbox, SandboxCondition, SandboxStatus}; + #[test] + fn provisioning_timeout_notes_distinguish_pending_and_completed_cleanup() { + let mut sandbox = Sandbox { + status: Some(SandboxStatus { + provisioning: Some(openshell_core::proto::SandboxProvisioning { + timeout_time: openshell_core::time::timestamp_from_millis(300_000).ok(), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }; + assert_eq!( + sandbox_notes(&sandbox, "fwd:8080".into()), + "Provisioning timed out; compute cleanup pending; fwd:8080" + ); + sandbox + .status + .as_mut() + .unwrap() + .provisioning + .as_mut() + .unwrap() + .cleanup_completed_time = openshell_core::time::timestamp_from_millis(301_000).ok(); + assert_eq!( + sandbox_notes(&sandbox, String::new()), + "Provisioning timed out; compute reclaimed" + ); + } + #[test] fn configuration_rejection_precedes_forwards_and_clears_after_repair() { let condition = SandboxCondition { diff --git a/docs/sandboxes/manage-sandboxes.mdx b/docs/sandboxes/manage-sandboxes.mdx index 641b46a3c0..c1d1b135c3 100644 --- a/docs/sandboxes/manage-sandboxes.mdx +++ b/docs/sandboxes/manage-sandboxes.mdx @@ -770,6 +770,31 @@ Management operations remain available while startup is blocked. After repair, the supervisor completes startup without recreating the sandbox. Starting a stopped sandbox repeats configuration admission before launching its workload. +The gateway enforces a 300-second provisioning repair window, independently of +the CLI wait timeout. An effective policy, settings, provider, profile, or +attachment change resets the window from its stored change time. The first +failed configuration load for that change grants another full window. Repeated +failures and reconnects do not extend it; reaching `Ready` clears it. + +When the window expires, the sandbox enters `Error` with reason +`ProvisioningTimedOut`. The gateway stops its workload and supervisor compute, +retaining the sandbox record, diagnostic, and restartable storage. Cleanup can +remain pending if the backend is unavailable; the gateway retries it. Inspect +`provisioning` in JSON output for the deadline, timeout, and cleanup timestamps. +TUI NOTES distinguishes pending cleanup from reclaimed compute. + +Repair the configuration, wait for cleanup to complete, then explicitly restart: + +```shell +openshell sandbox get my-sandbox --output json +openshell sandbox start my-sandbox +``` + +Editing configuration after expiry does not restart compute. A retry gets a new +300-second window, while static-policy restrictions from any previous activation +remain in force. Timed-out records are retained even for ephemeral creates; use +`sandbox delete` when you no longer need the diagnostic or stored state. + | Phase | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Provisioning | The runtime is setting up the sandbox environment, or the gateway is waiting for the sandbox supervisor to establish its authenticated control session. | diff --git a/docs/sandboxes/policies.mdx b/docs/sandboxes/policies.mdx index 2edbb0a222..a8b743b5cd 100644 --- a/docs/sandboxes/policies.mdx +++ b/docs/sandboxes/policies.mdx @@ -285,6 +285,12 @@ openshell sandbox provider detach ``` These are alternative repairs; choose the one that matches the intended access. +Startup has a [300-second provisioning repair window](/sandboxes/manage-sandboxes#sandbox-lifecycle). +Effective configuration changes and their first failed load reset the window; +repeated failures do not. If it expires, the gateway records `ProvisioningTimedOut` +and reclaims compute. Repair the configuration, wait for cleanup, then run +`openshell sandbox start ` to retry. Before expiry, repair resumes the same +provisioning attempt. The supervisor starts the workload after the repaired configuration passes validation. You can replace static policy fields while the first startup is blocked. After the first accepted activation, the usual static-field restrictions diff --git a/proto/openshell.proto b/proto/openshell.proto index 7b009d7430..586b7d435f 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -1120,6 +1120,8 @@ message SandboxStatus { SandboxConfigurationAdmission configuration_admission = 11; // Durable first-acceptance marker. Absent on legacy records; never reset by restart. optional bool configuration_activated = 12; + // Gateway-owned repair window. Retained after timeout for inspection and retry. + SandboxProvisioning provisioning = 13; } // User-facing sandbox condition derived from platform or gateway observations. @@ -3681,3 +3683,23 @@ message EndpointStatus { // same-sequence retries do not advance it. Absent until a result is reported. google.protobuf.Timestamp last_reported_time = 106; } + +// Durable provisioning attempt, independent of supervisor registration and polling. +message SandboxProvisioning { + string attempt_id = 1; + string configuration_change_id = 2; + google.protobuf.Timestamp configuration_change_time = 3; + google.protobuf.Timestamp first_rejection_time = 4; + // Present only while the repair window is armed. + google.protobuf.Timestamp deadline = 5; + google.protobuf.Timestamp timeout_time = 6; + // Set only after both supervisor and workload compute have been reclaimed. + google.protobuf.Timestamp cleanup_completed_time = 7; + // A safe gateway-authored diagnostic; never a raw driver error. + string cleanup_error = 8; + // Durable backoff for interrupted or failed reclamation. + google.protobuf.Timestamp cleanup_retry_time = 9; + // Attachment edits have their own durable clock; status writes do not change it. + string attachment_change_id = 10; + google.protobuf.Timestamp attachment_change_time = 11; +} diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index 322ddfeff9..253e42a09a 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -190,6 +190,12 @@ the image/effective policy or provider configuration. The supervisor remains alive while the workload stays unstarted. Inspect `openshell sandbox get` and repair the desired configuration with a complete policy replacement or provider change; do not treat a healthy container as proof that the workload is ready. +If the 300-second provisioning repair window expires, the gateway records +`ProvisioningTimedOut` and stops workload and supervisor compute. Inspect +`provisioning` in sandbox JSON and TUI NOTES to distinguish cleanup pending from +complete. Repairing configuration after expiry does not restart compute: wait +for cleanup, then explicitly use `sandbox start`. Repeated rejected reports do +not refresh the deadline, and the CLI wait timeout does not control it. See [policy validation and repair](https://docs.nvidia.com/openshell/latest/sandboxes/policies.md). The isolated supervisor requests image-policy discovery through the authenticated sandbox boundary before admission. The workload boundary can remain alive without diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index 13d78cfeac..057874fa3a 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -454,7 +454,12 @@ repair the complete policy or provider set through the gateway. The workload has not started on its first activation, so static fields can also be replaced during this initial repair. A previously activated sandbox retains static-field restrictions while restart admission is pending or rejected. -After validation succeeds, the supervisor completes startup. Follow the +Before the gateway's 300-second repair window expires, successful validation +completes startup in place. Effective stored configuration changes and their +first failed load reset that window; repeated failures do not. After +`ProvisioningTimedOut`, inspect the retained record and cleanup status, repair +configuration, and explicitly run `sandbox start` once cleanup completes. A CLI +wait timeout is separate from this gateway deadline. Follow the published [policy repair guidance](https://docs.nvidia.com/openshell/latest/sandboxes/policies.md) and confirm current replacement/detach syntax with installed CLI help. From a12528c0bb2444f7e8ccf99dd576b01bb35c30c5 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:15:20 -0700 Subject: [PATCH 20/23] fix(supervisor): reconcile admission with provider readiness Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- architecture/gateway.md | 6 +- crates/openshell-server/src/storage_proto.rs | 12 +- crates/openshell-supervisor/src/lib.rs | 147 +- .../v1/internal/converter/coverage_test.go | 6 +- sdk/go/proto/openshellv1/openshell.pb.go | 2367 ++++++++++------- sdk/go/proto/openshellv1/openshell_grpc.pb.go | 40 + sdk/go/proto/sandboxv1/sandbox.pb.go | 35 +- 7 files changed, 1629 insertions(+), 984 deletions(-) diff --git a/architecture/gateway.md b/architecture/gateway.md index a98afa2d3e..2df02d0f2d 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -454,9 +454,9 @@ including deletion tombstones. Legacy values acquire stable source identities on read; a subsequent write preserves them. These clocks distinguish effective edits from no-op writes without treating status updates as configuration edits. With timestamp types, deletion outcomes, and optional mutation request IDs, the -admission contract brings the public closure to 287 messages and 15 enums, the -durable closure to 85 messages and 10 enums, and their overlap to 75 messages -and 10 enums. Mutation request IDs extend public request fields without adding +admission contract brings the public closure to 298 messages and 21 enums, the +durable closure to 92 messages and 16 enums, and their overlap to 80 messages +and 16 enums. Mutation request IDs extend public request fields without adding messages to these closures or changing the durable protobuf schema. | Dual-purpose encoded root | Current decision | diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index 65b44a5a68..f8a01a0579 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -118,11 +118,11 @@ mod tests { const STORAGE_V1_SCHEMA_SHA256: &str = "d68401809d8cea445c35233ef32412bbd041cb2ac5acaf368a0d0bf74d2ddf17"; const PUBLIC_RPC_SCHEMA_SHA256: &str = - "beea342cc79c06b6efda934370a2425e444b8dab1c795b2858e9f241dec1ca2b"; + "dedd36f5fe509e8edf297425170493d56b55a95652696b116a2cf389ebd30ad0"; const DURABLE_SCHEMA_SHA256: &str = - "557ca283c55fd46b213d5573b950ba8604cc3f4b31bad3e433eb9c5f9975138d"; + "654649c8f65f44ac2ba04290f49c56de2f488271f99bc0fd4c6d025039c05128"; const PUBLIC_DURABLE_OVERLAP_SHA256: &str = - "f541c25bb3e1e5806865bc61470c66d10c16cca7399bf5909c77dc384d469171"; + "376cc8ecbc8b9b995e2170ccb5c2f18ef82adf9b5f55f1683571410722dfd4cf"; // 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"; @@ -570,9 +570,9 @@ mod tests { overlap_hash.as_str(), ), ( - (295, 20), - (90, 15), - (78, 15), + (298, 21), + (92, 16), + (80, 16), PUBLIC_RPC_SCHEMA_SHA256, DURABLE_SCHEMA_SHA256, PUBLIC_DURABLE_OVERLAP_SHA256 diff --git a/crates/openshell-supervisor/src/lib.rs b/crates/openshell-supervisor/src/lib.rs index 2ef24a63e0..0398cb09cd 100644 --- a/crates/openshell-supervisor/src/lib.rs +++ b/crates/openshell-supervisor/src/lib.rs @@ -2670,6 +2670,11 @@ fn prepare_startup_configuration( "Effective configuration admission rejected" )); } + if provider.readiness_reason != ProviderReadinessReason::Unspecified { + return Err(miette::miette!( + "Provider credentials are not ready for installation" + )); + } if snapshot.provider_env_revision != provider.provider_env_revision { return Err(miette::miette!( "Provider environment revision changed during configuration preparation" @@ -4173,8 +4178,11 @@ async fn run_policy_poll_loop_with_client( let recovering_rejected_policy = reloads_gateway_policy && rejected_policy_generation.is_some() && result.configuration_admitted; - let policy_runtime_changed = (reloads_gateway_policy && current_policy_generation.as_ref().is_some_and(PolicyGenerationGuard::is_stale)) - || (reloads_gateway_policy && provider_env_changed) + let policy_runtime_changed = (reloads_gateway_policy + && (provider_env_changed + || current_policy_generation + .as_ref() + .is_some_and(PolicyGenerationGuard::is_stale))) || recovering_rejected_policy || extension_authentication_changed || gateway_policy_runtime_needs_reconciliation( @@ -4276,17 +4284,35 @@ async fn run_policy_poll_loop_with_client( // Prepare the matching environment before activation. Failed refreshes // revoke static credentials while preserving independently bound dynamic grants. + let mut prepared_identity = None; let prepared_provider = if provider_env_changed { - let provider = match openshell_core::grpc_client::fetch_provider_environment( - &ctx.endpoint, - &ctx.sandbox_id, - ) - .await + ctx.provider_readiness.credentials_failed( + desired_identity.clone(), + ProviderReadinessReason::WaitingForCredentials, + ); + let provider = match client + .fetch_provider_environment(&ctx.endpoint, &ctx.sandbox_id) + .await { - Ok(provider) if provider.provider_env_revision == result.provider_env_revision => { + Ok(provider) + if EnvironmentIdentity::from_environment(&provider) == desired_identity + && provider.readiness_reason == ProviderReadinessReason::Unspecified => + { provider } - _ => { + failed => { + let reason = match failed { + Ok(provider) + if provider.readiness_reason + != ProviderReadinessReason::Unspecified => + { + provider.readiness_reason + } + Ok(_) => ProviderReadinessReason::SnapshotMismatch, + Err(_) => ProviderReadinessReason::CredentialInstallFailed, + }; + ctx.provider_readiness + .credentials_failed(desired_identity.clone(), reason); ocsf_emit!(ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::High) .status(StatusId::Failure) @@ -4313,6 +4339,15 @@ async fn run_policy_poll_loop_with_client( } }; if let Ok(prepared) = prepare_provider_environment(&provider) { + prepared_identity = Some(( + EnvironmentIdentity::from_environment(&provider), + provider + .credential_expires_at_ms + .values() + .copied() + .filter(|expiry| *expiry > 0) + .min(), + )); Some(prepared) } else { ocsf_emit!(ConfigStateChangeBuilder::new(ocsf_ctx()) @@ -4321,6 +4356,10 @@ async fn run_policy_poll_loop_with_client( .state(StateId::Disabled, "fail_closed") .message("Provider environment bindings failed validation; static credentials were revoked and fetched dynamic grants remain active") .build()); + ctx.provider_readiness.credentials_failed( + desired_identity.clone(), + ProviderReadinessReason::CredentialInstallFailed, + ); // Repeat the rejected binding validation on the live state to // revoke static material and retain the fetched dynamic grants. let _ = ctx.provider_credentials.install_bound_environment( @@ -4378,6 +4417,13 @@ async fn run_policy_poll_loop_with_client( match runtime_result { Ok(generation) => { policy_runtime_reconciled = true; + if let Some((identity, expires_at_ms)) = prepared_identity.as_ref() { + ctx.provider_readiness.credentials_installed( + identity.clone(), + &ctx.provider_credentials, + *expires_at_ms, + ); + } ctx.provider_readiness.policy_activated( &desired_identity, result.config_revision, @@ -4574,6 +4620,13 @@ async fn run_policy_poll_loop_with_client( } if !reloads_gateway_policy && let Some(prepared) = prepared_provider.as_ref() { ctx.provider_credentials.install_prepared(prepared); + if let Some((identity, expires_at_ms)) = prepared_identity.as_ref() { + ctx.provider_readiness.credentials_installed( + identity.clone(), + &ctx.provider_credentials, + *expires_at_ms, + ); + } } if provider_env_changed || policy_runtime_reconciled { current_provider_env_revision = result.provider_env_revision; @@ -5482,6 +5535,9 @@ network_policies: fn startup_provider(revision: u64) -> openshell_core::grpc_client::ProviderEnvironmentResult { openshell_core::grpc_client::ProviderEnvironmentResult { provider_env_revision: revision, + provider_attachment_epoch: String::new(), + policy_hash: String::new(), + readiness_reason: ProviderReadinessReason::Unspecified, environment: std::collections::HashMap::new(), credential_expires_at_ms: std::collections::HashMap::new(), dynamic_credentials: std::collections::HashMap::new(), @@ -5554,6 +5610,7 @@ network_policies: >, >, reports: UnboundedSender<(u32, bool, String)>, + environment_identity: Arc>, } #[tonic::async_trait] @@ -5562,12 +5619,31 @@ network_policies: &self, _sandbox_id: &str, ) -> Result { - self.polls + let result = self + .polls .lock() .await .recv() .await - .ok_or_else(|| miette::miette!("scripted policy poll channel closed")) + .ok_or_else(|| miette::miette!("scripted policy poll channel closed"))?; + *self.environment_identity.lock().unwrap() = + EnvironmentIdentity::from_settings(&result); + Ok(result) + } + + async fn fetch_provider_environment( + &self, + _endpoint: &str, + _sandbox_id: &str, + ) -> Result { + let identity = self.environment_identity.lock().unwrap().clone(); + let mut provider = startup_provider(identity.revision); + provider.provider_attachment_epoch = identity.attachment_epoch; + provider.policy_hash = identity.policy_hash; + if provider.policy_hash.is_empty() { + provider.readiness_reason = ProviderReadinessReason::SnapshotMismatch; + } + Ok(provider) } async fn report_policy_status( @@ -5602,6 +5678,16 @@ network_policies: self.inner.poll_settings(sandbox_id).await } + async fn fetch_provider_environment( + &self, + endpoint: &str, + sandbox_id: &str, + ) -> Result { + self.inner + .fetch_provider_environment(endpoint, sandbox_id) + .await + } + async fn report_policy_status( &self, sandbox_id: &str, @@ -5641,6 +5727,9 @@ network_policies: ScriptedPolicyGateway { polls: Arc::new(tokio::sync::Mutex::new(poll_rx)), reports: report_tx, + environment_identity: Arc::new(std::sync::Mutex::new( + EnvironmentIdentity::default(), + )), }, poll_tx, report_rx, @@ -5891,6 +5980,16 @@ network_policies: Ok(result) } + async fn fetch_provider_environment( + &self, + endpoint: &str, + sandbox_id: &str, + ) -> Result { + self.inner + .fetch_provider_environment(endpoint, sandbox_id) + .await + } + async fn report_policy_status( &self, sandbox_id: &str, @@ -6152,6 +6251,25 @@ network_policies: middleware_connector: MiddlewareConnector, ) -> PolicyPollLoopContext { let (workspace_tx, _workspace_rx) = tokio::sync::watch::channel(String::new()); + let provider_credentials = + ProviderCredentialState::from_child_env_snapshot(0, std::collections::HashMap::new()); + let provider_readiness = ProviderReadinessTracker::new(); + // This fixture models a successfully installed initial launch snapshot. + if let LoadedPolicyOrigin::Gateway { + revision: Some(revision), + .. + } = &loaded_policy_origin + { + provider_readiness.credentials_installed( + EnvironmentIdentity { + revision: 0, + attachment_epoch: String::new(), + policy_hash: revision.policy_hash.clone(), + }, + &provider_credentials, + None, + ); + } PolicyPollLoopContext { endpoint: String::new(), sandbox_id: "sandbox-test".to_string(), @@ -6160,11 +6278,8 @@ network_policies: entrypoint_pid: Arc::new(AtomicU32::new(0)), interval_secs: 0, ocsf_enabled: Arc::new(AtomicBool::new(false)), - provider_credentials: ProviderCredentialState::from_child_env_snapshot( - 0, - std::collections::HashMap::new(), - ), - provider_readiness: ProviderReadinessTracker::new(), + provider_credentials, + provider_readiness, policy_local_ctx: None, agent_proposals: AgentProposals::default(), middleware_registry_status: MiddlewareRegistryStatus::Synchronized, diff --git a/sdk/go/openshell/v1/internal/converter/coverage_test.go b/sdk/go/openshell/v1/internal/converter/coverage_test.go index 336ebdbe49..37ceeb92c0 100644 --- a/sdk/go/openshell/v1/internal/converter/coverage_test.go +++ b/sdk/go/openshell/v1/internal/converter/coverage_test.go @@ -128,8 +128,10 @@ func TestConverterCoversAllProtoFields_SandboxStatus(t *testing.T) { } // The instance ID coordinates internal gateway/supervisor lifecycle // fencing. The first-activation marker governs static policy repair. - // Both are exposed only through the raw protobuf API. - skipped := fieldSet{"main_process_instance_id": true, "configuration_activated": true} + // Provisioning carries gateway-owned attempt, deadline, and cleanup state; + // the curated API exposes its outcome through phase and conditions. Detailed + // lifecycle bookkeeping remains available through the raw protobuf API. + skipped := fieldSet{"main_process_instance_id": true, "configuration_activated": true, "provisioning": true} assertAllFieldsCovered(t, (&pb.SandboxStatus{}).ProtoReflect().Descriptor(), handled, skipped) } diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index d5f2b8aff4..319344a845 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -674,6 +674,58 @@ func (ProviderProfileCategory) EnumDescriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{9} } +type ConfigurationAdmissionState int32 + +const ( + ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_UNSPECIFIED ConfigurationAdmissionState = 0 + ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_PENDING ConfigurationAdmissionState = 1 + ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_ACCEPTED ConfigurationAdmissionState = 2 + ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_REJECTED ConfigurationAdmissionState = 3 +) + +// Enum value maps for ConfigurationAdmissionState. +var ( + ConfigurationAdmissionState_name = map[int32]string{ + 0: "CONFIGURATION_ADMISSION_STATE_UNSPECIFIED", + 1: "CONFIGURATION_ADMISSION_STATE_PENDING", + 2: "CONFIGURATION_ADMISSION_STATE_ACCEPTED", + 3: "CONFIGURATION_ADMISSION_STATE_REJECTED", + } + ConfigurationAdmissionState_value = map[string]int32{ + "CONFIGURATION_ADMISSION_STATE_UNSPECIFIED": 0, + "CONFIGURATION_ADMISSION_STATE_PENDING": 1, + "CONFIGURATION_ADMISSION_STATE_ACCEPTED": 2, + "CONFIGURATION_ADMISSION_STATE_REJECTED": 3, + } +) + +func (x ConfigurationAdmissionState) Enum() *ConfigurationAdmissionState { + p := new(ConfigurationAdmissionState) + *p = x + return p +} + +func (x ConfigurationAdmissionState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConfigurationAdmissionState) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[10].Descriptor() +} + +func (ConfigurationAdmissionState) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[10] +} + +func (x ConfigurationAdmissionState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConfigurationAdmissionState.Descriptor instead. +func (ConfigurationAdmissionState) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{10} +} + // Policy load status. type PolicyStatus int32 @@ -720,11 +772,11 @@ func (x PolicyStatus) String() string { } func (PolicyStatus) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[10].Descriptor() + return file_openshell_proto_enumTypes[11].Descriptor() } func (PolicyStatus) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[10] + return &file_openshell_proto_enumTypes[11] } func (x PolicyStatus) Number() protoreflect.EnumNumber { @@ -733,7 +785,7 @@ func (x PolicyStatus) Number() protoreflect.EnumNumber { // Deprecated: Use PolicyStatus.Descriptor instead. func (PolicyStatus) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{10} + return file_openshell_proto_rawDescGZIP(), []int{11} } // Service status enum. @@ -773,11 +825,11 @@ func (x ServiceStatus) String() string { } func (ServiceStatus) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[11].Descriptor() + return file_openshell_proto_enumTypes[12].Descriptor() } func (ServiceStatus) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[11] + return &file_openshell_proto_enumTypes[12] } func (x ServiceStatus) Number() protoreflect.EnumNumber { @@ -786,7 +838,7 @@ func (x ServiceStatus) Number() protoreflect.EnumNumber { // Deprecated: Use ServiceStatus.Descriptor instead. func (ServiceStatus) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{11} + return file_openshell_proto_rawDescGZIP(), []int{12} } // Workspace-scoped role for members. @@ -823,11 +875,11 @@ func (x WorkspaceRole) String() string { } func (WorkspaceRole) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[12].Descriptor() + return file_openshell_proto_enumTypes[13].Descriptor() } func (WorkspaceRole) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[12] + return &file_openshell_proto_enumTypes[13] } func (x WorkspaceRole) Number() protoreflect.EnumNumber { @@ -836,7 +888,7 @@ func (x WorkspaceRole) Number() protoreflect.EnumNumber { // Deprecated: Use WorkspaceRole.Descriptor instead. func (WorkspaceRole) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{12} + return file_openshell_proto_rawDescGZIP(), []int{13} } // Stable recovery action for the most recent provider credential refresh @@ -882,11 +934,11 @@ func (x ProviderCredentialRefreshRecoveryAction) String() string { } func (ProviderCredentialRefreshRecoveryAction) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[13].Descriptor() + return file_openshell_proto_enumTypes[14].Descriptor() } func (ProviderCredentialRefreshRecoveryAction) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[13] + return &file_openshell_proto_enumTypes[14] } func (x ProviderCredentialRefreshRecoveryAction) Number() protoreflect.EnumNumber { @@ -895,7 +947,7 @@ func (x ProviderCredentialRefreshRecoveryAction) Number() protoreflect.EnumNumbe // Deprecated: Use ProviderCredentialRefreshRecoveryAction.Descriptor instead. func (ProviderCredentialRefreshRecoveryAction) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{13} + return file_openshell_proto_rawDescGZIP(), []int{14} } // Result of a public delete, membership removal, or session revocation. @@ -945,11 +997,11 @@ func (x DeletionOutcome) String() string { } func (DeletionOutcome) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[14].Descriptor() + return file_openshell_proto_enumTypes[15].Descriptor() } func (DeletionOutcome) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[14] + return &file_openshell_proto_enumTypes[15] } func (x DeletionOutcome) Number() protoreflect.EnumNumber { @@ -958,7 +1010,7 @@ func (x DeletionOutcome) Number() protoreflect.EnumNumber { // Deprecated: Use DeletionOutcome.Descriptor instead. func (DeletionOutcome) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{14} + return file_openshell_proto_rawDescGZIP(), []int{15} } // Last observed network result for a configured external tool endpoint. @@ -1019,11 +1071,11 @@ func (x EndpointResult) String() string { } func (EndpointResult) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[15].Descriptor() + return file_openshell_proto_enumTypes[16].Descriptor() } func (EndpointResult) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[15] + return &file_openshell_proto_enumTypes[16] } func (x EndpointResult) Number() protoreflect.EnumNumber { @@ -1032,7 +1084,7 @@ func (x EndpointResult) Number() protoreflect.EnumNumber { // Deprecated: Use EndpointResult.Descriptor instead. func (EndpointResult) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{15} + return file_openshell_proto_rawDescGZIP(), []int{16} } // IssueSandboxToken request. Empty body; identity is established by the @@ -2775,8 +2827,14 @@ type SandboxStatus struct { // Currently populated for MCP-over-HTTP endpoints. These passive results // remain separate from sandbox lifecycle conditions and readiness. EndpointStatuses []*EndpointStatus `protobuf:"bytes,10,rep,name=endpoint_statuses,json=endpointStatuses,proto3" json:"endpoint_statuses,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Independent of infrastructure phase; retained across driver observations. + ConfigurationAdmission *SandboxConfigurationAdmission `protobuf:"bytes,11,opt,name=configuration_admission,json=configurationAdmission,proto3" json:"configuration_admission,omitempty"` + // Durable first-acceptance marker. Absent on legacy records; never reset by restart. + ConfigurationActivated *bool `protobuf:"varint,12,opt,name=configuration_activated,json=configurationActivated,proto3,oneof" json:"configuration_activated,omitempty"` + // Gateway-owned repair window. Retained after timeout for inspection and retry. + Provisioning *SandboxProvisioning `protobuf:"bytes,13,opt,name=provisioning,proto3" json:"provisioning,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SandboxStatus) Reset() { @@ -2879,6 +2937,27 @@ func (x *SandboxStatus) GetEndpointStatuses() []*EndpointStatus { return nil } +func (x *SandboxStatus) GetConfigurationAdmission() *SandboxConfigurationAdmission { + if x != nil { + return x.ConfigurationAdmission + } + return nil +} + +func (x *SandboxStatus) GetConfigurationActivated() bool { + if x != nil && x.ConfigurationActivated != nil { + return *x.ConfigurationActivated + } + return false +} + +func (x *SandboxStatus) GetProvisioning() *SandboxProvisioning { + if x != nil { + return x.Provisioning + } + return nil +} + // User-facing sandbox condition derived from platform or gateway observations. type SandboxCondition struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -11978,6 +12057,195 @@ func (*ReportPolicyStatusResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{153} } +type SandboxConfigurationAdmission struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstanceId string `protobuf:"bytes,1,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` + State ConfigurationAdmissionState `protobuf:"varint,2,opt,name=state,proto3,enum=openshell.v1.ConfigurationAdmissionState" json:"state,omitempty"` + PolicyVersion uint32 `protobuf:"varint,3,opt,name=policy_version,json=policyVersion,proto3" json:"policy_version,omitempty"` + PolicyHash string `protobuf:"bytes,4,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + ConfigRevision uint64 `protobuf:"varint,5,opt,name=config_revision,json=configRevision,proto3" json:"config_revision,omitempty"` + ProviderEnvRevision uint64 `protobuf:"varint,6,opt,name=provider_env_revision,json=providerEnvRevision,proto3" json:"provider_env_revision,omitempty"` + Error string `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxConfigurationAdmission) Reset() { + *x = SandboxConfigurationAdmission{} + mi := &file_openshell_proto_msgTypes[154] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxConfigurationAdmission) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxConfigurationAdmission) ProtoMessage() {} + +func (x *SandboxConfigurationAdmission) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[154] + 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 SandboxConfigurationAdmission.ProtoReflect.Descriptor instead. +func (*SandboxConfigurationAdmission) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{154} +} + +func (x *SandboxConfigurationAdmission) GetInstanceId() string { + if x != nil { + return x.InstanceId + } + return "" +} + +func (x *SandboxConfigurationAdmission) GetState() ConfigurationAdmissionState { + if x != nil { + return x.State + } + return ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_UNSPECIFIED +} + +func (x *SandboxConfigurationAdmission) GetPolicyVersion() uint32 { + if x != nil { + return x.PolicyVersion + } + return 0 +} + +func (x *SandboxConfigurationAdmission) GetPolicyHash() string { + if x != nil { + return x.PolicyHash + } + return "" +} + +func (x *SandboxConfigurationAdmission) GetConfigRevision() uint64 { + if x != nil { + return x.ConfigRevision + } + return 0 +} + +func (x *SandboxConfigurationAdmission) GetProviderEnvRevision() uint64 { + if x != nil { + return x.ProviderEnvRevision + } + return 0 +} + +func (x *SandboxConfigurationAdmission) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type ReportSandboxConfigurationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + Admission *SandboxConfigurationAdmission `protobuf:"bytes,2,opt,name=admission,proto3" json:"admission,omitempty"` + // Pending registration replaces only this previously observed instance. + ExpectedInstanceId string `protobuf:"bytes,3,opt,name=expected_instance_id,json=expectedInstanceId,proto3" json:"expected_instance_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportSandboxConfigurationRequest) Reset() { + *x = ReportSandboxConfigurationRequest{} + mi := &file_openshell_proto_msgTypes[155] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportSandboxConfigurationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportSandboxConfigurationRequest) ProtoMessage() {} + +func (x *ReportSandboxConfigurationRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[155] + 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 ReportSandboxConfigurationRequest.ProtoReflect.Descriptor instead. +func (*ReportSandboxConfigurationRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{155} +} + +func (x *ReportSandboxConfigurationRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ReportSandboxConfigurationRequest) GetAdmission() *SandboxConfigurationAdmission { + if x != nil { + return x.Admission + } + return nil +} + +func (x *ReportSandboxConfigurationRequest) GetExpectedInstanceId() string { + if x != nil { + return x.ExpectedInstanceId + } + return "" +} + +type ReportSandboxConfigurationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportSandboxConfigurationResponse) Reset() { + *x = ReportSandboxConfigurationResponse{} + mi := &file_openshell_proto_msgTypes[156] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportSandboxConfigurationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportSandboxConfigurationResponse) ProtoMessage() {} + +func (x *ReportSandboxConfigurationResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[156] + 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 ReportSandboxConfigurationResponse.ProtoReflect.Descriptor instead. +func (*ReportSandboxConfigurationResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{156} +} + // A versioned policy revision with metadata. type SandboxPolicyRevision struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -12008,7 +12276,7 @@ type SandboxPolicyRevision struct { func (x *SandboxPolicyRevision) Reset() { *x = SandboxPolicyRevision{} - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[157] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12020,7 +12288,7 @@ func (x *SandboxPolicyRevision) String() string { func (*SandboxPolicyRevision) ProtoMessage() {} func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[157] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12033,7 +12301,7 @@ func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxPolicyRevision.ProtoReflect.Descriptor instead. func (*SandboxPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{154} + return file_openshell_proto_rawDescGZIP(), []int{157} } func (x *SandboxPolicyRevision) GetVersion() uint32 { @@ -12113,7 +12381,7 @@ type GetSandboxLogsRequest struct { func (x *GetSandboxLogsRequest) Reset() { *x = GetSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[158] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12125,7 +12393,7 @@ func (x *GetSandboxLogsRequest) String() string { func (*GetSandboxLogsRequest) ProtoMessage() {} func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[158] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12138,7 +12406,7 @@ func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*GetSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{155} + return file_openshell_proto_rawDescGZIP(), []int{158} } func (x *GetSandboxLogsRequest) GetSandboxId() string { @@ -12196,7 +12464,7 @@ type PushSandboxLogsRequest struct { func (x *PushSandboxLogsRequest) Reset() { *x = PushSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[159] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12208,7 +12476,7 @@ func (x *PushSandboxLogsRequest) String() string { func (*PushSandboxLogsRequest) ProtoMessage() {} func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[159] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12221,7 +12489,7 @@ func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*PushSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{156} + return file_openshell_proto_rawDescGZIP(), []int{159} } func (x *PushSandboxLogsRequest) GetSandboxId() string { @@ -12247,7 +12515,7 @@ type PushSandboxLogsResponse struct { func (x *PushSandboxLogsResponse) Reset() { *x = PushSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[160] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12259,7 +12527,7 @@ func (x *PushSandboxLogsResponse) String() string { func (*PushSandboxLogsResponse) ProtoMessage() {} func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[160] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12272,7 +12540,7 @@ func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*PushSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{157} + return file_openshell_proto_rawDescGZIP(), []int{160} } // Get sandbox logs response. @@ -12288,7 +12556,7 @@ type GetSandboxLogsResponse struct { func (x *GetSandboxLogsResponse) Reset() { *x = GetSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[161] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12300,7 +12568,7 @@ func (x *GetSandboxLogsResponse) String() string { func (*GetSandboxLogsResponse) ProtoMessage() {} func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[161] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12313,7 +12581,7 @@ func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*GetSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{158} + return file_openshell_proto_rawDescGZIP(), []int{161} } func (x *GetSandboxLogsResponse) GetLogs() []*SandboxLogLine { @@ -12346,7 +12614,7 @@ type SupervisorMessage struct { func (x *SupervisorMessage) Reset() { *x = SupervisorMessage{} - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[162] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12358,7 +12626,7 @@ func (x *SupervisorMessage) String() string { func (*SupervisorMessage) ProtoMessage() {} func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[162] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12371,7 +12639,7 @@ func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorMessage.ProtoReflect.Descriptor instead. func (*SupervisorMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{159} + return file_openshell_proto_rawDescGZIP(), []int{162} } func (x *SupervisorMessage) GetPayload() isSupervisorMessage_Payload { @@ -12462,7 +12730,7 @@ type GatewayMessage struct { func (x *GatewayMessage) Reset() { *x = GatewayMessage{} - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[163] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12474,7 +12742,7 @@ func (x *GatewayMessage) String() string { func (*GatewayMessage) ProtoMessage() {} func (x *GatewayMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[163] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12487,7 +12755,7 @@ func (x *GatewayMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayMessage.ProtoReflect.Descriptor instead. func (*GatewayMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{160} + return file_openshell_proto_rawDescGZIP(), []int{163} } func (x *GatewayMessage) GetPayload() isGatewayMessage_Payload { @@ -12591,7 +12859,7 @@ type SupervisorHello struct { func (x *SupervisorHello) Reset() { *x = SupervisorHello{} - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[164] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12603,7 +12871,7 @@ func (x *SupervisorHello) String() string { func (*SupervisorHello) ProtoMessage() {} func (x *SupervisorHello) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[164] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12616,7 +12884,7 @@ func (x *SupervisorHello) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHello.ProtoReflect.Descriptor instead. func (*SupervisorHello) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{161} + return file_openshell_proto_rawDescGZIP(), []int{164} } func (x *SupervisorHello) GetSandboxId() string { @@ -12653,7 +12921,7 @@ type SessionAccepted struct { func (x *SessionAccepted) Reset() { *x = SessionAccepted{} - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[165] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12665,7 +12933,7 @@ func (x *SessionAccepted) String() string { func (*SessionAccepted) ProtoMessage() {} func (x *SessionAccepted) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[165] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12678,7 +12946,7 @@ func (x *SessionAccepted) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionAccepted.ProtoReflect.Descriptor instead. func (*SessionAccepted) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{162} + return file_openshell_proto_rawDescGZIP(), []int{165} } func (x *SessionAccepted) GetSessionId() string { @@ -12706,7 +12974,7 @@ type SessionRejected struct { func (x *SessionRejected) Reset() { *x = SessionRejected{} - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[166] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12718,7 +12986,7 @@ func (x *SessionRejected) String() string { func (*SessionRejected) ProtoMessage() {} func (x *SessionRejected) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[166] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12731,7 +12999,7 @@ func (x *SessionRejected) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionRejected.ProtoReflect.Descriptor instead. func (*SessionRejected) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{163} + return file_openshell_proto_rawDescGZIP(), []int{166} } func (x *SessionRejected) GetReason() string { @@ -12750,7 +13018,7 @@ type SupervisorHeartbeat struct { func (x *SupervisorHeartbeat) Reset() { *x = SupervisorHeartbeat{} - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[167] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12762,7 +13030,7 @@ func (x *SupervisorHeartbeat) String() string { func (*SupervisorHeartbeat) ProtoMessage() {} func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[167] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12775,7 +13043,7 @@ func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHeartbeat.ProtoReflect.Descriptor instead. func (*SupervisorHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{164} + return file_openshell_proto_rawDescGZIP(), []int{167} } // Gateway heartbeat. @@ -12787,7 +13055,7 @@ type GatewayHeartbeat struct { func (x *GatewayHeartbeat) Reset() { *x = GatewayHeartbeat{} - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[168] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12799,7 +13067,7 @@ func (x *GatewayHeartbeat) String() string { func (*GatewayHeartbeat) ProtoMessage() {} func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[168] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12812,7 +13080,7 @@ func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayHeartbeat.ProtoReflect.Descriptor instead. func (*GatewayHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{165} + return file_openshell_proto_rawDescGZIP(), []int{168} } // Terminal result reported before the supervisor shuts down. A successful RPC @@ -12829,7 +13097,7 @@ type ReportMainProcessExitRequest struct { func (x *ReportMainProcessExitRequest) Reset() { *x = ReportMainProcessExitRequest{} - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[169] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12841,7 +13109,7 @@ func (x *ReportMainProcessExitRequest) String() string { func (*ReportMainProcessExitRequest) ProtoMessage() {} func (x *ReportMainProcessExitRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[169] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12854,7 +13122,7 @@ func (x *ReportMainProcessExitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportMainProcessExitRequest.ProtoReflect.Descriptor instead. func (*ReportMainProcessExitRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{166} + return file_openshell_proto_rawDescGZIP(), []int{169} } func (x *ReportMainProcessExitRequest) GetSandboxId() string { @@ -12886,7 +13154,7 @@ type ReportMainProcessExitResponse struct { func (x *ReportMainProcessExitResponse) Reset() { *x = ReportMainProcessExitResponse{} - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[170] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12898,7 +13166,7 @@ func (x *ReportMainProcessExitResponse) String() string { func (*ReportMainProcessExitResponse) ProtoMessage() {} func (x *ReportMainProcessExitResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[170] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12911,7 +13179,7 @@ func (x *ReportMainProcessExitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportMainProcessExitResponse.ProtoReflect.Descriptor instead. func (*ReportMainProcessExitResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{167} + return file_openshell_proto_rawDescGZIP(), []int{170} } // Terminal-delivery completion reported after all expected foreground SSH @@ -12926,7 +13194,7 @@ type FinalizeMainProcessExitRequest struct { func (x *FinalizeMainProcessExitRequest) Reset() { *x = FinalizeMainProcessExitRequest{} - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[171] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12938,7 +13206,7 @@ func (x *FinalizeMainProcessExitRequest) String() string { func (*FinalizeMainProcessExitRequest) ProtoMessage() {} func (x *FinalizeMainProcessExitRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[171] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12951,7 +13219,7 @@ func (x *FinalizeMainProcessExitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeMainProcessExitRequest.ProtoReflect.Descriptor instead. func (*FinalizeMainProcessExitRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{168} + return file_openshell_proto_rawDescGZIP(), []int{171} } func (x *FinalizeMainProcessExitRequest) GetSandboxId() string { @@ -12976,7 +13244,7 @@ type FinalizeMainProcessExitResponse struct { func (x *FinalizeMainProcessExitResponse) Reset() { *x = FinalizeMainProcessExitResponse{} - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[172] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12988,7 +13256,7 @@ func (x *FinalizeMainProcessExitResponse) String() string { func (*FinalizeMainProcessExitResponse) ProtoMessage() {} func (x *FinalizeMainProcessExitResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[172] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13001,7 +13269,7 @@ func (x *FinalizeMainProcessExitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeMainProcessExitResponse.ProtoReflect.Descriptor instead. func (*FinalizeMainProcessExitResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{169} + return file_openshell_proto_rawDescGZIP(), []int{172} } // Gateway requests the supervisor to open a relay channel. @@ -13030,7 +13298,7 @@ type RelayOpen struct { func (x *RelayOpen) Reset() { *x = RelayOpen{} - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[173] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13042,7 +13310,7 @@ func (x *RelayOpen) String() string { func (*RelayOpen) ProtoMessage() {} func (x *RelayOpen) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[173] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13055,7 +13323,7 @@ func (x *RelayOpen) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpen.ProtoReflect.Descriptor instead. func (*RelayOpen) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{170} + return file_openshell_proto_rawDescGZIP(), []int{173} } func (x *RelayOpen) GetChannelId() string { @@ -13122,7 +13390,7 @@ type SshRelayTarget struct { func (x *SshRelayTarget) Reset() { *x = SshRelayTarget{} - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[174] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13134,7 +13402,7 @@ func (x *SshRelayTarget) String() string { func (*SshRelayTarget) ProtoMessage() {} func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[174] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13147,7 +13415,7 @@ func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use SshRelayTarget.ProtoReflect.Descriptor instead. func (*SshRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{171} + return file_openshell_proto_rawDescGZIP(), []int{174} } // TCP target dialed by the supervisor from inside the sandbox. @@ -13163,7 +13431,7 @@ type TcpRelayTarget struct { func (x *TcpRelayTarget) Reset() { *x = TcpRelayTarget{} - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[175] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13175,7 +13443,7 @@ func (x *TcpRelayTarget) String() string { func (*TcpRelayTarget) ProtoMessage() {} func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[175] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13188,7 +13456,7 @@ func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpRelayTarget.ProtoReflect.Descriptor instead. func (*TcpRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{172} + return file_openshell_proto_rawDescGZIP(), []int{175} } func (x *TcpRelayTarget) GetHost() string { @@ -13216,7 +13484,7 @@ type RelayInit struct { func (x *RelayInit) Reset() { *x = RelayInit{} - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[176] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13228,7 +13496,7 @@ func (x *RelayInit) String() string { func (*RelayInit) ProtoMessage() {} func (x *RelayInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[176] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13241,7 +13509,7 @@ func (x *RelayInit) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayInit.ProtoReflect.Descriptor instead. func (*RelayInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{173} + return file_openshell_proto_rawDescGZIP(), []int{176} } func (x *RelayInit) GetChannelId() string { @@ -13268,7 +13536,7 @@ type RelayFrame struct { func (x *RelayFrame) Reset() { *x = RelayFrame{} - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[177] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13280,7 +13548,7 @@ func (x *RelayFrame) String() string { func (*RelayFrame) ProtoMessage() {} func (x *RelayFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[177] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13293,7 +13561,7 @@ func (x *RelayFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayFrame.ProtoReflect.Descriptor instead. func (*RelayFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{174} + return file_openshell_proto_rawDescGZIP(), []int{177} } func (x *RelayFrame) GetPayload() isRelayFrame_Payload { @@ -13352,7 +13620,7 @@ type RelayOpenResult struct { func (x *RelayOpenResult) Reset() { *x = RelayOpenResult{} - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[178] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13364,7 +13632,7 @@ func (x *RelayOpenResult) String() string { func (*RelayOpenResult) ProtoMessage() {} func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[178] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13377,7 +13645,7 @@ func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpenResult.ProtoReflect.Descriptor instead. func (*RelayOpenResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{175} + return file_openshell_proto_rawDescGZIP(), []int{178} } func (x *RelayOpenResult) GetChannelId() string { @@ -13414,7 +13682,7 @@ type RelayClose struct { func (x *RelayClose) Reset() { *x = RelayClose{} - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[179] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13426,7 +13694,7 @@ func (x *RelayClose) String() string { func (*RelayClose) ProtoMessage() {} func (x *RelayClose) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[179] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13439,7 +13707,7 @@ func (x *RelayClose) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayClose.ProtoReflect.Descriptor instead. func (*RelayClose) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{176} + return file_openshell_proto_rawDescGZIP(), []int{179} } func (x *RelayClose) GetChannelId() string { @@ -13473,7 +13741,7 @@ type L7RequestSample struct { func (x *L7RequestSample) Reset() { *x = L7RequestSample{} - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[180] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13485,7 +13753,7 @@ func (x *L7RequestSample) String() string { func (*L7RequestSample) ProtoMessage() {} func (x *L7RequestSample) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[180] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13498,7 +13766,7 @@ func (x *L7RequestSample) ProtoReflect() protoreflect.Message { // Deprecated: Use L7RequestSample.ProtoReflect.Descriptor instead. func (*L7RequestSample) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{177} + return file_openshell_proto_rawDescGZIP(), []int{180} } func (x *L7RequestSample) GetMethod() string { @@ -13572,7 +13840,7 @@ type DenialSummary struct { func (x *DenialSummary) Reset() { *x = DenialSummary{} - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[181] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13584,7 +13852,7 @@ func (x *DenialSummary) String() string { func (*DenialSummary) ProtoMessage() {} func (x *DenialSummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[181] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13597,7 +13865,7 @@ func (x *DenialSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialSummary.ProtoReflect.Descriptor instead. func (*DenialSummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{178} + return file_openshell_proto_rawDescGZIP(), []int{181} } func (x *DenialSummary) GetSandboxId() string { @@ -13732,7 +14000,7 @@ type DenialGroupCount struct { func (x *DenialGroupCount) Reset() { *x = DenialGroupCount{} - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[182] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13744,7 +14012,7 @@ func (x *DenialGroupCount) String() string { func (*DenialGroupCount) ProtoMessage() {} func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[182] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13757,7 +14025,7 @@ func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialGroupCount.ProtoReflect.Descriptor instead. func (*DenialGroupCount) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{179} + return file_openshell_proto_rawDescGZIP(), []int{182} } func (x *DenialGroupCount) GetDenyGroup() string { @@ -13790,7 +14058,7 @@ type NetworkActivitySummary struct { func (x *NetworkActivitySummary) Reset() { *x = NetworkActivitySummary{} - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[183] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13802,7 +14070,7 @@ func (x *NetworkActivitySummary) String() string { func (*NetworkActivitySummary) ProtoMessage() {} func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[183] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13815,7 +14083,7 @@ func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkActivitySummary.ProtoReflect.Descriptor instead. func (*NetworkActivitySummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{180} + return file_openshell_proto_rawDescGZIP(), []int{183} } func (x *NetworkActivitySummary) GetNetworkActivityCount() uint32 { @@ -13903,7 +14171,7 @@ type PolicyChunk struct { func (x *PolicyChunk) Reset() { *x = PolicyChunk{} - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[184] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13915,7 +14183,7 @@ func (x *PolicyChunk) String() string { func (*PolicyChunk) ProtoMessage() {} func (x *PolicyChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[184] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13928,7 +14196,7 @@ func (x *PolicyChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyChunk.ProtoReflect.Descriptor instead. func (*PolicyChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{181} + return file_openshell_proto_rawDescGZIP(), []int{184} } func (x *PolicyChunk) GetId() string { @@ -14116,7 +14384,7 @@ type DraftPolicyUpdate struct { func (x *DraftPolicyUpdate) Reset() { *x = DraftPolicyUpdate{} - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[185] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14128,7 +14396,7 @@ func (x *DraftPolicyUpdate) String() string { func (*DraftPolicyUpdate) ProtoMessage() {} func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[185] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14141,7 +14409,7 @@ func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftPolicyUpdate.ProtoReflect.Descriptor instead. func (*DraftPolicyUpdate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{182} + return file_openshell_proto_rawDescGZIP(), []int{185} } func (x *DraftPolicyUpdate) GetDraftVersion() uint64 { @@ -14199,7 +14467,7 @@ type SubmitPolicyAnalysisRequest struct { func (x *SubmitPolicyAnalysisRequest) Reset() { *x = SubmitPolicyAnalysisRequest{} - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[186] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14211,7 +14479,7 @@ func (x *SubmitPolicyAnalysisRequest) String() string { func (*SubmitPolicyAnalysisRequest) ProtoMessage() {} func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[186] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14224,7 +14492,7 @@ func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisRequest.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{183} + return file_openshell_proto_rawDescGZIP(), []int{186} } func (x *SubmitPolicyAnalysisRequest) GetSummaries() []*DenialSummary { @@ -14287,7 +14555,7 @@ type SubmitPolicyAnalysisResponse struct { func (x *SubmitPolicyAnalysisResponse) Reset() { *x = SubmitPolicyAnalysisResponse{} - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[187] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14299,7 +14567,7 @@ func (x *SubmitPolicyAnalysisResponse) String() string { func (*SubmitPolicyAnalysisResponse) ProtoMessage() {} func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[187] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14312,7 +14580,7 @@ func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisResponse.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{184} + return file_openshell_proto_rawDescGZIP(), []int{187} } func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunks() uint32 { @@ -14358,7 +14626,7 @@ type GetDraftPolicyRequest struct { func (x *GetDraftPolicyRequest) Reset() { *x = GetDraftPolicyRequest{} - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[188] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14370,7 +14638,7 @@ func (x *GetDraftPolicyRequest) String() string { func (*GetDraftPolicyRequest) ProtoMessage() {} func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[188] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14383,7 +14651,7 @@ func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyRequest.ProtoReflect.Descriptor instead. func (*GetDraftPolicyRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{185} + return file_openshell_proto_rawDescGZIP(), []int{188} } func (x *GetDraftPolicyRequest) GetName() string { @@ -14423,7 +14691,7 @@ type GetDraftPolicyResponse struct { func (x *GetDraftPolicyResponse) Reset() { *x = GetDraftPolicyResponse{} - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[189] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14435,7 +14703,7 @@ func (x *GetDraftPolicyResponse) String() string { func (*GetDraftPolicyResponse) ProtoMessage() {} func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[189] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14448,7 +14716,7 @@ func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyResponse.ProtoReflect.Descriptor instead. func (*GetDraftPolicyResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{186} + return file_openshell_proto_rawDescGZIP(), []int{189} } func (x *GetDraftPolicyResponse) GetChunks() []*PolicyChunk { @@ -14500,7 +14768,7 @@ type ApproveDraftChunkRequest struct { func (x *ApproveDraftChunkRequest) Reset() { *x = ApproveDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[190] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14512,7 +14780,7 @@ func (x *ApproveDraftChunkRequest) String() string { func (*ApproveDraftChunkRequest) ProtoMessage() {} func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[190] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14525,7 +14793,7 @@ func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkRequest.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{187} + return file_openshell_proto_rawDescGZIP(), []int{190} } func (x *ApproveDraftChunkRequest) GetName() string { @@ -14575,7 +14843,7 @@ type ApproveDraftChunkResponse struct { func (x *ApproveDraftChunkResponse) Reset() { *x = ApproveDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[188] + mi := &file_openshell_proto_msgTypes[191] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14587,7 +14855,7 @@ func (x *ApproveDraftChunkResponse) String() string { func (*ApproveDraftChunkResponse) ProtoMessage() {} func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[188] + mi := &file_openshell_proto_msgTypes[191] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14600,7 +14868,7 @@ func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkResponse.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{188} + return file_openshell_proto_rawDescGZIP(), []int{191} } func (x *ApproveDraftChunkResponse) GetPolicyVersion() uint32 { @@ -14637,7 +14905,7 @@ type RejectDraftChunkRequest struct { func (x *RejectDraftChunkRequest) Reset() { *x = RejectDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[189] + mi := &file_openshell_proto_msgTypes[192] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14649,7 +14917,7 @@ func (x *RejectDraftChunkRequest) String() string { func (*RejectDraftChunkRequest) ProtoMessage() {} func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[189] + mi := &file_openshell_proto_msgTypes[192] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14662,7 +14930,7 @@ func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkRequest.ProtoReflect.Descriptor instead. func (*RejectDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{189} + return file_openshell_proto_rawDescGZIP(), []int{192} } func (x *RejectDraftChunkRequest) GetName() string { @@ -14708,7 +14976,7 @@ type RejectDraftChunkResponse struct { func (x *RejectDraftChunkResponse) Reset() { *x = RejectDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[190] + mi := &file_openshell_proto_msgTypes[193] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14720,7 +14988,7 @@ func (x *RejectDraftChunkResponse) String() string { func (*RejectDraftChunkResponse) ProtoMessage() {} func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[190] + mi := &file_openshell_proto_msgTypes[193] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14733,7 +15001,7 @@ func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkResponse.ProtoReflect.Descriptor instead. func (*RejectDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{190} + return file_openshell_proto_rawDescGZIP(), []int{193} } // Approve all pending chunks. @@ -14747,7 +15015,7 @@ type DraftChunkApproval struct { func (x *DraftChunkApproval) Reset() { *x = DraftChunkApproval{} - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[194] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14759,7 +15027,7 @@ func (x *DraftChunkApproval) String() string { func (*DraftChunkApproval) ProtoMessage() {} func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[194] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14772,7 +15040,7 @@ func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftChunkApproval.ProtoReflect.Descriptor instead. func (*DraftChunkApproval) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{191} + return file_openshell_proto_rawDescGZIP(), []int{194} } func (x *DraftChunkApproval) GetChunkId() string { @@ -14809,7 +15077,7 @@ type ApproveAllDraftChunksRequest struct { func (x *ApproveAllDraftChunksRequest) Reset() { *x = ApproveAllDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[192] + mi := &file_openshell_proto_msgTypes[195] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14821,7 +15089,7 @@ func (x *ApproveAllDraftChunksRequest) String() string { func (*ApproveAllDraftChunksRequest) ProtoMessage() {} func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[192] + mi := &file_openshell_proto_msgTypes[195] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14834,7 +15102,7 @@ func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{192} + return file_openshell_proto_rawDescGZIP(), []int{195} } func (x *ApproveAllDraftChunksRequest) GetName() string { @@ -14889,7 +15157,7 @@ type ApproveAllDraftChunksResponse struct { func (x *ApproveAllDraftChunksResponse) Reset() { *x = ApproveAllDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[193] + mi := &file_openshell_proto_msgTypes[196] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14901,7 +15169,7 @@ func (x *ApproveAllDraftChunksResponse) String() string { func (*ApproveAllDraftChunksResponse) ProtoMessage() {} func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[193] + mi := &file_openshell_proto_msgTypes[196] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14914,7 +15182,7 @@ func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{193} + return file_openshell_proto_rawDescGZIP(), []int{196} } func (x *ApproveAllDraftChunksResponse) GetPolicyVersion() uint32 { @@ -14965,7 +15233,7 @@ type EditDraftChunkRequest struct { func (x *EditDraftChunkRequest) Reset() { *x = EditDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[194] + mi := &file_openshell_proto_msgTypes[197] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14977,7 +15245,7 @@ func (x *EditDraftChunkRequest) String() string { func (*EditDraftChunkRequest) ProtoMessage() {} func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[194] + mi := &file_openshell_proto_msgTypes[197] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14990,7 +15258,7 @@ func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkRequest.ProtoReflect.Descriptor instead. func (*EditDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{194} + return file_openshell_proto_rawDescGZIP(), []int{197} } func (x *EditDraftChunkRequest) GetName() string { @@ -15036,7 +15304,7 @@ type EditDraftChunkResponse struct { func (x *EditDraftChunkResponse) Reset() { *x = EditDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[195] + mi := &file_openshell_proto_msgTypes[198] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15048,7 +15316,7 @@ func (x *EditDraftChunkResponse) String() string { func (*EditDraftChunkResponse) ProtoMessage() {} func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[195] + mi := &file_openshell_proto_msgTypes[198] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15061,7 +15329,7 @@ func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkResponse.ProtoReflect.Descriptor instead. func (*EditDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{195} + return file_openshell_proto_rawDescGZIP(), []int{198} } // Reverse an approval (remove merged rule from active policy). @@ -15082,7 +15350,7 @@ type UndoDraftChunkRequest struct { func (x *UndoDraftChunkRequest) Reset() { *x = UndoDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[196] + mi := &file_openshell_proto_msgTypes[199] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15094,7 +15362,7 @@ func (x *UndoDraftChunkRequest) String() string { func (*UndoDraftChunkRequest) ProtoMessage() {} func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[196] + mi := &file_openshell_proto_msgTypes[199] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15107,7 +15375,7 @@ func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkRequest.ProtoReflect.Descriptor instead. func (*UndoDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{196} + return file_openshell_proto_rawDescGZIP(), []int{199} } func (x *UndoDraftChunkRequest) GetName() string { @@ -15150,7 +15418,7 @@ type UndoDraftChunkResponse struct { func (x *UndoDraftChunkResponse) Reset() { *x = UndoDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[197] + mi := &file_openshell_proto_msgTypes[200] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15162,7 +15430,7 @@ func (x *UndoDraftChunkResponse) String() string { func (*UndoDraftChunkResponse) ProtoMessage() {} func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[197] + mi := &file_openshell_proto_msgTypes[200] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15175,7 +15443,7 @@ func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkResponse.ProtoReflect.Descriptor instead. func (*UndoDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{197} + return file_openshell_proto_rawDescGZIP(), []int{200} } func (x *UndoDraftChunkResponse) GetPolicyVersion() uint32 { @@ -15208,7 +15476,7 @@ type ClearDraftChunksRequest struct { func (x *ClearDraftChunksRequest) Reset() { *x = ClearDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[198] + mi := &file_openshell_proto_msgTypes[201] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15220,7 +15488,7 @@ func (x *ClearDraftChunksRequest) String() string { func (*ClearDraftChunksRequest) ProtoMessage() {} func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[198] + mi := &file_openshell_proto_msgTypes[201] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15233,7 +15501,7 @@ func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ClearDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{198} + return file_openshell_proto_rawDescGZIP(), []int{201} } func (x *ClearDraftChunksRequest) GetName() string { @@ -15267,7 +15535,7 @@ type ClearDraftChunksResponse struct { func (x *ClearDraftChunksResponse) Reset() { *x = ClearDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[199] + mi := &file_openshell_proto_msgTypes[202] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15279,7 +15547,7 @@ func (x *ClearDraftChunksResponse) String() string { func (*ClearDraftChunksResponse) ProtoMessage() {} func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[199] + mi := &file_openshell_proto_msgTypes[202] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15292,7 +15560,7 @@ func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ClearDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{199} + return file_openshell_proto_rawDescGZIP(), []int{202} } func (x *ClearDraftChunksResponse) GetChunksCleared() uint32 { @@ -15315,7 +15583,7 @@ type GetDraftHistoryRequest struct { func (x *GetDraftHistoryRequest) Reset() { *x = GetDraftHistoryRequest{} - mi := &file_openshell_proto_msgTypes[200] + mi := &file_openshell_proto_msgTypes[203] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15327,7 +15595,7 @@ func (x *GetDraftHistoryRequest) String() string { func (*GetDraftHistoryRequest) ProtoMessage() {} func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[200] + mi := &file_openshell_proto_msgTypes[203] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15340,7 +15608,7 @@ func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryRequest.ProtoReflect.Descriptor instead. func (*GetDraftHistoryRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{200} + return file_openshell_proto_rawDescGZIP(), []int{203} } func (x *GetDraftHistoryRequest) GetName() string { @@ -15374,7 +15642,7 @@ type DraftHistoryEntry struct { func (x *DraftHistoryEntry) Reset() { *x = DraftHistoryEntry{} - mi := &file_openshell_proto_msgTypes[201] + mi := &file_openshell_proto_msgTypes[204] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15386,7 +15654,7 @@ func (x *DraftHistoryEntry) String() string { func (*DraftHistoryEntry) ProtoMessage() {} func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[201] + mi := &file_openshell_proto_msgTypes[204] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15399,7 +15667,7 @@ func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftHistoryEntry.ProtoReflect.Descriptor instead. func (*DraftHistoryEntry) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{201} + return file_openshell_proto_rawDescGZIP(), []int{204} } func (x *DraftHistoryEntry) GetEventTime() *timestamppb.Timestamp { @@ -15440,7 +15708,7 @@ type GetDraftHistoryResponse struct { func (x *GetDraftHistoryResponse) Reset() { *x = GetDraftHistoryResponse{} - mi := &file_openshell_proto_msgTypes[202] + mi := &file_openshell_proto_msgTypes[205] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15452,7 +15720,7 @@ func (x *GetDraftHistoryResponse) String() string { func (*GetDraftHistoryResponse) ProtoMessage() {} func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[202] + mi := &file_openshell_proto_msgTypes[205] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15465,7 +15733,7 @@ func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryResponse.ProtoReflect.Descriptor instead. func (*GetDraftHistoryResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{202} + return file_openshell_proto_rawDescGZIP(), []int{205} } func (x *GetDraftHistoryResponse) GetEntries() []*DraftHistoryEntry { @@ -15490,7 +15758,7 @@ type CreateWorkspaceRequest struct { func (x *CreateWorkspaceRequest) Reset() { *x = CreateWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[203] + mi := &file_openshell_proto_msgTypes[206] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15502,7 +15770,7 @@ func (x *CreateWorkspaceRequest) String() string { func (*CreateWorkspaceRequest) ProtoMessage() {} func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[203] + mi := &file_openshell_proto_msgTypes[206] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15515,7 +15783,7 @@ func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceRequest.ProtoReflect.Descriptor instead. func (*CreateWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{203} + return file_openshell_proto_rawDescGZIP(), []int{206} } func (x *CreateWorkspaceRequest) GetName() string { @@ -15549,7 +15817,7 @@ type CreateWorkspaceResponse struct { func (x *CreateWorkspaceResponse) Reset() { *x = CreateWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[204] + mi := &file_openshell_proto_msgTypes[207] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15561,7 +15829,7 @@ func (x *CreateWorkspaceResponse) String() string { func (*CreateWorkspaceResponse) ProtoMessage() {} func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[204] + mi := &file_openshell_proto_msgTypes[207] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15574,7 +15842,7 @@ func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceResponse.ProtoReflect.Descriptor instead. func (*CreateWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{204} + return file_openshell_proto_rawDescGZIP(), []int{207} } func (x *CreateWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -15595,7 +15863,7 @@ type GetWorkspaceRequest struct { func (x *GetWorkspaceRequest) Reset() { *x = GetWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[205] + mi := &file_openshell_proto_msgTypes[208] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15607,7 +15875,7 @@ func (x *GetWorkspaceRequest) String() string { func (*GetWorkspaceRequest) ProtoMessage() {} func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[205] + mi := &file_openshell_proto_msgTypes[208] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15620,7 +15888,7 @@ func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceRequest.ProtoReflect.Descriptor instead. func (*GetWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{205} + return file_openshell_proto_rawDescGZIP(), []int{208} } func (x *GetWorkspaceRequest) GetName() string { @@ -15640,7 +15908,7 @@ type GetWorkspaceResponse struct { func (x *GetWorkspaceResponse) Reset() { *x = GetWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[206] + mi := &file_openshell_proto_msgTypes[209] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15652,7 +15920,7 @@ func (x *GetWorkspaceResponse) String() string { func (*GetWorkspaceResponse) ProtoMessage() {} func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[206] + mi := &file_openshell_proto_msgTypes[209] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15665,7 +15933,7 @@ func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceResponse.ProtoReflect.Descriptor instead. func (*GetWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{206} + return file_openshell_proto_rawDescGZIP(), []int{209} } func (x *GetWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -15692,7 +15960,7 @@ type ListWorkspacesRequest struct { func (x *ListWorkspacesRequest) Reset() { *x = ListWorkspacesRequest{} - mi := &file_openshell_proto_msgTypes[207] + mi := &file_openshell_proto_msgTypes[210] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15704,7 +15972,7 @@ func (x *ListWorkspacesRequest) String() string { func (*ListWorkspacesRequest) ProtoMessage() {} func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[207] + mi := &file_openshell_proto_msgTypes[210] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15717,7 +15985,7 @@ func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesRequest.ProtoReflect.Descriptor instead. func (*ListWorkspacesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{207} + return file_openshell_proto_rawDescGZIP(), []int{210} } func (x *ListWorkspacesRequest) GetPageSize() int32 { @@ -15753,7 +16021,7 @@ type ListWorkspacesResponse struct { func (x *ListWorkspacesResponse) Reset() { *x = ListWorkspacesResponse{} - mi := &file_openshell_proto_msgTypes[208] + mi := &file_openshell_proto_msgTypes[211] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15765,7 +16033,7 @@ func (x *ListWorkspacesResponse) String() string { func (*ListWorkspacesResponse) ProtoMessage() {} func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[208] + mi := &file_openshell_proto_msgTypes[211] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15778,7 +16046,7 @@ func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesResponse.ProtoReflect.Descriptor instead. func (*ListWorkspacesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{208} + return file_openshell_proto_rawDescGZIP(), []int{211} } func (x *ListWorkspacesResponse) GetWorkspaces() []*datamodelv1.Workspace { @@ -15809,7 +16077,7 @@ type DeleteWorkspaceRequest struct { func (x *DeleteWorkspaceRequest) Reset() { *x = DeleteWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[209] + mi := &file_openshell_proto_msgTypes[212] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15821,7 +16089,7 @@ func (x *DeleteWorkspaceRequest) String() string { func (*DeleteWorkspaceRequest) ProtoMessage() {} func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[209] + mi := &file_openshell_proto_msgTypes[212] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15834,7 +16102,7 @@ func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceRequest.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{209} + return file_openshell_proto_rawDescGZIP(), []int{212} } func (x *DeleteWorkspaceRequest) GetName() string { @@ -15868,7 +16136,7 @@ type DeleteWorkspaceResponse struct { func (x *DeleteWorkspaceResponse) Reset() { *x = DeleteWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[210] + mi := &file_openshell_proto_msgTypes[213] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15880,7 +16148,7 @@ func (x *DeleteWorkspaceResponse) String() string { func (*DeleteWorkspaceResponse) ProtoMessage() {} func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[210] + mi := &file_openshell_proto_msgTypes[213] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15893,7 +16161,7 @@ func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceResponse.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{210} + return file_openshell_proto_rawDescGZIP(), []int{213} } func (x *DeleteWorkspaceResponse) GetOutcome() DeletionOutcome { @@ -15917,7 +16185,7 @@ type WorkspaceMember struct { func (x *WorkspaceMember) Reset() { *x = WorkspaceMember{} - mi := &file_openshell_proto_msgTypes[211] + mi := &file_openshell_proto_msgTypes[214] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15929,7 +16197,7 @@ func (x *WorkspaceMember) String() string { func (*WorkspaceMember) ProtoMessage() {} func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[211] + mi := &file_openshell_proto_msgTypes[214] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15942,7 +16210,7 @@ func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceMember.ProtoReflect.Descriptor instead. func (*WorkspaceMember) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{211} + return file_openshell_proto_rawDescGZIP(), []int{214} } func (x *WorkspaceMember) GetMetadata() *datamodelv1.ObjectMeta { @@ -15983,7 +16251,7 @@ type AddWorkspaceMemberRequest struct { func (x *AddWorkspaceMemberRequest) Reset() { *x = AddWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[212] + mi := &file_openshell_proto_msgTypes[215] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15995,7 +16263,7 @@ func (x *AddWorkspaceMemberRequest) String() string { func (*AddWorkspaceMemberRequest) ProtoMessage() {} func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[212] + mi := &file_openshell_proto_msgTypes[215] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16008,7 +16276,7 @@ func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{212} + return file_openshell_proto_rawDescGZIP(), []int{215} } func (x *AddWorkspaceMemberRequest) GetWorkspace() string { @@ -16049,7 +16317,7 @@ type AddWorkspaceMemberResponse struct { func (x *AddWorkspaceMemberResponse) Reset() { *x = AddWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[213] + mi := &file_openshell_proto_msgTypes[216] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16061,7 +16329,7 @@ func (x *AddWorkspaceMemberResponse) String() string { func (*AddWorkspaceMemberResponse) ProtoMessage() {} func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[213] + mi := &file_openshell_proto_msgTypes[216] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16074,7 +16342,7 @@ func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{213} + return file_openshell_proto_rawDescGZIP(), []int{216} } func (x *AddWorkspaceMemberResponse) GetMember() *WorkspaceMember { @@ -16100,7 +16368,7 @@ type RemoveWorkspaceMemberRequest struct { func (x *RemoveWorkspaceMemberRequest) Reset() { *x = RemoveWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[214] + mi := &file_openshell_proto_msgTypes[217] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16112,7 +16380,7 @@ func (x *RemoveWorkspaceMemberRequest) String() string { func (*RemoveWorkspaceMemberRequest) ProtoMessage() {} func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[214] + mi := &file_openshell_proto_msgTypes[217] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16125,7 +16393,7 @@ func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{214} + return file_openshell_proto_rawDescGZIP(), []int{217} } func (x *RemoveWorkspaceMemberRequest) GetWorkspace() string { @@ -16166,7 +16434,7 @@ type RemoveWorkspaceMemberResponse struct { func (x *RemoveWorkspaceMemberResponse) Reset() { *x = RemoveWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[215] + mi := &file_openshell_proto_msgTypes[218] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16178,7 +16446,7 @@ func (x *RemoveWorkspaceMemberResponse) String() string { func (*RemoveWorkspaceMemberResponse) ProtoMessage() {} func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[215] + mi := &file_openshell_proto_msgTypes[218] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16191,7 +16459,7 @@ func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{215} + return file_openshell_proto_rawDescGZIP(), []int{218} } func (x *RemoveWorkspaceMemberResponse) GetOutcome() DeletionOutcome { @@ -16218,7 +16486,7 @@ type ListWorkspaceMembersRequest struct { func (x *ListWorkspaceMembersRequest) Reset() { *x = ListWorkspaceMembersRequest{} - mi := &file_openshell_proto_msgTypes[216] + mi := &file_openshell_proto_msgTypes[219] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16230,7 +16498,7 @@ func (x *ListWorkspaceMembersRequest) String() string { func (*ListWorkspaceMembersRequest) ProtoMessage() {} func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[216] + mi := &file_openshell_proto_msgTypes[219] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16243,7 +16511,7 @@ func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersRequest.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{216} + return file_openshell_proto_rawDescGZIP(), []int{219} } func (x *ListWorkspaceMembersRequest) GetWorkspace() string { @@ -16279,7 +16547,7 @@ type ListWorkspaceMembersResponse struct { func (x *ListWorkspaceMembersResponse) Reset() { *x = ListWorkspaceMembersResponse{} - mi := &file_openshell_proto_msgTypes[217] + mi := &file_openshell_proto_msgTypes[220] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16291,7 +16559,7 @@ func (x *ListWorkspaceMembersResponse) String() string { func (*ListWorkspaceMembersResponse) ProtoMessage() {} func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[217] + mi := &file_openshell_proto_msgTypes[220] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16304,7 +16572,7 @@ func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersResponse.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{217} + return file_openshell_proto_rawDescGZIP(), []int{220} } func (x *ListWorkspaceMembersResponse) GetMembers() []*WorkspaceMember { @@ -16339,7 +16607,7 @@ type ExtensionServiceCredential struct { func (x *ExtensionServiceCredential) Reset() { *x = ExtensionServiceCredential{} - mi := &file_openshell_proto_msgTypes[218] + mi := &file_openshell_proto_msgTypes[221] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16351,7 +16619,7 @@ func (x *ExtensionServiceCredential) String() string { func (*ExtensionServiceCredential) ProtoMessage() {} func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[218] + mi := &file_openshell_proto_msgTypes[221] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16364,7 +16632,7 @@ func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ExtensionServiceCredential.ProtoReflect.Descriptor instead. func (*ExtensionServiceCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{218} + return file_openshell_proto_rawDescGZIP(), []int{221} } func (x *ExtensionServiceCredential) GetServiceName() string { @@ -16401,7 +16669,7 @@ type EndpointObservation struct { func (x *EndpointObservation) Reset() { *x = EndpointObservation{} - mi := &file_openshell_proto_msgTypes[219] + mi := &file_openshell_proto_msgTypes[222] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16413,7 +16681,7 @@ func (x *EndpointObservation) String() string { func (*EndpointObservation) ProtoMessage() {} func (x *EndpointObservation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[219] + mi := &file_openshell_proto_msgTypes[222] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16426,7 +16694,7 @@ func (x *EndpointObservation) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointObservation.ProtoReflect.Descriptor instead. func (*EndpointObservation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{219} + return file_openshell_proto_rawDescGZIP(), []int{222} } func (x *EndpointObservation) GetEndpointId() string { @@ -16469,7 +16737,7 @@ type ReportEndpointStatusRequest struct { func (x *ReportEndpointStatusRequest) Reset() { *x = ReportEndpointStatusRequest{} - mi := &file_openshell_proto_msgTypes[220] + mi := &file_openshell_proto_msgTypes[223] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16481,7 +16749,7 @@ func (x *ReportEndpointStatusRequest) String() string { func (*ReportEndpointStatusRequest) ProtoMessage() {} func (x *ReportEndpointStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[220] + mi := &file_openshell_proto_msgTypes[223] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16494,7 +16762,7 @@ func (x *ReportEndpointStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportEndpointStatusRequest.ProtoReflect.Descriptor instead. func (*ReportEndpointStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{220} + return file_openshell_proto_rawDescGZIP(), []int{223} } func (x *ReportEndpointStatusRequest) GetSandboxId() string { @@ -16555,7 +16823,7 @@ type ReportEndpointStatusResponse struct { func (x *ReportEndpointStatusResponse) Reset() { *x = ReportEndpointStatusResponse{} - mi := &file_openshell_proto_msgTypes[221] + mi := &file_openshell_proto_msgTypes[224] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16567,7 +16835,7 @@ func (x *ReportEndpointStatusResponse) String() string { func (*ReportEndpointStatusResponse) ProtoMessage() {} func (x *ReportEndpointStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[221] + mi := &file_openshell_proto_msgTypes[224] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16580,7 +16848,7 @@ func (x *ReportEndpointStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportEndpointStatusResponse.ProtoReflect.Descriptor instead. func (*ReportEndpointStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{221} + return file_openshell_proto_rawDescGZIP(), []int{224} } // A configured endpoint and its last accepted network result in one record. @@ -16609,7 +16877,7 @@ type EndpointStatus struct { func (x *EndpointStatus) Reset() { *x = EndpointStatus{} - mi := &file_openshell_proto_msgTypes[222] + mi := &file_openshell_proto_msgTypes[225] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16621,7 +16889,7 @@ func (x *EndpointStatus) String() string { func (*EndpointStatus) ProtoMessage() {} func (x *EndpointStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[222] + mi := &file_openshell_proto_msgTypes[225] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16634,7 +16902,7 @@ func (x *EndpointStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointStatus.ProtoReflect.Descriptor instead. func (*EndpointStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{222} + return file_openshell_proto_rawDescGZIP(), []int{225} } func (x *EndpointStatus) GetEndpointId() string { @@ -16679,6 +16947,136 @@ func (x *EndpointStatus) GetLastReportedTime() *timestamppb.Timestamp { return nil } +// Durable provisioning attempt, independent of supervisor registration and polling. +type SandboxProvisioning struct { + state protoimpl.MessageState `protogen:"open.v1"` + AttemptId string `protobuf:"bytes,1,opt,name=attempt_id,json=attemptId,proto3" json:"attempt_id,omitempty"` + ConfigurationChangeId string `protobuf:"bytes,2,opt,name=configuration_change_id,json=configurationChangeId,proto3" json:"configuration_change_id,omitempty"` + ConfigurationChangeTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=configuration_change_time,json=configurationChangeTime,proto3" json:"configuration_change_time,omitempty"` + FirstRejectionTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=first_rejection_time,json=firstRejectionTime,proto3" json:"first_rejection_time,omitempty"` + // Present only while the repair window is armed. + Deadline *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=deadline,proto3" json:"deadline,omitempty"` + TimeoutTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=timeout_time,json=timeoutTime,proto3" json:"timeout_time,omitempty"` + // Set only after both supervisor and workload compute have been reclaimed. + CleanupCompletedTime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=cleanup_completed_time,json=cleanupCompletedTime,proto3" json:"cleanup_completed_time,omitempty"` + // A safe gateway-authored diagnostic; never a raw driver error. + CleanupError string `protobuf:"bytes,8,opt,name=cleanup_error,json=cleanupError,proto3" json:"cleanup_error,omitempty"` + // Durable backoff for interrupted or failed reclamation. + CleanupRetryTime *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=cleanup_retry_time,json=cleanupRetryTime,proto3" json:"cleanup_retry_time,omitempty"` + // Attachment edits have their own durable clock; status writes do not change it. + AttachmentChangeId string `protobuf:"bytes,10,opt,name=attachment_change_id,json=attachmentChangeId,proto3" json:"attachment_change_id,omitempty"` + AttachmentChangeTime *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=attachment_change_time,json=attachmentChangeTime,proto3" json:"attachment_change_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxProvisioning) Reset() { + *x = SandboxProvisioning{} + mi := &file_openshell_proto_msgTypes[226] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxProvisioning) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxProvisioning) ProtoMessage() {} + +func (x *SandboxProvisioning) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[226] + 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 SandboxProvisioning.ProtoReflect.Descriptor instead. +func (*SandboxProvisioning) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{226} +} + +func (x *SandboxProvisioning) GetAttemptId() string { + if x != nil { + return x.AttemptId + } + return "" +} + +func (x *SandboxProvisioning) GetConfigurationChangeId() string { + if x != nil { + return x.ConfigurationChangeId + } + return "" +} + +func (x *SandboxProvisioning) GetConfigurationChangeTime() *timestamppb.Timestamp { + if x != nil { + return x.ConfigurationChangeTime + } + return nil +} + +func (x *SandboxProvisioning) GetFirstRejectionTime() *timestamppb.Timestamp { + if x != nil { + return x.FirstRejectionTime + } + return nil +} + +func (x *SandboxProvisioning) GetDeadline() *timestamppb.Timestamp { + if x != nil { + return x.Deadline + } + return nil +} + +func (x *SandboxProvisioning) GetTimeoutTime() *timestamppb.Timestamp { + if x != nil { + return x.TimeoutTime + } + return nil +} + +func (x *SandboxProvisioning) GetCleanupCompletedTime() *timestamppb.Timestamp { + if x != nil { + return x.CleanupCompletedTime + } + return nil +} + +func (x *SandboxProvisioning) GetCleanupError() string { + if x != nil { + return x.CleanupError + } + return "" +} + +func (x *SandboxProvisioning) GetCleanupRetryTime() *timestamppb.Timestamp { + if x != nil { + return x.CleanupRetryTime + } + return nil +} + +func (x *SandboxProvisioning) GetAttachmentChangeId() string { + if x != nil { + return x.AttachmentChangeId + } + return "" +} + +func (x *SandboxProvisioning) GetAttachmentChangeTime() *timestamppb.Timestamp { + if x != nil { + return x.AttachmentChangeTime + } + return nil +} + var File_openshell_proto protoreflect.FileDescriptor const file_openshell_proto_rawDesc = "" + @@ -16806,7 +17204,7 @@ const file_openshell_proto_rawDesc = "" + "\tmax_burst\x18\x02 \x01(\rR\bmaxBurst\"b\n" + "!SandboxWorkloadTemplateProvenance\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12)\n" + - "\x10resource_version\x18\x02 \x01(\tR\x0fresourceVersion\"\xe5\x03\n" + + "\x10resource_version\x18\x02 \x01(\tR\x0fresourceVersion\"\xec\x05\n" + "\rSandboxStatus\x12!\n" + "\fsandbox_name\x18\x01 \x01(\tR\vsandboxName\x12\x1b\n" + "\tagent_pod\x18\x02 \x01(\tR\bagentPod\x12\x19\n" + @@ -16821,9 +17219,13 @@ const file_openshell_proto_rawDesc = "" + "\x18main_process_instance_id\x18\b \x01(\tR\x15mainProcessInstanceId\x12 \n" + "\texit_code\x18\t \x01(\x05H\x00R\bexitCode\x88\x01\x01\x12I\n" + "\x11endpoint_statuses\x18\n" + - " \x03(\v2\x1c.openshell.v1.EndpointStatusR\x10endpointStatusesB\f\n" + + " \x03(\v2\x1c.openshell.v1.EndpointStatusR\x10endpointStatuses\x12d\n" + + "\x17configuration_admission\x18\v \x01(\v2+.openshell.v1.SandboxConfigurationAdmissionR\x16configurationAdmission\x12<\n" + + "\x17configuration_activated\x18\f \x01(\bH\x01R\x16configurationActivated\x88\x01\x01\x12E\n" + + "\fprovisioning\x18\r \x01(\v2!.openshell.v1.SandboxProvisioningR\fprovisioningB\f\n" + "\n" + - "_exit_code\"\xd1\x01\n" + + "_exit_codeB\x1a\n" + + "\x18_configuration_activated\"\xd1\x01\n" + "\x10SandboxCondition\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x16\n" + "\x06status\x18\x02 \x01(\tR\x06status\x12\x16\n" + @@ -17566,7 +17968,23 @@ const file_openshell_proto_rawDesc = "" + "\x06status\x18\x03 \x01(\x0e2\x1a.openshell.v1.PolicyStatusR\x06status\x12\x1d\n" + "\n" + "load_error\x18\x04 \x01(\tR\tloadError\"\x1c\n" + - "\x1aReportPolicyStatusResponse\"\x9b\x04\n" + + "\x1aReportPolicyStatusResponse\"\xbc\x02\n" + + "\x1dSandboxConfigurationAdmission\x12\x1f\n" + + "\vinstance_id\x18\x01 \x01(\tR\n" + + "instanceId\x12?\n" + + "\x05state\x18\x02 \x01(\x0e2).openshell.v1.ConfigurationAdmissionStateR\x05state\x12%\n" + + "\x0epolicy_version\x18\x03 \x01(\rR\rpolicyVersion\x12\x1f\n" + + "\vpolicy_hash\x18\x04 \x01(\tR\n" + + "policyHash\x12'\n" + + "\x0fconfig_revision\x18\x05 \x01(\x04R\x0econfigRevision\x122\n" + + "\x15provider_env_revision\x18\x06 \x01(\x04R\x13providerEnvRevision\x12\x14\n" + + "\x05error\x18\a \x01(\tR\x05error\"\xbf\x01\n" + + "!ReportSandboxConfigurationRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12I\n" + + "\tadmission\x18\x02 \x01(\v2+.openshell.v1.SandboxConfigurationAdmissionR\tadmission\x120\n" + + "\x14expected_instance_id\x18\x03 \x01(\tR\x12expectedInstanceId\"$\n" + + "\"ReportSandboxConfigurationResponse\"\x9b\x04\n" + "\x15SandboxPolicyRevision\x12\x18\n" + "\aversion\x18\x01 \x01(\rR\aversion\x12\x1f\n" + "\vpolicy_hash\x18\x02 \x01(\tR\n" + @@ -17925,7 +18343,21 @@ const file_openshell_proto_rawDesc = "" + "\x04path\x18\x04 \x01(\tR\x04path\x12=\n" + "\vlast_result\x18\x05 \x01(\x0e2\x1c.openshell.v1.EndpointResultR\n" + "lastResult\x12H\n" + - "\x12last_reported_time\x18j \x01(\v2\x1a.google.protobuf.TimestampR\x10lastReportedTimeJ\x04\b\x06\x10\aR\x10last_reported_at*\xa6\x02\n" + + "\x12last_reported_time\x18j \x01(\v2\x1a.google.protobuf.TimestampR\x10lastReportedTimeJ\x04\b\x06\x10\aR\x10last_reported_at\"\xce\x05\n" + + "\x13SandboxProvisioning\x12\x1d\n" + + "\n" + + "attempt_id\x18\x01 \x01(\tR\tattemptId\x126\n" + + "\x17configuration_change_id\x18\x02 \x01(\tR\x15configurationChangeId\x12V\n" + + "\x19configuration_change_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x17configurationChangeTime\x12L\n" + + "\x14first_rejection_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x12firstRejectionTime\x126\n" + + "\bdeadline\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\bdeadline\x12=\n" + + "\ftimeout_time\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\vtimeoutTime\x12P\n" + + "\x16cleanup_completed_time\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\x14cleanupCompletedTime\x12#\n" + + "\rcleanup_error\x18\b \x01(\tR\fcleanupError\x12H\n" + + "\x12cleanup_retry_time\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\x10cleanupRetryTime\x120\n" + + "\x14attachment_change_id\x18\n" + + " \x01(\tR\x12attachmentChangeId\x12P\n" + + "\x16attachment_change_time\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\x14attachmentChangeTime*\xa6\x02\n" + "\fSandboxPhase\x12\x1d\n" + "\x19SANDBOX_PHASE_UNSPECIFIED\x10\x00\x12\x1e\n" + "\x1aSANDBOX_PHASE_PROVISIONING\x10\x01\x12\x17\n" + @@ -18012,7 +18444,12 @@ const file_openshell_proto_rawDesc = "" + "(PROVIDER_PROFILE_CATEGORY_SOURCE_CONTROL\x10\x04\x12'\n" + "#PROVIDER_PROFILE_CATEGORY_MESSAGING\x10\x05\x12\"\n" + "\x1ePROVIDER_PROFILE_CATEGORY_DATA\x10\x06\x12'\n" + - "#PROVIDER_PROFILE_CATEGORY_KNOWLEDGE\x10\a*\x9a\x01\n" + + "#PROVIDER_PROFILE_CATEGORY_KNOWLEDGE\x10\a*\xcf\x01\n" + + "\x1bConfigurationAdmissionState\x12-\n" + + ")CONFIGURATION_ADMISSION_STATE_UNSPECIFIED\x10\x00\x12)\n" + + "%CONFIGURATION_ADMISSION_STATE_PENDING\x10\x01\x12*\n" + + "&CONFIGURATION_ADMISSION_STATE_ACCEPTED\x10\x02\x12*\n" + + "&CONFIGURATION_ADMISSION_STATE_REJECTED\x10\x03*\x9a\x01\n" + "\fPolicyStatus\x12\x1d\n" + "\x19POLICY_STATUS_UNSPECIFIED\x10\x00\x12\x19\n" + "\x15POLICY_STATUS_PENDING\x10\x01\x12\x18\n" + @@ -18047,7 +18484,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\xb1P\n" + + "!ENDPOINT_RESULT_UPSTREAM_REJECTED\x10\a2\xc2Q\n" + "\tOpenShell\x12Z\n" + "\x06Health\x12\x1b.openshell.v1.HealthRequest\x1a\x1c.openshell.v1.HealthResponse\"\x15\x82\xb5\x18\x11\n" + "\x0funauthenticated\x12i\n" + @@ -18151,6 +18588,8 @@ const file_openshell_proto_rawDesc = "" + "\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\x8e\x01\n" + + "\x1aReportSandboxConfiguration\x12/.openshell.v1.ReportSandboxConfigurationRequest\x1a0.openshell.v1.ReportSandboxConfigurationResponse\"\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" + @@ -18219,8 +18658,8 @@ func file_openshell_proto_rawDescGZIP() []byte { return file_openshell_proto_rawDescData } -var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 16) -var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 244) +var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 17) +var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 248) var file_openshell_proto_goTypes = []any{ (SandboxPhase)(0), // 0: openshell.v1.SandboxPhase (ProviderMutationKind)(0), // 1: openshell.v1.ProviderMutationKind @@ -18232,722 +18671,740 @@ var file_openshell_proto_goTypes = []any{ (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 - (DeletionOutcome)(0), // 14: openshell.v1.DeletionOutcome - (EndpointResult)(0), // 15: openshell.v1.EndpointResult - (*IssueSandboxTokenRequest)(nil), // 16: openshell.v1.IssueSandboxTokenRequest - (*IssueSandboxTokenResponse)(nil), // 17: openshell.v1.IssueSandboxTokenResponse - (*RefreshSandboxTokenRequest)(nil), // 18: openshell.v1.RefreshSandboxTokenRequest - (*RefreshSandboxTokenResponse)(nil), // 19: openshell.v1.RefreshSandboxTokenResponse - (*HealthRequest)(nil), // 20: openshell.v1.HealthRequest - (*HealthResponse)(nil), // 21: openshell.v1.HealthResponse - (*GetCurrentUserRequest)(nil), // 22: openshell.v1.GetCurrentUserRequest - (*GetCurrentUserResponse)(nil), // 23: openshell.v1.GetCurrentUserResponse - (*GetGatewayInfoRequest)(nil), // 24: openshell.v1.GetGatewayInfoRequest - (*GetGatewayInfoResponse)(nil), // 25: openshell.v1.GetGatewayInfoResponse - (*ComputeDriverInfo)(nil), // 26: openshell.v1.ComputeDriverInfo - (*ComputeDriverCapabilities)(nil), // 27: openshell.v1.ComputeDriverCapabilities - (*ResourceCapabilities)(nil), // 28: openshell.v1.ResourceCapabilities - (*CpuResourceCapabilities)(nil), // 29: openshell.v1.CpuResourceCapabilities - (*MemoryResourceCapabilities)(nil), // 30: openshell.v1.MemoryResourceCapabilities - (*GpuResourceCapabilities)(nil), // 31: openshell.v1.GpuResourceCapabilities - (*Sandbox)(nil), // 32: openshell.v1.Sandbox - (*SandboxSpec)(nil), // 33: openshell.v1.SandboxSpec - (*ResourceRequirements)(nil), // 34: openshell.v1.ResourceRequirements - (*GpuResourceRequirements)(nil), // 35: openshell.v1.GpuResourceRequirements - (*SandboxTemplate)(nil), // 36: openshell.v1.SandboxTemplate - (*SandboxWorkloadTemplate)(nil), // 37: openshell.v1.SandboxWorkloadTemplate - (*SandboxWorkloadTemplateSpec)(nil), // 38: openshell.v1.SandboxWorkloadTemplateSpec - (*SandboxWorkloadConfig)(nil), // 39: openshell.v1.SandboxWorkloadConfig - (*SandboxResources)(nil), // 40: openshell.v1.SandboxResources - (*SandboxServiceLevel)(nil), // 41: openshell.v1.SandboxServiceLevel - (*SandboxStartup)(nil), // 42: openshell.v1.SandboxStartup - (*SandboxWorkloadTemplateProvenance)(nil), // 43: openshell.v1.SandboxWorkloadTemplateProvenance - (*SandboxStatus)(nil), // 44: openshell.v1.SandboxStatus - (*SandboxCondition)(nil), // 45: openshell.v1.SandboxCondition - (*PlatformEvent)(nil), // 46: openshell.v1.PlatformEvent - (*CreateSandboxRequest)(nil), // 47: openshell.v1.CreateSandboxRequest - (*CreateSandboxTemplateRequest)(nil), // 48: openshell.v1.CreateSandboxTemplateRequest - (*GetSandboxTemplateRequest)(nil), // 49: openshell.v1.GetSandboxTemplateRequest - (*ListSandboxTemplatesRequest)(nil), // 50: openshell.v1.ListSandboxTemplatesRequest - (*DeleteSandboxTemplateRequest)(nil), // 51: openshell.v1.DeleteSandboxTemplateRequest - (*SandboxTemplateResponse)(nil), // 52: openshell.v1.SandboxTemplateResponse - (*ListSandboxTemplatesResponse)(nil), // 53: openshell.v1.ListSandboxTemplatesResponse - (*DeleteSandboxTemplateResponse)(nil), // 54: openshell.v1.DeleteSandboxTemplateResponse - (*BeginRootfsTarStagingRequest)(nil), // 55: openshell.v1.BeginRootfsTarStagingRequest - (*BeginRootfsTarStagingResponse)(nil), // 56: openshell.v1.BeginRootfsTarStagingResponse - (*GetSandboxRequest)(nil), // 57: openshell.v1.GetSandboxRequest - (*ListSandboxesRequest)(nil), // 58: openshell.v1.ListSandboxesRequest - (*ListSandboxProvidersRequest)(nil), // 59: openshell.v1.ListSandboxProvidersRequest - (*AttachSandboxProviderRequest)(nil), // 60: openshell.v1.AttachSandboxProviderRequest - (*DetachSandboxProviderRequest)(nil), // 61: openshell.v1.DetachSandboxProviderRequest - (*DeleteSandboxRequest)(nil), // 62: openshell.v1.DeleteSandboxRequest - (*StopSandboxRequest)(nil), // 63: openshell.v1.StopSandboxRequest - (*StartSandboxRequest)(nil), // 64: openshell.v1.StartSandboxRequest - (*SandboxResponse)(nil), // 65: openshell.v1.SandboxResponse - (*ListSandboxesResponse)(nil), // 66: openshell.v1.ListSandboxesResponse - (*ListSandboxProvidersResponse)(nil), // 67: openshell.v1.ListSandboxProvidersResponse - (*AttachSandboxProviderResponse)(nil), // 68: openshell.v1.AttachSandboxProviderResponse - (*DetachSandboxProviderResponse)(nil), // 69: openshell.v1.DetachSandboxProviderResponse - (*ProviderDesiredIdentity)(nil), // 70: openshell.v1.ProviderDesiredIdentity - (*ConfigSnapshotRevision)(nil), // 71: openshell.v1.ConfigSnapshotRevision - (*SandboxConfigRevision)(nil), // 72: openshell.v1.SandboxConfigRevision - (*ConfigUpdateOperation)(nil), // 73: openshell.v1.ConfigUpdateOperation - (*ProviderMutationReceipt)(nil), // 74: openshell.v1.ProviderMutationReceipt - (*ProviderReadinessObservation)(nil), // 75: openshell.v1.ProviderReadinessObservation - (*ProviderReadinessStatus)(nil), // 76: openshell.v1.ProviderReadinessStatus - (*GetSandboxProviderStatusRequest)(nil), // 77: openshell.v1.GetSandboxProviderStatusRequest - (*GetSandboxProviderStatusResponse)(nil), // 78: openshell.v1.GetSandboxProviderStatusResponse - (*ReportProviderReadinessRequest)(nil), // 79: openshell.v1.ReportProviderReadinessRequest - (*ReportProviderReadinessResponse)(nil), // 80: openshell.v1.ReportProviderReadinessResponse - (*DeleteSandboxResponse)(nil), // 81: openshell.v1.DeleteSandboxResponse - (*CreateSshSessionRequest)(nil), // 82: openshell.v1.CreateSshSessionRequest - (*CreateSshSessionResponse)(nil), // 83: openshell.v1.CreateSshSessionResponse - (*ExposeServiceRequest)(nil), // 84: openshell.v1.ExposeServiceRequest - (*GetServiceRequest)(nil), // 85: openshell.v1.GetServiceRequest - (*ListServicesRequest)(nil), // 86: openshell.v1.ListServicesRequest - (*ListServicesResponse)(nil), // 87: openshell.v1.ListServicesResponse - (*DeleteServiceRequest)(nil), // 88: openshell.v1.DeleteServiceRequest - (*DeleteServiceResponse)(nil), // 89: openshell.v1.DeleteServiceResponse - (*ServiceEndpoint)(nil), // 90: openshell.v1.ServiceEndpoint - (*ServiceEndpointResponse)(nil), // 91: openshell.v1.ServiceEndpointResponse - (*RevokeSshSessionRequest)(nil), // 92: openshell.v1.RevokeSshSessionRequest - (*RevokeSshSessionResponse)(nil), // 93: openshell.v1.RevokeSshSessionResponse - (*ExecSandboxRequest)(nil), // 94: openshell.v1.ExecSandboxRequest - (*ExecSandboxStdout)(nil), // 95: openshell.v1.ExecSandboxStdout - (*ExecSandboxStderr)(nil), // 96: openshell.v1.ExecSandboxStderr - (*ExecSandboxExit)(nil), // 97: openshell.v1.ExecSandboxExit - (*ExecSandboxEvent)(nil), // 98: openshell.v1.ExecSandboxEvent - (*TcpForwardInit)(nil), // 99: openshell.v1.TcpForwardInit - (*TcpForwardFrame)(nil), // 100: openshell.v1.TcpForwardFrame - (*ExecSandboxInput)(nil), // 101: openshell.v1.ExecSandboxInput - (*ExecSandboxWindowResize)(nil), // 102: openshell.v1.ExecSandboxWindowResize - (*SshSession)(nil), // 103: openshell.v1.SshSession - (*WatchSandboxRequest)(nil), // 104: openshell.v1.WatchSandboxRequest - (*SandboxStreamEvent)(nil), // 105: openshell.v1.SandboxStreamEvent - (*SandboxLogLine)(nil), // 106: openshell.v1.SandboxLogLine - (*SandboxStreamWarning)(nil), // 107: openshell.v1.SandboxStreamWarning - (*CreateProviderRequest)(nil), // 108: openshell.v1.CreateProviderRequest - (*GetProviderRequest)(nil), // 109: openshell.v1.GetProviderRequest - (*ListProvidersRequest)(nil), // 110: openshell.v1.ListProvidersRequest - (*UpdateProviderRequest)(nil), // 111: openshell.v1.UpdateProviderRequest - (*DeleteProviderRequest)(nil), // 112: openshell.v1.DeleteProviderRequest - (*ProviderResponse)(nil), // 113: openshell.v1.ProviderResponse - (*ListProvidersResponse)(nil), // 114: openshell.v1.ListProvidersResponse - (*ListProviderProfilesRequest)(nil), // 115: openshell.v1.ListProviderProfilesRequest - (*GetProviderProfileRequest)(nil), // 116: openshell.v1.GetProviderProfileRequest - (*ProviderProfileImportItem)(nil), // 117: openshell.v1.ProviderProfileImportItem - (*ProviderProfileDiagnostic)(nil), // 118: openshell.v1.ProviderProfileDiagnostic - (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 119: openshell.v1.ProviderCredentialTokenGrantAudienceOverride - (*ProviderCredentialTokenGrantSubjectToken)(nil), // 120: openshell.v1.ProviderCredentialTokenGrantSubjectToken - (*ProviderCredentialTokenGrant)(nil), // 121: openshell.v1.ProviderCredentialTokenGrant - (*ProviderProfileCredential)(nil), // 122: openshell.v1.ProviderProfileCredential - (*ProviderCredentialRefreshMaterial)(nil), // 123: openshell.v1.ProviderCredentialRefreshMaterial - (*ProviderCredentialRefreshOutput)(nil), // 124: openshell.v1.ProviderCredentialRefreshOutput - (*ProviderCredentialRefresh)(nil), // 125: openshell.v1.ProviderCredentialRefresh - (*ProviderCredentialRefreshStatus)(nil), // 126: openshell.v1.ProviderCredentialRefreshStatus - (*ProviderProfileDiscovery)(nil), // 127: openshell.v1.ProviderProfileDiscovery - (*GetProviderRefreshStatusRequest)(nil), // 128: openshell.v1.GetProviderRefreshStatusRequest - (*GetProviderRefreshStatusResponse)(nil), // 129: openshell.v1.GetProviderRefreshStatusResponse - (*ConfigureProviderRefreshRequest)(nil), // 130: openshell.v1.ConfigureProviderRefreshRequest - (*ConfigureProviderRefreshResponse)(nil), // 131: openshell.v1.ConfigureProviderRefreshResponse - (*RotateProviderCredentialRequest)(nil), // 132: openshell.v1.RotateProviderCredentialRequest - (*RotateProviderCredentialResponse)(nil), // 133: openshell.v1.RotateProviderCredentialResponse - (*DeleteProviderRefreshRequest)(nil), // 134: openshell.v1.DeleteProviderRefreshRequest - (*DeleteProviderRefreshResponse)(nil), // 135: openshell.v1.DeleteProviderRefreshResponse - (*ProviderProfile)(nil), // 136: openshell.v1.ProviderProfile - (*ProviderProfileResponse)(nil), // 137: openshell.v1.ProviderProfileResponse - (*ListProviderProfilesResponse)(nil), // 138: openshell.v1.ListProviderProfilesResponse - (*ImportProviderProfilesRequest)(nil), // 139: openshell.v1.ImportProviderProfilesRequest - (*ImportProviderProfilesResponse)(nil), // 140: openshell.v1.ImportProviderProfilesResponse - (*UpdateProviderProfilesRequest)(nil), // 141: openshell.v1.UpdateProviderProfilesRequest - (*UpdateProviderProfilesResponse)(nil), // 142: openshell.v1.UpdateProviderProfilesResponse - (*LintProviderProfilesRequest)(nil), // 143: openshell.v1.LintProviderProfilesRequest - (*LintProviderProfilesResponse)(nil), // 144: openshell.v1.LintProviderProfilesResponse - (*DeleteProviderResponse)(nil), // 145: openshell.v1.DeleteProviderResponse - (*DeleteProviderProfileRequest)(nil), // 146: openshell.v1.DeleteProviderProfileRequest - (*DeleteProviderProfileResponse)(nil), // 147: openshell.v1.DeleteProviderProfileResponse - (*GetSandboxProviderEnvironmentRequest)(nil), // 148: openshell.v1.GetSandboxProviderEnvironmentRequest - (*StaticCredentialEndpointBinding)(nil), // 149: openshell.v1.StaticCredentialEndpointBinding - (*StaticCredentialBinding)(nil), // 150: openshell.v1.StaticCredentialBinding - (*GetSandboxProviderEnvironmentResponse)(nil), // 151: openshell.v1.GetSandboxProviderEnvironmentResponse - (*ExchangeProviderSubjectTokenRequest)(nil), // 152: openshell.v1.ExchangeProviderSubjectTokenRequest - (*ExchangeProviderSubjectTokenResponse)(nil), // 153: openshell.v1.ExchangeProviderSubjectTokenResponse - (*UpdateConfigRequest)(nil), // 154: openshell.v1.UpdateConfigRequest - (*PolicyMergeOperation)(nil), // 155: openshell.v1.PolicyMergeOperation - (*AddNetworkRule)(nil), // 156: openshell.v1.AddNetworkRule - (*RemoveNetworkEndpoint)(nil), // 157: openshell.v1.RemoveNetworkEndpoint - (*RemoveNetworkRule)(nil), // 158: openshell.v1.RemoveNetworkRule - (*L7RuleTarget)(nil), // 159: openshell.v1.L7RuleTarget - (*AddDenyRules)(nil), // 160: openshell.v1.AddDenyRules - (*AddAllowRules)(nil), // 161: openshell.v1.AddAllowRules - (*RemoveNetworkBinary)(nil), // 162: openshell.v1.RemoveNetworkBinary - (*UpdateConfigResponse)(nil), // 163: openshell.v1.UpdateConfigResponse - (*GetSandboxPolicyStatusRequest)(nil), // 164: openshell.v1.GetSandboxPolicyStatusRequest - (*GetSandboxPolicyStatusResponse)(nil), // 165: openshell.v1.GetSandboxPolicyStatusResponse - (*ListSandboxPoliciesRequest)(nil), // 166: openshell.v1.ListSandboxPoliciesRequest - (*ListSandboxPoliciesResponse)(nil), // 167: openshell.v1.ListSandboxPoliciesResponse - (*ReportPolicyStatusRequest)(nil), // 168: openshell.v1.ReportPolicyStatusRequest - (*ReportPolicyStatusResponse)(nil), // 169: openshell.v1.ReportPolicyStatusResponse - (*SandboxPolicyRevision)(nil), // 170: openshell.v1.SandboxPolicyRevision - (*GetSandboxLogsRequest)(nil), // 171: openshell.v1.GetSandboxLogsRequest - (*PushSandboxLogsRequest)(nil), // 172: openshell.v1.PushSandboxLogsRequest - (*PushSandboxLogsResponse)(nil), // 173: openshell.v1.PushSandboxLogsResponse - (*GetSandboxLogsResponse)(nil), // 174: openshell.v1.GetSandboxLogsResponse - (*SupervisorMessage)(nil), // 175: openshell.v1.SupervisorMessage - (*GatewayMessage)(nil), // 176: openshell.v1.GatewayMessage - (*SupervisorHello)(nil), // 177: openshell.v1.SupervisorHello - (*SessionAccepted)(nil), // 178: openshell.v1.SessionAccepted - (*SessionRejected)(nil), // 179: openshell.v1.SessionRejected - (*SupervisorHeartbeat)(nil), // 180: openshell.v1.SupervisorHeartbeat - (*GatewayHeartbeat)(nil), // 181: openshell.v1.GatewayHeartbeat - (*ReportMainProcessExitRequest)(nil), // 182: openshell.v1.ReportMainProcessExitRequest - (*ReportMainProcessExitResponse)(nil), // 183: openshell.v1.ReportMainProcessExitResponse - (*FinalizeMainProcessExitRequest)(nil), // 184: openshell.v1.FinalizeMainProcessExitRequest - (*FinalizeMainProcessExitResponse)(nil), // 185: openshell.v1.FinalizeMainProcessExitResponse - (*RelayOpen)(nil), // 186: openshell.v1.RelayOpen - (*SshRelayTarget)(nil), // 187: openshell.v1.SshRelayTarget - (*TcpRelayTarget)(nil), // 188: openshell.v1.TcpRelayTarget - (*RelayInit)(nil), // 189: openshell.v1.RelayInit - (*RelayFrame)(nil), // 190: openshell.v1.RelayFrame - (*RelayOpenResult)(nil), // 191: openshell.v1.RelayOpenResult - (*RelayClose)(nil), // 192: openshell.v1.RelayClose - (*L7RequestSample)(nil), // 193: openshell.v1.L7RequestSample - (*DenialSummary)(nil), // 194: openshell.v1.DenialSummary - (*DenialGroupCount)(nil), // 195: openshell.v1.DenialGroupCount - (*NetworkActivitySummary)(nil), // 196: openshell.v1.NetworkActivitySummary - (*PolicyChunk)(nil), // 197: openshell.v1.PolicyChunk - (*DraftPolicyUpdate)(nil), // 198: openshell.v1.DraftPolicyUpdate - (*SubmitPolicyAnalysisRequest)(nil), // 199: openshell.v1.SubmitPolicyAnalysisRequest - (*SubmitPolicyAnalysisResponse)(nil), // 200: openshell.v1.SubmitPolicyAnalysisResponse - (*GetDraftPolicyRequest)(nil), // 201: openshell.v1.GetDraftPolicyRequest - (*GetDraftPolicyResponse)(nil), // 202: openshell.v1.GetDraftPolicyResponse - (*ApproveDraftChunkRequest)(nil), // 203: openshell.v1.ApproveDraftChunkRequest - (*ApproveDraftChunkResponse)(nil), // 204: openshell.v1.ApproveDraftChunkResponse - (*RejectDraftChunkRequest)(nil), // 205: openshell.v1.RejectDraftChunkRequest - (*RejectDraftChunkResponse)(nil), // 206: openshell.v1.RejectDraftChunkResponse - (*DraftChunkApproval)(nil), // 207: openshell.v1.DraftChunkApproval - (*ApproveAllDraftChunksRequest)(nil), // 208: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 209: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 210: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 211: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 212: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 213: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 214: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 215: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 216: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 217: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 218: openshell.v1.GetDraftHistoryResponse - (*CreateWorkspaceRequest)(nil), // 219: openshell.v1.CreateWorkspaceRequest - (*CreateWorkspaceResponse)(nil), // 220: openshell.v1.CreateWorkspaceResponse - (*GetWorkspaceRequest)(nil), // 221: openshell.v1.GetWorkspaceRequest - (*GetWorkspaceResponse)(nil), // 222: openshell.v1.GetWorkspaceResponse - (*ListWorkspacesRequest)(nil), // 223: openshell.v1.ListWorkspacesRequest - (*ListWorkspacesResponse)(nil), // 224: openshell.v1.ListWorkspacesResponse - (*DeleteWorkspaceRequest)(nil), // 225: openshell.v1.DeleteWorkspaceRequest - (*DeleteWorkspaceResponse)(nil), // 226: openshell.v1.DeleteWorkspaceResponse - (*WorkspaceMember)(nil), // 227: openshell.v1.WorkspaceMember - (*AddWorkspaceMemberRequest)(nil), // 228: openshell.v1.AddWorkspaceMemberRequest - (*AddWorkspaceMemberResponse)(nil), // 229: openshell.v1.AddWorkspaceMemberResponse - (*RemoveWorkspaceMemberRequest)(nil), // 230: openshell.v1.RemoveWorkspaceMemberRequest - (*RemoveWorkspaceMemberResponse)(nil), // 231: openshell.v1.RemoveWorkspaceMemberResponse - (*ListWorkspaceMembersRequest)(nil), // 232: openshell.v1.ListWorkspaceMembersRequest - (*ListWorkspaceMembersResponse)(nil), // 233: openshell.v1.ListWorkspaceMembersResponse - (*ExtensionServiceCredential)(nil), // 234: openshell.v1.ExtensionServiceCredential - (*EndpointObservation)(nil), // 235: openshell.v1.EndpointObservation - (*ReportEndpointStatusRequest)(nil), // 236: openshell.v1.ReportEndpointStatusRequest - (*ReportEndpointStatusResponse)(nil), // 237: openshell.v1.ReportEndpointStatusResponse - (*EndpointStatus)(nil), // 238: openshell.v1.EndpointStatus - nil, // 239: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 240: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 241: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 242: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 243: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry - nil, // 244: openshell.v1.PlatformEvent.MetadataEntry - nil, // 245: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 246: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 247: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 248: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 249: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry - nil, // 250: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 251: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 252: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 253: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry - nil, // 254: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 255: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 256: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 257: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 258: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 259: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*timestamppb.Timestamp)(nil), // 260: google.protobuf.Timestamp - (*datamodelv1.ObjectMeta)(nil), // 261: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 262: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 263: google.protobuf.Struct - (*durationpb.Duration)(nil), // 264: google.protobuf.Duration - (*datamodelv1.WorkspaceSelector)(nil), // 265: openshell.datamodel.v1.WorkspaceSelector - (*datamodelv1.Provider)(nil), // 266: openshell.datamodel.v1.Provider - (sandboxv1.PolicySource)(0), // 267: openshell.sandbox.v1.PolicySource - (*sandboxv1.NetworkEndpoint)(nil), // 268: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 269: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 270: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 271: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 272: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 273: openshell.sandbox.v1.L7Rule - (*datamodelv1.Workspace)(nil), // 274: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 275: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 276: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 277: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 278: openshell.sandbox.v1.GetGatewayConfigResponse + (ConfigurationAdmissionState)(0), // 10: openshell.v1.ConfigurationAdmissionState + (PolicyStatus)(0), // 11: openshell.v1.PolicyStatus + (ServiceStatus)(0), // 12: openshell.v1.ServiceStatus + (WorkspaceRole)(0), // 13: openshell.v1.WorkspaceRole + (ProviderCredentialRefreshRecoveryAction)(0), // 14: openshell.v1.ProviderCredentialRefreshRecoveryAction + (DeletionOutcome)(0), // 15: openshell.v1.DeletionOutcome + (EndpointResult)(0), // 16: openshell.v1.EndpointResult + (*IssueSandboxTokenRequest)(nil), // 17: openshell.v1.IssueSandboxTokenRequest + (*IssueSandboxTokenResponse)(nil), // 18: openshell.v1.IssueSandboxTokenResponse + (*RefreshSandboxTokenRequest)(nil), // 19: openshell.v1.RefreshSandboxTokenRequest + (*RefreshSandboxTokenResponse)(nil), // 20: openshell.v1.RefreshSandboxTokenResponse + (*HealthRequest)(nil), // 21: openshell.v1.HealthRequest + (*HealthResponse)(nil), // 22: openshell.v1.HealthResponse + (*GetCurrentUserRequest)(nil), // 23: openshell.v1.GetCurrentUserRequest + (*GetCurrentUserResponse)(nil), // 24: openshell.v1.GetCurrentUserResponse + (*GetGatewayInfoRequest)(nil), // 25: openshell.v1.GetGatewayInfoRequest + (*GetGatewayInfoResponse)(nil), // 26: openshell.v1.GetGatewayInfoResponse + (*ComputeDriverInfo)(nil), // 27: openshell.v1.ComputeDriverInfo + (*ComputeDriverCapabilities)(nil), // 28: openshell.v1.ComputeDriverCapabilities + (*ResourceCapabilities)(nil), // 29: openshell.v1.ResourceCapabilities + (*CpuResourceCapabilities)(nil), // 30: openshell.v1.CpuResourceCapabilities + (*MemoryResourceCapabilities)(nil), // 31: openshell.v1.MemoryResourceCapabilities + (*GpuResourceCapabilities)(nil), // 32: openshell.v1.GpuResourceCapabilities + (*Sandbox)(nil), // 33: openshell.v1.Sandbox + (*SandboxSpec)(nil), // 34: openshell.v1.SandboxSpec + (*ResourceRequirements)(nil), // 35: openshell.v1.ResourceRequirements + (*GpuResourceRequirements)(nil), // 36: openshell.v1.GpuResourceRequirements + (*SandboxTemplate)(nil), // 37: openshell.v1.SandboxTemplate + (*SandboxWorkloadTemplate)(nil), // 38: openshell.v1.SandboxWorkloadTemplate + (*SandboxWorkloadTemplateSpec)(nil), // 39: openshell.v1.SandboxWorkloadTemplateSpec + (*SandboxWorkloadConfig)(nil), // 40: openshell.v1.SandboxWorkloadConfig + (*SandboxResources)(nil), // 41: openshell.v1.SandboxResources + (*SandboxServiceLevel)(nil), // 42: openshell.v1.SandboxServiceLevel + (*SandboxStartup)(nil), // 43: openshell.v1.SandboxStartup + (*SandboxWorkloadTemplateProvenance)(nil), // 44: openshell.v1.SandboxWorkloadTemplateProvenance + (*SandboxStatus)(nil), // 45: openshell.v1.SandboxStatus + (*SandboxCondition)(nil), // 46: openshell.v1.SandboxCondition + (*PlatformEvent)(nil), // 47: openshell.v1.PlatformEvent + (*CreateSandboxRequest)(nil), // 48: openshell.v1.CreateSandboxRequest + (*CreateSandboxTemplateRequest)(nil), // 49: openshell.v1.CreateSandboxTemplateRequest + (*GetSandboxTemplateRequest)(nil), // 50: openshell.v1.GetSandboxTemplateRequest + (*ListSandboxTemplatesRequest)(nil), // 51: openshell.v1.ListSandboxTemplatesRequest + (*DeleteSandboxTemplateRequest)(nil), // 52: openshell.v1.DeleteSandboxTemplateRequest + (*SandboxTemplateResponse)(nil), // 53: openshell.v1.SandboxTemplateResponse + (*ListSandboxTemplatesResponse)(nil), // 54: openshell.v1.ListSandboxTemplatesResponse + (*DeleteSandboxTemplateResponse)(nil), // 55: openshell.v1.DeleteSandboxTemplateResponse + (*BeginRootfsTarStagingRequest)(nil), // 56: openshell.v1.BeginRootfsTarStagingRequest + (*BeginRootfsTarStagingResponse)(nil), // 57: openshell.v1.BeginRootfsTarStagingResponse + (*GetSandboxRequest)(nil), // 58: openshell.v1.GetSandboxRequest + (*ListSandboxesRequest)(nil), // 59: openshell.v1.ListSandboxesRequest + (*ListSandboxProvidersRequest)(nil), // 60: openshell.v1.ListSandboxProvidersRequest + (*AttachSandboxProviderRequest)(nil), // 61: openshell.v1.AttachSandboxProviderRequest + (*DetachSandboxProviderRequest)(nil), // 62: openshell.v1.DetachSandboxProviderRequest + (*DeleteSandboxRequest)(nil), // 63: openshell.v1.DeleteSandboxRequest + (*StopSandboxRequest)(nil), // 64: openshell.v1.StopSandboxRequest + (*StartSandboxRequest)(nil), // 65: openshell.v1.StartSandboxRequest + (*SandboxResponse)(nil), // 66: openshell.v1.SandboxResponse + (*ListSandboxesResponse)(nil), // 67: openshell.v1.ListSandboxesResponse + (*ListSandboxProvidersResponse)(nil), // 68: openshell.v1.ListSandboxProvidersResponse + (*AttachSandboxProviderResponse)(nil), // 69: openshell.v1.AttachSandboxProviderResponse + (*DetachSandboxProviderResponse)(nil), // 70: openshell.v1.DetachSandboxProviderResponse + (*ProviderDesiredIdentity)(nil), // 71: openshell.v1.ProviderDesiredIdentity + (*ConfigSnapshotRevision)(nil), // 72: openshell.v1.ConfigSnapshotRevision + (*SandboxConfigRevision)(nil), // 73: openshell.v1.SandboxConfigRevision + (*ConfigUpdateOperation)(nil), // 74: openshell.v1.ConfigUpdateOperation + (*ProviderMutationReceipt)(nil), // 75: openshell.v1.ProviderMutationReceipt + (*ProviderReadinessObservation)(nil), // 76: openshell.v1.ProviderReadinessObservation + (*ProviderReadinessStatus)(nil), // 77: openshell.v1.ProviderReadinessStatus + (*GetSandboxProviderStatusRequest)(nil), // 78: openshell.v1.GetSandboxProviderStatusRequest + (*GetSandboxProviderStatusResponse)(nil), // 79: openshell.v1.GetSandboxProviderStatusResponse + (*ReportProviderReadinessRequest)(nil), // 80: openshell.v1.ReportProviderReadinessRequest + (*ReportProviderReadinessResponse)(nil), // 81: openshell.v1.ReportProviderReadinessResponse + (*DeleteSandboxResponse)(nil), // 82: openshell.v1.DeleteSandboxResponse + (*CreateSshSessionRequest)(nil), // 83: openshell.v1.CreateSshSessionRequest + (*CreateSshSessionResponse)(nil), // 84: openshell.v1.CreateSshSessionResponse + (*ExposeServiceRequest)(nil), // 85: openshell.v1.ExposeServiceRequest + (*GetServiceRequest)(nil), // 86: openshell.v1.GetServiceRequest + (*ListServicesRequest)(nil), // 87: openshell.v1.ListServicesRequest + (*ListServicesResponse)(nil), // 88: openshell.v1.ListServicesResponse + (*DeleteServiceRequest)(nil), // 89: openshell.v1.DeleteServiceRequest + (*DeleteServiceResponse)(nil), // 90: openshell.v1.DeleteServiceResponse + (*ServiceEndpoint)(nil), // 91: openshell.v1.ServiceEndpoint + (*ServiceEndpointResponse)(nil), // 92: openshell.v1.ServiceEndpointResponse + (*RevokeSshSessionRequest)(nil), // 93: openshell.v1.RevokeSshSessionRequest + (*RevokeSshSessionResponse)(nil), // 94: openshell.v1.RevokeSshSessionResponse + (*ExecSandboxRequest)(nil), // 95: openshell.v1.ExecSandboxRequest + (*ExecSandboxStdout)(nil), // 96: openshell.v1.ExecSandboxStdout + (*ExecSandboxStderr)(nil), // 97: openshell.v1.ExecSandboxStderr + (*ExecSandboxExit)(nil), // 98: openshell.v1.ExecSandboxExit + (*ExecSandboxEvent)(nil), // 99: openshell.v1.ExecSandboxEvent + (*TcpForwardInit)(nil), // 100: openshell.v1.TcpForwardInit + (*TcpForwardFrame)(nil), // 101: openshell.v1.TcpForwardFrame + (*ExecSandboxInput)(nil), // 102: openshell.v1.ExecSandboxInput + (*ExecSandboxWindowResize)(nil), // 103: openshell.v1.ExecSandboxWindowResize + (*SshSession)(nil), // 104: openshell.v1.SshSession + (*WatchSandboxRequest)(nil), // 105: openshell.v1.WatchSandboxRequest + (*SandboxStreamEvent)(nil), // 106: openshell.v1.SandboxStreamEvent + (*SandboxLogLine)(nil), // 107: openshell.v1.SandboxLogLine + (*SandboxStreamWarning)(nil), // 108: openshell.v1.SandboxStreamWarning + (*CreateProviderRequest)(nil), // 109: openshell.v1.CreateProviderRequest + (*GetProviderRequest)(nil), // 110: openshell.v1.GetProviderRequest + (*ListProvidersRequest)(nil), // 111: openshell.v1.ListProvidersRequest + (*UpdateProviderRequest)(nil), // 112: openshell.v1.UpdateProviderRequest + (*DeleteProviderRequest)(nil), // 113: openshell.v1.DeleteProviderRequest + (*ProviderResponse)(nil), // 114: openshell.v1.ProviderResponse + (*ListProvidersResponse)(nil), // 115: openshell.v1.ListProvidersResponse + (*ListProviderProfilesRequest)(nil), // 116: openshell.v1.ListProviderProfilesRequest + (*GetProviderProfileRequest)(nil), // 117: openshell.v1.GetProviderProfileRequest + (*ProviderProfileImportItem)(nil), // 118: openshell.v1.ProviderProfileImportItem + (*ProviderProfileDiagnostic)(nil), // 119: openshell.v1.ProviderProfileDiagnostic + (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 120: openshell.v1.ProviderCredentialTokenGrantAudienceOverride + (*ProviderCredentialTokenGrantSubjectToken)(nil), // 121: openshell.v1.ProviderCredentialTokenGrantSubjectToken + (*ProviderCredentialTokenGrant)(nil), // 122: openshell.v1.ProviderCredentialTokenGrant + (*ProviderProfileCredential)(nil), // 123: openshell.v1.ProviderProfileCredential + (*ProviderCredentialRefreshMaterial)(nil), // 124: openshell.v1.ProviderCredentialRefreshMaterial + (*ProviderCredentialRefreshOutput)(nil), // 125: openshell.v1.ProviderCredentialRefreshOutput + (*ProviderCredentialRefresh)(nil), // 126: openshell.v1.ProviderCredentialRefresh + (*ProviderCredentialRefreshStatus)(nil), // 127: openshell.v1.ProviderCredentialRefreshStatus + (*ProviderProfileDiscovery)(nil), // 128: openshell.v1.ProviderProfileDiscovery + (*GetProviderRefreshStatusRequest)(nil), // 129: openshell.v1.GetProviderRefreshStatusRequest + (*GetProviderRefreshStatusResponse)(nil), // 130: openshell.v1.GetProviderRefreshStatusResponse + (*ConfigureProviderRefreshRequest)(nil), // 131: openshell.v1.ConfigureProviderRefreshRequest + (*ConfigureProviderRefreshResponse)(nil), // 132: openshell.v1.ConfigureProviderRefreshResponse + (*RotateProviderCredentialRequest)(nil), // 133: openshell.v1.RotateProviderCredentialRequest + (*RotateProviderCredentialResponse)(nil), // 134: openshell.v1.RotateProviderCredentialResponse + (*DeleteProviderRefreshRequest)(nil), // 135: openshell.v1.DeleteProviderRefreshRequest + (*DeleteProviderRefreshResponse)(nil), // 136: openshell.v1.DeleteProviderRefreshResponse + (*ProviderProfile)(nil), // 137: openshell.v1.ProviderProfile + (*ProviderProfileResponse)(nil), // 138: openshell.v1.ProviderProfileResponse + (*ListProviderProfilesResponse)(nil), // 139: openshell.v1.ListProviderProfilesResponse + (*ImportProviderProfilesRequest)(nil), // 140: openshell.v1.ImportProviderProfilesRequest + (*ImportProviderProfilesResponse)(nil), // 141: openshell.v1.ImportProviderProfilesResponse + (*UpdateProviderProfilesRequest)(nil), // 142: openshell.v1.UpdateProviderProfilesRequest + (*UpdateProviderProfilesResponse)(nil), // 143: openshell.v1.UpdateProviderProfilesResponse + (*LintProviderProfilesRequest)(nil), // 144: openshell.v1.LintProviderProfilesRequest + (*LintProviderProfilesResponse)(nil), // 145: openshell.v1.LintProviderProfilesResponse + (*DeleteProviderResponse)(nil), // 146: openshell.v1.DeleteProviderResponse + (*DeleteProviderProfileRequest)(nil), // 147: openshell.v1.DeleteProviderProfileRequest + (*DeleteProviderProfileResponse)(nil), // 148: openshell.v1.DeleteProviderProfileResponse + (*GetSandboxProviderEnvironmentRequest)(nil), // 149: openshell.v1.GetSandboxProviderEnvironmentRequest + (*StaticCredentialEndpointBinding)(nil), // 150: openshell.v1.StaticCredentialEndpointBinding + (*StaticCredentialBinding)(nil), // 151: openshell.v1.StaticCredentialBinding + (*GetSandboxProviderEnvironmentResponse)(nil), // 152: openshell.v1.GetSandboxProviderEnvironmentResponse + (*ExchangeProviderSubjectTokenRequest)(nil), // 153: openshell.v1.ExchangeProviderSubjectTokenRequest + (*ExchangeProviderSubjectTokenResponse)(nil), // 154: openshell.v1.ExchangeProviderSubjectTokenResponse + (*UpdateConfigRequest)(nil), // 155: openshell.v1.UpdateConfigRequest + (*PolicyMergeOperation)(nil), // 156: openshell.v1.PolicyMergeOperation + (*AddNetworkRule)(nil), // 157: openshell.v1.AddNetworkRule + (*RemoveNetworkEndpoint)(nil), // 158: openshell.v1.RemoveNetworkEndpoint + (*RemoveNetworkRule)(nil), // 159: openshell.v1.RemoveNetworkRule + (*L7RuleTarget)(nil), // 160: openshell.v1.L7RuleTarget + (*AddDenyRules)(nil), // 161: openshell.v1.AddDenyRules + (*AddAllowRules)(nil), // 162: openshell.v1.AddAllowRules + (*RemoveNetworkBinary)(nil), // 163: openshell.v1.RemoveNetworkBinary + (*UpdateConfigResponse)(nil), // 164: openshell.v1.UpdateConfigResponse + (*GetSandboxPolicyStatusRequest)(nil), // 165: openshell.v1.GetSandboxPolicyStatusRequest + (*GetSandboxPolicyStatusResponse)(nil), // 166: openshell.v1.GetSandboxPolicyStatusResponse + (*ListSandboxPoliciesRequest)(nil), // 167: openshell.v1.ListSandboxPoliciesRequest + (*ListSandboxPoliciesResponse)(nil), // 168: openshell.v1.ListSandboxPoliciesResponse + (*ReportPolicyStatusRequest)(nil), // 169: openshell.v1.ReportPolicyStatusRequest + (*ReportPolicyStatusResponse)(nil), // 170: openshell.v1.ReportPolicyStatusResponse + (*SandboxConfigurationAdmission)(nil), // 171: openshell.v1.SandboxConfigurationAdmission + (*ReportSandboxConfigurationRequest)(nil), // 172: openshell.v1.ReportSandboxConfigurationRequest + (*ReportSandboxConfigurationResponse)(nil), // 173: openshell.v1.ReportSandboxConfigurationResponse + (*SandboxPolicyRevision)(nil), // 174: openshell.v1.SandboxPolicyRevision + (*GetSandboxLogsRequest)(nil), // 175: openshell.v1.GetSandboxLogsRequest + (*PushSandboxLogsRequest)(nil), // 176: openshell.v1.PushSandboxLogsRequest + (*PushSandboxLogsResponse)(nil), // 177: openshell.v1.PushSandboxLogsResponse + (*GetSandboxLogsResponse)(nil), // 178: openshell.v1.GetSandboxLogsResponse + (*SupervisorMessage)(nil), // 179: openshell.v1.SupervisorMessage + (*GatewayMessage)(nil), // 180: openshell.v1.GatewayMessage + (*SupervisorHello)(nil), // 181: openshell.v1.SupervisorHello + (*SessionAccepted)(nil), // 182: openshell.v1.SessionAccepted + (*SessionRejected)(nil), // 183: openshell.v1.SessionRejected + (*SupervisorHeartbeat)(nil), // 184: openshell.v1.SupervisorHeartbeat + (*GatewayHeartbeat)(nil), // 185: openshell.v1.GatewayHeartbeat + (*ReportMainProcessExitRequest)(nil), // 186: openshell.v1.ReportMainProcessExitRequest + (*ReportMainProcessExitResponse)(nil), // 187: openshell.v1.ReportMainProcessExitResponse + (*FinalizeMainProcessExitRequest)(nil), // 188: openshell.v1.FinalizeMainProcessExitRequest + (*FinalizeMainProcessExitResponse)(nil), // 189: openshell.v1.FinalizeMainProcessExitResponse + (*RelayOpen)(nil), // 190: openshell.v1.RelayOpen + (*SshRelayTarget)(nil), // 191: openshell.v1.SshRelayTarget + (*TcpRelayTarget)(nil), // 192: openshell.v1.TcpRelayTarget + (*RelayInit)(nil), // 193: openshell.v1.RelayInit + (*RelayFrame)(nil), // 194: openshell.v1.RelayFrame + (*RelayOpenResult)(nil), // 195: openshell.v1.RelayOpenResult + (*RelayClose)(nil), // 196: openshell.v1.RelayClose + (*L7RequestSample)(nil), // 197: openshell.v1.L7RequestSample + (*DenialSummary)(nil), // 198: openshell.v1.DenialSummary + (*DenialGroupCount)(nil), // 199: openshell.v1.DenialGroupCount + (*NetworkActivitySummary)(nil), // 200: openshell.v1.NetworkActivitySummary + (*PolicyChunk)(nil), // 201: openshell.v1.PolicyChunk + (*DraftPolicyUpdate)(nil), // 202: openshell.v1.DraftPolicyUpdate + (*SubmitPolicyAnalysisRequest)(nil), // 203: openshell.v1.SubmitPolicyAnalysisRequest + (*SubmitPolicyAnalysisResponse)(nil), // 204: openshell.v1.SubmitPolicyAnalysisResponse + (*GetDraftPolicyRequest)(nil), // 205: openshell.v1.GetDraftPolicyRequest + (*GetDraftPolicyResponse)(nil), // 206: openshell.v1.GetDraftPolicyResponse + (*ApproveDraftChunkRequest)(nil), // 207: openshell.v1.ApproveDraftChunkRequest + (*ApproveDraftChunkResponse)(nil), // 208: openshell.v1.ApproveDraftChunkResponse + (*RejectDraftChunkRequest)(nil), // 209: openshell.v1.RejectDraftChunkRequest + (*RejectDraftChunkResponse)(nil), // 210: openshell.v1.RejectDraftChunkResponse + (*DraftChunkApproval)(nil), // 211: openshell.v1.DraftChunkApproval + (*ApproveAllDraftChunksRequest)(nil), // 212: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 213: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 214: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 215: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 216: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 217: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 218: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 219: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 220: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 221: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 222: openshell.v1.GetDraftHistoryResponse + (*CreateWorkspaceRequest)(nil), // 223: openshell.v1.CreateWorkspaceRequest + (*CreateWorkspaceResponse)(nil), // 224: openshell.v1.CreateWorkspaceResponse + (*GetWorkspaceRequest)(nil), // 225: openshell.v1.GetWorkspaceRequest + (*GetWorkspaceResponse)(nil), // 226: openshell.v1.GetWorkspaceResponse + (*ListWorkspacesRequest)(nil), // 227: openshell.v1.ListWorkspacesRequest + (*ListWorkspacesResponse)(nil), // 228: openshell.v1.ListWorkspacesResponse + (*DeleteWorkspaceRequest)(nil), // 229: openshell.v1.DeleteWorkspaceRequest + (*DeleteWorkspaceResponse)(nil), // 230: openshell.v1.DeleteWorkspaceResponse + (*WorkspaceMember)(nil), // 231: openshell.v1.WorkspaceMember + (*AddWorkspaceMemberRequest)(nil), // 232: openshell.v1.AddWorkspaceMemberRequest + (*AddWorkspaceMemberResponse)(nil), // 233: openshell.v1.AddWorkspaceMemberResponse + (*RemoveWorkspaceMemberRequest)(nil), // 234: openshell.v1.RemoveWorkspaceMemberRequest + (*RemoveWorkspaceMemberResponse)(nil), // 235: openshell.v1.RemoveWorkspaceMemberResponse + (*ListWorkspaceMembersRequest)(nil), // 236: openshell.v1.ListWorkspaceMembersRequest + (*ListWorkspaceMembersResponse)(nil), // 237: openshell.v1.ListWorkspaceMembersResponse + (*ExtensionServiceCredential)(nil), // 238: openshell.v1.ExtensionServiceCredential + (*EndpointObservation)(nil), // 239: openshell.v1.EndpointObservation + (*ReportEndpointStatusRequest)(nil), // 240: openshell.v1.ReportEndpointStatusRequest + (*ReportEndpointStatusResponse)(nil), // 241: openshell.v1.ReportEndpointStatusResponse + (*EndpointStatus)(nil), // 242: openshell.v1.EndpointStatus + (*SandboxProvisioning)(nil), // 243: openshell.v1.SandboxProvisioning + nil, // 244: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 245: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 246: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 247: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 248: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + nil, // 249: openshell.v1.PlatformEvent.MetadataEntry + nil, // 250: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 251: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 252: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 253: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 254: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + nil, // 255: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 256: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 257: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 258: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + nil, // 259: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 260: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 261: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 262: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 263: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 264: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*timestamppb.Timestamp)(nil), // 265: google.protobuf.Timestamp + (*datamodelv1.ObjectMeta)(nil), // 266: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 267: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 268: google.protobuf.Struct + (*durationpb.Duration)(nil), // 269: google.protobuf.Duration + (*datamodelv1.WorkspaceSelector)(nil), // 270: openshell.datamodel.v1.WorkspaceSelector + (*datamodelv1.Provider)(nil), // 271: openshell.datamodel.v1.Provider + (sandboxv1.PolicySource)(0), // 272: openshell.sandbox.v1.PolicySource + (*sandboxv1.NetworkEndpoint)(nil), // 273: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 274: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 275: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 276: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 277: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 278: openshell.sandbox.v1.L7Rule + (*datamodelv1.Workspace)(nil), // 279: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 280: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 281: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 282: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 283: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 260, // 0: openshell.v1.IssueSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp - 260, // 1: openshell.v1.RefreshSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp - 234, // 2: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential - 260, // 3: openshell.v1.RefreshSandboxTokenResponse.sandbox_expiration_time:type_name -> google.protobuf.Timestamp - 11, // 4: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus - 11, // 5: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus - 26, // 6: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo - 27, // 7: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities - 28, // 8: openshell.v1.ComputeDriverCapabilities.resource_capabilities:type_name -> openshell.v1.ResourceCapabilities - 29, // 9: openshell.v1.ResourceCapabilities.cpu:type_name -> openshell.v1.CpuResourceCapabilities - 30, // 10: openshell.v1.ResourceCapabilities.memory:type_name -> openshell.v1.MemoryResourceCapabilities - 31, // 11: openshell.v1.ResourceCapabilities.gpu:type_name -> openshell.v1.GpuResourceCapabilities - 261, // 12: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 33, // 13: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec - 44, // 14: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus - 43, // 15: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance - 239, // 16: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry - 36, // 17: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 262, // 18: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 34, // 19: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements - 35, // 20: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 240, // 21: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 241, // 22: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 242, // 23: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 263, // 24: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 263, // 25: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 261, // 26: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 38, // 27: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec - 39, // 28: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig - 263, // 29: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct - 41, // 30: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel - 243, // 31: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry - 40, // 32: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources - 35, // 33: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements - 42, // 34: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup - 264, // 35: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration - 45, // 36: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition + 265, // 0: openshell.v1.IssueSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp + 265, // 1: openshell.v1.RefreshSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp + 238, // 2: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential + 265, // 3: openshell.v1.RefreshSandboxTokenResponse.sandbox_expiration_time:type_name -> google.protobuf.Timestamp + 12, // 4: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus + 12, // 5: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus + 27, // 6: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo + 28, // 7: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities + 29, // 8: openshell.v1.ComputeDriverCapabilities.resource_capabilities:type_name -> openshell.v1.ResourceCapabilities + 30, // 9: openshell.v1.ResourceCapabilities.cpu:type_name -> openshell.v1.CpuResourceCapabilities + 31, // 10: openshell.v1.ResourceCapabilities.memory:type_name -> openshell.v1.MemoryResourceCapabilities + 32, // 11: openshell.v1.ResourceCapabilities.gpu:type_name -> openshell.v1.GpuResourceCapabilities + 266, // 12: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 34, // 13: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec + 45, // 14: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus + 44, // 15: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance + 244, // 16: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 37, // 17: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate + 267, // 18: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 35, // 19: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements + 36, // 20: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements + 245, // 21: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 246, // 22: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 247, // 23: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 268, // 24: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 268, // 25: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 266, // 26: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 39, // 27: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec + 40, // 28: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig + 268, // 29: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct + 42, // 30: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel + 248, // 31: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + 41, // 32: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources + 36, // 33: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements + 43, // 34: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup + 269, // 35: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration + 46, // 36: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition 0, // 37: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 238, // 38: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus - 260, // 39: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp - 260, // 40: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp - 244, // 41: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 33, // 42: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 245, // 43: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 246, // 44: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 265, // 45: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 37, // 46: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 265, // 47: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 48: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 49: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 50: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 37, // 51: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 37, // 52: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate - 14, // 53: openshell.v1.DeleteSandboxTemplateResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 265, // 54: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 260, // 55: openshell.v1.BeginRootfsTarStagingResponse.expiration_time:type_name -> google.protobuf.Timestamp - 265, // 56: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 57: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 58: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 59: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 60: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 61: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 62: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 63: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 32, // 64: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 32, // 65: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 266, // 66: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 32, // 67: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 74, // 68: openshell.v1.AttachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt - 32, // 69: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 74, // 70: openshell.v1.DetachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt - 72, // 71: openshell.v1.ConfigSnapshotRevision.sandbox_config:type_name -> openshell.v1.SandboxConfigRevision - 70, // 72: openshell.v1.ConfigSnapshotRevision.provider_target:type_name -> openshell.v1.ProviderDesiredIdentity - 267, // 73: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource - 4, // 74: openshell.v1.ConfigUpdateOperation.component:type_name -> openshell.v1.ConfigComponent - 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 - 260, // 78: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp - 260, // 79: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp - 260, // 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 - 260, // 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 - 260, // 89: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp - 260, // 90: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp - 73, // 91: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation - 265, // 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 - 264, // 95: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration - 264, // 96: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration - 14, // 97: openshell.v1.DeleteSandboxResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 260, // 98: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp - 265, // 99: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 100: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 101: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 91, // 102: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 265, // 103: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 14, // 104: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 261, // 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 - 247, // 108: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 264, // 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 - 187, // 113: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 188, // 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 - 261, // 118: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 260, // 119: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp - 260, // 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 - 198, // 125: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 260, // 126: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp - 248, // 127: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 266, // 128: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 265, // 129: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 130: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 131: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 266, // 132: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 249, // 133: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry - 265, // 134: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 135: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 266, // 136: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 74, // 137: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt - 266, // 138: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 136, // 139: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 264, // 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 - 264, // 147: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration - 264, // 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 - 260, // 152: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp - 260, // 153: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp - 260, // 154: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp - 13, // 155: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 260, // 156: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp - 265, // 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 - 250, // 160: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 260, // 161: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp - 265, // 162: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 126, // 163: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 265, // 164: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 126, // 165: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 265, // 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 - 268, // 170: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 269, // 171: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 127, // 172: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 251, // 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 - 252, // 187: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 253, // 188: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry - 254, // 189: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 255, // 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 - 264, // 192: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration - 262, // 193: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 270, // 194: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 155, // 195: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 256, // 196: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 265, // 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 - 160, // 201: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 161, // 202: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 162, // 203: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 271, // 204: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 269, // 205: openshell.v1.L7RuleTarget.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 272, // 206: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 159, // 207: openshell.v1.AddDenyRules.target:type_name -> openshell.v1.L7RuleTarget - 273, // 208: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 159, // 209: openshell.v1.AddAllowRules.target:type_name -> openshell.v1.L7RuleTarget - 257, // 210: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 265, // 211: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 170, // 212: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 265, // 213: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 170, // 214: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 10, // 215: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 10, // 216: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 260, // 217: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp - 260, // 218: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp - 262, // 219: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 258, // 220: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 260, // 221: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp - 265, // 222: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 106, // 223: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 106, // 224: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 177, // 225: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 180, // 226: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 191, // 227: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 192, // 228: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 178, // 229: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 179, // 230: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 181, // 231: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 186, // 232: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 192, // 233: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 264, // 234: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration - 187, // 235: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 188, // 236: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 189, // 237: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 260, // 238: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp - 260, // 239: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp - 193, // 240: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 195, // 241: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 271, // 242: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 260, // 243: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp - 260, // 244: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp - 260, // 245: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp - 260, // 246: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp - 262, // 247: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 262, // 248: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 194, // 249: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 197, // 250: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 196, // 251: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 265, // 252: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 197, // 253: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 260, // 254: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp - 265, // 255: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 256: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 207, // 257: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 265, // 258: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 271, // 259: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 265, // 260: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 261: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 262: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 263: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 260, // 264: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp - 217, // 265: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 259, // 266: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 274, // 267: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 274, // 268: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 274, // 269: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 14, // 270: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 261, // 271: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 12, // 272: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 12, // 273: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 227, // 274: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 14, // 275: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 227, // 276: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 260, // 277: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp - 15, // 278: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult - 235, // 279: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation - 15, // 280: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult - 260, // 281: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp - 260, // 282: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 260, // 283: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 122, // 284: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 150, // 285: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 20, // 286: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 22, // 287: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 24, // 288: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 47, // 289: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 55, // 290: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest - 57, // 291: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 58, // 292: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 48, // 293: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest - 49, // 294: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest - 50, // 295: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest - 51, // 296: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest - 59, // 297: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 60, // 298: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 61, // 299: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 77, // 300: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest - 62, // 301: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 63, // 302: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 64, // 303: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 82, // 304: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 84, // 305: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 85, // 306: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 86, // 307: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 88, // 308: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 92, // 309: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 94, // 310: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 100, // 311: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 101, // 312: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 108, // 313: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 109, // 314: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 110, // 315: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 115, // 316: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 116, // 317: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 139, // 318: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 141, // 319: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 143, // 320: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 111, // 321: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 128, // 322: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 130, // 323: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 132, // 324: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 134, // 325: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 112, // 326: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 146, // 327: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 275, // 328: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 276, // 329: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 154, // 330: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 164, // 331: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 166, // 332: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 168, // 333: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 236, // 334: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest - 79, // 335: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest - 148, // 336: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 152, // 337: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 171, // 338: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 172, // 339: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 175, // 340: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 182, // 341: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 184, // 342: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest - 190, // 343: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 104, // 344: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 199, // 345: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 201, // 346: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 203, // 347: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 205, // 348: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 208, // 349: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 210, // 350: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 212, // 351: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 214, // 352: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 216, // 353: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 16, // 354: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 18, // 355: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 219, // 356: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 221, // 357: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 223, // 358: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 225, // 359: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 228, // 360: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 230, // 361: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 232, // 362: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 21, // 363: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 23, // 364: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 25, // 365: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 65, // 366: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 56, // 367: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse - 65, // 368: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 66, // 369: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 52, // 370: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 52, // 371: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 53, // 372: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse - 54, // 373: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse - 67, // 374: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 68, // 375: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 69, // 376: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 78, // 377: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse - 81, // 378: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 65, // 379: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 65, // 380: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 83, // 381: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 91, // 382: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 91, // 383: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 87, // 384: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 89, // 385: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 93, // 386: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 98, // 387: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 100, // 388: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 98, // 389: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 113, // 390: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 113, // 391: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 114, // 392: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 138, // 393: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 137, // 394: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 140, // 395: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 142, // 396: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 144, // 397: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 113, // 398: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 129, // 399: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 131, // 400: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 133, // 401: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 135, // 402: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 145, // 403: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 147, // 404: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 277, // 405: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 278, // 406: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 163, // 407: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 165, // 408: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 167, // 409: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 169, // 410: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 237, // 411: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse - 80, // 412: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse - 151, // 413: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 153, // 414: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 174, // 415: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 173, // 416: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 176, // 417: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 183, // 418: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 185, // 419: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse - 190, // 420: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 105, // 421: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 200, // 422: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 202, // 423: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 204, // 424: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 206, // 425: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 209, // 426: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 211, // 427: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 213, // 428: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 215, // 429: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 218, // 430: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 17, // 431: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 19, // 432: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 220, // 433: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 222, // 434: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 224, // 435: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 226, // 436: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 229, // 437: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 231, // 438: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 233, // 439: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 363, // [363:440] is the sub-list for method output_type - 286, // [286:363] is the sub-list for method input_type - 286, // [286:286] is the sub-list for extension type_name - 286, // [286:286] is the sub-list for extension extendee - 0, // [0:286] is the sub-list for field type_name + 242, // 38: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus + 171, // 39: openshell.v1.SandboxStatus.configuration_admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 243, // 40: openshell.v1.SandboxStatus.provisioning:type_name -> openshell.v1.SandboxProvisioning + 265, // 41: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp + 265, // 42: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp + 249, // 43: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 34, // 44: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec + 250, // 45: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 251, // 46: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 270, // 47: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 38, // 48: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 270, // 49: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 50: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 51: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 52: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 38, // 53: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 38, // 54: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate + 15, // 55: openshell.v1.DeleteSandboxTemplateResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 270, // 56: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 265, // 57: openshell.v1.BeginRootfsTarStagingResponse.expiration_time:type_name -> google.protobuf.Timestamp + 270, // 58: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 59: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 60: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 61: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 62: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 63: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 64: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 65: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 33, // 66: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 33, // 67: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 271, // 68: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 33, // 69: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 75, // 70: openshell.v1.AttachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 33, // 71: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 75, // 72: openshell.v1.DetachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 73, // 73: openshell.v1.ConfigSnapshotRevision.sandbox_config:type_name -> openshell.v1.SandboxConfigRevision + 71, // 74: openshell.v1.ConfigSnapshotRevision.provider_target:type_name -> openshell.v1.ProviderDesiredIdentity + 272, // 75: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource + 4, // 76: openshell.v1.ConfigUpdateOperation.component:type_name -> openshell.v1.ConfigComponent + 72, // 77: openshell.v1.ConfigUpdateOperation.target_revision:type_name -> openshell.v1.ConfigSnapshotRevision + 6, // 78: openshell.v1.ConfigUpdateOperation.state:type_name -> openshell.v1.ConfigUpdateOperationState + 5, // 79: openshell.v1.ConfigUpdateOperation.outcome:type_name -> openshell.v1.ConfigApplyOutcome + 265, // 80: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp + 265, // 81: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp + 265, // 82: openshell.v1.ConfigUpdateOperation.completed_time:type_name -> google.protobuf.Timestamp + 1, // 83: openshell.v1.ProviderMutationReceipt.kind:type_name -> openshell.v1.ProviderMutationKind + 71, // 84: openshell.v1.ProviderMutationReceipt.desired:type_name -> openshell.v1.ProviderDesiredIdentity + 265, // 85: openshell.v1.ProviderMutationReceipt.persisted_time:type_name -> google.protobuf.Timestamp + 3, // 86: openshell.v1.ProviderReadinessObservation.reason:type_name -> openshell.v1.ProviderReadinessReason + 75, // 87: openshell.v1.ProviderReadinessStatus.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 2, // 88: openshell.v1.ProviderReadinessStatus.state:type_name -> openshell.v1.ProviderReadinessState + 3, // 89: openshell.v1.ProviderReadinessStatus.reason:type_name -> openshell.v1.ProviderReadinessReason + 76, // 90: openshell.v1.ProviderReadinessStatus.observed:type_name -> openshell.v1.ProviderReadinessObservation + 265, // 91: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp + 265, // 92: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp + 74, // 93: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation + 270, // 94: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 77, // 95: openshell.v1.GetSandboxProviderStatusResponse.status:type_name -> openshell.v1.ProviderReadinessStatus + 76, // 96: openshell.v1.ReportProviderReadinessRequest.observation:type_name -> openshell.v1.ProviderReadinessObservation + 269, // 97: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration + 269, // 98: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration + 15, // 99: openshell.v1.DeleteSandboxResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 265, // 100: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp + 270, // 101: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 102: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 103: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 92, // 104: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 270, // 105: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 15, // 106: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 266, // 107: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 91, // 108: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 15, // 109: openshell.v1.RevokeSshSessionResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 252, // 110: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 269, // 111: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration + 96, // 112: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 97, // 113: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 98, // 114: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 191, // 115: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 192, // 116: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 100, // 117: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 95, // 118: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 103, // 119: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 266, // 120: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 265, // 121: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp + 265, // 122: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp + 33, // 123: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 107, // 124: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 47, // 125: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 108, // 126: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 202, // 127: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 265, // 128: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp + 253, // 129: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 271, // 130: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 270, // 131: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 132: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 133: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 271, // 134: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 254, // 135: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + 270, // 136: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 137: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 271, // 138: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 75, // 139: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt + 271, // 140: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 137, // 141: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 269, // 142: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration + 120, // 143: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 7, // 144: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType + 121, // 145: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 126, // 146: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 122, // 147: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 8, // 148: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 269, // 149: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration + 269, // 150: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration + 124, // 151: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 125, // 152: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 8, // 153: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 265, // 154: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp + 265, // 155: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp + 265, // 156: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp + 14, // 157: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 265, // 158: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp + 270, // 159: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 127, // 160: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 8, // 161: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 255, // 162: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 265, // 163: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp + 270, // 164: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 127, // 165: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 270, // 166: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 127, // 167: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 270, // 168: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 15, // 169: openshell.v1.DeleteProviderRefreshResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 9, // 170: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 123, // 171: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 273, // 172: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 274, // 173: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 128, // 174: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 256, // 175: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 137, // 176: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 137, // 177: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 118, // 178: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 119, // 179: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 137, // 180: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 118, // 181: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 119, // 182: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 137, // 183: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 118, // 184: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 119, // 185: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 15, // 186: openshell.v1.DeleteProviderResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 15, // 187: openshell.v1.DeleteProviderProfileResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 150, // 188: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 257, // 189: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 258, // 190: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + 259, // 191: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 260, // 192: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 3, // 193: openshell.v1.GetSandboxProviderEnvironmentResponse.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason + 269, // 194: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration + 267, // 195: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 275, // 196: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 156, // 197: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 261, // 198: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 270, // 199: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 157, // 200: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 158, // 201: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 159, // 202: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 161, // 203: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 162, // 204: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 163, // 205: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 276, // 206: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 274, // 207: openshell.v1.L7RuleTarget.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 277, // 208: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 160, // 209: openshell.v1.AddDenyRules.target:type_name -> openshell.v1.L7RuleTarget + 278, // 210: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 160, // 211: openshell.v1.AddAllowRules.target:type_name -> openshell.v1.L7RuleTarget + 262, // 212: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 270, // 213: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 174, // 214: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 270, // 215: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 174, // 216: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 11, // 217: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 10, // 218: openshell.v1.SandboxConfigurationAdmission.state:type_name -> openshell.v1.ConfigurationAdmissionState + 171, // 219: openshell.v1.ReportSandboxConfigurationRequest.admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 11, // 220: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 265, // 221: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp + 265, // 222: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp + 267, // 223: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 263, // 224: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 265, // 225: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp + 270, // 226: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 107, // 227: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 107, // 228: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 181, // 229: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 184, // 230: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 195, // 231: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 196, // 232: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 182, // 233: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 183, // 234: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 185, // 235: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 190, // 236: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 196, // 237: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 269, // 238: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration + 191, // 239: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 192, // 240: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 193, // 241: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 265, // 242: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp + 265, // 243: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp + 197, // 244: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 199, // 245: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 276, // 246: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 265, // 247: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp + 265, // 248: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp + 265, // 249: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp + 265, // 250: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp + 267, // 251: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 267, // 252: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 198, // 253: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 201, // 254: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 200, // 255: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 270, // 256: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 201, // 257: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 265, // 258: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp + 270, // 259: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 260: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 211, // 261: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 270, // 262: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 276, // 263: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 270, // 264: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 265: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 266: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 270, // 267: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 265, // 268: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp + 221, // 269: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 264, // 270: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 279, // 271: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 279, // 272: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 279, // 273: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 15, // 274: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 266, // 275: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 13, // 276: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 13, // 277: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 231, // 278: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 15, // 279: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 231, // 280: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 265, // 281: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp + 16, // 282: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult + 239, // 283: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation + 16, // 284: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult + 265, // 285: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp + 265, // 286: openshell.v1.SandboxProvisioning.configuration_change_time:type_name -> google.protobuf.Timestamp + 265, // 287: openshell.v1.SandboxProvisioning.first_rejection_time:type_name -> google.protobuf.Timestamp + 265, // 288: openshell.v1.SandboxProvisioning.deadline:type_name -> google.protobuf.Timestamp + 265, // 289: openshell.v1.SandboxProvisioning.timeout_time:type_name -> google.protobuf.Timestamp + 265, // 290: openshell.v1.SandboxProvisioning.cleanup_completed_time:type_name -> google.protobuf.Timestamp + 265, // 291: openshell.v1.SandboxProvisioning.cleanup_retry_time:type_name -> google.protobuf.Timestamp + 265, // 292: openshell.v1.SandboxProvisioning.attachment_change_time:type_name -> google.protobuf.Timestamp + 265, // 293: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 265, // 294: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 123, // 295: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 151, // 296: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 21, // 297: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 23, // 298: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 25, // 299: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 48, // 300: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 56, // 301: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest + 58, // 302: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 59, // 303: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 49, // 304: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest + 50, // 305: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest + 51, // 306: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest + 52, // 307: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest + 60, // 308: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 61, // 309: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 62, // 310: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 78, // 311: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest + 63, // 312: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 64, // 313: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 65, // 314: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 83, // 315: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 85, // 316: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 86, // 317: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 87, // 318: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 89, // 319: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 93, // 320: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 95, // 321: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 101, // 322: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 102, // 323: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 109, // 324: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 110, // 325: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 111, // 326: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 116, // 327: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 117, // 328: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 140, // 329: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 142, // 330: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 144, // 331: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 112, // 332: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 129, // 333: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 131, // 334: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 133, // 335: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 135, // 336: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 113, // 337: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 147, // 338: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 280, // 339: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 281, // 340: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 155, // 341: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 165, // 342: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 167, // 343: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 169, // 344: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 240, // 345: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest + 80, // 346: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest + 172, // 347: openshell.v1.OpenShell.ReportSandboxConfiguration:input_type -> openshell.v1.ReportSandboxConfigurationRequest + 149, // 348: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 153, // 349: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 175, // 350: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 176, // 351: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 179, // 352: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 186, // 353: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 188, // 354: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest + 194, // 355: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 105, // 356: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 203, // 357: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 205, // 358: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 207, // 359: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 209, // 360: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 212, // 361: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 214, // 362: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 216, // 363: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 218, // 364: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 220, // 365: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 17, // 366: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 19, // 367: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 223, // 368: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 225, // 369: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 227, // 370: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 229, // 371: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 232, // 372: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 234, // 373: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 236, // 374: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 22, // 375: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 24, // 376: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 26, // 377: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 66, // 378: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 57, // 379: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse + 66, // 380: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 67, // 381: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 53, // 382: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 53, // 383: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 54, // 384: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse + 55, // 385: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse + 68, // 386: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 69, // 387: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 70, // 388: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 79, // 389: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse + 82, // 390: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 66, // 391: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 66, // 392: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 84, // 393: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 92, // 394: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 92, // 395: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 88, // 396: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 90, // 397: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 94, // 398: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 99, // 399: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 101, // 400: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 99, // 401: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 114, // 402: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 114, // 403: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 115, // 404: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 139, // 405: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 138, // 406: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 141, // 407: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 143, // 408: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 145, // 409: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 114, // 410: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 130, // 411: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 132, // 412: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 134, // 413: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 136, // 414: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 146, // 415: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 148, // 416: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 282, // 417: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 283, // 418: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 164, // 419: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 166, // 420: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 168, // 421: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 170, // 422: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 241, // 423: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse + 81, // 424: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse + 173, // 425: openshell.v1.OpenShell.ReportSandboxConfiguration:output_type -> openshell.v1.ReportSandboxConfigurationResponse + 152, // 426: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 154, // 427: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 178, // 428: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 177, // 429: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 180, // 430: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 187, // 431: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 189, // 432: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse + 194, // 433: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 106, // 434: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 204, // 435: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 206, // 436: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 208, // 437: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 210, // 438: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 213, // 439: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 215, // 440: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 217, // 441: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 219, // 442: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 222, // 443: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 18, // 444: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 20, // 445: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 224, // 446: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 226, // 447: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 228, // 448: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 230, // 449: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 233, // 450: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 235, // 451: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 237, // 452: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 375, // [375:453] is the sub-list for method output_type + 297, // [297:375] is the sub-list for method input_type + 297, // [297:297] is the sub-list for extension type_name + 297, // [297:297] is the sub-list for extension extendee + 0, // [0:297] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -18997,24 +19454,24 @@ func file_openshell_proto_init() { (*PolicyMergeOperation_RemoveBinary)(nil), } file_openshell_proto_msgTypes[143].OneofWrappers = []any{} - file_openshell_proto_msgTypes[159].OneofWrappers = []any{ + file_openshell_proto_msgTypes[162].OneofWrappers = []any{ (*SupervisorMessage_Hello)(nil), (*SupervisorMessage_Heartbeat)(nil), (*SupervisorMessage_RelayOpenResult)(nil), (*SupervisorMessage_RelayClose)(nil), } - file_openshell_proto_msgTypes[160].OneofWrappers = []any{ + file_openshell_proto_msgTypes[163].OneofWrappers = []any{ (*GatewayMessage_SessionAccepted)(nil), (*GatewayMessage_SessionRejected)(nil), (*GatewayMessage_Heartbeat)(nil), (*GatewayMessage_RelayOpen)(nil), (*GatewayMessage_RelayClose)(nil), } - file_openshell_proto_msgTypes[170].OneofWrappers = []any{ + file_openshell_proto_msgTypes[173].OneofWrappers = []any{ (*RelayOpen_Ssh)(nil), (*RelayOpen_Tcp)(nil), } - file_openshell_proto_msgTypes[174].OneofWrappers = []any{ + file_openshell_proto_msgTypes[177].OneofWrappers = []any{ (*RelayFrame_Init)(nil), (*RelayFrame_Data)(nil), } @@ -19023,8 +19480,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: 16, - NumMessages: 244, + NumEnums: 17, + NumMessages: 248, 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 fab991628c..0609450c89 100644 --- a/sdk/go/proto/openshellv1/openshell_grpc.pb.go +++ b/sdk/go/proto/openshellv1/openshell_grpc.pb.go @@ -73,6 +73,7 @@ const ( OpenShell_ReportPolicyStatus_FullMethodName = "/openshell.v1.OpenShell/ReportPolicyStatus" OpenShell_ReportEndpointStatus_FullMethodName = "/openshell.v1.OpenShell/ReportEndpointStatus" OpenShell_ReportProviderReadiness_FullMethodName = "/openshell.v1.OpenShell/ReportProviderReadiness" + OpenShell_ReportSandboxConfiguration_FullMethodName = "/openshell.v1.OpenShell/ReportSandboxConfiguration" OpenShell_GetSandboxProviderEnvironment_FullMethodName = "/openshell.v1.OpenShell/GetSandboxProviderEnvironment" OpenShell_ExchangeProviderSubjectToken_FullMethodName = "/openshell.v1.OpenShell/ExchangeProviderSubjectToken" OpenShell_GetSandboxLogs_FullMethodName = "/openshell.v1.OpenShell/GetSandboxLogs" @@ -231,6 +232,8 @@ type OpenShellClient interface { // 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) + // Register startup and acknowledge an exact validated runtime configuration. + ReportSandboxConfiguration(ctx context.Context, in *ReportSandboxConfigurationRequest, opts ...grpc.CallOption) (*ReportSandboxConfigurationResponse, 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 @@ -841,6 +844,16 @@ func (c *openShellClient) ReportProviderReadiness(ctx context.Context, in *Repor return out, nil } +func (c *openShellClient) ReportSandboxConfiguration(ctx context.Context, in *ReportSandboxConfigurationRequest, opts ...grpc.CallOption) (*ReportSandboxConfigurationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReportSandboxConfigurationResponse) + err := c.cc.Invoke(ctx, OpenShell_ReportSandboxConfiguration_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) @@ -1258,6 +1271,8 @@ type OpenShellServer interface { // Report installed provider state for the current ConnectSupervisor session. // Replacing or losing that session invalidates its observations. ReportProviderReadiness(context.Context, *ReportProviderReadinessRequest) (*ReportProviderReadinessResponse, error) + // Register startup and acknowledge an exact validated runtime configuration. + ReportSandboxConfiguration(context.Context, *ReportSandboxConfigurationRequest) (*ReportSandboxConfigurationResponse, 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 @@ -1503,6 +1518,9 @@ func (UnimplementedOpenShellServer) ReportEndpointStatus(context.Context, *Repor func (UnimplementedOpenShellServer) ReportProviderReadiness(context.Context, *ReportProviderReadinessRequest) (*ReportProviderReadinessResponse, error) { return nil, status.Error(codes.Unimplemented, "method ReportProviderReadiness not implemented") } +func (UnimplementedOpenShellServer) ReportSandboxConfiguration(context.Context, *ReportSandboxConfigurationRequest) (*ReportSandboxConfigurationResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReportSandboxConfiguration not implemented") +} func (UnimplementedOpenShellServer) GetSandboxProviderEnvironment(context.Context, *GetSandboxProviderEnvironmentRequest) (*GetSandboxProviderEnvironmentResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetSandboxProviderEnvironment not implemented") } @@ -2476,6 +2494,24 @@ func _OpenShell_ReportProviderReadiness_Handler(srv interface{}, ctx context.Con return interceptor(ctx, in, info, handler) } +func _OpenShell_ReportSandboxConfiguration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportSandboxConfigurationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ReportSandboxConfiguration(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ReportSandboxConfiguration_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ReportSandboxConfiguration(ctx, req.(*ReportSandboxConfigurationRequest)) + } + 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 { @@ -3117,6 +3153,10 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "ReportProviderReadiness", Handler: _OpenShell_ReportProviderReadiness_Handler, }, + { + MethodName: "ReportSandboxConfiguration", + Handler: _OpenShell_ReportSandboxConfiguration_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 c4a7d5f417..3153c0ecae 100644 --- a/sdk/go/proto/sandboxv1/sandbox.pb.go +++ b/sdk/go/proto/sandboxv1/sandbox.pb.go @@ -1829,6 +1829,13 @@ type GetSandboxConfigResponse struct { // 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"` + // True only after validating this complete policy/provider composition. + // Missing (older gateway) is deliberately not admission. + ConfigurationAdmitted bool `protobuf:"varint,13,opt,name=configuration_admitted,json=configurationAdmitted,proto3" json:"configuration_admitted,omitempty"` + // Bounded, credential-free admission diagnostic. Empty for admitted policy. + ConfigurationError string `protobuf:"bytes,16,opt,name=configuration_error,json=configurationError,proto3" json:"configuration_error,omitempty"` + // Registration fence for a new supervisor; capture once and retain on retry. + ConfigurationInstanceId string `protobuf:"bytes,15,opt,name=configuration_instance_id,json=configurationInstanceId,proto3" json:"configuration_instance_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1954,6 +1961,27 @@ func (x *GetSandboxConfigResponse) GetProviderAttachmentEpoch() string { return "" } +func (x *GetSandboxConfigResponse) GetConfigurationAdmitted() bool { + if x != nil { + return x.ConfigurationAdmitted + } + return false +} + +func (x *GetSandboxConfigResponse) GetConfigurationError() string { + if x != nil { + return x.ConfigurationError + } + return "" +} + +func (x *GetSandboxConfigResponse) GetConfigurationInstanceId() string { + if x != nil { + return x.ConfigurationInstanceId + } + return "" +} + // Connection details for one operator-registered supervisor middleware service. // V1 supports plaintext and server-authenticated TLS gRPC. type SupervisorMiddlewareService struct { @@ -2217,7 +2245,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\"\x8d\a\n" + + "\x05scope\x18\x02 \x01(\x0e2\".openshell.sandbox.v1.SettingScopeR\x05scope\"\xb1\b\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" + @@ -2233,7 +2261,10 @@ const file_sandbox_proto_rawDesc = "" + " \x01(\tR\tworkspace\x12C\n" + "\x1epolicy_validation_failure_mode\x18\v \x01(\tR\x1bpolicyValidationFailureMode\x12H\n" + " extension_authentication_enabled\x18\f \x01(\bR\x1eextensionAuthenticationEnabled\x12:\n" + - "\x19provider_attachment_epoch\x18\x0e \x01(\tR\x17providerAttachmentEpoch\x1ac\n" + + "\x19provider_attachment_epoch\x18\x0e \x01(\tR\x17providerAttachmentEpoch\x125\n" + + "\x16configuration_admitted\x18\r \x01(\bR\x15configurationAdmitted\x12/\n" + + "\x13configuration_error\x18\x10 \x01(\tR\x12configurationError\x12:\n" + + "\x19configuration_instance_id\x18\x0f \x01(\tR\x17configurationInstanceId\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\"\xd2\x02\n" + From 88a50ba9abb6c3689254a6ab81e9ce6d527e1e18 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:39:14 -0700 Subject: [PATCH 21/23] fix(tui): separate configuration summaries from full diagnostics Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- .agents/skills/tui-development/SKILL.md | 2 +- architecture/sandbox.md | 6 ++- crates/openshell-tui/src/app.rs | 3 ++ crates/openshell-tui/src/lib.rs | 52 ++++++++++++------ crates/openshell-tui/src/ui/mod.rs | 53 ++++++++++++++++++- crates/openshell-tui/src/ui/sandbox_detail.rs | 38 ++++++++----- crates/openshell-tui/src/ui/sandbox_draft.rs | 2 +- crates/openshell-tui/src/ui/sandboxes.rs | 4 +- docs/sandboxes/manage-sandboxes.mdx | 2 +- 9 files changed, 122 insertions(+), 40 deletions(-) diff --git a/.agents/skills/tui-development/SKILL.md b/.agents/skills/tui-development/SKILL.md index f3f821a32a..834f7e80a0 100644 --- a/.agents/skills/tui-development/SKILL.md +++ b/.agents/skills/tui-development/SKILL.md @@ -170,7 +170,7 @@ Phase 1: GetSandboxLogs → 500 initial lines → send via Event::LogLines Phase 2: WatchSandbox(follow_logs: true) → live tail → send via Event::LogLines ``` -**Sandboxes**: Fetched via `ListSandboxes` in a background collection-refresh task scheduled from the 2-second tick, scoped to the current workspace (or all workspaces). Follow `next_page_token` until empty so the dashboard reflects the complete collection. The NOTES column puts active `ConfigurationInvalid` readiness diagnostics before port forwards and clears them on refresh after repair. Timed-out provisioning attempts show `Provisioning timed out` with cleanup pending or compute reclaimed, preserving port forwards. The sandbox detail pane uses the same Notes field. +**Sandboxes**: Fetched via `ListSandboxes` in a background collection-refresh task scheduled from the 2-second tick, scoped to the current workspace (or all workspaces). Follow `next_page_token` until empty so the dashboard reflects the complete collection. The NOTES column summarizes active `ConfigurationInvalid` readiness conditions as `inspect for config` before port forwards and clears the note on refresh after repair. Full diagnostics remain available through `openshell sandbox get -o json`. Timed-out provisioning attempts show `Provisioning timed out` with cleanup pending or compute reclaimed, preserving port forwards. The sandbox detail pane wraps the full configuration error in its Notes field. **Providers**: Fetched via `ListProviders` in the background collection-refresh task. Provider profiles are fetched per-workspace via `ListProviderProfiles` and cached in a `ProviderProfileCache` keyed by `(workspace, profile_id)`. Follow each list RPC's `next_page_token` until empty. diff --git a/architecture/sandbox.md b/architecture/sandbox.md index eecb5fdf6d..74bbfb4aea 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -624,8 +624,10 @@ default. The gateway tracks configuration admission independently of compute health. A blocked startup remains `Provisioning` with a `ConfigurationInvalid` readiness condition, even when the container backend reports readiness. Gateway management -operations remain available. The TUI exposes configuration rejection conditions -in sandbox NOTES alongside active port forwards. Replacing the policy or repairing providers allows +operations remain available. The TUI summarizes configuration rejection in sandbox +NOTES alongside active port forwards; the detail view wraps the full diagnostic, +which is also available through sandbox inspection. +Replacing the policy or repairing providers allows the same supervisor to reconcile and launch; it does not recreate the sandbox. Startup retries continue reporting readiness, but unchanged configuration rejections produce only one log event. A changed configuration or diagnostic emits a new diff --git a/crates/openshell-tui/src/app.rs b/crates/openshell-tui/src/app.rs index ee86b5f4f6..e0689ee8a4 100644 --- a/crates/openshell-tui/src/app.rs +++ b/crates/openshell-tui/src/app.rs @@ -657,6 +657,7 @@ pub struct App { pub sandbox_created: Vec, pub sandbox_images: Vec, pub sandbox_notes: Vec, + pub sandbox_detail_notes: Vec, /// Formatted labels for each sandbox (e.g., "env=prod,team=platform" or empty string). pub sandbox_labels: Vec, /// Formatted annotations for each sandbox (e.g., "policy-signature=abc" or empty string). @@ -1019,6 +1020,7 @@ impl App { sandbox_created: Vec::new(), sandbox_images: Vec::new(), sandbox_notes: Vec::new(), + sandbox_detail_notes: Vec::new(), sandbox_labels: Vec::new(), sandbox_annotations: Vec::new(), sandbox_workspaces: Vec::new(), @@ -3528,6 +3530,7 @@ impl App { self.sandbox_created.clear(); self.sandbox_images.clear(); self.sandbox_notes.clear(); + self.sandbox_detail_notes.clear(); self.sandbox_labels.clear(); self.sandbox_annotations.clear(); self.sandbox_policy_versions.clear(); diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 9e48d95a0a..7b2a9fc1c1 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -2732,6 +2732,14 @@ async fn fetch_sandboxes( } fn sandbox_notes(sandbox: &openshell_core::proto::Sandbox, forwards: String) -> String { + sandbox_notes_for_view(sandbox, forwards, false) +} + +fn sandbox_notes_for_view( + sandbox: &openshell_core::proto::Sandbox, + forwards: String, + detail: bool, +) -> String { if let Some(record) = sandbox .status .as_ref() @@ -2760,17 +2768,18 @@ fn sandbox_notes(sandbox: &openshell_core::proto::Sandbox, forwards: String) -> let Some(rejection) = rejection else { return forwards; }; - // Keep the table row on one line even when a diagnostic contains newlines. - let message = rejection - .message - .split_whitespace() - .collect::>() - .join(" "); - let mut notes = "Config invalid".to_string(); - if !message.is_empty() { - notes.push_str(": "); - notes.push_str(&message); - } + let mut notes = if detail { + format!( + "Invalid config: {}", + rejection + .message + .split_whitespace() + .collect::>() + .join(" ") + ) + } else { + "inspect for config".to_string() + }; if !forwards.is_empty() { notes.push_str("; "); notes.push_str(&forwards); @@ -2838,6 +2847,14 @@ fn apply_sandbox_refresh(app: &mut App, sandboxes: Vec, app: &mut App, area: Rect) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(sandbox_detail::required_height(app)), // metadata - Constraint::Min(0), // policy or logs + Constraint::Length(sandbox_detail::required_height(app, area.width)), // metadata + Constraint::Min(0), // policy or logs ]) .split(area); @@ -738,6 +738,55 @@ mod tests { assert!(!rendered.contains("ALPHA")); } + #[tokio::test] + async fn configuration_notes_fit_dashboard_and_wrap_in_detail() { + let mut app = test_app(); + app.sandbox_names = vec!["quarantined".into()]; + app.sandbox_count = 1; + app.sandbox_notes = vec!["inspect for config".into()]; + let diagnostic = "Invalid config: credentialed endpoint api.example.com:443 requires L7 inspection before the sandbox workload can start safely"; + app.sandbox_detail_notes = vec![diagnostic.into()]; + for width in [80, 100, 120] { + for all_workspaces in [false, true] { + app.all_workspaces = all_workspaces; + let mut terminal = Terminal::new(TestBackend::new(width, 24)).unwrap(); + terminal + .draw(|frame| { + sandboxes::draw(frame, &app, frame.size(), true); + }) + .unwrap(); + let text: String = terminal + .backend() + .buffer() + .content() + .iter() + .map(ratatui::buffer::Cell::symbol) + .collect(); + assert!( + text.contains("inspect for config"), + "dashboard at {width}: {text}" + ); + assert!(!text.contains("credentialed endpoint")); + } + app.screen = Screen::Sandbox; + let mut terminal = Terminal::new(TestBackend::new(width, 24)).unwrap(); + terminal.draw(|frame| draw(frame, &mut app)).unwrap(); + let text: String = terminal + .backend() + .buffer() + .content() + .iter() + .map(ratatui::buffer::Cell::symbol) + .collect(); + let words = text + .replace('│', " ") + .split_whitespace() + .collect::>() + .join(" "); + assert!(words.contains(diagnostic), "detail at {width}: {words}"); + } + } + #[tokio::test] async fn sandbox_delete_confirmation_is_visible_in_standard_terminal() { let mut app = test_app(); diff --git a/crates/openshell-tui/src/ui/sandbox_detail.rs b/crates/openshell-tui/src/ui/sandbox_detail.rs index b9b7d3e6a9..341e46c71a 100644 --- a/crates/openshell-tui/src/ui/sandbox_detail.rs +++ b/crates/openshell-tui/src/ui/sandbox_detail.rs @@ -27,8 +27,20 @@ fn pending_draft_count(app: &App) -> usize { } } +fn note_lines(app: &App, width: u16) -> Vec { + let notes = app + .sandbox_detail_notes + .get(app.sandbox_selected) + .filter(|s| !s.is_empty()) + .map_or("none", String::as_str); + super::sandbox_draft::wrap_value( + &format!(" Notes: {notes}"), + usize::from(width.saturating_sub(4).max(1)), + ) +} + /// Return the rows needed to render every metadata line without clipping. -pub(super) fn required_height(app: &App) -> u16 { +pub(super) fn required_height(app: &App, width: u16) -> u16 { let policy_rows = u16::from(app.sandbox_policy_is_global); let action_rows = if app.confirm_delete { 2 // spacer plus confirmation @@ -36,7 +48,11 @@ pub(super) fn required_height(app: &App) -> u16 { u16::from(pending_draft_count(app) > 0) }; - BASE_CONTENT_ROWS + policy_rows + action_rows + BORDER_ROWS + BASE_CONTENT_ROWS + + policy_rows + + action_rows + + BORDER_ROWS + + u16::try_from(note_lines(app, width).len().saturating_sub(1)).unwrap_or(u16::MAX - 16) } /// Draw a compact metadata pane for the currently selected sandbox. @@ -130,18 +146,12 @@ pub fn draw(frame: &mut Frame<'_>, app: &App, area: Rect) { Span::styled(providers_str, t.text), ]); - // Row 6: Configuration diagnostics and forwarded ports - let notes_str = app - .sandbox_notes - .get(idx) - .filter(|s| !s.is_empty()) - .map_or("none", String::as_str); - let row6 = Line::from(vec![ - Span::styled(" Notes: ", t.muted), - Span::styled(notes_str, t.text), - ]); - - let mut lines = vec![row1, row2, row3, row4, row5, row6]; + let mut lines = vec![row1, row2, row3, row4, row5]; + lines.extend( + note_lines(app, area.width) + .into_iter() + .map(|line| Line::from(Span::styled(line, t.text))), + ); // Show global policy indicator when the sandbox's policy is managed at // gateway scope. diff --git a/crates/openshell-tui/src/ui/sandbox_draft.rs b/crates/openshell-tui/src/ui/sandbox_draft.rs index dd2360b4d8..7be11ddbc2 100644 --- a/crates/openshell-tui/src/ui/sandbox_draft.rs +++ b/crates/openshell-tui/src/ui/sandbox_draft.rs @@ -574,7 +574,7 @@ fn display_width(text: &str) -> usize { /// /// A word wider than `width` is hard-broken rather than allowed to overflow. /// Always returns at least one row so callers can index the first row safely. -fn wrap_value(text: &str, width: usize) -> Vec { +pub(super) fn wrap_value(text: &str, width: usize) -> Vec { if width == 0 { return vec![text.to_string()]; } diff --git a/crates/openshell-tui/src/ui/sandboxes.rs b/crates/openshell-tui/src/ui/sandboxes.rs index b1d3edc1fe..3f8a20991e 100644 --- a/crates/openshell-tui/src/ui/sandboxes.rs +++ b/crates/openshell-tui/src/ui/sandboxes.rs @@ -94,7 +94,7 @@ pub fn draw(frame: &mut Frame<'_>, app: &App, area: Rect, focused: bool) { Constraint::Percentage(7), Constraint::Percentage(18), Constraint::Percentage(15), - Constraint::Percentage(12), + Constraint::Length(18), ] } else { vec![ @@ -104,7 +104,7 @@ pub fn draw(frame: &mut Frame<'_>, app: &App, area: Rect, focused: bool) { Constraint::Percentage(8), Constraint::Percentage(20), Constraint::Percentage(15), - Constraint::Percentage(12), + Constraint::Length(18), ] }; diff --git a/docs/sandboxes/manage-sandboxes.mdx b/docs/sandboxes/manage-sandboxes.mdx index c1d1b135c3..b2d7222ffe 100644 --- a/docs/sandboxes/manage-sandboxes.mdx +++ b/docs/sandboxes/manage-sandboxes.mdx @@ -609,7 +609,7 @@ Use the terminal to spot blocked connections marked `action=deny` and provider-r The dashboard has three panels stacked vertically: Gateways, Providers (or Global Settings), and Sandboxes. Navigate within a panel with `Up`/`Down` or `j`/`k`. At a list boundary the cursor overflows into the adjacent panel, skipping empty panels. Use `Tab`/`Shift+Tab` to cycle panels directly. Press `h`/`l` or `Left`/`Right` in the middle panel to switch between the Providers and Global Settings tabs. -The sandbox table’s NOTES column shows `Config invalid` and the rejection reason when policy or provider configuration blocks provisioning. Open the sandbox detail view to see the Notes field. The diagnostic clears after the configuration is repaired; active port forwards remain listed. +The sandbox table’s NOTES column shows `inspect for config` when policy or provider configuration blocks provisioning. Open the sandbox detail view for the full rejection reason, or run `openshell sandbox get -o json`. The note clears after the configuration is repaired; active port forwards remain listed. ## Port Forwarding From f863181243ad8c5e1f6adecc268df99990638214 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:44:33 -0700 Subject: [PATCH 22/23] fix(tui): shorten invalid configuration note Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- .agents/skills/tui-development/SKILL.md | 2 +- crates/openshell-tui/src/lib.rs | 8 ++++---- crates/openshell-tui/src/ui/mod.rs | 4 ++-- docs/sandboxes/manage-sandboxes.mdx | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.agents/skills/tui-development/SKILL.md b/.agents/skills/tui-development/SKILL.md index 834f7e80a0..6aeb75091c 100644 --- a/.agents/skills/tui-development/SKILL.md +++ b/.agents/skills/tui-development/SKILL.md @@ -170,7 +170,7 @@ Phase 1: GetSandboxLogs → 500 initial lines → send via Event::LogLines Phase 2: WatchSandbox(follow_logs: true) → live tail → send via Event::LogLines ``` -**Sandboxes**: Fetched via `ListSandboxes` in a background collection-refresh task scheduled from the 2-second tick, scoped to the current workspace (or all workspaces). Follow `next_page_token` until empty so the dashboard reflects the complete collection. The NOTES column summarizes active `ConfigurationInvalid` readiness conditions as `inspect for config` before port forwards and clears the note on refresh after repair. Full diagnostics remain available through `openshell sandbox get -o json`. Timed-out provisioning attempts show `Provisioning timed out` with cleanup pending or compute reclaimed, preserving port forwards. The sandbox detail pane wraps the full configuration error in its Notes field. +**Sandboxes**: Fetched via `ListSandboxes` in a background collection-refresh task scheduled from the 2-second tick, scoped to the current workspace (or all workspaces). Follow `next_page_token` until empty so the dashboard reflects the complete collection. The NOTES column summarizes active `ConfigurationInvalid` readiness conditions as `Invalid config` before port forwards and clears the note on refresh after repair. Full diagnostics remain available through `openshell sandbox get -o json`. Timed-out provisioning attempts show `Provisioning timed out` with cleanup pending or compute reclaimed, preserving port forwards. The sandbox detail pane wraps the full configuration error in its Notes field. **Providers**: Fetched via `ListProviders` in the background collection-refresh task. Provider profiles are fetched per-workspace via `ListProviderProfiles` and cached in a `ProviderProfileCache` keyed by `(workspace, profile_id)`. Follow each list RPC's `next_page_token` until empty. diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 7b2a9fc1c1..5e9f7aee7c 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -2778,7 +2778,7 @@ fn sandbox_notes_for_view( .join(" ") ) } else { - "inspect for config".to_string() + "Invalid config".to_string() }; if !forwards.is_empty() { notes.push_str("; "); @@ -3347,7 +3347,7 @@ mod sandbox_notes_tests { }; assert_eq!( sandbox_notes(&sandbox, "fwd:8080".into()), - "inspect for config; fwd:8080" + "Invalid config; fwd:8080" ); assert_eq!( super::sandbox_notes_for_view(&sandbox, "fwd:8080".into(), true), @@ -3355,11 +3355,11 @@ mod sandbox_notes_tests { ); // Older gateways can expose only Ready; retain the note there too. sandbox.status.as_mut().unwrap().conditions.remove(0); - assert_eq!(sandbox_notes(&sandbox, String::new()), "inspect for config"); + assert_eq!(sandbox_notes(&sandbox, String::new()), "Invalid config"); sandbox.status.as_mut().unwrap().conditions[0] .message .clear(); - assert_eq!(sandbox_notes(&sandbox, String::new()), "inspect for config"); + assert_eq!(sandbox_notes(&sandbox, String::new()), "Invalid config"); sandbox.status.as_mut().unwrap().conditions[0].status = "True".into(); assert_eq!(sandbox_notes(&sandbox, "fwd:8080".into()), "fwd:8080"); sandbox.status = None; diff --git a/crates/openshell-tui/src/ui/mod.rs b/crates/openshell-tui/src/ui/mod.rs index 2135b06ce8..9b33146179 100644 --- a/crates/openshell-tui/src/ui/mod.rs +++ b/crates/openshell-tui/src/ui/mod.rs @@ -743,7 +743,7 @@ mod tests { let mut app = test_app(); app.sandbox_names = vec!["quarantined".into()]; app.sandbox_count = 1; - app.sandbox_notes = vec!["inspect for config".into()]; + app.sandbox_notes = vec!["Invalid config".into()]; let diagnostic = "Invalid config: credentialed endpoint api.example.com:443 requires L7 inspection before the sandbox workload can start safely"; app.sandbox_detail_notes = vec![diagnostic.into()]; for width in [80, 100, 120] { @@ -763,7 +763,7 @@ mod tests { .map(ratatui::buffer::Cell::symbol) .collect(); assert!( - text.contains("inspect for config"), + text.contains("Invalid config"), "dashboard at {width}: {text}" ); assert!(!text.contains("credentialed endpoint")); diff --git a/docs/sandboxes/manage-sandboxes.mdx b/docs/sandboxes/manage-sandboxes.mdx index b2d7222ffe..879a90fa98 100644 --- a/docs/sandboxes/manage-sandboxes.mdx +++ b/docs/sandboxes/manage-sandboxes.mdx @@ -609,7 +609,7 @@ Use the terminal to spot blocked connections marked `action=deny` and provider-r The dashboard has three panels stacked vertically: Gateways, Providers (or Global Settings), and Sandboxes. Navigate within a panel with `Up`/`Down` or `j`/`k`. At a list boundary the cursor overflows into the adjacent panel, skipping empty panels. Use `Tab`/`Shift+Tab` to cycle panels directly. Press `h`/`l` or `Left`/`Right` in the middle panel to switch between the Providers and Global Settings tabs. -The sandbox table’s NOTES column shows `inspect for config` when policy or provider configuration blocks provisioning. Open the sandbox detail view for the full rejection reason, or run `openshell sandbox get -o json`. The note clears after the configuration is repaired; active port forwards remain listed. +The sandbox table’s NOTES column shows `Invalid config` when policy or provider configuration blocks provisioning. Open the sandbox detail view for the full rejection reason, or run `openshell sandbox get -o json`. The note clears after the configuration is repaired; active port forwards remain listed. ## Port Forwarding From 8229868dafa7a4e05b0187634f1b6b35271f4183 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:46:45 -0700 Subject: [PATCH 23/23] test(server): refresh schema inventory after rebase Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- crates/openshell-server/src/storage_proto.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index f8a01a0579..209d600a82 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -118,7 +118,7 @@ mod tests { const STORAGE_V1_SCHEMA_SHA256: &str = "d68401809d8cea445c35233ef32412bbd041cb2ac5acaf368a0d0bf74d2ddf17"; const PUBLIC_RPC_SCHEMA_SHA256: &str = - "dedd36f5fe509e8edf297425170493d56b55a95652696b116a2cf389ebd30ad0"; + "87be23fc0ac4eaf8ce5890a1c87e6a48279f65fc8fbcc0cea7d7cbe426f2cc46"; const DURABLE_SCHEMA_SHA256: &str = "654649c8f65f44ac2ba04290f49c56de2f488271f99bc0fd4c6d025039c05128"; const PUBLIC_DURABLE_OVERLAP_SHA256: &str = @@ -570,7 +570,7 @@ mod tests { overlap_hash.as_str(), ), ( - (298, 21), + (299, 21), (92, 16), (80, 16), PUBLIC_RPC_SCHEMA_SHA256,