From ea204f9cb2dd83ee35f4de43720978ed77a073b4 Mon Sep 17 00:00:00 2001 From: Shiju Date: Tue, 3 Nov 2026 17:11:03 +0530 Subject: [PATCH 1/3] feat(compute)!: distinguish hard limits from sampled budgets Resource requests need to say whether a driver provides a hard limit, a scheduler request or termination after sampled excess. This reports those guarantees and adds explicit CPU-time and resident-memory budgets. Unsupported guarantees are rejected before provisioning or retained-launch state changes. Signed-off-by: Shiju --- crates/openshell-cli/src/commands/gateway.rs | 154 ++ crates/openshell-cli/src/main.rs | 24 + crates/openshell-cli/src/run.rs | 69 +- .../tests/ensure_providers_integration.rs | 1 + .../openshell-cli/tests/mtls_integration.rs | 1 + .../tests/provider_commands_integration.rs | 1 + .../sandbox_create_lifecycle_integration.rs | 59 +- .../sandbox_name_fallback_integration.rs | 1 + .../openshell-core/src/extension_protocol.rs | 4 + crates/openshell-core/src/lib.rs | 1 + .../openshell-core/src/resource_contract.rs | 328 +++++ crates/openshell-driver-docker/src/lib.rs | 74 +- crates/openshell-driver-docker/src/tests.rs | 7 + .../openshell-driver-kubernetes/src/driver.rs | 59 +- crates/openshell-driver-mxc/src/driver.rs | 4 +- .../openshell-driver-podman/src/container.rs | 108 +- crates/openshell-driver-podman/src/driver.rs | 53 +- crates/openshell-driver-vm/src/driver.rs | 45 +- crates/openshell-sdk/README.md | 8 + crates/openshell-sdk/src/client.rs | 57 +- crates/openshell-sdk/src/types.rs | 2 + crates/openshell-sdk/tests/client_mock.rs | 85 +- .../src/compute/image_update.rs | 28 +- crates/openshell-server/src/compute/mod.rs | 357 ++++- crates/openshell-server/src/grpc/mod.rs | 22 +- crates/openshell-server/src/grpc/sandbox.rs | 169 ++- .../openshell-server/src/grpc/validation.rs | 12 + crates/openshell-server/src/storage_proto.rs | 17 +- crates/openshell-server/src/test_support.rs | 16 + crates/openshell-server/tests/common/mod.rs | 1 + docs/extensibility/drivers.mdx | 44 + proto/compute_driver.proto | 9 + proto/openshell.proto | 14 + proto/sandbox.proto | 45 + python/openshell/sandbox.py | 26 +- python/openshell/sandbox_test.py | 47 +- sdk/go/openshell/v1/fake/health.go | 35 +- sdk/go/openshell/v1/fake/sandbox.go | 22 +- sdk/go/openshell/v1/fake/sandbox_template.go | 1 + sdk/go/openshell/v1/health.go | 15 + .../v1/internal/converter/coverage_test.go | 26 +- .../openshell/v1/internal/converter/health.go | 59 + .../v1/internal/converter/health_test.go | 26 + .../v1/internal/converter/sandbox.go | 61 +- .../v1/internal/converter/sandbox_test.go | 15 +- sdk/go/openshell/v1/resource.go | 23 + sdk/go/openshell/v1/sandbox.go | 6 + sdk/go/openshell/v1/sandbox_client.go | 8 +- sdk/go/openshell/v1/sandbox_client_test.go | 23 +- .../openshell/v1/sandbox_template_client.go | 6 + .../v1/sandbox_template_client_test.go | 27 +- sdk/go/openshell/v1/types/health.go | 40 + sdk/go/openshell/v1/types/sandbox.go | 27 +- sdk/go/proto/openshellv1/openshell.pb.go | 1236 +++++++++-------- sdk/go/proto/sandboxv1/sandbox.pb.go | 545 ++++++-- sdk/typescript/README.md | 4 + sdk/typescript/src/client.test.ts | 54 +- sdk/typescript/src/client.ts | 37 +- sdk/typescript/src/index.ts | 1 + skills/openshell-cli/SKILL.md | 11 + 60 files changed, 3348 insertions(+), 912 deletions(-) create mode 100644 crates/openshell-core/src/resource_contract.rs create mode 100644 sdk/go/openshell/v1/resource.go diff --git a/crates/openshell-cli/src/commands/gateway.rs b/crates/openshell-cli/src/commands/gateway.rs index 705976f7af..eafea8b148 100644 --- a/crates/openshell-cli/src/commands/gateway.rs +++ b/crates/openshell-cli/src/commands/gateway.rs @@ -46,6 +46,7 @@ struct ComputeDriverInfoView { struct ComputeDriverCapabilitiesView { driver_name: String, driver_version: String, + resources: Option, } #[derive(Debug, Clone)] @@ -417,6 +418,7 @@ pub async fn gateway_info( capabilities: ComputeDriverCapabilitiesView { driver_name: capabilities.driver_name, driver_version: capabilities.driver_version, + resources: capabilities.resource_capabilities, }, } }) @@ -514,9 +516,125 @@ fn print_compute_driver_info(drivers: &[ComputeDriverInfoView]) { "Driver version:".dimmed(), driver.capabilities.driver_version ); + if let Some(resources) = &driver.capabilities.resources { + for (name, supported, limit, request) in [ + ( + "CPU", + resources.cpu.as_ref().map(|c| c.limit_supported), + resources + .cpu + .as_ref() + .and_then(|c| c.limit_enforcement.as_ref()), + resources + .cpu + .as_ref() + .and_then(|c| c.request_enforcement.as_ref()), + ), + ( + "Memory", + resources.memory.as_ref().map(|c| c.limit_supported), + resources + .memory + .as_ref() + .and_then(|c| c.limit_enforcement.as_ref()), + resources + .memory + .as_ref() + .and_then(|c| c.request_enforcement.as_ref()), + ), + ] { + let guarantee = if supported == Some(false) { + "unsupported".into() + } else { + enforcement_label(limit) + }; + println!( + " {name} limits: {guarantee}; requests: {}", + enforcement_label(request) + ); + } + if let Some(sampled) = &resources.sampled_budget { + println!( + " Sampled budgets: CPU time={}, resident bytes={}, scope={}; terminate on excess={}", + sampled.cpu_time_supported, + sampled.resident_bytes_supported, + scope_name(sampled.scope), + sampled.terminate_on_excess + ); + let interval = sampled.sampling_interval.map_or_else( + || "unreported".into(), + |d| format!("{}.{:09} s", d.seconds, d.nanos), + ); + println!( + " Sampling interval: {interval} (plus collection time; overshoot unbounded)" + ); + let resolution = sampled.cpu_time_resolution.map_or_else( + || "unreported".into(), + |d| format!("{}.{:09} s", d.seconds, d.nanos), + ); + println!( + " CPU-time resolution: {resolution}; per-process CPU limit: {}", + sampled.per_process_cpu_limit + ); + } + } else { + println!(" Resource guarantees: unreported"); + } + } +} + +fn scope_name(scope: i32) -> &'static str { + use openshell_core::proto::sandbox::v1::ResourceEnforcementScope as Scope; + match Scope::try_from(scope) { + Ok(Scope::Process) => "process", + Ok(Scope::Container) => "container", + Ok(Scope::SandboxAccount) => "sandbox account", + Ok(Scope::VirtualMachine) => "virtual machine", + _ => "unreported", } } +fn enforcement_label( + enforcement: Option<&openshell_core::proto::sandbox::v1::ResourceEnforcement>, +) -> String { + use openshell_core::proto::sandbox::v1::ResourceEnforcementKind as Kind; + let Some(enforcement) = enforcement else { + return "unreported".into(); + }; + let kind = match Kind::try_from(enforcement.kind) { + Ok(Kind::KernelLimit) => "kernel limit", + Ok(Kind::SchedulerRequest) => "scheduler request", + Ok(Kind::Unsupported) => "unsupported", + _ => "unreported", + }; + format!("{kind} ({})", scope_name(enforcement.scope)) +} + +fn resource_capabilities_json( + resources: &openshell_core::proto::ResourceCapabilities, +) -> serde_json::Value { + fn enforcement( + e: Option<&openshell_core::proto::sandbox::v1::ResourceEnforcement>, + ) -> serde_json::Value { + e.map_or( + serde_json::Value::Null, + |e| serde_json::json!({"kind": e.kind, "scope": e.scope}), + ) + } + fn duration(d: Option) -> serde_json::Value { + d.map_or( + serde_json::Value::Null, + |d| serde_json::json!({"seconds": d.seconds.to_string(), "nanos": d.nanos}), + ) + } + serde_json::json!({ + "cpu": resources.cpu.as_ref().map(|c| serde_json::json!({"limit_supported": c.limit_supported, "limit_enforcement": enforcement(c.limit_enforcement.as_ref()), "request_enforcement": enforcement(c.request_enforcement.as_ref())})), + "memory": resources.memory.as_ref().map(|c| serde_json::json!({"limit_supported": c.limit_supported, "limit_enforcement": enforcement(c.limit_enforcement.as_ref()), "request_enforcement": enforcement(c.request_enforcement.as_ref())})), + "gpu": resources.gpu.as_ref().map(|c| serde_json::json!({"default_selection_supported": c.default_selection_supported, "count_selection_supported": c.count_selection_supported})), + "sampled_budget": resources.sampled_budget.as_ref().map(|c| serde_json::json!({"cpu_time_supported": c.cpu_time_supported, "resident_bytes_supported": c.resident_bytes_supported, "scope": c.scope, "sampling_interval": duration(c.sampling_interval), "terminate_on_excess": c.terminate_on_excess, "cpu_time_resolution": duration(c.cpu_time_resolution), "per_process_cpu_limit": c.per_process_cpu_limit})), + }) +} + fn gateway_info_to_json(view: &GatewayInfoView) -> serde_json::Value { serde_json::json!({ "gateway": &view.gateway, @@ -532,6 +650,7 @@ fn gateway_info_to_json(view: &GatewayInfoView) -> serde_json::Value { "capabilities": { "driver_name": &driver.capabilities.driver_name, "driver_version": &driver.capabilities.driver_version, + "resource_capabilities": driver.capabilities.resources.as_ref().map(resource_capabilities_json), }, })) .collect::>(), @@ -1905,6 +2024,31 @@ mod tests { capabilities: ComputeDriverCapabilitiesView { driver_name: "podman".to_string(), driver_version: "0.0.75".to_string(), + resources: Some(openshell_core::proto::ResourceCapabilities { + cpu: Some(openshell_core::proto::CpuResourceCapabilities { + limit_supported: true, + limit_enforcement: Some( + openshell_core::proto::sandbox::v1::ResourceEnforcement { + kind: 2, + scope: 2, + }, + ), + ..Default::default() + }), + sampled_budget: Some( + openshell_core::proto::sandbox::v1::SampledResourceBudgetCapabilities { + resident_bytes_supported: true, + scope: 3, + sampling_interval: Some(prost_types::Duration { + seconds: 0, + nanos: 100_000_000, + }), + terminate_on_excess: true, + ..Default::default() + }, + ), + ..Default::default() + }), }, }], extensions: vec![ExtensionInfoView { @@ -1928,6 +2072,16 @@ mod tests { json["compute_drivers"][0]["capabilities"]["driver_name"], "podman" ); + let resources = &json["compute_drivers"][0]["capabilities"]["resource_capabilities"]; + assert_eq!( + resources["cpu"]["limit_enforcement"], + serde_json::json!({"kind": 2, "scope": 2}) + ); + assert_eq!( + resources["sampled_budget"]["sampling_interval"]["nanos"], + 100_000_000 + ); + assert_eq!(resources["sampled_budget"]["terminate_on_excess"], true); assert_eq!(json["extensions"][0]["protocol_version"], "1.0"); assert_eq!( json["compute_drivers"][0]["capabilities"]["driver_version"], diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index 7411629483..ab2febd4f9 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -1997,6 +1997,16 @@ enum SandboxTemplateCommands { #[arg(long)] memory: Option, + /// Sampled cumulative CPU-time budget (for example: 30s). Terminates on + /// observed excess; this is not a CPU-rate limit. + #[arg(long, value_name = "DURATION")] + cpu_time_budget: Option, + + /// Sampled account-wide resident-memory budget in bytes. Terminates on + /// observed excess; this is not a hard memory limit. + #[arg(long, value_name = "BYTES", value_parser = clap::value_parser!(u64).range(1..))] + resident_memory_budget_bytes: Option, + /// Request GPU resources for sandboxes created from this template. /// /// Omit COUNT for the driver's default GPU selection, or pass COUNT @@ -3764,6 +3774,8 @@ async fn run_async() -> Result<()> { image, cpu, memory, + cpu_time_budget, + resident_memory_budget_bytes, gpu, driver_config_json, ready_within, @@ -3786,6 +3798,10 @@ async fn run_async() -> Result<()> { image.as_deref(), cpu.as_deref(), memory.as_deref(), + run::sampled_resource_budget( + cpu_time_budget.as_deref(), + resident_memory_budget_bytes, + )?, gpu_requirements, driver_config_json.as_deref(), ready_within.as_deref(), @@ -6657,6 +6673,10 @@ mod tests { "2", "--memory", "4Gi", + "--cpu-time-budget", + "30s", + "--resident-memory-budget-bytes", + "1073741824", "--gpu", "1", "--driver-config-json", @@ -6685,6 +6705,8 @@ mod tests { image, cpu, memory, + cpu_time_budget, + resident_memory_budget_bytes, gpu, driver_config_json, ready_within, @@ -6697,6 +6719,8 @@ mod tests { })), .. }) => { + assert_eq!(cpu_time_budget.as_deref(), Some("30s")); + assert_eq!(resident_memory_budget_bytes, Some(1_073_741_824)); assert_eq!(name, "gpu-kata"); assert_eq!(image.as_deref(), Some("registry.example.com/agent:latest")); assert_eq!(cpu.as_deref(), Some("2")); diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index eb224685ed..259d72e1c6 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -651,7 +651,10 @@ pub async fn sandbox_create( None }; - let resource_requirements = gpu_requirements.map(|gpu| ResourceRequirements { gpu: Some(gpu) }); + let resource_requirements = gpu_requirements.map(|gpu| ResourceRequirements { + gpu: Some(gpu), + ..Default::default() + }); let main_terminal = tty_override .unwrap_or_else(|| std::io::stdin().is_terminal() && std::io::stdout().is_terminal()); @@ -3380,6 +3383,7 @@ pub async fn sandbox_template_create( image: Option<&str>, cpu: Option<&str>, memory: Option<&str>, + sampled_budget: Option, gpu_requirements: Option, driver_config_json: Option<&str>, ready_within: Option<&str>, @@ -3392,7 +3396,11 @@ pub async fn sandbox_template_create( tls: &TlsOptions, suppress_credential_warnings: bool, ) -> Result<()> { - let resources = if cpu.is_some() || memory.is_some() || gpu_requirements.is_some() { + let resources = if cpu.is_some() + || memory.is_some() + || gpu_requirements.is_some() + || sampled_budget.is_some() + { Some(SandboxResources { cpu: cpu .map(validate_cpu_quantity) @@ -3403,6 +3411,7 @@ pub async fn sandbox_template_create( .transpose()? .unwrap_or_default(), gpu: gpu_requirements, + sampled_budget, }) } else { None @@ -3413,6 +3422,19 @@ pub async fn sandbox_template_create( let desired_service_level = build_template_service_level(ready_within, max_burst)?; let mut client = grpc_client(server, tls).await?; + if sampled_budget.is_some() + && !client + .health(openshell_core::proto::HealthRequest {}) + .await + .into_diagnostic()? + .into_inner() + .supports_sampled_resource_budget + { + return Err(miette!( + "gateway does not acknowledge sampled resource budgets; upgrade it before creating this template" + )); + } + let profile_catalog = fetch_provider_profile_catalog(&mut client, workspace) .await .unwrap_or_default(); @@ -3463,6 +3485,25 @@ pub async fn sandbox_template_create( Ok(()) } +pub fn sampled_resource_budget( + cpu_time: Option<&str>, + resident_bytes: Option, +) -> Result> { + if cpu_time.is_none() && resident_bytes.is_none() { + return Ok(None); + } + let budget = openshell_core::proto::sandbox::v1::SampledResourceBudget { + cpu_time: cpu_time + .map(parse_duration_to_ms) + .transpose()? + .map(duration_ms_to_proto), + resident_bytes, + }; + openshell_core::resource_contract::validate_sampled_budget(&budget) + .map_err(|error| miette!("{error}"))?; + Ok(Some(budget)) +} + fn build_template_service_level( ready_within: Option<&str>, max_burst: Option, @@ -3691,6 +3732,12 @@ fn sandbox_template_to_json(template: &SandboxWorkloadTemplate) -> serde_json::V .map_or_else(|| serde_json::json!("default"), serde_json::Value::from); resources_json.insert("gpu".to_string(), value); } + if let Some(budget) = &resources.sampled_budget { + resources_json.insert("sampled_budget".into(), serde_json::json!({ + "cpu_time": budget.cpu_time.map(|d| serde_json::json!({"seconds": d.seconds.to_string(), "nanos": d.nanos})), + "resident_bytes": budget.resident_bytes.map(|n| n.to_string()), + })); + } if !resources_json.is_empty() { obj.insert( "resources".to_string(), @@ -3777,6 +3824,18 @@ fn print_sandbox_template_detail(template: &SandboxWorkloadTemplate) { workload.environment.len() ); if let Some(resources) = &workload.resources { + if let Some(budget) = &resources.sampled_budget { + if let Some(cpu) = budget.cpu_time.as_ref() { + println!( + " Sampled CPU-time budget: {}.{:09} s", + cpu.seconds, cpu.nanos + ); + } + if let Some(resident) = budget.resident_bytes { + println!(" Sampled resident-memory budget: {resident} bytes"); + } + println!(" Budgets terminate on observed excess; sampling can overshoot."); + } println!( " {} {}", "CPU:".dimmed(), @@ -8262,6 +8321,7 @@ mod tests { fn provisioning_timeout_message_includes_condition_and_gpu_hint() { let resource_requirements = ResourceRequirements { gpu: Some(GpuResourceRequirements { count: None }), + ..Default::default() }; let message = provisioning_timeout_message( 120, @@ -8283,7 +8343,10 @@ mod tests { #[test] fn provisioning_timeout_message_omits_gpu_hint_without_gpu_requirements() { - let resource_requirements = ResourceRequirements { gpu: None }; + let resource_requirements = ResourceRequirements { + gpu: None, + ..Default::default() + }; let message = provisioning_timeout_message(120, Some(&resource_requirements), None); assert_eq!(message, "sandbox provisioning timed out after 120s"); diff --git a/crates/openshell-cli/tests/ensure_providers_integration.rs b/crates/openshell-cli/tests/ensure_providers_integration.rs index be3e4f2e49..3228a99bc8 100644 --- a/crates/openshell-cli/tests/ensure_providers_integration.rs +++ b/crates/openshell-cli/tests/ensure_providers_integration.rs @@ -152,6 +152,7 @@ impl OpenShell for TestOpenShell { Ok(Response::new(HealthResponse { status: ServiceStatus::Healthy.into(), version: "test".to_string(), + supports_sampled_resource_budget: false, })) } diff --git a/crates/openshell-cli/tests/mtls_integration.rs b/crates/openshell-cli/tests/mtls_integration.rs index 1017712035..355487858c 100644 --- a/crates/openshell-cli/tests/mtls_integration.rs +++ b/crates/openshell-cli/tests/mtls_integration.rs @@ -104,6 +104,7 @@ impl OpenShell for TestOpenShell { Ok(Response::new(HealthResponse { status: ServiceStatus::Healthy.into(), version: "test".to_string(), + supports_sampled_resource_budget: false, })) } diff --git a/crates/openshell-cli/tests/provider_commands_integration.rs b/crates/openshell-cli/tests/provider_commands_integration.rs index 89247ba718..5d4ec1c4a3 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -307,6 +307,7 @@ impl OpenShell for TestOpenShell { Ok(Response::new(HealthResponse { status: ServiceStatus::Healthy.into(), version: "test".to_string(), + supports_sampled_resource_budget: false, })) } diff --git a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs index 587910fbaf..d80428358e 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -60,6 +60,7 @@ fn selected_workspace( #[derive(Clone, Default)] struct SandboxState { + sampled_budget_ack: Arc, /// Make `ListProviderProfiles` fail while every other RPC keeps working, /// so a catalog lookup failure can be told apart from an empty catalog. fail_list_provider_profiles: Arc, @@ -165,6 +166,7 @@ impl OpenShell for TestOpenShell { Ok(Response::new(HealthResponse { status: ServiceStatus::Healthy.into(), version: "test".to_string(), + supports_sampled_resource_budget: self.state.sampled_budget_ack.load(Ordering::SeqCst), })) } @@ -2330,26 +2332,36 @@ async fn sandbox_template_create_sends_non_default_workspace_in_scope_and_metada .run_async(async { let tls = test_tls(&server); - run::sandbox_template_create( - &server.endpoint, - "gpu-kata", - Some("registry.example.com/agent:latest"), - Some("2"), - Some("4Gi"), - Some(GpuResourceRequirements { count: Some(1) }), - Some(r#"{"kubernetes":{"pod":{"node_selector":{"pool":"gpu"}}}}"#), - Some("5m"), - Some(3), - HashMap::from([("team".to_string(), "runtime".to_string())]), - HashMap::from([("owner".to_string(), "platform".to_string())]), - HashMap::from([("FEATURE_FLAG".to_string(), "on".to_string())]), - "table", - "team-a", - &tls, - true, - ) - .await - .expect("template create should succeed"); + let create = || { + run::sandbox_template_create( + &server.endpoint, + "gpu-kata", + Some("registry.example.com/agent:latest"), + Some("2"), + Some("4Gi"), + run::sampled_resource_budget(Some("30s"), Some(1_073_741_824)).unwrap(), + Some(GpuResourceRequirements { count: Some(1) }), + Some(r#"{"kubernetes":{"pod":{"node_selector":{"pool":"gpu"}}}}"#), + Some("5m"), + Some(3), + HashMap::from([("team".to_string(), "runtime".to_string())]), + HashMap::from([("owner".to_string(), "platform".to_string())]), + HashMap::from([("FEATURE_FLAG".to_string(), "on".to_string())]), + "table", + "team-a", + &tls, + true, + ) + }; + let error = create().await.unwrap_err(); + assert!(format!("{error:?}").contains("does not acknowledge")); + assert!(template_create_requests(&server).await.is_empty()); + server + .openshell + .state + .sampled_budget_ack + .store(true, Ordering::SeqCst); + create().await.expect("template create should succeed"); let requests = template_create_requests(&server).await; let request = requests @@ -2377,6 +2389,12 @@ async fn sandbox_template_create_sends_non_default_workspace_in_scope_and_metada .resources .as_ref() .expect("resources should be sent"); + let budget = resources + .sampled_budget + .as_ref() + .expect("sampled budget must survive the CLI request"); + assert_eq!(budget.cpu_time.as_ref().unwrap().seconds, 30); + assert_eq!(budget.resident_bytes, Some(1_073_741_824)); assert_eq!(resources.cpu, "2"); assert_eq!(resources.memory, "4Gi"); assert_eq!(resources.gpu.as_ref().and_then(|gpu| gpu.count), Some(1)); @@ -2475,6 +2493,7 @@ async fn sandbox_template_create_allows_omitted_image() { None, None, None, + None, HashMap::new(), HashMap::new(), HashMap::new(), diff --git a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs index 497f3ce778..82312dd58e 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -119,6 +119,7 @@ impl OpenShell for TestOpenShell { Ok(Response::new(HealthResponse { status: ServiceStatus::Healthy.into(), version: "test".to_string(), + supports_sampled_resource_budget: false, })) } diff --git a/crates/openshell-core/src/extension_protocol.rs b/crates/openshell-core/src/extension_protocol.rs index d6d93418d8..f75a9a3bf8 100644 --- a/crates/openshell-core/src/extension_protocol.rs +++ b/crates/openshell-core/src/extension_protocol.rs @@ -19,6 +19,9 @@ pub const PROTOCOL_MINOR: u32 = 0; /// configured signer before accepting sandbox creation. pub const COMPUTE_LAUNCH_AUTHENTICATION: &str = "openshell.compute.launch-authentication"; +/// Driver describes resource guarantees and admits explicit sampled budgets. +pub const COMPUTE_RESOURCE_CONTRACT: &str = "openshell.compute.resource-contract"; + /// Driver stages operator-selected external isolation routes for the supervisor. pub const COMPUTE_ISOLATION_BACKEND: &str = "openshell.compute.isolation-backend"; @@ -133,6 +136,7 @@ pub fn gateway_metadata(family: ExtensionFamily) -> PeerMetadata { supported_capabilities.push(COMPUTE_LAUNCH_AUTHENTICATION.to_string()); supported_capabilities.push(COMPUTE_ISOLATION_BACKEND.to_string()); supported_capabilities.push(COMPUTE_UPDATE_SANDBOX_CONFIGURATION.to_string()); + supported_capabilities.push(COMPUTE_RESOURCE_CONTRACT.to_string()); } PeerMetadata { protocol_version: Some(ProtocolVersion { diff --git a/crates/openshell-core/src/lib.rs b/crates/openshell-core/src/lib.rs index 83faf18ef0..45f2958524 100644 --- a/crates/openshell-core/src/lib.rs +++ b/crates/openshell-core/src/lib.rs @@ -56,6 +56,7 @@ pub mod proto_struct; pub mod provider_credentials; pub mod replica_routing; pub mod resource_admission; +pub mod resource_contract; pub mod rpc_error; pub mod sandbox_env; pub mod sandbox_generation; diff --git a/crates/openshell-core/src/resource_contract.rs b/crates/openshell-core/src/resource_contract.rs new file mode 100644 index 0000000000..d996de8d13 --- /dev/null +++ b/crates/openshell-core/src/resource_contract.rs @@ -0,0 +1,328 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +//! Admission of resource guarantees, separate from operator host-resource policy. + +use crate::proto::compute::v1::{DriverSandbox, ResourceCapabilities}; +use crate::proto::sandbox::v1::{ + ResourceEnforcement, ResourceEnforcementKind as Kind, ResourceEnforcementScope as Scope, + SampledResourceBudget, +}; + +/// Canonical positive protobuf duration, in nanoseconds without narrowing. +pub fn positive_duration_nanos(value: &prost_types::Duration) -> Result { + if !(0..=315_576_000_000).contains(&value.seconds) || !(0..1_000_000_000).contains(&value.nanos) + { + return Err("resource duration must be positive and canonical"); + } + let nanos = u128::from(value.seconds.unsigned_abs()) * 1_000_000_000 + + u128::from(value.nanos.unsigned_abs()); + if nanos == 0 { + return Err("resource duration must be positive"); + } + Ok(nanos) +} + +pub fn validate_sampled_budget(budget: &SampledResourceBudget) -> Result<(), &'static str> { + if budget.cpu_time.is_none() && budget.resident_bytes.is_none() { + return Err("sampled resource budget must select CPU time or resident bytes"); + } + if let Some(cpu) = &budget.cpu_time { + positive_duration_nanos(cpu)?; + } + if budget.resident_bytes == Some(0) { + return Err("sampled resident bytes must be positive"); + } + Ok(()) +} + +fn require_enforcement( + enforcement: Option<&ResourceEnforcement>, + kind: Kind, +) -> Result<(), &'static str> { + let Some(enforcement) = enforcement else { + return Err("requested resource guarantee is unreported by the compute driver"); + }; + if Kind::try_from(enforcement.kind).ok() != Some(kind) + || !matches!( + Scope::try_from(enforcement.scope), + Ok(Scope::Container | Scope::SandboxAccount | Scope::VirtualMachine) + ) + { + return Err("requested resource guarantee is unsupported at sandbox scope"); + } + Ok(()) +} + +/// Validate both independent CPU/memory axes before any provisioning. +/// +/// Legacy drivers retain their existing hard-limit behavior, but cannot accept the new +/// sampled request by silently ignoring an unknown protobuf field. +pub fn validate_request( + sandbox: &DriverSandbox, + capabilities: Option<&ResourceCapabilities>, + negotiated: bool, +) -> Result<(), &'static str> { + let spec = sandbox.spec.as_ref(); + let resources = spec + .and_then(|s| s.template.as_ref()) + .and_then(|t| t.resources.as_ref()); + if negotiated && let Some(resources) = resources { + let cpu = capabilities.and_then(|c| c.cpu.as_ref()); + let memory = capabilities.and_then(|c| c.memory.as_ref()); + if !resources.cpu_limit.is_empty() { + if !cpu.is_some_and(|c| c.limit_supported) { + return Err("compute driver does not support a hard CPU-rate limit"); + } + require_enforcement( + cpu.and_then(|c| c.limit_enforcement.as_ref()), + Kind::KernelLimit, + )?; + } + if !resources.memory_limit.is_empty() { + if !memory.is_some_and(|c| c.limit_supported) { + return Err("compute driver does not support a hard memory limit"); + } + require_enforcement( + memory.and_then(|c| c.limit_enforcement.as_ref()), + Kind::KernelLimit, + )?; + } + if !resources.cpu_request.is_empty() { + require_enforcement( + cpu.and_then(|c| c.request_enforcement.as_ref()), + Kind::SchedulerRequest, + )?; + } + if !resources.memory_request.is_empty() { + require_enforcement( + memory.and_then(|c| c.request_enforcement.as_ref()), + Kind::SchedulerRequest, + )?; + } + } + if let Some(budget) = spec + .and_then(|s| s.resource_requirements.as_ref()) + .and_then(|r| r.sampled_budget.as_ref()) + { + validate_sampled_budget(budget)?; + if !negotiated { + return Err("sampled budgets require openshell.compute.resource-contract"); + } + let Some(cap) = capabilities.and_then(|c| c.sampled_budget.as_ref()) else { + return Err("compute driver does not support sampled resource budgets"); + }; + if !cap.terminate_on_excess + || Scope::try_from(cap.scope).ok() != Some(Scope::SandboxAccount) + { + return Err("sampled budgets require terminating account-wide observation"); + } + positive_duration_nanos( + cap.sampling_interval + .as_ref() + .ok_or("sampling interval is unreported")?, + )?; + if let Some(cpu) = &budget.cpu_time { + if !cap.cpu_time_supported { + return Err("sampled CPU-time budget is unsupported"); + } + let resolution = positive_duration_nanos( + cap.cpu_time_resolution + .as_ref() + .ok_or("CPU-time resolution is unreported")?, + )?; + if positive_duration_nanos(cpu)? % resolution != 0 { + return Err( + "sampled CPU-time budget cannot be represented at the driver resolution", + ); + } + } + if budget.resident_bytes.is_some() && !cap.resident_bytes_supported { + return Err("sampled resident-memory budget is unsupported"); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::proto::compute::v1::{ + CpuResourceCapabilities, DriverResourceRequirements, DriverSandboxSpec, + DriverSandboxTemplate, MemoryResourceCapabilities, ResourceRequirements, + }; + use crate::proto::sandbox::v1::SampledResourceBudgetCapabilities; + + #[test] + fn resource_guarantees_validate_both_axes_and_never_infer_support() { + let mut sandbox = DriverSandbox { + spec: Some(DriverSandboxSpec { + template: Some(DriverSandboxTemplate { + resources: Some(DriverResourceRequirements::default()), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }; + for (cpu, memory) in [("1", ""), ("", "512Mi")] { + let resources = sandbox + .spec + .as_mut() + .unwrap() + .template + .as_mut() + .unwrap() + .resources + .as_mut() + .unwrap(); + resources.cpu_limit = cpu.into(); + resources.memory_limit = memory.into(); + for (kind, scope, accepted) in [ + (Kind::KernelLimit as i32, Scope::Container as i32, true), + (Kind::KernelLimit as i32, Scope::Process as i32, false), + ( + Kind::SchedulerRequest as i32, + Scope::Container as i32, + false, + ), + (99, Scope::Container as i32, false), + (Kind::KernelLimit as i32, 99, false), + ] { + let enforcement = Some(ResourceEnforcement { kind, scope }); + let capabilities = ResourceCapabilities { + cpu: Some(CpuResourceCapabilities { + limit_supported: true, + limit_enforcement: enforcement, + ..Default::default() + }), + memory: Some(MemoryResourceCapabilities { + limit_supported: true, + limit_enforcement: enforcement, + ..Default::default() + }), + ..Default::default() + }; + assert_eq!( + validate_request(&sandbox, Some(&capabilities), true).is_ok(), + accepted + ); + } + assert!(validate_request(&sandbox, None, true).is_err()); + assert!(validate_request(&sandbox, None, false).is_ok()); + } + for (cpu, memory) in [("1", ""), ("", "512Mi")] { + let resources = sandbox + .spec + .as_mut() + .unwrap() + .template + .as_mut() + .unwrap() + .resources + .as_mut() + .unwrap(); + *resources = DriverResourceRequirements { + cpu_request: cpu.into(), + memory_request: memory.into(), + ..Default::default() + }; + for (kind, scope, accepted) in [ + (Kind::SchedulerRequest as i32, Scope::Container as i32, true), + (Kind::KernelLimit as i32, Scope::Container as i32, false), + (Kind::SchedulerRequest as i32, Scope::Process as i32, false), + (99, Scope::Container as i32, false), + ] { + let enforcement = Some(ResourceEnforcement { kind, scope }); + let capabilities = ResourceCapabilities { + cpu: Some(CpuResourceCapabilities { + request_enforcement: enforcement, + ..Default::default() + }), + memory: Some(MemoryResourceCapabilities { + request_enforcement: enforcement, + ..Default::default() + }), + ..Default::default() + }; + assert_eq!( + validate_request(&sandbox, Some(&capabilities), true).is_ok(), + accepted + ); + } + assert!( + validate_request(&sandbox, Some(&ResourceCapabilities::default()), true).is_err() + ); + } + sandbox.spec.as_mut().unwrap().template = None; + let capabilities = ResourceCapabilities { + sampled_budget: Some(SampledResourceBudgetCapabilities { + cpu_time_supported: true, + resident_bytes_supported: true, + scope: Scope::SandboxAccount as i32, + sampling_interval: Some(prost_types::Duration { + seconds: 0, + nanos: 100_000_000, + }), + cpu_time_resolution: Some(prost_types::Duration { + seconds: 0, + nanos: 1_000_000, + }), + terminate_on_excess: true, + per_process_cpu_limit: true, + }), + ..Default::default() + }; + for (seconds, nanos, resident, accepted) in [ + (0, 1_000_000, None, true), + (1, 0, Some(4096), true), + (0, 1, None, false), + (0, 0, None, false), + (-1, 0, None, false), + (1, -1, None, false), + (1, 1_000_000_000, None, false), + (315_576_000_001, 0, None, false), + (1, 0, Some(0), false), + ] { + sandbox.spec.as_mut().unwrap().resource_requirements = Some(ResourceRequirements { + sampled_budget: Some(SampledResourceBudget { + cpu_time: Some(prost_types::Duration { seconds, nanos }), + resident_bytes: resident, + }), + ..Default::default() + }); + assert_eq!( + validate_request(&sandbox, Some(&capabilities), true).is_ok(), + accepted + ); + assert!(validate_request(&sandbox, Some(&capabilities), false).is_err()); + } + sandbox.spec.as_mut().unwrap().resource_requirements = Some(ResourceRequirements { + sampled_budget: Some(SampledResourceBudget { + cpu_time: Some(prost_types::Duration { + seconds: 1, + nanos: 0, + }), + resident_bytes: Some(4096), + }), + ..Default::default() + }); + for change in 0..8 { + let mut changed = capabilities; + let sampled = changed.sampled_budget.as_mut().unwrap(); + match change { + 0 => sampled.terminate_on_excess = false, + 1 => sampled.scope = Scope::Process as i32, + 2 => sampled.cpu_time_supported = false, + 3 => sampled.resident_bytes_supported = false, + 4 => sampled.sampling_interval = None, + 5 => sampled.cpu_time_resolution = None, + 6 => sampled.sampling_interval.as_mut().unwrap().nanos = -1, + _ => changed.sampled_budget = None, + } + assert!( + validate_request(&sandbox, Some(&changed), true).is_err(), + "changed capability {change}" + ); + } + } +} diff --git a/crates/openshell-driver-docker/src/lib.rs b/crates/openshell-driver-docker/src/lib.rs index 6b761f2f59..7cf58b2489 100644 --- a/crates/openshell-driver-docker/src/lib.rs +++ b/crates/openshell-driver-docker/src/lib.rs @@ -1063,6 +1063,36 @@ impl DockerComputeDriver { Ok(driver) } + fn resource_capabilities(gpu_supported: bool) -> ResourceCapabilities { + ResourceCapabilities { + cpu: Some(CpuResourceCapabilities { + limit_supported: true, + limit_enforcement: Some(openshell_core::proto::sandbox::v1::ResourceEnforcement { + kind: openshell_core::proto::sandbox::v1::ResourceEnforcementKind::KernelLimit + as i32, + scope: openshell_core::proto::sandbox::v1::ResourceEnforcementScope::Container + as i32, + }), + ..Default::default() + }), + memory: Some(MemoryResourceCapabilities { + limit_supported: true, + limit_enforcement: Some(openshell_core::proto::sandbox::v1::ResourceEnforcement { + kind: openshell_core::proto::sandbox::v1::ResourceEnforcementKind::KernelLimit + as i32, + scope: openshell_core::proto::sandbox::v1::ResourceEnforcementScope::Container + as i32, + }), + ..Default::default() + }), + gpu: Some(GpuResourceCapabilities { + default_selection_supported: gpu_supported, + count_selection_supported: gpu_supported, + }), + sampled_budget: None, + } + } + fn capabilities(&self) -> GetCapabilitiesResponse { GetCapabilitiesResponse { resource_admission_policy: openshell_core::resource_admission::DriverAdmissionConfig { @@ -1076,25 +1106,14 @@ impl DockerComputeDriver { gateway_manages_lifecycle: true, supports_sandbox_authentication: false, driver_reports_runtime_readiness: false, - resource_capabilities: Some(ResourceCapabilities { - cpu: Some(CpuResourceCapabilities { - limit_supported: true, - }), - memory: Some(MemoryResourceCapabilities { - limit_supported: true, - }), - gpu: Some(GpuResourceCapabilities { - default_selection_supported: self.config.gpu.cdi_supported, - count_selection_supported: self.config.gpu.cdi_supported, - }), - }), + resource_capabilities: Some(Self::resource_capabilities(self.config.gpu.cdi_supported)), rootfs_tar_staging_dir: String::new(), rootfs_tar_max_bytes: 0, extension: Some(openshell_core::extension_protocol::extension_metadata( openshell_core::extension_protocol::ExtensionFamily::Compute, "openshell/docker", openshell_core::VERSION, - [], + [openshell_core::extension_protocol::COMPUTE_RESOURCE_CONTRACT.to_string()], )), } } @@ -1116,6 +1135,12 @@ impl DockerComputeDriver { config.allow_driver_config, sandbox, )?; + openshell_core::resource_contract::validate_request( + sandbox, + Some(&Self::resource_capabilities(config.gpu.cdi_supported)), + true, + ) + .map_err(Status::failed_precondition)?; let spec = sandbox .spec .as_ref() @@ -6438,7 +6463,6 @@ fn docker_pids_limit(value: Option) -> Result, Ok(value.map(std::num::NonZeroI64::get)) } -#[allow(clippy::cast_possible_truncation)] fn parse_cpu_limit(value: &str) -> Result, Status> { let value = value.trim(); if value.is_empty() { @@ -6455,7 +6479,9 @@ fn parse_cpu_limit(value: &str) -> Result, Status> { "docker cpu_limit must be greater than zero", )); } - return Ok(Some(millicores.saturating_mul(1_000_000))); + return millicores.checked_mul(1_000_000).map(Some).ok_or_else(|| { + Status::failed_precondition("docker CPU limit exceeds the supported range") + }); } let cores = value.parse::().map_err(|_| { @@ -6469,10 +6495,9 @@ fn parse_cpu_limit(value: &str) -> Result, Status> { )); } - Ok(Some((cores * 1_000_000_000.0).round() as i64)) + checked_docker_limit(cores * 1_000_000_000.0, "CPU").map(Some) } -#[allow(clippy::cast_possible_truncation)] fn parse_memory_limit(value: &str) -> Result, Status> { let value = value.trim(); if value.is_empty() { @@ -6515,7 +6540,20 @@ fn parse_memory_limit(value: &str) -> Result, Status> { } }; - Ok(Some((amount * multiplier).round() as i64)) + checked_docker_limit(amount * multiplier, "memory").map(Some) +} + +// Docker uses zero as an unlimited control. Positive authored quantities must +// remain positive after conversion and fit the signed wire field exactly. +#[allow(clippy::cast_possible_truncation)] +fn checked_docker_limit(value: f64, resource: &str) -> Result { + let value = value.floor(); + if !value.is_finite() || value < 1.0 || value >= 2_f64.powi(63) { + return Err(Status::failed_precondition(format!( + "docker {resource} limit is outside the supported positive range" + ))); + } + Ok(value as i64) } fn sandbox_from_container_summary(summary: &ContainerSummary) -> Option { diff --git a/crates/openshell-driver-docker/src/tests.rs b/crates/openshell-driver-docker/src/tests.rs index 92d20739f3..a6c01c8a77 100644 --- a/crates/openshell-driver-docker/src/tests.rs +++ b/crates/openshell-driver-docker/src/tests.rs @@ -311,6 +311,7 @@ fn list_string_driver_config(field: &str, values: &[&str]) -> prost_types::Struc fn gpu_resources(count: Option) -> ResourceRequirements { ResourceRequirements { gpu: Some(GpuResourceRequirements { count }), + ..Default::default() } } @@ -1401,6 +1402,9 @@ async fn tracing_in_process_stream_leaves_status_unset_when_dropped() { fn parse_cpu_limit_supports_cores_and_millicores() { assert_eq!(parse_cpu_limit("250m").unwrap(), Some(250_000_000)); assert_eq!(parse_cpu_limit("2").unwrap(), Some(2_000_000_000)); + for value in ["0.0000000001", "1e100", "9223372036854775807m"] { + assert!(parse_cpu_limit(value).is_err(), "{value}"); + } assert!(parse_cpu_limit("0").is_err()); } @@ -1408,6 +1412,9 @@ fn parse_cpu_limit_supports_cores_and_millicores() { fn parse_memory_limit_supports_binary_quantities() { assert_eq!(parse_memory_limit("512Mi").unwrap(), Some(536_870_912)); assert_eq!(parse_memory_limit("1G").unwrap(), Some(1_000_000_000)); + for value in ["0.1", "999999999999999999999Ei"] { + assert!(parse_memory_limit(value).is_err(), "{value}"); + } assert!(parse_memory_limit("12XB").is_err()); } diff --git a/crates/openshell-driver-kubernetes/src/driver.rs b/crates/openshell-driver-kubernetes/src/driver.rs index afc06e58c2..ced507549b 100644 --- a/crates/openshell-driver-kubernetes/src/driver.rs +++ b/crates/openshell-driver-kubernetes/src/driver.rs @@ -1296,6 +1296,38 @@ impl KubernetesComputeDriver { Ok(driver) } + fn resource_capabilities() -> ResourceCapabilities { + ResourceCapabilities { + cpu: Some(CpuResourceCapabilities { + limit_supported: true, + request_enforcement: Some(openshell_core::proto::sandbox::v1::ResourceEnforcement { + kind: openshell_core::proto::sandbox::v1::ResourceEnforcementKind::SchedulerRequest as i32, + scope: openshell_core::proto::sandbox::v1::ResourceEnforcementScope::Container as i32, + }), + limit_enforcement: Some(openshell_core::proto::sandbox::v1::ResourceEnforcement { + kind: openshell_core::proto::sandbox::v1::ResourceEnforcementKind::KernelLimit as i32, + scope: openshell_core::proto::sandbox::v1::ResourceEnforcementScope::Container as i32, + }), +}), + memory: Some(MemoryResourceCapabilities { + limit_supported: true, + request_enforcement: Some(openshell_core::proto::sandbox::v1::ResourceEnforcement { + kind: openshell_core::proto::sandbox::v1::ResourceEnforcementKind::SchedulerRequest as i32, + scope: openshell_core::proto::sandbox::v1::ResourceEnforcementScope::Container as i32, + }), + limit_enforcement: Some(openshell_core::proto::sandbox::v1::ResourceEnforcement { + kind: openshell_core::proto::sandbox::v1::ResourceEnforcementKind::KernelLimit as i32, + scope: openshell_core::proto::sandbox::v1::ResourceEnforcementScope::Container as i32, + }), +}), + gpu: Some(GpuResourceCapabilities { + default_selection_supported: true, + count_selection_supported: true, + }), + sampled_budget: None, +} + } + pub fn capabilities(&self) -> Result { Ok(GetCapabilitiesResponse { resource_admission_policy: openshell_core::resource_admission::DriverAdmissionConfig { @@ -1309,18 +1341,7 @@ impl KubernetesComputeDriver { gateway_manages_lifecycle: false, supports_sandbox_authentication: true, driver_reports_runtime_readiness: false, - resource_capabilities: Some(ResourceCapabilities { - cpu: Some(CpuResourceCapabilities { - limit_supported: true, - }), - memory: Some(MemoryResourceCapabilities { - limit_supported: true, - }), - gpu: Some(GpuResourceCapabilities { - default_selection_supported: true, - count_selection_supported: true, - }), - }), + resource_capabilities: Some(Self::resource_capabilities()), rootfs_tar_staging_dir: String::new(), rootfs_tar_max_bytes: 0, extension: Some(openshell_core::extension_protocol::extension_metadata( @@ -1330,6 +1351,7 @@ impl KubernetesComputeDriver { [ openshell_core::extension_protocol::COMPUTE_UPDATE_SANDBOX_CONFIGURATION .to_string(), + openshell_core::extension_protocol::COMPUTE_RESOURCE_CONTRACT.to_string(), ], )), }) @@ -2105,6 +2127,12 @@ impl KubernetesComputeDriver { self.config.allow_driver_config, sandbox, )?; + openshell_core::resource_contract::validate_request( + sandbox, + Some(&Self::resource_capabilities()), + true, + ) + .map_err(tonic::Status::failed_precondition)?; self.config .resource_admission .validate() @@ -2359,6 +2387,12 @@ impl KubernetesComputeDriver { ) )] pub async fn create_sandbox(&self, sandbox: &Sandbox) -> Result { + openshell_core::resource_contract::validate_request( + sandbox, + Some(&Self::resource_capabilities()), + true, + ) + .map_err(|error| admission_error(tonic::Status::failed_precondition(error)))?; let span_status = openshell_otel::ErrorStatusGuard::current(); let _guard = self .lifecycle_gates @@ -11625,6 +11659,7 @@ mod tests { spec: Some(SandboxSpec { resource_requirements: Some(ResourceRequirements { gpu: Some(GpuResourceRequirements { count: Some(0) }), + ..Default::default() }), ..SandboxSpec::default() }), diff --git a/crates/openshell-driver-mxc/src/driver.rs b/crates/openshell-driver-mxc/src/driver.rs index 2772f1a69c..d2197b513d 100644 --- a/crates/openshell-driver-mxc/src/driver.rs +++ b/crates/openshell-driver-mxc/src/driver.rs @@ -480,12 +480,14 @@ impl MxcComputeBackend { openshell_core::extension_protocol::ExtensionFamily::Compute, "openshell/mxc", openshell_core::VERSION, - [], + [openshell_core::extension_protocol::COMPUTE_RESOURCE_CONTRACT.to_string()], )), } } fn validate_sandbox_fields(sandbox: &DriverSandbox) -> Result<(), tonic::Status> { + openshell_core::resource_contract::validate_request(sandbox, None, true) + .map_err(tonic::Status::failed_precondition)?; if let Some(spec) = &sandbox.spec { if effective_driver_gpu_count(driver_gpu_requirements( spec.resource_requirements.as_ref(), diff --git a/crates/openshell-driver-podman/src/container.rs b/crates/openshell-driver-podman/src/container.rs index 2f606dfa6e..bc5ea2a962 100644 --- a/crates/openshell-driver-podman/src/container.rs +++ b/crates/openshell-driver-podman/src/container.rs @@ -860,8 +860,38 @@ fn build_labels(sandbox: &DriverSandbox) -> BTreeMap { labels } -/// Parse resource limits from the sandbox template, falling back to defaults. -fn build_resource_limits(sandbox: &DriverSandbox, config: &PodmanComputeConfig) -> ResourceLimits { +/// Reject requests that must not silently become default limits. +pub fn validate_resource_limits(sandbox: &DriverSandbox) -> Result<(), String> { + let resources = sandbox + .spec + .as_ref() + .and_then(|s| s.template.as_ref()) + .and_then(|t| t.resources.as_ref()); + if let Some(resources) = resources { + if !resources.cpu_request.is_empty() || !resources.memory_request.is_empty() { + return Err("podman does not support resource reservations".into()); + } + if !resources.cpu_limit.is_empty() + && parse_cpu_to_microseconds(&resources.cpu_limit).is_none() + { + return Err("invalid podman CPU limit".into()); + } + if !resources.memory_limit.is_empty() + && parse_memory_to_bytes(&resources.memory_limit) + .is_none_or(|n| n == 0 || i64::try_from(n).is_err()) + { + return Err("invalid podman memory limit".into()); + } + } + Ok(()) +} + +/// Defaults apply only when a limit was not requested. +fn build_resource_limits( + sandbox: &DriverSandbox, + config: &PodmanComputeConfig, +) -> Result { + validate_resource_limits(sandbox).map_err(ComputeDriverError::Precondition)?; let resources = sandbox .spec .as_ref() @@ -878,14 +908,14 @@ fn build_resource_limits(sandbox: &DriverSandbox, config: &PodmanComputeConfig) .and_then(|r| parse_memory_to_bytes(&r.memory_limit)) .unwrap_or(DEFAULT_MEMORY_LIMIT); - ResourceLimits { + Ok(ResourceLimits { cpu: CpuLimits { quota: cpu_micros, period: DEFAULT_CPU_PERIOD, }, memory: MemoryLimits { limit: mem_bytes }, pids: podman_pids_limit(config.sandbox_pids_limit).map(|limit| PidsLimits { limit }), - } + }) } fn podman_pids_limit(value: Option) -> Option { @@ -1282,7 +1312,7 @@ fn build_base_spec( .map(|path| path.display().to_string()) .unwrap_or_default(), ); - let resource_limits = build_resource_limits(sandbox, config); + let resource_limits = build_resource_limits(sandbox, config)?; let user_mounts = podman_user_mounts(sandbox, config.enable_bind_mounts) .map_err(ComputeDriverError::InvalidArgument)?; driver_mounts::validate_workspace_mount_targets( @@ -1877,15 +1907,19 @@ fn parse_cpu_to_microseconds(quantity: &str) -> Option { millis.checked_mul(100)? } else { let cores: f64 = quantity.parse().ok()?; - if cores <= 0.0 || cores.is_nan() || cores.is_infinite() { + if cores <= 0.0 || !cores.is_finite() || cores * 100_000.0 >= 2_f64.powi(63) { return None; } #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] let val = (cores * 100_000.0) as u64; val }; - // A quota of 0 microseconds is invalid — treat as no limit. - if micros == 0 { None } else { Some(micros) } + // OCI CPU quotas are positive signed 64-bit microseconds. + if micros == 0 || i64::try_from(micros).is_err() { + None + } else { + Some(micros) + } } /// Parse a Kubernetes-style memory quantity to bytes. @@ -2065,6 +2099,59 @@ mod tests { let _ = std::fs::remove_file(artifact); } + #[test] + fn resource_limits_reject_invalid_values_and_reservations_without_defaults() { + use openshell_core::proto::compute::v1::{ + DriverResourceRequirements, DriverSandboxSpec, DriverSandboxTemplate, + }; + for resources in [ + DriverResourceRequirements { + cpu_limit: "garbage".into(), + ..Default::default() + }, + DriverResourceRequirements { + cpu_limit: "1e100".into(), + ..Default::default() + }, + DriverResourceRequirements { + cpu_limit: "92233720368547759m".into(), + ..Default::default() + }, + DriverResourceRequirements { + memory_limit: "9223372036854775808".into(), + ..Default::default() + }, + DriverResourceRequirements { + memory_limit: "garbage".into(), + ..Default::default() + }, + DriverResourceRequirements { + memory_limit: "0".into(), + ..Default::default() + }, + DriverResourceRequirements { + cpu_request: "1".into(), + ..Default::default() + }, + DriverResourceRequirements { + memory_request: "1Gi".into(), + ..Default::default() + }, + ] { + let sandbox = DriverSandbox { + spec: Some(DriverSandboxSpec { + template: Some(DriverSandboxTemplate { + resources: Some(resources), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }; + assert!(build_resource_limits(&sandbox, &test_config()).is_err()); + } + } + #[test] fn isolated_pair_keeps_privileges_network_and_secrets_out_of_workload() { let sandbox = DriverSandbox { @@ -2312,6 +2399,7 @@ mod tests { fn gpu_resources(count: Option) -> ResourceRequirements { ResourceRequirements { gpu: Some(GpuResourceRequirements { count }), + ..Default::default() } } @@ -2367,7 +2455,7 @@ mod tests { ..Default::default() }); let config = test_config(); - let limits = build_resource_limits(&sandbox, &config); + let limits = build_resource_limits(&sandbox, &config).unwrap(); assert_eq!(limits.cpu.quota, 50_000); assert_eq!(limits.memory.limit, 2 * 1024 * 1024 * 1024); @@ -2385,7 +2473,7 @@ mod tests { let sandbox = test_sandbox("test-id", "test-name"); let mut config = test_config(); config.sandbox_pids_limit = None; - let limits = build_resource_limits(&sandbox, &config); + let limits = build_resource_limits(&sandbox, &config).unwrap(); assert!(limits.pids.is_none()); } diff --git a/crates/openshell-driver-podman/src/driver.rs b/crates/openshell-driver-podman/src/driver.rs index c70cc6eca1..d58847798a 100644 --- a/crates/openshell-driver-podman/src/driver.rs +++ b/crates/openshell-driver-podman/src/driver.rs @@ -713,6 +713,36 @@ impl PodmanComputeDriver { } /// Report driver capabilities. + fn resource_capabilities() -> ResourceCapabilities { + ResourceCapabilities { + cpu: Some(CpuResourceCapabilities { + limit_supported: true, + limit_enforcement: Some(openshell_core::proto::sandbox::v1::ResourceEnforcement { + kind: openshell_core::proto::sandbox::v1::ResourceEnforcementKind::KernelLimit + as i32, + scope: openshell_core::proto::sandbox::v1::ResourceEnforcementScope::Container + as i32, + }), + ..Default::default() + }), + memory: Some(MemoryResourceCapabilities { + limit_supported: true, + limit_enforcement: Some(openshell_core::proto::sandbox::v1::ResourceEnforcement { + kind: openshell_core::proto::sandbox::v1::ResourceEnforcementKind::KernelLimit + as i32, + scope: openshell_core::proto::sandbox::v1::ResourceEnforcementScope::Container + as i32, + }), + ..Default::default() + }), + gpu: Some(GpuResourceCapabilities { + default_selection_supported: true, + count_selection_supported: true, + }), + sampled_budget: None, + } + } + pub fn capabilities(&self) -> Result { Ok(GetCapabilitiesResponse { resource_admission_policy: openshell_core::resource_admission::DriverAdmissionConfig { @@ -726,25 +756,14 @@ impl PodmanComputeDriver { gateway_manages_lifecycle: true, supports_sandbox_authentication: false, driver_reports_runtime_readiness: false, - resource_capabilities: Some(ResourceCapabilities { - cpu: Some(CpuResourceCapabilities { - limit_supported: true, - }), - memory: Some(MemoryResourceCapabilities { - limit_supported: true, - }), - gpu: Some(GpuResourceCapabilities { - default_selection_supported: true, - count_selection_supported: true, - }), - }), + resource_capabilities: Some(Self::resource_capabilities()), rootfs_tar_staging_dir: String::new(), rootfs_tar_max_bytes: 0, extension: Some(openshell_core::extension_protocol::extension_metadata( openshell_core::extension_protocol::ExtensionFamily::Compute, "openshell/podman", openshell_core::VERSION, - [], + [openshell_core::extension_protocol::COMPUTE_RESOURCE_CONTRACT.to_string()], )), }) } @@ -772,6 +791,13 @@ impl PodmanComputeDriver { sandbox, ) .map_err(|error| ComputeDriverError::Precondition(error.message().into()))?; + openshell_core::resource_contract::validate_request( + sandbox, + Some(&Self::resource_capabilities()), + true, + ) + .map_err(|error| ComputeDriverError::Precondition(error.into()))?; + container::validate_resource_limits(sandbox).map_err(ComputeDriverError::Precondition)?; let gpu_requirements = sandbox .spec .as_ref() @@ -2739,6 +2765,7 @@ mod tests { fn gpu_resources(count: Option) -> ResourceRequirements { ResourceRequirements { gpu: Some(GpuResourceRequirements { count }), + ..Default::default() } } diff --git a/crates/openshell-driver-vm/src/driver.rs b/crates/openshell-driver-vm/src/driver.rs index f7946c34c2..03ab10acf5 100644 --- a/crates/openshell-driver-vm/src/driver.rs +++ b/crates/openshell-driver-vm/src/driver.rs @@ -1534,6 +1534,24 @@ impl VmDriver { } #[must_use] + fn resource_capabilities(gpu_supported: bool) -> ResourceCapabilities { + ResourceCapabilities { + cpu: Some(CpuResourceCapabilities { + limit_supported: false, + ..Default::default() + }), + memory: Some(MemoryResourceCapabilities { + limit_supported: false, + ..Default::default() + }), + gpu: Some(GpuResourceCapabilities { + default_selection_supported: gpu_supported, + count_selection_supported: gpu_supported, + }), + sampled_budget: None, + } + } + pub fn capabilities(&self) -> GetCapabilitiesResponse { let mut extension = openshell_core::extension_protocol::extension_metadata( openshell_core::extension_protocol::ExtensionFamily::Compute, @@ -1544,6 +1562,9 @@ impl VmDriver { extension .required_capabilities .push(openshell_core::extension_protocol::COMPUTE_LAUNCH_AUTHENTICATION.to_string()); + extension + .supported_capabilities + .push(openshell_core::extension_protocol::COMPUTE_RESOURCE_CONTRACT.to_string()); GetCapabilitiesResponse { resource_admission_policy: openshell_core::resource_admission::DriverAdmissionConfig { allow_driver_config: self.config.allow_driver_config, @@ -1556,18 +1577,7 @@ impl VmDriver { gateway_manages_lifecycle: true, supports_sandbox_authentication: false, driver_reports_runtime_readiness: false, - resource_capabilities: Some(ResourceCapabilities { - cpu: Some(CpuResourceCapabilities { - limit_supported: false, - }), - memory: Some(MemoryResourceCapabilities { - limit_supported: false, - }), - gpu: Some(GpuResourceCapabilities { - default_selection_supported: self.config.gpu_enabled, - count_selection_supported: self.config.gpu_enabled, - }), - }), + resource_capabilities: Some(Self::resource_capabilities(self.config.gpu_enabled)), rootfs_tar_staging_dir: self .config .rootfs_tar_staging_dir() @@ -5541,6 +5551,8 @@ fn decode_launch_authentication( #[allow(clippy::result_large_err)] fn validate_vm_sandbox(sandbox: &Sandbox, gpu_enabled: bool) -> Result<(), Status> { + openshell_core::resource_contract::validate_request(sandbox, None, true) + .map_err(Status::failed_precondition)?; validate_sandbox_id(&sandbox.id)?; let spec = sandbox @@ -9575,6 +9587,7 @@ mod tests { fn gpu_resources(count: Option) -> ResourceRequirements { ResourceRequirements { gpu: Some(GpuResourceRequirements { count }), + ..Default::default() } } @@ -9946,7 +9959,7 @@ mod tests { } #[test] - fn validate_vm_sandbox_accepts_template_resources_as_noop() { + fn validate_vm_sandbox_rejects_unenforced_template_resources() { use openshell_core::proto::compute::v1::DriverResourceRequirements; let sandbox = Sandbox { @@ -9964,8 +9977,10 @@ mod tests { }), ..Default::default() }; - validate_vm_sandbox(&sandbox, false) - .expect("template.resources should be accepted and ignored"); + assert_eq!( + validate_vm_sandbox(&sandbox, false).unwrap_err().code(), + Code::FailedPrecondition + ); } #[test] diff --git a/crates/openshell-sdk/README.md b/crates/openshell-sdk/README.md index 001b2e9249..850200e110 100644 --- a/crates/openshell-sdk/README.md +++ b/crates/openshell-sdk/README.md @@ -212,3 +212,11 @@ These helpers use the raw client's authentication slot. They do not perform OIDC - Async-only. Tonic is async-native; callers needing a blocking call can wrap with their own runtime. - The curated surface will grow as more RPCs graduate from `raw`. + +## Next minor API migration + +This source-breaking API change targets the next minor release and must not be backported into the `0.1.x` patch series. + +Raw `HealthResponse`, public and compute resource requirements, resource capabilities and workload-template resource messages gain acknowledgement, enforcement and sampled-budget fields. Complete generated Rust literals must initialize the new fields or use `..Default::default()`. Complete TypeScript Health responses need `supportsSampledResourceBudget: false` or a generated message builder. + +Legacy wire fields retain their identities and new optional fields remain absent on old payloads. Before sending a budget-bearing write, check the Health acknowledgement for field preservation. The selected driver's admission separately checks enforcement. Upgrade gateway writers together before persisting budgets because older writers can discard them. diff --git a/crates/openshell-sdk/src/client.rs b/crates/openshell-sdk/src/client.rs index 1f03916ef2..11069b71dd 100644 --- a/crates/openshell-sdk/src/client.rs +++ b/crates/openshell-sdk/src/client.rs @@ -158,16 +158,21 @@ impl OpenShellClient { }) } + // Public Health acknowledges wire/admission support without requiring the + // elevated GetGatewayInfo permission. Driver support remains authoritative. + async fn require_sampled_budget_support(&self) -> Result<()> { + let health = self + .unary(|mut grpc| async move { grpc.health(proto::HealthRequest {}).await }) + .await?; + if !health.supports_sampled_resource_budget { + return Err(SdkError::InvalidConfig { message: "gateway does not acknowledge sampled resource budgets; upgrade it before creating this workload".into(), status: None }); + } + Ok(()) + } + /// Create a new sandbox from a curated [`SandboxSpec`]. pub async fn create_sandbox(&self, spec: SandboxSpec) -> Result { - let request = create_sandbox_request(spec); - let response = self - .unary(|mut grpc| { - let request = request.clone(); - async move { grpc.create_sandbox(request).await } - }) - .await?; - sandbox_from_create_response(response) + self.workspace("default").create_sandbox(spec).await } /// Create a new sandbox from a workspace-scoped workload template name. @@ -190,17 +195,9 @@ impl OpenShellClient { &self, template: SandboxWorkloadTemplate, ) -> Result { - let response = self - .unary(|mut grpc| { - let request = proto::CreateSandboxTemplateRequest { - request_id: String::new(), - template: Some(template.clone()), - workspace_scope: Some(proto::workspace_selector("default")), - }; - async move { grpc.create_sandbox_template(request).await } - }) - .await?; - sandbox_template_from_response(response.template) + self.workspace("default") + .create_sandbox_template(template) + .await } /// Fetch a reusable sandbox template by name from the default workspace. @@ -1005,6 +1002,9 @@ impl WorkspaceScopedClient { /// Create a new sandbox in this workspace. pub async fn create_sandbox(&self, spec: SandboxSpec) -> Result { + if spec.sampled_budget.is_some() { + self.client.require_sampled_budget_support().await?; + } let mut request = create_sandbox_request(spec); request.workspace_scope = Some(proto::workspace_selector(&self.workspace)); let response = self @@ -1039,6 +1039,16 @@ impl WorkspaceScopedClient { &self, template: SandboxWorkloadTemplate, ) -> Result { + if template + .spec + .as_ref() + .and_then(|s| s.workload.as_ref()) + .and_then(|w| w.resources.as_ref()) + .and_then(|r| r.sampled_budget.as_ref()) + .is_some() + { + self.client.require_sampled_budget_support().await?; + } let response = self .client .unary(|mut grpc| { @@ -1468,6 +1478,7 @@ fn create_sandbox_request(spec: SandboxSpec) -> proto::CreateSandboxRequest { environment, providers, gpu, + sampled_budget, command, tty, service_exposures, @@ -1477,9 +1488,11 @@ fn create_sandbox_request(spec: SandboxSpec) -> proto::CreateSandboxRequest { image, ..proto::SandboxTemplate::default() }); - let resource_requirements = gpu.then_some(proto::ResourceRequirements { - gpu: Some(proto::GpuResourceRequirements { count: None }), - }); + let resource_requirements = + (gpu || sampled_budget.is_some()).then_some(proto::ResourceRequirements { + gpu: gpu.then_some(proto::GpuResourceRequirements { count: None }), + sampled_budget, + }); proto::CreateSandboxRequest { request_id: String::new(), spec: Some(proto::SandboxSpec { diff --git a/crates/openshell-sdk/src/types.rs b/crates/openshell-sdk/src/types.rs index 571ee42b68..03f362e0eb 100644 --- a/crates/openshell-sdk/src/types.rs +++ b/crates/openshell-sdk/src/types.rs @@ -291,6 +291,8 @@ pub struct SandboxSpec { /// Request a GPU. Driver-specific device selection is configured via /// driver config on the raw proto surface (see [`crate::raw`]). pub gpu: bool, + /// Explicit sampled account-wide budgets, distinct from hard resource limits. + pub sampled_budget: Option, /// Exact canonical command. Empty selects the gateway's scratch login shell. pub command: Vec, /// Allocate a retained pseudo-terminal for the canonical command. diff --git a/crates/openshell-sdk/tests/client_mock.rs b/crates/openshell-sdk/tests/client_mock.rs index 1f3f791a08..e4b338bdc8 100644 --- a/crates/openshell-sdk/tests/client_mock.rs +++ b/crates/openshell-sdk/tests/client_mock.rs @@ -20,7 +20,7 @@ use openshell_sdk::{ }; use std::collections::HashMap; use std::sync::Arc; -use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::time::Duration; use tokio::net::TcpListener; use tokio::sync::Mutex; @@ -48,6 +48,8 @@ fn selects_all_workspaces(scope: &Option>, last_get_workspace: Mutex>, last_create: Mutex>, @@ -312,6 +314,9 @@ impl OpenShell for TestOpenShell { &self, request: tonic::Request, ) -> Result, Status> { + if self.state.health_failure.load(Ordering::SeqCst) { + return Err(Status::unimplemented("legacy health RPC")); + } if let Some(expected) = &self.state.require_bearer { let got = request .metadata() @@ -325,6 +330,7 @@ impl OpenShell for TestOpenShell { Ok(Response::new(proto::HealthResponse { status: proto::ServiceStatus::Healthy.into(), version: "test-1.2.3".to_string(), + supports_sampled_resource_budget: self.state.sampled_budget_ack.load(Ordering::SeqCst), })) } @@ -1241,11 +1247,16 @@ async fn create_sandbox_passes_spec_through() { let mut labels = HashMap::new(); labels.insert("team".to_string(), "core".to_string()); + let budget = proto::sandbox::v1::SampledResourceBudget { + cpu_time: Some(Duration::from_secs(2).try_into().unwrap()), + resident_bytes: Some(4096), + }; let spec = SandboxSpec { name: Some("my-box".to_string()), image: Some("ghcr.io/foo:bar".to_string()), labels: labels.clone(), gpu: true, + sampled_budget: Some(budget), service_exposures: vec![ServiceExposure { service: "web".to_string(), target_port: 8080, @@ -1254,6 +1265,13 @@ async fn create_sandbox_passes_spec_through() { ..Default::default() }; + assert!(client.create_sandbox(spec.clone()).await.is_err()); + assert!(state.last_create.lock().await.is_none()); + state.health_failure.store(true, Ordering::SeqCst); + assert!(client.create_sandbox(spec.clone()).await.is_err()); + assert!(state.last_create.lock().await.is_none()); + state.health_failure.store(false, Ordering::SeqCst); + state.sampled_budget_ack.store(true, Ordering::SeqCst); let result = client.create_sandbox(spec).await.unwrap(); assert_eq!(result.name, "my-box"); assert_eq!(result.phase, SandboxPhase::Provisioning); @@ -1274,6 +1292,14 @@ async fn create_sandbox_passes_spec_through() { proto::ServiceAuthorizationMode::BearerPassthrough ); let observed_spec = observed.spec.unwrap(); + let budget = observed_spec + .resource_requirements + .as_ref() + .unwrap() + .sampled_budget + .unwrap(); + assert_eq!(budget.cpu_time.unwrap().seconds, 2); + assert_eq!(budget.resident_bytes, Some(4096)); assert!( observed_spec .resource_requirements @@ -1326,10 +1352,30 @@ async fn sandbox_template_crud_uses_default_workspace() { let endpoint = start_mock(state.clone()).await; let client = connect(&endpoint).await; - let created = client - .create_sandbox_template(workload_template_proto("python", "")) - .await - .unwrap(); + let mut template = workload_template_proto("python", ""); + template + .spec + .as_mut() + .unwrap() + .workload + .as_mut() + .unwrap() + .resources = Some(proto::SandboxResources { + sampled_budget: Some(proto::sandbox::v1::SampledResourceBudget { + cpu_time: None, + resident_bytes: Some(4096), + }), + ..Default::default() + }); + assert!( + client + .create_sandbox_template(template.clone()) + .await + .is_err() + ); + assert!(state.last_template_create.lock().await.is_none()); + state.sampled_budget_ack.store(true, Ordering::SeqCst); + let created = client.create_sandbox_template(template).await.unwrap(); assert_eq!(created.metadata.as_ref().unwrap().name, "python"); let observed_create = state.last_template_create.lock().await.clone().unwrap(); @@ -2105,8 +2151,15 @@ async fn workspace_scoped_create_passes_workspace() { let ws = client.workspace("staging"); let spec = SandboxSpec { name: Some("my-box".to_string()), + sampled_budget: Some(proto::sandbox::v1::SampledResourceBudget { + cpu_time: None, + resident_bytes: Some(4096), + }), ..Default::default() }; + assert!(ws.create_sandbox(spec.clone()).await.is_err()); + assert!(state.last_create.lock().await.is_none()); + state.sampled_budget_ack.store(true, Ordering::SeqCst); let result = ws.create_sandbox(spec).await.unwrap(); assert_eq!(result.name, "my-box"); @@ -2185,9 +2238,25 @@ async fn workspace_scoped_sandbox_template_crud_passes_workspace() { let client = connect(&endpoint).await; let ws = client.workspace("staging"); - ws.create_sandbox_template(workload_template_proto("python", "staging")) - .await - .unwrap(); + let mut template = workload_template_proto("python", "staging"); + template + .spec + .as_mut() + .unwrap() + .workload + .as_mut() + .unwrap() + .resources = Some(proto::SandboxResources { + sampled_budget: Some(proto::sandbox::v1::SampledResourceBudget { + cpu_time: None, + resident_bytes: Some(4096), + }), + ..Default::default() + }); + assert!(ws.create_sandbox_template(template.clone()).await.is_err()); + assert!(state.last_template_create.lock().await.is_none()); + state.sampled_budget_ack.store(true, Ordering::SeqCst); + ws.create_sandbox_template(template).await.unwrap(); let observed_create = state.last_template_create.lock().await.clone().unwrap(); assert_eq!( selected_workspace(&observed_create.workspace_scope), diff --git a/crates/openshell-server/src/compute/image_update.rs b/crates/openshell-server/src/compute/image_update.rs index 4a19ee2a53..018c33230e 100644 --- a/crates/openshell-server/src/compute/image_update.rs +++ b/crates/openshell-server/src/compute/image_update.rs @@ -398,7 +398,9 @@ impl ComputeRuntime { // Each sandbox owns independent locks even though these // futures are polled concurrently in the same Tokio task. crate::persistence::lock_order::branch(async move { - if let Err(error) = self.recover_one_sandbox_update(&id).await { + // Resource input growth makes this recovery future exceed the + // stack-size lint budget; keep the existing await on the heap. + if let Err(error) = Box::pin(self.recover_one_sandbox_update(&id)).await { tracing::warn!(sandbox_id = %id, %error, "Cannot read pending sandbox update for recovery"); } @@ -970,15 +972,16 @@ mod tests { .get_sandbox_image_state(original.object_id()) .await .unwrap(); - runtime - .recover_one_sandbox_update(original.object_id()) - .await - .unwrap(); - runtime - .clone() - .recover_one_sandbox_update(original.object_id()) + Box::pin(runtime.recover_one_sandbox_update(original.object_id())) .await .unwrap(); + Box::pin( + runtime + .clone() + .recover_one_sandbox_update(original.object_id()), + ) + .await + .unwrap(); assert_eq!(image_calls(&driver).len(), 2); assert_eq!( runtime @@ -994,8 +997,7 @@ mod tests { let mut restarted = runtime.clone(); restarted.update_recovery_backoff = Arc::default(); driver.set_image_update_error(None); - restarted - .recover_one_sandbox_update(original.object_id()) + Box::pin(restarted.recover_one_sandbox_update(original.object_id())) .await .unwrap(); assert_eq!(image_calls(&driver).len(), 3); @@ -1018,8 +1020,7 @@ mod tests { .unwrap_err(); // The original runtime still remembers the previous operation's delay, // but the new operation must be eligible immediately. - runtime - .recover_one_sandbox_update(original.object_id()) + Box::pin(runtime.recover_one_sandbox_update(original.object_id())) .await .unwrap(); assert_eq!(image_calls(&driver).len(), 5); @@ -1029,8 +1030,7 @@ mod tests { ); driver.set_image_update_error(None); - restarted - .recover_one_sandbox_update(original.object_id()) + Box::pin(restarted.recover_one_sandbox_update(original.object_id())) .await .unwrap(); runtime.recover_pending_sandbox_updates().await.unwrap(); diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 81cbd7ca6f..8e61ec5811 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -685,7 +685,14 @@ impl AcquiredRemoteDriverEndpoint { struct RemoteComputeDriver { client: RemoteComputeDriverClient, configuration_updates_client: openshell_core::proto::compute::v1::compute_driver_configuration_updates_client::ComputeDriverConfigurationUpdatesClient>, - admission_acknowledgement: Arc>>, + admission_handshake: Arc>>, +} + +#[derive(Debug, Clone)] +struct RemoteAdmissionHandshake { + policy: String, + resource_contract: bool, + resources: Option, } type RemoteComputeDriverClient = ComputeDriverClient< @@ -697,7 +704,7 @@ impl RemoteComputeDriver { Self { client: ComputeDriverClient::with_interceptor(channel.clone(), TraceContextInterceptor), configuration_updates_client: openshell_core::proto::compute::v1::compute_driver_configuration_updates_client::ComputeDriverConfigurationUpdatesClient::with_interceptor(channel, TraceContextInterceptor), - admission_acknowledgement: Arc::new(StdMutex::new(None)), + admission_handshake: Arc::new(StdMutex::new(None)), } } @@ -707,7 +714,7 @@ impl RemoteComputeDriver { async fn verify_admission_policy(&self, external_isolation: bool) -> Result<(), Status> { let expected = self - .admission_acknowledgement + .admission_handshake .lock() .map_err(|_| Status::internal("admission handshake lock poisoned"))? .clone() @@ -728,15 +735,36 @@ impl RemoteComputeDriver { "remote compute driver no longer supports isolation backend registration; restart the gateway after upgrading the driver", )); } - if current.resource_admission_policy != expected { + if current.resource_admission_policy != expected.policy { return Err(Status::failed_precondition( "remote driver admission policy changed; restart gateway after configuring matching policy", )); } + verify_resource_contract(expected.resource_contract, &expected.resources, ¤t)?; Ok(()) } } +fn verify_resource_contract( + negotiated: bool, + resources: &Option, + current: &openshell_core::proto::compute::v1::GetCapabilitiesResponse, +) -> Result<(), Status> { + if negotiated + && (!current.extension.as_ref().is_some_and(|metadata| { + metadata + .supported_capabilities + .iter() + .any(|cap| cap == openshell_core::extension_protocol::COMPUTE_RESOURCE_CONTRACT) + }) || ¤t.resource_capabilities != resources) + { + return Err(Status::failed_precondition( + "compute resource contract changed; restart the gateway after updating the driver", + )); + } + Ok(()) +} + #[tonic::async_trait] impl openshell_core::compute::ComputeDriver for RemoteComputeDriver { async fn update_sandbox_configuration( @@ -765,10 +793,18 @@ impl ComputeDriver for RemoteComputeDriver { { let mut client = self.client(); let response = client.get_capabilities(request).await?; - self.admission_acknowledgement + self.admission_handshake .lock() .map_err(|_| Status::internal("admission handshake lock poisoned"))? - .get_or_insert_with(|| response.get_ref().resource_admission_policy.clone()); + .get_or_insert_with(|| RemoteAdmissionHandshake { + policy: response.get_ref().resource_admission_policy.clone(), + resource_contract: response.get_ref().extension.as_ref().is_some_and(|e| { + e.supported_capabilities + .iter() + .any(|c| c == openshell_core::extension_protocol::COMPUTE_RESOURCE_CONTRACT) + }), + resources: response.get_ref().resource_capabilities, + }); Ok(response) } @@ -1262,9 +1298,52 @@ impl ComputeRuntime { if let Some(spec) = driver_sandbox.spec.as_mut() { spec.isolation_backend.clone_from(&self.isolation_backend); } + openshell_core::resource_contract::validate_request( + &driver_sandbox, + self.driver_info.resource_capabilities.as_ref(), + self.driver_info + .negotiated_extension + .supported_capabilities + .iter() + .any(|cap| cap == openshell_core::extension_protocol::COMPUTE_RESOURCE_CONTRACT), + ) + .map_err(Status::failed_precondition)?; Ok(driver_sandbox) } + /// Verify current guarantees before claiming lifecycle work or consuming staging. + async fn admit_driver_sandbox_for_launch( + &self, + sandbox: &Sandbox, + ) -> Result { + let projected = self.driver_sandbox_for_launch(sandbox)?; + let negotiated = self + .driver_info + .negotiated_extension + .supported_capabilities + .iter() + .any(|cap| cap == openshell_core::extension_protocol::COMPUTE_RESOURCE_CONTRACT); + if negotiated { + let current = self + .driver + .call( + openshell_otel::rpc::GET_CAPABILITIES, + Some(sandbox.object_id()), + |driver| async move { + driver + .get_capabilities(Request::new(GetCapabilitiesRequest { + gateway: Some(gateway_metadata(ExtensionFamily::Compute)), + })) + .await + }, + ) + .await? + .into_inner(); + verify_resource_contract(true, &self.driver_info.resource_capabilities, ¤t)?; + } + Ok(projected) + } + fn driver_start_request( &self, sandbox: &Sandbox, @@ -1458,7 +1537,7 @@ impl ComputeRuntime { .as_ref() .and_then(|spec| spec.template.as_ref()), )?; - let mut driver_sandbox = self.driver_sandbox_for_launch(sandbox)?; + let mut driver_sandbox = self.admit_driver_sandbox_for_launch(sandbox).await?; if let Some(spec) = driver_sandbox.spec.as_mut() { spec.await_main_process_attachment = await_main_process_attachment; } @@ -1623,6 +1702,7 @@ impl ComputeRuntime { .as_ref() .and_then(|spec| spec.template.as_ref()), )?; + self.admit_driver_sandbox_for_launch(&sandbox).await?; let sandbox_id = sandbox.object_id().to_string(); let mut sandbox = sandbox; let metadata = sandbox @@ -2403,6 +2483,7 @@ impl ComputeRuntime { if phase == SandboxPhase::Ready { return Ok(current); } + self.admit_driver_sandbox_for_launch(¤t).await?; let provisioning_timeout = phase == SandboxPhase::Error && provisioning_deadline::timed_out(¤t); if provisioning_timeout @@ -4291,6 +4372,11 @@ impl ComputeRuntime { continue; } + if let Err(error) = self.admit_driver_sandbox_for_launch(&sandbox).await { + warn!(sandbox_id, %error, "Rejected changed recovered resource guarantees"); + failed += 1; + continue; + } if let Err(error) = sandbox_runtime_generation(&sandbox) { warn!(sandbox_id, %error, "Persisted sandbox runtime identity is invalid"); authentication_failed(sandbox.object_id()); @@ -4563,6 +4649,10 @@ impl ComputeRuntime { .await; continue; } + if let Err(error) = self.admit_driver_sandbox_for_launch(&sandbox).await { + warn!(%error, "Rejected changed pending resource guarantees"); + continue; + } let sandbox_id = sandbox.object_id().to_string(); let expected_runtime_identity = sandbox_compute_runtime_identity(&sandbox); let launch_authentication = if self.isolation_backend.is_some() { @@ -5047,6 +5137,9 @@ impl ComputeRuntime { let sandbox_name = current.object_name().to_string(); self.validate_isolation_backend_binding(¤t) .map_err(|error| error.to_string())?; + self.admit_driver_sandbox_for_launch(¤t) + .await + .map_err(|error| error.to_string())?; let already_claimed = current .status .as_ref() @@ -7137,6 +7230,7 @@ fn driver_sandbox_spec_from_public( policy: spec.policy.clone(), resource_requirements: spec.resource_requirements.as_ref().map(|requirements| { DriverSandboxResourceRequirements { + sampled_budget: requirements.sampled_budget, gpu: requirements .gpu .as_ref() @@ -8767,6 +8861,7 @@ mod tests { let public = SandboxSpec { resource_requirements: Some(openshell_core::proto::ResourceRequirements { gpu: Some(openshell_core::proto::GpuResourceRequirements { count: Some(2) }), + ..Default::default() }), ..Default::default() }; @@ -12629,6 +12724,7 @@ mod tests { Some(&SandboxSpec { resource_requirements: Some(openshell_core::proto::ResourceRequirements { gpu: Some(openshell_core::proto::GpuResourceRequirements { count: None }), + ..Default::default() }), ..Default::default() }), @@ -16739,6 +16835,7 @@ mod tests { spec: Some(SandboxSpec { resource_requirements: Some(openshell_core::proto::ResourceRequirements { gpu: Some(openshell_core::proto::GpuResourceRequirements { count: None }), + ..Default::default() }), ..Default::default() }), @@ -18457,61 +18554,213 @@ endpoint = { kind = "unix", socket_path = "/run/agent-substrate/isolation.sock" ); } + #[tokio::test] + #[cfg(unix)] + async fn retained_resource_refusal_precedes_lifecycle_claim_and_stop() { + use crate::test_support::{FakeComputeDriver, FakeComputeDriverCall}; + use openshell_core::proto::sandbox::v1::{ + ResourceEnforcement, ResourceEnforcementKind, ResourceEnforcementScope, + }; + for remove_contract in [false, true] { + for mode in 0..4 { + let directory = tempfile::tempdir().unwrap(); + let socket = directory.path().join("resource-drift.sock"); + let driver = FakeComputeDriver::new().with_gateway_manages_lifecycle(); + driver.set_resource_contract( + true, + Some(DriverResourceCapabilities { + cpu: Some( + openshell_core::proto::compute::v1::CpuResourceCapabilities { + limit_supported: true, + limit_enforcement: Some(ResourceEnforcement { + kind: ResourceEnforcementKind::KernelLimit as i32, + scope: ResourceEnforcementScope::Container as i32, + }), + ..Default::default() + }, + ), + ..Default::default() + }), + ); + let _server = driver.serve_uds(&socket).unwrap(); + let endpoint = connect_remote_compute_driver("external-test", &socket) + .await + .unwrap(); + let store = Arc::new(crate::persistence::test_store().await); + crate::ensure_default_workspace(&store).await.unwrap(); + let runtime = ComputeRuntime::new_remote_driver( + endpoint, + store, + SandboxIndex::new(), + SandboxWatchBus::new(), + TracingLogBus::new(), + Arc::new(SupervisorSessionRegistry::new()), + ) + .await + .unwrap(); + driver.set_resource_contract(!remove_contract, None); + driver.clear_calls(); + let mut sandbox = sandbox_record("retained", "retained", SandboxPhase::Stopped); + sandbox.spec = Some(SandboxSpec { + template: Some(SandboxTemplate { + resources: Some( + openshell_core::proto_struct::json_object_to_struct( + serde_json::json!({"limits": {"cpu": "1"}}) + .as_object() + .unwrap() + .clone(), + ) + .unwrap(), + ), + ..Default::default() + }), + restart_policy: SandboxRestartPolicy::Always as i32, + ..Default::default() + }); + if mode == 1 { + apply_main_process_restart( + &mut sandbox, + "previous-main", + 9, + openshell_core::time::now_ms() + .saturating_sub(RESTART_TERMINAL_DELIVERY_GRACE_MS + 1), + ); + } + if mode == 2 { + sandbox.set_phase(SandboxPhase::Ready as i32); + } + if mode == 3 { + sandbox.set_phase(SandboxPhase::Starting as i32); + } + runtime.store.put_message(&sandbox).await.unwrap(); + let before = runtime + .store + .get_message::("retained") + .await + .unwrap() + .unwrap(); + match mode { + 0 => { + let error = runtime + .start_sandbox("default", "retained") + .await + .unwrap_err(); + assert_eq!(error.code(), Code::FailedPrecondition); + assert!(error.message().contains("resource contract changed")); + } + 1 => { + let error = runtime + .restart_sandbox_runtime("retained") + .await + .unwrap_err(); + assert!(error.contains("resource contract changed"), "{error}"); + } + 2 => { + runtime + .start_persisted_sandboxes_with_authentication( + |_| async { + panic!( + "changed guarantees must precede recovered authentication" + ) + }, + |_| async { Ok(()) }, + |_| {}, + ) + .await + .unwrap(); + } + _ => { + runtime + .recover_persisted_lifecycle_transitions() + .await + .unwrap(); + } + } + let after = runtime + .store + .get_message::("retained") + .await + .unwrap() + .unwrap(); + assert_eq!( + after, before, + "refusal must preserve generation, lifecycle, and restart schedule" + ); + assert_eq!( + driver.calls(), + vec![FakeComputeDriverCall::GetCapabilities], + "current guarantees must be checked before driver STOP/START" + ); + } + } + } + #[tokio::test] #[cfg(unix)] async fn remote_compute_driver_rejects_policy_changes_before_forwarding() { use crate::test_support::{FakeComputeDriver, FakeComputeDriverCall}; - let dir = tempfile::tempdir().unwrap(); - let socket_path = dir.path().join("admission.sock"); - let driver = FakeComputeDriver::new(); - let _server = driver.serve_uds(&socket_path).unwrap(); - let endpoint = connect_remote_compute_driver("external-test", &socket_path) - .await - .unwrap(); - let remote = RemoteComputeDriver::new(endpoint.channel); - remote - .get_capabilities(Request::new(GetCapabilitiesRequest { - gateway: Some(gateway_metadata(ExtensionFamily::Compute)), - })) - .await - .unwrap(); - driver.set_admission_acknowledgement(String::new()); - driver.clear_calls(); - let sandbox = DriverSandbox::default(); - let status = remote - .validate_sandbox_create(Request::new(ValidateSandboxCreateRequest { - sandbox: Some(sandbox.clone()), - })) - .await - .unwrap_err(); - assert_eq!(status.code(), Code::FailedPrecondition); - assert!( + for change in 0..3 { + let dir = tempfile::tempdir().unwrap(); + let socket_path = dir.path().join("admission.sock"); + let driver = FakeComputeDriver::new(); + driver.set_resource_contract(true, Some(DriverResourceCapabilities::default())); + let _server = driver.serve_uds(&socket_path).unwrap(); + let endpoint = connect_remote_compute_driver("external-test", &socket_path) + .await + .unwrap(); + let remote = RemoteComputeDriver::new(endpoint.channel); remote - .create_sandbox(Request::new(CreateSandboxRequest { - sandbox: Some(sandbox) + .get_capabilities(Request::new(GetCapabilitiesRequest { + gateway: Some(gateway_metadata(ExtensionFamily::Compute)), })) .await - .is_err() - ); - assert!( - remote - .start_sandbox(Request::new(StartSandboxRequest::default())) + .unwrap(); + match change { + 0 => driver.set_admission_acknowledgement(String::new()), + 1 => { + driver + .set_resource_contract(false, Some(DriverResourceCapabilities::default())); + } + _ => driver.set_resource_contract(true, None), + } + driver.clear_calls(); + let sandbox = DriverSandbox::default(); + let status = remote + .validate_sandbox_create(Request::new(ValidateSandboxCreateRequest { + sandbox: Some(sandbox.clone()), + })) .await - .is_err() - ); - let status = openshell_core::compute::ComputeDriver::update_sandbox_configuration( - &remote, - Request::new( - openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest::default(), - ), - ) - .await - .unwrap_err(); - assert_eq!(status.code(), Code::FailedPrecondition); - assert_eq!( - driver.calls(), - vec![FakeComputeDriverCall::GetCapabilities; 4] - ); + .unwrap_err(); + assert_eq!(status.code(), Code::FailedPrecondition); + assert!( + remote + .create_sandbox(Request::new(CreateSandboxRequest { + sandbox: Some(sandbox) + })) + .await + .is_err() + ); + assert!( + remote + .start_sandbox(Request::new(StartSandboxRequest::default())) + .await + .is_err() + ); + let status = openshell_core::compute::ComputeDriver::update_sandbox_configuration( + &remote, + Request::new( + openshell_core::proto::compute::v1::UpdateSandboxConfigurationRequest::default( + ), + ), + ) + .await + .unwrap_err(); + assert_eq!(status.code(), Code::FailedPrecondition); + assert_eq!( + driver.calls(), + vec![FakeComputeDriverCall::GetCapabilities; 4] + ); + } } #[tokio::test] diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index d066845fa4..43c0d36d0f 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -261,6 +261,7 @@ impl OpenShell for OpenShellService { Ok(Response::new(HealthResponse { status: ServiceStatus::Healthy.into(), version: openshell_core::VERSION.to_string(), + supports_sampled_resource_budget: true, })) } @@ -970,14 +971,19 @@ fn public_resource_capabilities( resources: openshell_core::proto::compute::v1::ResourceCapabilities, ) -> ResourceCapabilities { ResourceCapabilities { + sampled_budget: resources.sampled_budget, cpu: resources.cpu.as_ref().map(|cpu| CpuResourceCapabilities { limit_supported: cpu.limit_supported, + limit_enforcement: cpu.limit_enforcement, + request_enforcement: cpu.request_enforcement, }), memory: resources .memory .as_ref() .map(|memory| MemoryResourceCapabilities { limit_supported: memory.limit_supported, + limit_enforcement: memory.limit_enforcement, + request_enforcement: memory.request_enforcement, }), gpu: resources.gpu.as_ref().map(|gpu| GpuResourceCapabilities { default_selection_supported: gpu.default_selection_supported, @@ -1315,19 +1321,33 @@ mod tests { let driver_capabilities = DriverResourceCapabilities { cpu: Some(DriverCpuResourceCapabilities { limit_supported: true, + limit_enforcement: Some(openshell_core::proto::sandbox::v1::ResourceEnforcement { + kind: openshell_core::proto::sandbox::v1::ResourceEnforcementKind::KernelLimit + as i32, + scope: openshell_core::proto::sandbox::v1::ResourceEnforcementScope::Container + as i32, + }), + ..Default::default() }), memory: Some(DriverMemoryResourceCapabilities { limit_supported: false, + ..Default::default() }), gpu: Some(DriverGpuResourceCapabilities { default_selection_supported: true, count_selection_supported: true, }), + sampled_budget: None, }; let capabilities = public_resource_capabilities(driver_capabilities); - assert!(capabilities.cpu.expect("CPU capabilities").limit_supported); + let cpu = capabilities.cpu.expect("CPU capabilities"); + assert!(cpu.limit_supported); + assert_eq!( + cpu.limit_enforcement.unwrap().scope, + openshell_core::proto::sandbox::v1::ResourceEnforcementScope::Container as i32 + ); assert!( !capabilities .memory diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index a797da2589..6d4d58123a 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -980,15 +980,20 @@ fn sandbox_spec_from_workload_template( driver_config: spec.driver_config.clone(), ..SandboxTemplate::default() }), - resource_requirements: resources.and_then(template_gpu_requirements), + resource_requirements: resources.and_then(template_typed_resource_requirements), ..SandboxSpec::default() }) } -fn template_gpu_requirements(resources: &SandboxResources) -> Option { - Some(ResourceRequirements { - gpu: Some(resources.gpu?), - }) +fn template_typed_resource_requirements( + resources: &SandboxResources, +) -> Option { + (resources.gpu.is_some() || resources.sampled_budget.is_some()).then_some( + ResourceRequirements { + gpu: resources.gpu, + sampled_budget: resources.sampled_budget, + }, + ) } fn template_resource_struct(resources: &SandboxResources) -> Option { @@ -4302,6 +4307,158 @@ mod tests { GpuResourceRequirements, SandboxServiceExposure, ServiceAuthorizationMode, ServiceEndpoint, }; + #[tokio::test] + async fn sampled_resource_contract_is_enforced_before_create() { + use crate::test_support::{FakeComputeDriver, FakeComputeDriverCall}; + use openshell_core::extension_protocol::{ + COMPUTE_RESOURCE_CONTRACT, ExtensionFamily, extension_metadata, + }; + use openshell_core::proto::compute::v1::{GetCapabilitiesResponse, ResourceCapabilities}; + use openshell_core::proto::sandbox::v1::{ + ResourceEnforcementScope, SampledResourceBudget, SampledResourceBudgetCapabilities, + }; + let budget = SampledResourceBudget { + cpu_time: Some(prost_types::Duration { + seconds: 2, + nanos: 0, + }), + resident_bytes: Some(512 * 1024 * 1024), + }; + let caps = ResourceCapabilities { + sampled_budget: Some(SampledResourceBudgetCapabilities { + cpu_time_supported: true, + resident_bytes_supported: true, + scope: ResourceEnforcementScope::SandboxAccount as i32, + sampling_interval: Some(prost_types::Duration { + seconds: 0, + nanos: 100_000_000, + }), + terminate_on_excess: true, + cpu_time_resolution: Some(prost_types::Duration { + seconds: 0, + nanos: 1_000_000, + }), + per_process_cpu_limit: true, + }), + ..Default::default() + }; + for (modern, capabilities, cpu, memory, sampled, accepted) in [ + (true, Some(caps), "", "", Some(budget), true), + (true, Some(caps), "1", "", None, false), + (true, Some(caps), "", "512Mi", None, false), + (true, Some(caps), "1", "", Some(budget), false), + (true, None, "", "", Some(budget), false), + (false, Some(caps), "", "", Some(budget), false), + (false, None, "1", "", None, true), + ] { + let mut state = test_server_state().await; + let driver = Arc::new( + FakeComputeDriver::new().with_capabilities(GetCapabilitiesResponse { + driver_name: "test".into(), + default_image: "test/image".into(), + resource_capabilities: capabilities, + resource_admission_policy: + openshell_core::resource_admission::DriverAdmissionConfig::default() + .acknowledgement(), + extension: Some(extension_metadata( + ExtensionFamily::Compute, + "openshell/test", + "test", + if modern { + vec![COMPUTE_RESOURCE_CONTRACT.into()] + } else { + vec![] + }, + )), + ..Default::default() + }), + ); + let compute = crate::compute::ComputeRuntime::from_driver( + "test".into(), + driver.clone(), + None, + state.store.clone(), + crate::sandbox_index::SandboxIndex::new(), + crate::sandbox_watch::SandboxWatchBus::new(), + crate::tracing_bus::TracingLogBus::new(), + Arc::new(crate::supervisor_session::SupervisorSessionRegistry::new()), + ) + .await + .unwrap(); + Arc::get_mut(&mut state).unwrap().compute = compute; + // Exercise the typed workload-template path as well as public-to-driver mapping. + let resources = SandboxResources { + cpu: cpu.into(), + memory: memory.into(), + sampled_budget: sampled, + ..Default::default() + }; + let spec = SandboxSpec { + template: Some(SandboxTemplate { + resources: template_resource_struct(&resources), + ..Default::default() + }), + resource_requirements: template_typed_resource_requirements(&resources), + policy: Some(openshell_policy::restrictive_default_policy()), + ..Default::default() + }; + let result = handle_create_sandbox( + &state, + authed_request(CreateSandboxRequest { + name: "budget".into(), + spec: Some(spec), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + ..Default::default() + }), + ) + .await; + assert_eq!( + result.is_ok(), + accepted, + "modern={modern}, cpu={cpu}, memory={memory}" + ); + let requests: Vec<_> = driver + .calls() + .into_iter() + .filter_map(|call| match call { + FakeComputeDriverCall::ValidateSandboxCreate { sandbox } + | FakeComputeDriverCall::CreateSandbox { sandbox } => Some(sandbox.unwrap()), + _ => None, + }) + .collect(); + assert_eq!(requests.len(), if accepted { 2 } else { 0 }); + let stored = state + .store + .get_message_by_name::("default", "budget") + .await + .unwrap(); + assert_eq!(stored.is_some(), accepted); + if accepted && sampled.is_some() { + for request in requests { + assert_eq!( + request + .spec + .unwrap() + .resource_requirements + .unwrap() + .sampled_budget, + sampled + ); + } + assert_eq!( + stored + .unwrap() + .spec + .unwrap() + .resource_requirements + .unwrap() + .sampled_budget, + sampled + ); + } + } + } + #[tokio::test] async fn missing_launch_signer_precedes_driver_validation_and_preserves_staged_archive() { let directory = tempfile::tempdir().unwrap(); @@ -4466,6 +4623,7 @@ mod tests { policy: Some(openshell_core::proto::SandboxPolicy::default()), resource_requirements: Some(ResourceRequirements { gpu: Some(GpuResourceRequirements { count: Some(1) }), + ..Default::default() }), ..SandboxSpec::default() }), @@ -5196,6 +5354,7 @@ mod tests { cpu: "2".to_string(), memory: "4Gi".to_string(), gpu: Some(GpuResourceRequirements { count: Some(1) }), + ..Default::default() }), }), driver_config: None, diff --git a/crates/openshell-server/src/grpc/validation.rs b/crates/openshell-server/src/grpc/validation.rs index d84488bcca..aaf8f5b28e 100644 --- a/crates/openshell-server/src/grpc/validation.rs +++ b/crates/openshell-server/src/grpc/validation.rs @@ -312,6 +312,15 @@ fn validate_main_process_command(command: &[String]) -> Result<(), Status> { } fn validate_gpu_request_fields(spec: &SandboxSpec) -> Result<(), Status> { + if let Some(budget) = spec + .resource_requirements + .as_ref() + .and_then(|r| r.sampled_budget.as_ref()) + { + openshell_core::resource_contract::validate_sampled_budget(budget).map_err(|error| { + invalid_argument("spec.resource_requirements.sampled_budget", error) + })?; + } if openshell_core::gpu::sandbox_gpu_count(spec.resource_requirements.as_ref()) == Some(0) { return Err(invalid_argument( "spec.resource_requirements.gpu.count", @@ -1242,6 +1251,7 @@ mod tests { let spec = SandboxSpec { resource_requirements: Some(openshell_core::proto::ResourceRequirements { gpu: Some(openshell_core::proto::GpuResourceRequirements { count: None }), + ..Default::default() }), ..Default::default() }; @@ -1253,6 +1263,7 @@ mod tests { let spec = SandboxSpec { resource_requirements: Some(openshell_core::proto::ResourceRequirements { gpu: Some(openshell_core::proto::GpuResourceRequirements { count: Some(2) }), + ..Default::default() }), ..Default::default() }; @@ -1264,6 +1275,7 @@ mod tests { let spec = SandboxSpec { resource_requirements: Some(openshell_core::proto::ResourceRequirements { gpu: Some(openshell_core::proto::GpuResourceRequirements { count: Some(0) }), + ..Default::default() }), ..Default::default() }; diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index 5f7d87555b..6fcfb47990 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -178,11 +178,11 @@ mod tests { // image wire identity, now with explicit presence. Compute configuration // updates use a separate capability/service and add one classified RPC. const PUBLIC_RPC_SCHEMA_SHA256: &str = - "33e32bb6cb2251b3e75c6425555502e6c2fd675b640094437ef06856a46866e3"; + "0f59872712ab077a9cf9f3e44fae8c47c0bc2dc5befba274449f81f681b812c3"; const DURABLE_SCHEMA_SHA256: &str = - "cb15f8f79a855064a43fe8c6cd908bf07eca6604e2bf8ab7d2de0ef33e97da4e"; + "3716f9d2b96cdfc2dc92d6825a3729e3d695d7d7e1dd84733369b5c38dc91342"; const PUBLIC_DURABLE_OVERLAP_SHA256: &str = - "c0c3ea17f105aaa6d843c466fd4240451c2105dce7675384935d4aaecdac8abf"; + "a2ce7f9483846f81e024bdf31dc944bdd109aaaae294d8b3c58eac3ca5079f83"; // Encoded with the schema that omitted stable_placeholder. Keep these // bytes fixed so the current encoder cannot hide a compatibility change. const LEGACY_STATIC_PROFILE: &str = "0a240a116c65676163792d70726f66696c652d6964120d6c65676163792d737461746963280712510a0d6c65676163792d73746174696312184c6567616379207374617469632063726564656e7469616c2a260a076170695f6b65791a1153594e5448455449435f4150495f4b455920012a06626561726572"; @@ -723,9 +723,9 @@ mod tests { overlap_hash.as_str(), ), ( - (335, 31), - (100, 22), - (86, 22), + (338, 33), + (101, 22), + (87, 22), PUBLIC_RPC_SCHEMA_SHA256, DURABLE_SCHEMA_SHA256, PUBLIC_DURABLE_OVERLAP_SHA256 @@ -786,6 +786,11 @@ mod tests { assert!(!crate::policy_store::permits_initial_static_policy_repair( &sandbox )); + let resources = openshell_core::proto::ResourceRequirements::decode(&[10, 0][..]).unwrap(); + assert!(resources.gpu.is_some()); + assert!(resources.sampled_budget.is_none()); + let health = openshell_core::proto::HealthResponse::decode(&[8, 1][..]).unwrap(); + assert!(!health.supports_sampled_resource_budget); assert_eq!(sandbox.encode_to_vec(), bytes); } diff --git a/crates/openshell-server/src/test_support.rs b/crates/openshell-server/src/test_support.rs index e12bd617a9..1b238c8a08 100644 --- a/crates/openshell-server/src/test_support.rs +++ b/crates/openshell-server/src/test_support.rs @@ -267,6 +267,22 @@ impl FakeComputeDriver { self.with_state(|state| state.calls.clear()); } + pub fn set_resource_contract( + &self, + supported: bool, + resources: Option, + ) { + self.with_state(|state| { + let metadata = state.capabilities.extension.as_mut().unwrap(); + let capability = openshell_core::extension_protocol::COMPUTE_RESOURCE_CONTRACT; + metadata.supported_capabilities.retain(|c| c != capability); + if supported { + metadata.supported_capabilities.push(capability.into()); + } + state.capabilities.resource_capabilities = resources; + }); + } + pub fn set_admission_acknowledgement(&self, acknowledgement: String) { self.with_state(|state| state.capabilities.resource_admission_policy = acknowledgement); } diff --git a/crates/openshell-server/tests/common/mod.rs b/crates/openshell-server/tests/common/mod.rs index dd91664d39..032c172968 100644 --- a/crates/openshell-server/tests/common/mod.rs +++ b/crates/openshell-server/tests/common/mod.rs @@ -124,6 +124,7 @@ impl OpenShell for TestOpenShell { Ok(Response::new(HealthResponse { status: ServiceStatus::Healthy.into(), version: "test".to_string(), + supports_sampled_resource_budget: false, })) } diff --git a/docs/extensibility/drivers.mdx b/docs/extensibility/drivers.mdx index 67f9cd7b51..bd894cbba1 100644 --- a/docs/extensibility/drivers.mdx +++ b/docs/extensibility/drivers.mdx @@ -63,6 +63,44 @@ active driver and negotiated capabilities. See the [Extension Driver configuration reference](/how-it-works/gateways/configuration#extension-driver) for configuration details. +### Resource Guarantees + +`openshell gateway info` reports resource enforcement for the configured compute +driver. CPU-rate and hard memory limits require aggregate kernel enforcement; +scheduler requests describe placement admission, not dedicated physical capacity. +Docker and Podman enforce container limits and reject reservations. Kubernetes +supports container limits and scheduler requests. VM and MXC reject these +portable requests until their drivers can enforce them. + +Drivers advertising `openshell.compute.resource-contract` must describe the +mechanism and scope of accepted limits and requests. Missing, unknown, or +per-process enforcement cannot satisfy an aggregate hard limit. Legacy drivers +retain their existing behavior and report absent descriptors as unreported. + +Sampled budgets are a separate opt-in request. For a driver that supports them: + +```shell +openshell sandbox template create bounded-worker \ + --cpu-time-budget 30s \ + --resident-memory-budget-bytes 1073741824 +openshell sandbox create --template bounded-worker +``` + +The descriptor reports account-wide observation, the interval between completed +samples, termination on excess, CPU-time resolution, and any companion +per-process CPU limit. Collection adds latency; short-lived processes can escape +observation and overshoot has no fixed bound. These budgets do not satisfy a +hard CPU-rate or memory quota. First-party container and VM drivers currently +reject sampled budgets; external drivers may provide them. + +Before writing budget-bearing requests or templates, curated SDKs and the CLI +require `HealthResponse.supports_sampled_resource_budget`. This public protocol +acknowledgement prevents older gateways from silently discarding the request; +it does not promise driver support. Raw API callers must check it too. Use a +homogeneous upgraded gateway deployment: a probe cannot make mixed-version +load-balancer routing atomic. The gateway and driver remain responsible for +resource admission before provisioning and on retained launches. + ## Credential Drivers Credential drivers store provider credentials behind opaque handles. The @@ -88,3 +126,9 @@ Drivers exchange peer metadata with the gateway and advertise their extension family capability. Upgrade both peers together when the protocol version changes. Refer to [Extension Protocol Negotiation](/extensibility/overview#protocol-negotiation) for the negotiation contract. + +## Release compatibility + +These API changes target the next minor release and must not be backported into the `0.1.x` patch series. Upgrade gateway replicas together before sending sampled budgets. An older replica can discard budget fields even if another replica acknowledged support. + +For raw Rust and TypeScript field changes and defaults, see the [SDK migration notes](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-sdk/README.md#next-minor-api-migration). diff --git a/proto/compute_driver.proto b/proto/compute_driver.proto index e50f41d0ee..9ad0c66ff0 100644 --- a/proto/compute_driver.proto +++ b/proto/compute_driver.proto @@ -168,16 +168,23 @@ message ResourceCapabilities { CpuResourceCapabilities cpu = 1; MemoryResourceCapabilities memory = 2; GpuResourceCapabilities gpu = 3; + openshell.sandbox.v1.SampledResourceBudgetCapabilities sampled_budget = 4; } message CpuResourceCapabilities { // The driver accepts and enforces a portable CPU limit. bool limit_supported = 1; + // Required for a negotiated resource contract. Absence means unreported. + openshell.sandbox.v1.ResourceEnforcement limit_enforcement = 2; + openshell.sandbox.v1.ResourceEnforcement request_enforcement = 3; } message MemoryResourceCapabilities { // The driver accepts and enforces a portable memory limit. bool limit_supported = 1; + // Required for a negotiated resource contract. Absence means unreported. + openshell.sandbox.v1.ResourceEnforcement limit_enforcement = 2; + openshell.sandbox.v1.ResourceEnforcement request_enforcement = 3; } message GpuResourceCapabilities { @@ -314,6 +321,8 @@ message DriverFenceEvidence { message ResourceRequirements { // GPU requirements for the sandbox. Presence indicates a GPU request. GpuResourceRequirements gpu = 1; + // Explicit opt-in to weaker sampled budgets; never inferred from hard limits. + openshell.sandbox.v1.SampledResourceBudget sampled_budget = 4; } // Driver GPU resource requirements. diff --git a/proto/openshell.proto b/proto/openshell.proto index e498a84af9..e1b0efa70a 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -936,6 +936,10 @@ message HealthResponse { // Service version. string version = 2; + // Gateway acknowledgement that sampled budget fields survive public writes + // and receive compute admission. This does not promise driver support. + // Budget-bearing clients must require true before writes to older gateways. + bool supports_sampled_resource_budget = 3; } // Current-user request. The identity comes from the authenticated request. @@ -1030,16 +1034,23 @@ message ResourceCapabilities { CpuResourceCapabilities cpu = 1; MemoryResourceCapabilities memory = 2; GpuResourceCapabilities gpu = 3; + openshell.sandbox.v1.SampledResourceBudgetCapabilities sampled_budget = 4; } message CpuResourceCapabilities { // The driver accepts and enforces a portable CPU limit. bool limit_supported = 1; + // Required for a negotiated resource contract. Absence means unreported. + openshell.sandbox.v1.ResourceEnforcement limit_enforcement = 2; + openshell.sandbox.v1.ResourceEnforcement request_enforcement = 3; } message MemoryResourceCapabilities { // The driver accepts and enforces a portable memory limit. bool limit_supported = 1; + // Required for a negotiated resource contract. Absence means unreported. + openshell.sandbox.v1.ResourceEnforcement limit_enforcement = 2; + openshell.sandbox.v1.ResourceEnforcement request_enforcement = 3; } message GpuResourceCapabilities { @@ -1115,6 +1126,8 @@ message SandboxSpec { message ResourceRequirements { // GPU requirements for the sandbox. Presence indicates a GPU request. GpuResourceRequirements gpu = 1; + // Explicit opt-in to weaker sampled budgets; never inferred from hard limits. + openshell.sandbox.v1.SampledResourceBudget sampled_budget = 4; } // Public GPU resource requirements. @@ -1200,6 +1213,7 @@ message SandboxResources { // request. When count is omitted, the request uses the selected driver's // default GPU assignment behavior. GpuResourceRequirements gpu = 3; + openshell.sandbox.v1.SampledResourceBudget sampled_budget = 4; } message SandboxServiceLevel { diff --git a/proto/sandbox.proto b/proto/sandbox.proto index b57c5d4cda..149803a491 100644 --- a/proto/sandbox.proto +++ b/proto/sandbox.proto @@ -548,3 +548,48 @@ enum SupervisorMiddlewareBinding { SUPERVISOR_MIDDLEWARE_BINDING_HTTP_RESPONSE_PRE_RETURN = 2; SUPERVISOR_MIDDLEWARE_BINDING_WEBSOCKET_MESSAGE_PRE_CREDENTIALS = 3; } + +// Explicit budgets observed across a sandbox account. These are not CPU-rate +// quotas or hard memory limits. Empty budgets are invalid. +message SampledResourceBudget { + google.protobuf.Duration cpu_time = 1; + optional uint64 resident_bytes = 2; +} + +// Mechanism and aggregation boundary for a resource request. +message ResourceEnforcement { + ResourceEnforcementKind kind = 1; + ResourceEnforcementScope scope = 2; +} + +enum ResourceEnforcementKind { + RESOURCE_ENFORCEMENT_KIND_UNSPECIFIED = 0; + RESOURCE_ENFORCEMENT_KIND_UNSUPPORTED = 1; + RESOURCE_ENFORCEMENT_KIND_KERNEL_LIMIT = 2; + // Admission by the platform scheduler, not dedicated physical capacity. + RESOURCE_ENFORCEMENT_KIND_SCHEDULER_REQUEST = 3; +} + +enum ResourceEnforcementScope { + RESOURCE_ENFORCEMENT_SCOPE_UNSPECIFIED = 0; + RESOURCE_ENFORCEMENT_SCOPE_PROCESS = 1; + RESOURCE_ENFORCEMENT_SCOPE_CONTAINER = 2; + RESOURCE_ENFORCEMENT_SCOPE_SANDBOX_ACCOUNT = 3; + RESOURCE_ENFORCEMENT_SCOPE_VIRTUAL_MACHINE = 4; +} + +message SampledResourceBudgetCapabilities { + bool cpu_time_supported = 1; + bool resident_bytes_supported = 2; + ResourceEnforcementScope scope = 3; + // Sleep between completed observations; collection adds latency. This is + // neither an enforcement deadline nor a bound on overshoot. Short-lived + // processes may escape observation. + google.protobuf.Duration sampling_interval = 4; + bool terminate_on_excess = 5; + // Smallest exactly representable CPU-time request. + google.protobuf.Duration cpu_time_resolution = 6; + // A per-process CPU-time kernel limit also accompanies the sampled budget. + // It may use a coarser resolution than the account-wide observation. + bool per_process_cpu_limit = 7; +} diff --git a/python/openshell/sandbox.py b/python/openshell/sandbox.py index c02a33cdbf..89c63e290e 100644 --- a/python/openshell/sandbox.py +++ b/python/openshell/sandbox.py @@ -28,6 +28,7 @@ datamodel_pb2, openshell_pb2, openshell_pb2_grpc, + sandbox_pb2, ) from .errors import _error_mapping_channel, from_grpc_error from .mutations import DeletionOutcome, DeletionResult @@ -147,6 +148,16 @@ def _retry_unrouted(error: grpc.RpcError) -> bool: ) +def _require_sampled_budget_support(stub: Any, timeout: float) -> None: + # This protocol acknowledgement is public; driver guarantees are checked + # by the gateway. Never write fields an older gateway would discard. + response = stub.Health(openshell_pb2.HealthRequest(), timeout=timeout) + if not response.supports_sampled_resource_budget: + raise SandboxError( + "gateway does not acknowledge sampled resource budgets; upgrade it before creating this workload" + ) + + def _workspace_scope(workspace: str) -> datamodel_pb2.WorkspaceSelector: if not workspace: raise ValueError("workspace must be non-empty") @@ -925,6 +936,8 @@ def create( service_exposures: Sequence[ServiceExposure] | None = None, ) -> SandboxRef: request_spec = spec if spec is not None else _default_spec() + if request_spec.resource_requirements.HasField("sampled_budget"): + _require_sampled_budget_support(self._stub, self._timeout) response = self._stub.CreateSandbox( openshell_pb2.CreateSandboxRequest( workspace_scope=_workspace_scope(workspace), @@ -1497,6 +1510,7 @@ def create( memory: str | None = None, gpu_count: int | None = None, gpu: bool = False, + sampled_budget: sandbox_pb2.SampledResourceBudget | None = None, driver_config: Mapping[str, Any] | None = None, ) -> openshell_pb2.SandboxWorkloadTemplate: if template is None: @@ -1512,10 +1526,14 @@ def create( memory=memory, gpu_count=gpu_count, gpu=gpu, + sampled_budget=sampled_budget, driver_config=driver_config, ) elif ( - any(value is not None for value in (name, image, cpu, memory, gpu_count)) + any( + value is not None + for value in (name, image, cpu, memory, gpu_count, sampled_budget) + ) or any( bool(value) for value in (labels, annotations, environment, driver_config) @@ -1526,6 +1544,9 @@ def create( "template cannot be combined with template builder fields" ) + if template.spec.workload.resources.HasField("sampled_budget"): + _require_sampled_budget_support(self._stub, self._timeout) + response = self._stub.CreateSandboxTemplate( openshell_pb2.CreateSandboxTemplateRequest( workspace_scope=_workspace_scope(workspace), @@ -2040,6 +2061,7 @@ def _sandbox_workload_template( memory: str | None = None, gpu_count: int | None = None, gpu: bool = False, + sampled_budget: sandbox_pb2.SampledResourceBudget | None = None, driver_config: Mapping[str, Any] | None = None, ) -> openshell_pb2.SandboxWorkloadTemplate: if gpu_count is not None and gpu_count <= 0: @@ -2059,6 +2081,8 @@ def _sandbox_workload_template( template.spec.workload.image = image if environment: template.spec.workload.environment.update(dict(environment)) + if sampled_budget is not None: + template.spec.workload.resources.sampled_budget.CopyFrom(sampled_budget) if cpu is not None: template.spec.workload.resources.cpu = cpu if memory is not None: diff --git a/python/openshell/sandbox_test.py b/python/openshell/sandbox_test.py index 3fed31734e..255f1ec334 100644 --- a/python/openshell/sandbox_test.py +++ b/python/openshell/sandbox_test.py @@ -21,7 +21,7 @@ from google.protobuf.struct_pb2 import Struct import openshell.sandbox as sandbox_module -from openshell._proto import openshell_pb2, openshell_pb2_grpc +from openshell._proto import openshell_pb2, openshell_pb2_grpc, sandbox_pb2 from openshell.mutations import DeletionOutcome from openshell.sandbox import ( _OIDC_TOKEN_EXPIRY_GRACE_SECONDS, @@ -2110,11 +2110,20 @@ def __init__( self.delete_template_request: ( openshell_pb2.DeleteSandboxTemplateRequest | None ) = None + self.sampled_budget_ack = False self._listed = listed or [] self._listed_pages = listed_pages self.list_requests: list[openshell_pb2.ListSandboxesRequest] = [] self._templates: list[openshell_pb2.SandboxWorkloadTemplate] = [] + def Health( + self, request: object, timeout: float | None = None + ) -> openshell_pb2.HealthResponse: + _ = request, timeout + return openshell_pb2.HealthResponse( + supports_sampled_resource_budget=self.sampled_budget_ack + ) + def GetSandbox( self, request: openshell_pb2.GetSandboxRequest, @@ -2426,6 +2435,7 @@ def test_create_from_template_rejects_empty_workload_template() -> None: def test_sandbox_template_create_builds_template_from_public_fields() -> None: stub = _FakeSandboxStub() + stub.sampled_budget_ack = True client = _template_client_with_fake_stub(stub) created = client.create( @@ -2438,6 +2448,7 @@ def test_sandbox_template_create_builds_template_from_public_fields() -> None: cpu="1", memory="512Mi", gpu_count=2, + sampled_budget=sandbox_pb2.SampledResourceBudget(resident_bytes=4096), driver_config={"kubernetes": {"runtime_class_name": "kata"}}, ) @@ -2457,6 +2468,40 @@ def test_sandbox_template_create_builds_template_from_public_fields() -> None: assert isinstance(config, Struct) assert config["runtime_class_name"] == "kata" + assert template.spec.workload.resources.sampled_budget.resident_bytes == 4096 + + +@pytest.mark.parametrize("template", [False, True]) +def test_sampled_budget_requires_gateway_acknowledgement(template: bool) -> None: + stub = _FakeSandboxStub() + budget = sandbox_pb2.SampledResourceBudget(resident_bytes=4096) + if template: + template_client = _template_client_with_fake_stub(stub) + + def write() -> object: + return template_client.create( + workspace="default", name="budget", sampled_budget=budget + ) + else: + client = _client_with_fake_stub(stub) + spec = openshell_pb2.SandboxSpec() + spec.resource_requirements.sampled_budget.CopyFrom(budget) + + def write() -> object: + return client.create(workspace="default", name="budget", spec=spec) + + with pytest.raises(SandboxError, match="does not acknowledge"): + write() + assert stub.create_request is None + assert stub.create_template_request is None + stub.sampled_budget_ack = True + write() + assert ( + (stub.create_template_request is not None) + if template + else (stub.create_request is not None) + ) + def test_sandbox_template_create_materializes_default_workload() -> None: stub = _FakeSandboxStub() diff --git a/sdk/go/openshell/v1/fake/health.go b/sdk/go/openshell/v1/fake/health.go index 304107a50f..0f1008cb0c 100644 --- a/sdk/go/openshell/v1/fake/health.go +++ b/sdk/go/openshell/v1/fake/health.go @@ -87,7 +87,10 @@ func copyGatewayInfo(info *types.GatewayInfo) *types.GatewayInfo { cp := *info if info.ComputeDrivers != nil { cp.ComputeDrivers = make([]types.ComputeDriverInfo, len(info.ComputeDrivers)) - copy(cp.ComputeDrivers, info.ComputeDrivers) + for index, driver := range info.ComputeDrivers { + cp.ComputeDrivers[index] = driver + cp.ComputeDrivers[index].Resources = copyResourceCapabilities(driver.Resources) + } } if info.Extensions != nil { cp.Extensions = make([]types.ExtensionInfo, len(info.Extensions)) @@ -109,3 +112,33 @@ func copyCurrentUser(user *types.CurrentUser) *types.CurrentUser { cp.Scopes = copyStringSlice(user.Scopes) return &cp } + +func copyResourceValue[T any](value *T) *T { + if value == nil { + return nil + } + copied := *value + return &copied +} +func copyResourceDomain(c *types.ResourceDomainCapabilities) *types.ResourceDomainCapabilities { + result := copyResourceValue(c) + if result != nil { + result.LimitEnforcement = copyResourceValue(c.LimitEnforcement) + result.RequestEnforcement = copyResourceValue(c.RequestEnforcement) + } + return result +} +func copyResourceCapabilities(c *types.ResourceCapabilities) *types.ResourceCapabilities { + result := copyResourceValue(c) + if result != nil { + result.CPU = copyResourceDomain(c.CPU) + result.Memory = copyResourceDomain(c.Memory) + result.GPU = copyResourceValue(c.GPU) + result.SampledBudget = copyResourceValue(c.SampledBudget) + if result.SampledBudget != nil { + result.SampledBudget.SamplingInterval = copyResourceValue(c.SampledBudget.SamplingInterval) + result.SampledBudget.CPUTimeResolution = copyResourceValue(c.SampledBudget.CPUTimeResolution) + } + } + return result +} diff --git a/sdk/go/openshell/v1/fake/sandbox.go b/sdk/go/openshell/v1/fake/sandbox.go index 221e1f7524..92b43c8f7f 100644 --- a/sdk/go/openshell/v1/fake/sandbox.go +++ b/sdk/go/openshell/v1/fake/sandbox.go @@ -43,6 +43,7 @@ func copySandbox(sb *types.Sandbox) *types.Sandbox { } func copySandboxSpec(s types.SandboxSpec) types.SandboxSpec { + s.SampledBudget = copySampledBudget(s.SampledBudget) s.Environment = copyStringMap(s.Environment) s.Providers = copyStringSlice(s.Providers) s.Command = copyStringSlice(s.Command) @@ -408,7 +409,7 @@ func validateTemplateCreateSpec(spec *types.SandboxSpec) error { if spec == nil { return nil } - if spec.LogLevel != "" || len(spec.Environment) > 0 || spec.Template != nil || spec.GPU || spec.GPUCount != nil { + if spec.LogLevel != "" || len(spec.Environment) > 0 || spec.Template != nil || spec.GPU || spec.GPUCount != nil || spec.SampledBudget != nil { return &types.StatusError{Code: types.ErrorInvalidArgument, Message: "template creates only allow policy, providers, command, and tty in spec"} } return nil @@ -421,6 +422,9 @@ func sandboxSpecFromWorkloadTemplate(template *types.SandboxWorkloadTemplate) ty } workload := template.Spec.Workload + if workload.Resources != nil { + spec.SampledBudget = copySampledBudget(workload.Resources.SampledBudget) + } spec.Environment = copyStringMap(workload.Environment) spec.Template = &types.SandboxTemplate{ Image: workload.Image, @@ -930,3 +934,19 @@ func (c *fakeSandboxClient) ListAllProviders(ctx context.Context, workspace, san } return pager.All(ctx) } + +func copySampledBudget(budget *types.SampledResourceBudget) *types.SampledResourceBudget { + if budget == nil { + return nil + } + result := *budget + if budget.CPUTime != nil { + value := *budget.CPUTime + result.CPUTime = &value + } + if budget.ResidentBytes != nil { + value := *budget.ResidentBytes + result.ResidentBytes = &value + } + return &result +} diff --git a/sdk/go/openshell/v1/fake/sandbox_template.go b/sdk/go/openshell/v1/fake/sandbox_template.go index ba5651d79a..54df6dd923 100644 --- a/sdk/go/openshell/v1/fake/sandbox_template.go +++ b/sdk/go/openshell/v1/fake/sandbox_template.go @@ -38,6 +38,7 @@ func copySandboxWorkloadTemplateSpec(spec types.SandboxWorkloadTemplateSpec) typ workload.Environment = copyStringMap(spec.Workload.Environment) if spec.Workload.Resources != nil { resources := *spec.Workload.Resources + resources.SampledBudget = copySampledBudget(resources.SampledBudget) if spec.Workload.Resources.GPU != nil { gpu := *spec.Workload.Resources.GPU if spec.Workload.Resources.GPU.Count != nil { diff --git a/sdk/go/openshell/v1/health.go b/sdk/go/openshell/v1/health.go index ac05337460..e98d4e9664 100644 --- a/sdk/go/openshell/v1/health.go +++ b/sdk/go/openshell/v1/health.go @@ -53,3 +53,18 @@ type HealthInterface interface { GetGatewayInfo(ctx context.Context) (*GatewayInfo, error) GetCurrentUser(ctx context.Context) (*CurrentUser, error) } + +// ResourceCapabilities reports the configured compute driver's resource guarantees. +type ResourceCapabilities = types.ResourceCapabilities + +// ResourceDomainCapabilities describes resource limit and request enforcement. +type ResourceDomainCapabilities = types.ResourceDomainCapabilities + +// ResourceEnforcement identifies the mechanism and scope of a resource guarantee. +type ResourceEnforcement = types.ResourceEnforcement + +// GPUResourceCapabilities describes supported GPU selection methods. +type GPUResourceCapabilities = types.GPUResourceCapabilities + +// SampledBudgetCapabilities describes supported sampled workload budgets. +type SampledBudgetCapabilities = types.SampledBudgetCapabilities diff --git a/sdk/go/openshell/v1/internal/converter/coverage_test.go b/sdk/go/openshell/v1/internal/converter/coverage_test.go index 8a457d0014..3f1ebfd639 100644 --- a/sdk/go/openshell/v1/internal/converter/coverage_test.go +++ b/sdk/go/openshell/v1/internal/converter/coverage_test.go @@ -89,9 +89,10 @@ func TestConverterCoversAllProtoFields_SandboxWorkloadConfig(t *testing.T) { func TestConverterCoversAllProtoFields_SandboxResources(t *testing.T) { handled := fieldSet{ - "cpu": true, - "memory": true, - "gpu": true, + "cpu": true, + "memory": true, + "gpu": true, + "sampled_budget": true, } assertAllFieldsCovered(t, (&pb.SandboxResources{}).ProtoReflect().Descriptor(), handled, nil) @@ -462,3 +463,22 @@ func assertAllFieldsCovered( } } } + +// Resource descriptors are public operator data; no fields are intentionally skipped. +func TestConverterCoversResourceContracts(t *testing.T) { + for _, entry := range []struct { + message protoreflect.MessageDescriptor + fields fieldSet + }{ + {(&pb.ComputeDriverCapabilities{}).ProtoReflect().Descriptor(), fieldSet{"driver_name": true, "driver_version": true, "resource_capabilities": true}}, + {(&pb.ResourceCapabilities{}).ProtoReflect().Descriptor(), fieldSet{"cpu": true, "memory": true, "gpu": true, "sampled_budget": true}}, + {(&pb.CpuResourceCapabilities{}).ProtoReflect().Descriptor(), fieldSet{"limit_supported": true, "limit_enforcement": true, "request_enforcement": true}}, + {(&pb.MemoryResourceCapabilities{}).ProtoReflect().Descriptor(), fieldSet{"limit_supported": true, "limit_enforcement": true, "request_enforcement": true}}, + {(&sandboxpb.ResourceEnforcement{}).ProtoReflect().Descriptor(), fieldSet{"kind": true, "scope": true}}, + {(&sandboxpb.SampledResourceBudgetCapabilities{}).ProtoReflect().Descriptor(), fieldSet{"cpu_time_supported": true, "resident_bytes_supported": true, "scope": true, "sampling_interval": true, "terminate_on_excess": true, "cpu_time_resolution": true, "per_process_cpu_limit": true}}, + {(&sandboxpb.SampledResourceBudget{}).ProtoReflect().Descriptor(), fieldSet{"cpu_time": true, "resident_bytes": true}}, + {(&pb.ResourceRequirements{}).ProtoReflect().Descriptor(), fieldSet{"gpu": true, "sampled_budget": true}}, + } { + assertAllFieldsCovered(t, entry.message, entry.fields, nil) + } +} diff --git a/sdk/go/openshell/v1/internal/converter/health.go b/sdk/go/openshell/v1/internal/converter/health.go index 8e1c2b98f6..5892050d1a 100644 --- a/sdk/go/openshell/v1/internal/converter/health.go +++ b/sdk/go/openshell/v1/internal/converter/health.go @@ -6,6 +6,7 @@ package converter import ( "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types" pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1" + sb "github.com/NVIDIA/OpenShell/sdk/go/proto/sandboxv1" ) // GatewayInfoFromProto converts a proto GetGatewayInfoResponse to an SDK GatewayInfo. @@ -83,6 +84,7 @@ func ComputeDriverInfoFromProto(d *pb.ComputeDriverInfo) types.ComputeDriverInfo if caps := d.GetCapabilities(); caps != nil { result.DriverName = caps.GetDriverName() result.DriverVersion = caps.GetDriverVersion() + result.Resources = resourceCapabilitiesFromProto(caps.GetResourceCapabilities()) } return result } @@ -101,3 +103,60 @@ func CurrentUserFromProto(resp *pb.GetCurrentUserResponse) *types.CurrentUser { IdentityProvider: resp.GetIdentityProvider(), } } + +func resourceScope(scope sb.ResourceEnforcementScope) string { + switch scope { + case sb.ResourceEnforcementScope_RESOURCE_ENFORCEMENT_SCOPE_PROCESS: + return "Process" + case sb.ResourceEnforcementScope_RESOURCE_ENFORCEMENT_SCOPE_CONTAINER: + return "Container" + case sb.ResourceEnforcementScope_RESOURCE_ENFORCEMENT_SCOPE_SANDBOX_ACCOUNT: + return "SandboxAccount" + case sb.ResourceEnforcementScope_RESOURCE_ENFORCEMENT_SCOPE_VIRTUAL_MACHINE: + return "VirtualMachine" + default: + return "Unknown" + } +} +func resourceEnforcement(e *sb.ResourceEnforcement) *types.ResourceEnforcement { + if e == nil { + return nil + } + kind := "Unknown" + switch e.Kind { + case sb.ResourceEnforcementKind_RESOURCE_ENFORCEMENT_KIND_UNSUPPORTED: + kind = "Unsupported" + case sb.ResourceEnforcementKind_RESOURCE_ENFORCEMENT_KIND_KERNEL_LIMIT: + kind = "KernelLimit" + case sb.ResourceEnforcementKind_RESOURCE_ENFORCEMENT_KIND_SCHEDULER_REQUEST: + kind = "SchedulerRequest" + } + return &types.ResourceEnforcement{Kind: kind, Scope: resourceScope(e.Scope)} +} +func resourceCapabilitiesFromProto(c *pb.ResourceCapabilities) *types.ResourceCapabilities { + if c == nil { + return nil + } + result := &types.ResourceCapabilities{} + if d := c.Cpu; d != nil { + result.CPU = &types.ResourceDomainCapabilities{ + LimitSupported: d.LimitSupported, LimitEnforcement: resourceEnforcement(d.LimitEnforcement), RequestEnforcement: resourceEnforcement(d.RequestEnforcement), + } + } + if d := c.Memory; d != nil { + result.Memory = &types.ResourceDomainCapabilities{ + LimitSupported: d.LimitSupported, LimitEnforcement: resourceEnforcement(d.LimitEnforcement), RequestEnforcement: resourceEnforcement(d.RequestEnforcement), + } + } + if d := c.Gpu; d != nil { + result.GPU = &types.GPUResourceCapabilities{DefaultSelectionSupported: d.DefaultSelectionSupported, CountSelectionSupported: d.CountSelectionSupported} + } + if d := c.SampledBudget; d != nil { + result.SampledBudget = &types.SampledBudgetCapabilities{ + CPUTimeSupported: d.CpuTimeSupported, ResidentBytesSupported: d.ResidentBytesSupported, + Scope: resourceScope(d.Scope), SamplingInterval: resourceDurationFromProto(d.SamplingInterval), TerminateOnExcess: d.TerminateOnExcess, + CPUTimeResolution: resourceDurationFromProto(d.CpuTimeResolution), PerProcessCPULimit: d.PerProcessCpuLimit, + } + } + return result +} diff --git a/sdk/go/openshell/v1/internal/converter/health_test.go b/sdk/go/openshell/v1/internal/converter/health_test.go index ab8e0c2415..c17f43be21 100644 --- a/sdk/go/openshell/v1/internal/converter/health_test.go +++ b/sdk/go/openshell/v1/internal/converter/health_test.go @@ -8,8 +8,10 @@ import ( v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types" pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1" + sb "github.com/NVIDIA/OpenShell/sdk/go/proto/sandboxv1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" ) func TestGatewayInfoFromProto(t *testing.T) { @@ -22,6 +24,14 @@ func TestGatewayInfoFromProto(t *testing.T) { Capabilities: &pb.ComputeDriverCapabilities{ DriverName: "kubernetes", DriverVersion: "2.1.0", + ResourceCapabilities: &pb.ResourceCapabilities{ + Cpu: &pb.CpuResourceCapabilities{LimitSupported: true, LimitEnforcement: &sb.ResourceEnforcement{Kind: 2, Scope: 2}}, + Memory: &pb.MemoryResourceCapabilities{LimitSupported: true, RequestEnforcement: &sb.ResourceEnforcement{Kind: 3, Scope: 2}}, + Gpu: &pb.GpuResourceCapabilities{CountSelectionSupported: true}, + SampledBudget: &sb.SampledResourceBudgetCapabilities{CpuTimeSupported: true, ResidentBytesSupported: true, Scope: 3, + SamplingInterval: &durationpb.Duration{Nanos: 100000000}, TerminateOnExcess: true, + CpuTimeResolution: &durationpb.Duration{Nanos: 1000000}, PerProcessCpuLimit: true}, + }, }, }, { @@ -60,6 +70,22 @@ func TestGatewayInfoFromProto(t *testing.T) { assert.Equal(t, v1.ExtensionKindComputeDriver, info.Extensions[0].Kind) assert.Equal(t, "openshell/kubernetes", info.Extensions[0].ImplementationName) assert.Equal(t, uint32(1), info.Extensions[0].ProtocolMajor) + resources := info.ComputeDrivers[0].Resources + require.NotNil(t, resources) + assert.Equal(t, "KernelLimit", resources.CPU.LimitEnforcement.Kind) + assert.Equal(t, "Container", resources.CPU.LimitEnforcement.Scope) + assert.Equal(t, "SchedulerRequest", resources.Memory.RequestEnforcement.Kind) + assert.True(t, resources.GPU.CountSelectionSupported) + assert.Equal(t, int32(100000000), resources.SampledBudget.SamplingInterval.Nanos) + assert.Equal(t, int32(1000000), resources.SampledBudget.CPUTimeResolution.Nanos) + assert.True(t, resources.SampledBudget.TerminateOnExcess) + assert.True(t, resources.SampledBudget.PerProcessCPULimit) + proto.ComputeDrivers[0].Capabilities.ResourceCapabilities.SampledBudget.SamplingInterval.Nanos = 1 + assert.Equal(t, int32(100000000), resources.SampledBudget.SamplingInterval.Nanos) + unknown := resourceEnforcement(&sb.ResourceEnforcement{Kind: 99, Scope: 99}) + assert.Equal(t, "Unknown", unknown.Kind) + assert.Equal(t, "Unknown", unknown.Scope) + } func TestGatewayInfoFromProto_NoDrivers(t *testing.T) { diff --git a/sdk/go/openshell/v1/internal/converter/sandbox.go b/sdk/go/openshell/v1/internal/converter/sandbox.go index e679fba176..4ddcaa40de 100644 --- a/sdk/go/openshell/v1/internal/converter/sandbox.go +++ b/sdk/go/openshell/v1/internal/converter/sandbox.go @@ -11,6 +11,7 @@ import ( "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types" dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1" pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1" + sb "github.com/NVIDIA/OpenShell/sdk/go/proto/sandboxv1" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" ) @@ -83,6 +84,7 @@ func sandboxSpecFromProto(spec *pb.SandboxSpec) types.SandboxSpec { } if rr := spec.GetResourceRequirements(); rr != nil { + result.SampledBudget = SampledBudgetFromProto(rr.GetSampledBudget()) if gpu := rr.GetGpu(); gpu != nil { result.GPU = true if gpu.Count != nil { @@ -336,6 +338,12 @@ func SandboxSpecToProto(spec *types.SandboxSpec) *pb.SandboxSpec { } } + if spec.SampledBudget != nil { + if result.ResourceRequirements == nil { + result.ResourceRequirements = &pb.ResourceRequirements{} + } + result.ResourceRequirements.SampledBudget = SampledBudgetToProto(spec.SampledBudget) + } result.Command = CopyStringSlice(spec.Command) result.Tty = spec.TTY result.RestartPolicy = SandboxRestartPolicyToProto(spec.RestartPolicy) @@ -454,9 +462,10 @@ func SandboxResourcesFromProto(resources *pb.SandboxResources) *types.SandboxRes return nil } return &types.SandboxResources{ - CPU: resources.GetCpu(), - Memory: resources.GetMemory(), - GPU: sandboxResourceGpuFromProto(resources), + CPU: resources.GetCpu(), + Memory: resources.GetMemory(), + GPU: sandboxResourceGpuFromProto(resources), + SampledBudget: SampledBudgetFromProto(resources.GetSampledBudget()), } } @@ -536,9 +545,10 @@ func SandboxResourcesToProto(resources *types.SandboxResources) *pb.SandboxResou return nil } return &pb.SandboxResources{ - Cpu: resources.CPU, - Memory: resources.Memory, - Gpu: sandboxResourceGpuToProto(resources), + Cpu: resources.CPU, + Memory: resources.Memory, + Gpu: sandboxResourceGpuToProto(resources), + SampledBudget: SampledBudgetToProto(resources.SampledBudget), } } @@ -616,3 +626,42 @@ func durationToProto(d time.Duration) *durationpb.Duration { } return durationpb.New(d) } + +func resourceDurationFromProto(d *durationpb.Duration) *types.ResourceDuration { + if d == nil { + return nil + } + return &types.ResourceDuration{Seconds: d.Seconds, Nanos: d.Nanos} +} +func resourceDurationToProto(d *types.ResourceDuration) *durationpb.Duration { + if d == nil { + return nil + } + return &durationpb.Duration{Seconds: d.Seconds, Nanos: d.Nanos} +} + +// SampledBudgetFromProto copies a protobuf budget into the SDK model. +func SampledBudgetFromProto(b *sb.SampledResourceBudget) *types.SampledResourceBudget { + if b == nil { + return nil + } + result := &types.SampledResourceBudget{CPUTime: resourceDurationFromProto(b.CpuTime)} + if b.ResidentBytes != nil { + value := *b.ResidentBytes + result.ResidentBytes = &value + } + return result +} + +// SampledBudgetToProto copies an SDK budget into its protobuf form. +func SampledBudgetToProto(b *types.SampledResourceBudget) *sb.SampledResourceBudget { + if b == nil { + return nil + } + result := &sb.SampledResourceBudget{CpuTime: resourceDurationToProto(b.CPUTime)} + if b.ResidentBytes != nil { + value := *b.ResidentBytes + result.ResidentBytes = &value + } + 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 756fd72338..80e39e78a4 100644 --- a/sdk/go/openshell/v1/internal/converter/sandbox_test.go +++ b/sdk/go/openshell/v1/internal/converter/sandbox_test.go @@ -522,6 +522,7 @@ func TestSandboxToProto_DefaultGPURequest(t *testing.T) { func TestSandboxWorkloadTemplateRoundTrip(t *testing.T) { gpuCount := uint32(2) + resident := uint64(1<<54 + 1) delTime := time.UnixMilli(1700000060000).UTC() original := &v1.SandboxWorkloadTemplate{ ID: "tmpl-1", @@ -537,9 +538,10 @@ func TestSandboxWorkloadTemplateRoundTrip(t *testing.T) { Image: "nvcr.io/nvidia/openshell:latest", Environment: map[string]string{"CUDA_VISIBLE_DEVICES": "all"}, Resources: &v1.SandboxResources{ - CPU: "2", - Memory: "8Gi", - GPU: &v1.SandboxGPURequirements{Count: &gpuCount}, + SampledBudget: &v1.SampledResourceBudget{CPUTime: &v1.ResourceDuration{Seconds: 315576000000, Nanos: 1}, ResidentBytes: &resident}, + CPU: "2", + Memory: "8Gi", + GPU: &v1.SandboxGPURequirements{Count: &gpuCount}, }, }, DriverConfig: map[string]any{ @@ -600,6 +602,13 @@ func TestSandboxWorkloadTemplateRoundTrip(t *testing.T) { require.NotNil(t, back.Spec.DesiredServiceLevel.Startup) assert.Equal(t, 30*time.Second, back.Spec.DesiredServiceLevel.Startup.ReadyWithin) assert.Equal(t, uint32(3), back.Spec.DesiredServiceLevel.Startup.MaxBurst) + assert.Equal(t, original.Spec.Workload.Resources.SampledBudget, back.Spec.Workload.Resources.SampledBudget) + protoTemplate.Spec.Workload.Resources.SampledBudget.CpuTime.Seconds = 1 + *protoTemplate.Spec.Workload.Resources.SampledBudget.ResidentBytes = 1 + assert.Equal(t, int64(315576000000), back.Spec.Workload.Resources.SampledBudget.CPUTime.Seconds) + assert.Equal(t, resident, *back.Spec.Workload.Resources.SampledBudget.ResidentBytes) + assert.Equal(t, int64(315576000000), original.Spec.Workload.Resources.SampledBudget.CPUTime.Seconds) + } func TestSandboxWorkloadTemplateRoundTrip_DefaultGpuRequest(t *testing.T) { diff --git a/sdk/go/openshell/v1/resource.go b/sdk/go/openshell/v1/resource.go new file mode 100644 index 0000000000..c0e7de5398 --- /dev/null +++ b/sdk/go/openshell/v1/resource.go @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + "context" + "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/converter" + pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1" +) + +// Probe only budget-bearing writes. Public health acknowledges wire support; +// compute admission remains responsible for the requested resource guarantee. +func requireSampledBudgetSupport(ctx context.Context, client pb.OpenShellClient) error { + health, err := client.Health(ctx, &pb.HealthRequest{}) + if err != nil { + return converter.FromGRPCError(err) + } + if !health.GetSupportsSampledResourceBudget() { + return &StatusError{Code: ErrorInvalidArgument, Message: "gateway does not acknowledge sampled resource budgets; upgrade it before creating this workload"} + } + return nil +} diff --git a/sdk/go/openshell/v1/sandbox.go b/sdk/go/openshell/v1/sandbox.go index 1e6c24cc9c..5a9b3ad3c6 100644 --- a/sdk/go/openshell/v1/sandbox.go +++ b/sdk/go/openshell/v1/sandbox.go @@ -122,3 +122,9 @@ type SandboxSuspendResumeInterface interface { // WaitSuspended polls until the sandbox reaches the Suspended phase. WaitSuspended(ctx context.Context, workspace, name string, opts ...WaitOptions) (*Sandbox, error) } + +// ResourceDuration preserves protobuf duration precision and range. +type ResourceDuration = types.ResourceDuration + +// SampledResourceBudget explicitly selects account-wide sampled budgets. +type SampledResourceBudget = types.SampledResourceBudget diff --git a/sdk/go/openshell/v1/sandbox_client.go b/sdk/go/openshell/v1/sandbox_client.go index 5d042b2101..fd1db0c260 100644 --- a/sdk/go/openshell/v1/sandbox_client.go +++ b/sdk/go/openshell/v1/sandbox_client.go @@ -44,6 +44,12 @@ func (s *sandboxClient) Create(ctx context.Context, workspace, name string, spec WorkspaceScope: namedWorkspaceScope(workspace), ServiceExposures: serviceExposuresToProto(cfg.ServiceExposures()), } + if req.GetSpec().GetResourceRequirements().GetSampledBudget() != nil { + if err := requireSampledBudgetSupport(ctx, s.client); err != nil { + return nil, err + } + } + resp, err := s.client.CreateSandbox(ctx, req) if err != nil { return nil, converter.FromGRPCError(err) @@ -110,7 +116,7 @@ func validateTemplateCreateSpec(spec *SandboxSpec) error { if spec == nil { return nil } - if spec.LogLevel != "" || len(spec.Environment) > 0 || spec.Template != nil || spec.GPU || spec.GPUCount != nil { + if spec.LogLevel != "" || len(spec.Environment) > 0 || spec.Template != nil || spec.GPU || spec.GPUCount != nil || spec.SampledBudget != nil { return &StatusError{Code: ErrorInvalidArgument, Message: "template creates only allow policy, providers, command, and tty in spec"} } return nil diff --git a/sdk/go/openshell/v1/sandbox_client_test.go b/sdk/go/openshell/v1/sandbox_client_test.go index 8e2e83b50d..ba387043d4 100644 --- a/sdk/go/openshell/v1/sandbox_client_test.go +++ b/sdk/go/openshell/v1/sandbox_client_test.go @@ -24,6 +24,7 @@ import ( ) type mockSandboxServer struct { + sampledBudgetAck bool pb.UnimplementedOpenShellServer mu sync.Mutex sandboxes map[string]*pb.Sandbox @@ -55,6 +56,12 @@ type mockSandboxServer struct { getLogsRequest *pb.GetSandboxLogsRequest // recorded request } +func (s *mockSandboxServer) Health(context.Context, *pb.HealthRequest) (*pb.HealthResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + return &pb.HealthResponse{SupportsSampledResourceBudget: s.sampledBudgetAck}, nil +} + func newMockSandboxServer() *mockSandboxServer { return &mockSandboxServer{ sandboxes: make(map[string]*pb.Sandbox), @@ -329,13 +336,20 @@ func TestSandboxCreate(t *testing.T) { client, cleanup := setupSandboxTest(t, mock) defer cleanup() + resident := uint64(1<<54 + 1) spec := &SandboxSpec{ - LogLevel: "debug", - Environment: map[string]string{"FOO": "bar"}, - Providers: []string{"claude"}, + SampledBudget: &SampledResourceBudget{ResidentBytes: &resident}, + LogLevel: "debug", + Environment: map[string]string{"FOO": "bar"}, + Providers: []string{"claude"}, } labels := map[string]string{"env": "dev"} + _, err := client.Create(context.Background(), "default", "probe", spec) + require.Error(t, err) + assert.Nil(t, mock.createRequest) + mock.sampledBudgetAck = true + result, err := client.Create( context.Background(), "default", @@ -367,6 +381,9 @@ func TestSandboxCreate(t *testing.T) { assert.Equal(t, pb.ServiceAuthorizationMode_SERVICE_AUTHORIZATION_MODE_STRIP, mock.createRequest.GetServiceExposures()[0].GetAuthorizationMode()) assert.Equal(t, "metrics", mock.createRequest.GetServiceExposures()[1].GetService()) assert.Equal(t, pb.ServiceAuthorizationMode_SERVICE_AUTHORIZATION_MODE_BEARER_PASSTHROUGH, mock.createRequest.GetServiceExposures()[1].GetAuthorizationMode()) + assert.Equal(t, resident, *mock.createRequest.GetSpec().GetResourceRequirements().GetSampledBudget().ResidentBytes) + assert.Equal(t, resident, *result.Spec.SampledBudget.ResidentBytes) + } // Callers that passed nil for the old positional labels parameter still compile diff --git a/sdk/go/openshell/v1/sandbox_template_client.go b/sdk/go/openshell/v1/sandbox_template_client.go index bcf48185e9..666fb03285 100644 --- a/sdk/go/openshell/v1/sandbox_template_client.go +++ b/sdk/go/openshell/v1/sandbox_template_client.go @@ -29,6 +29,12 @@ func (s *sandboxTemplateClient) Create(ctx context.Context, workspace string, te if err != nil { return nil, &StatusError{Code: ErrorInvalidArgument, Message: err.Error()} } + if protoTemplate.GetSpec().GetWorkload().GetResources().GetSampledBudget() != nil { + if err := requireSampledBudgetSupport(ctx, s.client); err != nil { + return nil, err + } + } + resp, err := s.client.CreateSandboxTemplate(ctx, &pb.CreateSandboxTemplateRequest{ Template: protoTemplate, WorkspaceScope: namedWorkspaceScope(workspace), diff --git a/sdk/go/openshell/v1/sandbox_template_client_test.go b/sdk/go/openshell/v1/sandbox_template_client_test.go index 8d745344e2..4592ef4090 100644 --- a/sdk/go/openshell/v1/sandbox_template_client_test.go +++ b/sdk/go/openshell/v1/sandbox_template_client_test.go @@ -24,6 +24,7 @@ import ( ) type mockSandboxTemplateServer struct { + sampledBudgetAck bool pb.UnimplementedOpenShellServer mu sync.Mutex @@ -40,6 +41,12 @@ type mockSandboxTemplateServer struct { deleteErr error } +func (s *mockSandboxTemplateServer) Health(context.Context, *pb.HealthRequest) (*pb.HealthResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + return &pb.HealthResponse{SupportsSampledResourceBudget: s.sampledBudgetAck}, nil +} + func newMockSandboxTemplateServer() *mockSandboxTemplateServer { return &mockSandboxTemplateServer{ templates: make(map[string]*pb.SandboxWorkloadTemplate), @@ -129,7 +136,8 @@ func TestSandboxTemplateCreate(t *testing.T) { defer cleanup() gpuCount := uint32(1) - template, err := client.Create(context.Background(), "default", &SandboxWorkloadTemplate{ + resident := uint64(4096) + input := &SandboxWorkloadTemplate{ Name: "gpu-kata", Labels: map[string]string{ "team": "platform", @@ -139,9 +147,10 @@ func TestSandboxTemplateCreate(t *testing.T) { Image: "nvcr.io/nvidia/openshell:latest", Environment: map[string]string{"NVIDIA_VISIBLE_DEVICES": "all"}, Resources: &SandboxResources{ - CPU: "2", - Memory: "8Gi", - GPU: &SandboxGPURequirements{Count: &gpuCount}, + SampledBudget: &SampledResourceBudget{ResidentBytes: &resident}, + CPU: "2", + Memory: "8Gi", + GPU: &SandboxGPURequirements{Count: &gpuCount}, }, }, DriverConfig: map[string]any{ @@ -154,8 +163,12 @@ func TestSandboxTemplateCreate(t *testing.T) { }, }, }, - }) - + } + _, err := client.Create(context.Background(), "default", input) + require.Error(t, err) + assert.Nil(t, mock.createRequest) + mock.sampledBudgetAck = true + template, err := client.Create(context.Background(), "default", input) require.NoError(t, err) require.NotNil(t, template) assert.Equal(t, "gpu-kata", template.Name) @@ -172,6 +185,8 @@ func TestSandboxTemplateCreate(t *testing.T) { require.NotNil(t, mock.createRequest.Template.Spec.Workload.Resources.Gpu.Count) assert.Equal(t, uint32(1), *mock.createRequest.Template.Spec.Workload.Resources.Gpu.Count) assert.Equal(t, durationpb.New(45*time.Second), mock.createRequest.Template.Spec.DesiredServiceLevel.Startup.ReadyWithin) + assert.Equal(t, resident, *mock.createRequest.Template.Spec.Workload.Resources.SampledBudget.ResidentBytes) + } func TestSandboxTemplateCreate_RejectsNilTemplate(t *testing.T) { diff --git a/sdk/go/openshell/v1/types/health.go b/sdk/go/openshell/v1/types/health.go index 3039f1c80d..4ef96795ea 100644 --- a/sdk/go/openshell/v1/types/health.go +++ b/sdk/go/openshell/v1/types/health.go @@ -54,6 +54,7 @@ type ExtensionInfo struct { // ComputeDriverInfo describes a compute backend available on the gateway. type ComputeDriverInfo struct { + Resources *ResourceCapabilities Name string DriverName string DriverVersion string @@ -67,3 +68,42 @@ type CurrentUser struct { Scopes []string IdentityProvider string } + +// ResourceCapabilities reports the configured compute driver's guarantees. +type ResourceCapabilities struct { + CPU *ResourceDomainCapabilities + Memory *ResourceDomainCapabilities + GPU *GPUResourceCapabilities + SampledBudget *SampledBudgetCapabilities +} + +// ResourceDomainCapabilities describes resource limit and request enforcement. +type ResourceDomainCapabilities struct { + LimitSupported bool + LimitEnforcement *ResourceEnforcement + RequestEnforcement *ResourceEnforcement +} + +// ResourceEnforcement identifies the mechanism and scope of a resource guarantee. +type ResourceEnforcement struct { + // Unknown enum values are preserved as "Unknown", never inferred as support. + Kind string + Scope string +} + +// GPUResourceCapabilities describes supported GPU selection methods. +type GPUResourceCapabilities struct { + DefaultSelectionSupported bool + CountSelectionSupported bool +} + +// SampledBudgetCapabilities describes supported sampled workload budgets. +type SampledBudgetCapabilities struct { + CPUTimeSupported bool + ResidentBytesSupported bool + Scope string + SamplingInterval *ResourceDuration + TerminateOnExcess bool + CPUTimeResolution *ResourceDuration + PerProcessCPULimit bool +} diff --git a/sdk/go/openshell/v1/types/sandbox.go b/sdk/go/openshell/v1/types/sandbox.go index f1cedb595f..3fdd6fa2c7 100644 --- a/sdk/go/openshell/v1/types/sandbox.go +++ b/sdk/go/openshell/v1/types/sandbox.go @@ -27,10 +27,11 @@ type Sandbox struct { // SandboxSpec holds the desired state of a sandbox. type SandboxSpec struct { - LogLevel string - Environment map[string]string - Template *SandboxTemplate - Providers []string + SampledBudget *SampledResourceBudget + LogLevel string + Environment map[string]string + Template *SandboxTemplate + Providers []string // GPU requests GPU resources using the active driver's default GPU assignment // when GPUCount is nil. GPUCount implies GPU for backward compatibility. GPU bool @@ -84,8 +85,9 @@ type SandboxWorkloadConfig struct { // SandboxResources defines portable sandbox resource requirements. type SandboxResources struct { - CPU string - Memory string + SampledBudget *SampledResourceBudget + CPU string + Memory string // GPU requests GPU resources for template-backed sandboxes. A non-nil GPU // with nil Count requests the active driver's default GPU assignment. GPU *SandboxGPURequirements @@ -276,3 +278,16 @@ type DetachProviderResult struct { Sandbox *Sandbox Detached bool } + +// ResourceDuration preserves protobuf duration precision and range. +type ResourceDuration struct { + Seconds int64 + Nanos int32 +} + +// SampledResourceBudget requests account-wide sampled observations followed by +// termination on excess. It does not request CPU-rate or hard memory limits. +type SampledResourceBudget struct { + CPUTime *ResourceDuration + ResidentBytes *uint64 +} diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 1ae1800784..675b083edb 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -1656,9 +1656,13 @@ type HealthResponse struct { // Service status. Status ServiceStatus `protobuf:"varint,1,opt,name=status,proto3,enum=openshell.v1.ServiceStatus" json:"status,omitempty"` // Service version. - Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + // Gateway acknowledgement that sampled budget fields survive public writes + // and receive compute admission. This does not promise driver support. + // Budget-bearing clients must require true before writes to older gateways. + SupportsSampledResourceBudget bool `protobuf:"varint,3,opt,name=supports_sampled_resource_budget,json=supportsSampledResourceBudget,proto3" json:"supports_sampled_resource_budget,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *HealthResponse) Reset() { @@ -1705,6 +1709,13 @@ func (x *HealthResponse) GetVersion() string { return "" } +func (x *HealthResponse) GetSupportsSampledResourceBudget() bool { + if x != nil { + return x.SupportsSampledResourceBudget + } + return false +} + // Current-user request. The identity comes from the authenticated request. type GetCurrentUserRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2163,10 +2174,11 @@ func (x *ComputeDriverCapabilities) GetResourceCapabilities() *ResourceCapabilit // Static portable resource request forms reported by a compute driver. // An omitted domain means the driver does not report that domain. type ResourceCapabilities struct { - state protoimpl.MessageState `protogen:"open.v1"` - Cpu *CpuResourceCapabilities `protobuf:"bytes,1,opt,name=cpu,proto3" json:"cpu,omitempty"` - Memory *MemoryResourceCapabilities `protobuf:"bytes,2,opt,name=memory,proto3" json:"memory,omitempty"` - Gpu *GpuResourceCapabilities `protobuf:"bytes,3,opt,name=gpu,proto3" json:"gpu,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Cpu *CpuResourceCapabilities `protobuf:"bytes,1,opt,name=cpu,proto3" json:"cpu,omitempty"` + Memory *MemoryResourceCapabilities `protobuf:"bytes,2,opt,name=memory,proto3" json:"memory,omitempty"` + Gpu *GpuResourceCapabilities `protobuf:"bytes,3,opt,name=gpu,proto3" json:"gpu,omitempty"` + SampledBudget *sandboxv1.SampledResourceBudgetCapabilities `protobuf:"bytes,4,opt,name=sampled_budget,json=sampledBudget,proto3" json:"sampled_budget,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2222,12 +2234,22 @@ func (x *ResourceCapabilities) GetGpu() *GpuResourceCapabilities { return nil } +func (x *ResourceCapabilities) GetSampledBudget() *sandboxv1.SampledResourceBudgetCapabilities { + if x != nil { + return x.SampledBudget + } + return nil +} + type CpuResourceCapabilities struct { state protoimpl.MessageState `protogen:"open.v1"` // The driver accepts and enforces a portable CPU limit. LimitSupported bool `protobuf:"varint,1,opt,name=limit_supported,json=limitSupported,proto3" json:"limit_supported,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Required for a negotiated resource contract. Absence means unreported. + LimitEnforcement *sandboxv1.ResourceEnforcement `protobuf:"bytes,2,opt,name=limit_enforcement,json=limitEnforcement,proto3" json:"limit_enforcement,omitempty"` + RequestEnforcement *sandboxv1.ResourceEnforcement `protobuf:"bytes,3,opt,name=request_enforcement,json=requestEnforcement,proto3" json:"request_enforcement,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CpuResourceCapabilities) Reset() { @@ -2267,12 +2289,29 @@ func (x *CpuResourceCapabilities) GetLimitSupported() bool { return false } +func (x *CpuResourceCapabilities) GetLimitEnforcement() *sandboxv1.ResourceEnforcement { + if x != nil { + return x.LimitEnforcement + } + return nil +} + +func (x *CpuResourceCapabilities) GetRequestEnforcement() *sandboxv1.ResourceEnforcement { + if x != nil { + return x.RequestEnforcement + } + return nil +} + type MemoryResourceCapabilities struct { state protoimpl.MessageState `protogen:"open.v1"` // The driver accepts and enforces a portable memory limit. LimitSupported bool `protobuf:"varint,1,opt,name=limit_supported,json=limitSupported,proto3" json:"limit_supported,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Required for a negotiated resource contract. Absence means unreported. + LimitEnforcement *sandboxv1.ResourceEnforcement `protobuf:"bytes,2,opt,name=limit_enforcement,json=limitEnforcement,proto3" json:"limit_enforcement,omitempty"` + RequestEnforcement *sandboxv1.ResourceEnforcement `protobuf:"bytes,3,opt,name=request_enforcement,json=requestEnforcement,proto3" json:"request_enforcement,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *MemoryResourceCapabilities) Reset() { @@ -2312,6 +2351,20 @@ func (x *MemoryResourceCapabilities) GetLimitSupported() bool { return false } +func (x *MemoryResourceCapabilities) GetLimitEnforcement() *sandboxv1.ResourceEnforcement { + if x != nil { + return x.LimitEnforcement + } + return nil +} + +func (x *MemoryResourceCapabilities) GetRequestEnforcement() *sandboxv1.ResourceEnforcement { + if x != nil { + return x.RequestEnforcement + } + return nil +} + type GpuResourceCapabilities struct { state protoimpl.MessageState `protogen:"open.v1"` // The driver accepts a GPU request with no explicit count. @@ -2592,7 +2645,9 @@ func (x *SandboxSpec) GetRestartPolicy() SandboxRestartPolicy { type ResourceRequirements struct { state protoimpl.MessageState `protogen:"open.v1"` // GPU requirements for the sandbox. Presence indicates a GPU request. - Gpu *GpuResourceRequirements `protobuf:"bytes,1,opt,name=gpu,proto3" json:"gpu,omitempty"` + Gpu *GpuResourceRequirements `protobuf:"bytes,1,opt,name=gpu,proto3" json:"gpu,omitempty"` + // Explicit opt-in to weaker sampled budgets; never inferred from hard limits. + SampledBudget *sandboxv1.SampledResourceBudget `protobuf:"bytes,4,opt,name=sampled_budget,json=sampledBudget,proto3" json:"sampled_budget,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2634,6 +2689,13 @@ func (x *ResourceRequirements) GetGpu() *GpuResourceRequirements { return nil } +func (x *ResourceRequirements) GetSampledBudget() *sandboxv1.SampledResourceBudget { + if x != nil { + return x.SampledBudget + } + return nil +} + // Public GPU resource requirements. type GpuResourceRequirements struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -3006,7 +3068,8 @@ type SandboxResources struct { // GPU requirements for the sandbox workload. Presence indicates a GPU // request. When count is omitted, the request uses the selected driver's // default GPU assignment behavior. - Gpu *GpuResourceRequirements `protobuf:"bytes,3,opt,name=gpu,proto3" json:"gpu,omitempty"` + Gpu *GpuResourceRequirements `protobuf:"bytes,3,opt,name=gpu,proto3" json:"gpu,omitempty"` + SampledBudget *sandboxv1.SampledResourceBudget `protobuf:"bytes,4,opt,name=sampled_budget,json=sampledBudget,proto3" json:"sampled_budget,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3062,6 +3125,13 @@ func (x *SandboxResources) GetGpu() *GpuResourceRequirements { return nil } +func (x *SandboxResources) GetSampledBudget() *sandboxv1.SampledResourceBudget { + if x != nil { + return x.SampledBudget + } + return nil +} + type SandboxServiceLevel struct { state protoimpl.MessageState `protogen:"open.v1"` Startup *SandboxStartup `protobuf:"bytes,1,opt,name=startup,proto3" json:"startup,omitempty"` @@ -19760,10 +19830,11 @@ const file_openshell_proto_rawDesc = "" + "\n" + "session_id\x18\x06 \x01(\tR\tsessionId\x12)\n" + "\x10credential_epoch\x18\a \x01(\x04R\x0fcredentialEpochJ\x04\b\x02\x10\x03J\x04\b\x05\x10\x06R\rexpires_at_msR\x15sandbox_expires_at_ms\"\x0f\n" + - "\rHealthRequest\"_\n" + + "\rHealthRequest\"\xa8\x01\n" + "\x0eHealthResponse\x123\n" + "\x06status\x18\x01 \x01(\x0e2\x1b.openshell.v1.ServiceStatusR\x06status\x12\x18\n" + - "\aversion\x18\x02 \x01(\tR\aversion\"\x17\n" + + "\aversion\x18\x02 \x01(\tR\aversion\x12G\n" + + " supports_sampled_resource_budget\x18\x03 \x01(\bR\x1dsupportsSampledResourceBudget\"\x17\n" + "\x15GetCurrentUserRequest\"\xb0\x01\n" + "\x16GetCurrentUserResponse\x12\x18\n" + "\asubject\x18\x01 \x01(\tR\asubject\x12!\n" + @@ -19795,15 +19866,20 @@ const file_openshell_proto_rawDesc = "" + "\vdriver_name\x18\x01 \x01(\tR\n" + "driverName\x12%\n" + "\x0edriver_version\x18\x02 \x01(\tR\rdriverVersion\x12W\n" + - "\x15resource_capabilities\x18\x03 \x01(\v2\".openshell.v1.ResourceCapabilitiesR\x14resourceCapabilities\"\xca\x01\n" + + "\x15resource_capabilities\x18\x03 \x01(\v2\".openshell.v1.ResourceCapabilitiesR\x14resourceCapabilities\"\xaa\x02\n" + "\x14ResourceCapabilities\x127\n" + "\x03cpu\x18\x01 \x01(\v2%.openshell.v1.CpuResourceCapabilitiesR\x03cpu\x12@\n" + "\x06memory\x18\x02 \x01(\v2(.openshell.v1.MemoryResourceCapabilitiesR\x06memory\x127\n" + - "\x03gpu\x18\x03 \x01(\v2%.openshell.v1.GpuResourceCapabilitiesR\x03gpu\"B\n" + + "\x03gpu\x18\x03 \x01(\v2%.openshell.v1.GpuResourceCapabilitiesR\x03gpu\x12^\n" + + "\x0esampled_budget\x18\x04 \x01(\v27.openshell.sandbox.v1.SampledResourceBudgetCapabilitiesR\rsampledBudget\"\xf6\x01\n" + "\x17CpuResourceCapabilities\x12'\n" + - "\x0flimit_supported\x18\x01 \x01(\bR\x0elimitSupported\"E\n" + + "\x0flimit_supported\x18\x01 \x01(\bR\x0elimitSupported\x12V\n" + + "\x11limit_enforcement\x18\x02 \x01(\v2).openshell.sandbox.v1.ResourceEnforcementR\x10limitEnforcement\x12Z\n" + + "\x13request_enforcement\x18\x03 \x01(\v2).openshell.sandbox.v1.ResourceEnforcementR\x12requestEnforcement\"\xf9\x01\n" + "\x1aMemoryResourceCapabilities\x12'\n" + - "\x0flimit_supported\x18\x01 \x01(\bR\x0elimitSupported\"\x95\x01\n" + + "\x0flimit_supported\x18\x01 \x01(\bR\x0elimitSupported\x12V\n" + + "\x11limit_enforcement\x18\x02 \x01(\v2).openshell.sandbox.v1.ResourceEnforcementR\x10limitEnforcement\x12Z\n" + + "\x13request_enforcement\x18\x03 \x01(\v2).openshell.sandbox.v1.ResourceEnforcementR\x12requestEnforcement\"\x95\x01\n" + "\x17GpuResourceCapabilities\x12>\n" + "\x1bdefault_selection_supported\x18\x01 \x01(\bR\x19defaultSelectionSupported\x12:\n" + "\x19count_selection_supported\x18\x02 \x01(\bR\x17countSelectionSupported\"\x80\x03\n" + @@ -19828,9 +19904,10 @@ const file_openshell_proto_rawDesc = "" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\n" + "\x10\vJ\x04\b\v\x10\fR\n" + - "gpu_deviceR\x16proposal_approval_mode\"O\n" + + "gpu_deviceR\x16proposal_approval_mode\"\xa3\x01\n" + "\x14ResourceRequirements\x127\n" + - "\x03gpu\x18\x01 \x01(\v2%.openshell.v1.GpuResourceRequirementsR\x03gpu\">\n" + + "\x03gpu\x18\x01 \x01(\v2%.openshell.v1.GpuResourceRequirementsR\x03gpu\x12R\n" + + "\x0esampled_budget\x18\x04 \x01(\v2+.openshell.sandbox.v1.SampledResourceBudgetR\rsampledBudget\">\n" + "\x17GpuResourceRequirements\x12\x19\n" + "\x05count\x18\x01 \x01(\rH\x00R\x05count\x88\x01\x01B\b\n" + "\x06_count\"\xef\x05\n" + @@ -19869,11 +19946,12 @@ const file_openshell_proto_rawDesc = "" + "\tresources\x18\x03 \x01(\v2\x1e.openshell.v1.SandboxResourcesR\tresources\x1a>\n" + "\x10EnvironmentEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"u\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc9\x01\n" + "\x10SandboxResources\x12\x10\n" + "\x03cpu\x18\x01 \x01(\tR\x03cpu\x12\x16\n" + "\x06memory\x18\x02 \x01(\tR\x06memory\x127\n" + - "\x03gpu\x18\x03 \x01(\v2%.openshell.v1.GpuResourceRequirementsR\x03gpu\"M\n" + + "\x03gpu\x18\x03 \x01(\v2%.openshell.v1.GpuResourceRequirementsR\x03gpu\x12R\n" + + "\x0esampled_budget\x18\x04 \x01(\v2+.openshell.sandbox.v1.SampledResourceBudgetR\rsampledBudget\"M\n" + "\x13SandboxServiceLevel\x126\n" + "\astartup\x18\x01 \x01(\v2\x1c.openshell.v1.SandboxStartupR\astartup\"k\n" + "\x0eSandboxStartup\x12<\n" + @@ -21879,25 +21957,28 @@ var file_openshell_proto_goTypes = []any{ nil, // 301: openshell.v1.SandboxPolicyRevision.ProvenanceEntry nil, // 302: openshell.v1.CreateWorkspaceRequest.LabelsEntry (*timestamppb.Timestamp)(nil), // 303: google.protobuf.Timestamp - (*datamodelv1.ObjectMeta)(nil), // 304: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 305: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 306: google.protobuf.Struct - (*durationpb.Duration)(nil), // 307: google.protobuf.Duration - (*datamodelv1.WorkspaceSelector)(nil), // 308: openshell.datamodel.v1.WorkspaceSelector - (*datamodelv1.Provider)(nil), // 309: openshell.datamodel.v1.Provider - (sandboxv1.PolicySource)(0), // 310: openshell.sandbox.v1.PolicySource - (*sandboxv1.NetworkEndpoint)(nil), // 311: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 312: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 313: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 314: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 315: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 316: openshell.sandbox.v1.L7Rule - (*sandboxv1.SandboxConfigSnapshot)(nil), // 317: openshell.sandbox.v1.SandboxConfigSnapshot - (*datamodelv1.Workspace)(nil), // 318: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 319: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 320: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 321: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 322: openshell.sandbox.v1.GetGatewayConfigResponse + (*sandboxv1.SampledResourceBudgetCapabilities)(nil), // 304: openshell.sandbox.v1.SampledResourceBudgetCapabilities + (*sandboxv1.ResourceEnforcement)(nil), // 305: openshell.sandbox.v1.ResourceEnforcement + (*datamodelv1.ObjectMeta)(nil), // 306: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 307: openshell.sandbox.v1.SandboxPolicy + (*sandboxv1.SampledResourceBudget)(nil), // 308: openshell.sandbox.v1.SampledResourceBudget + (*structpb.Struct)(nil), // 309: google.protobuf.Struct + (*durationpb.Duration)(nil), // 310: google.protobuf.Duration + (*datamodelv1.WorkspaceSelector)(nil), // 311: openshell.datamodel.v1.WorkspaceSelector + (*datamodelv1.Provider)(nil), // 312: openshell.datamodel.v1.Provider + (sandboxv1.PolicySource)(0), // 313: openshell.sandbox.v1.PolicySource + (*sandboxv1.NetworkEndpoint)(nil), // 314: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 315: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 316: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 317: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 318: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 319: openshell.sandbox.v1.L7Rule + (*sandboxv1.SandboxConfigSnapshot)(nil), // 320: openshell.sandbox.v1.SandboxConfigSnapshot + (*datamodelv1.Workspace)(nil), // 321: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 322: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 323: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 324: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 325: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ 303, // 0: openshell.v1.IssueSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp @@ -21914,538 +21995,545 @@ var file_openshell_proto_depIdxs = []int32{ 36, // 11: openshell.v1.ResourceCapabilities.cpu:type_name -> openshell.v1.CpuResourceCapabilities 37, // 12: openshell.v1.ResourceCapabilities.memory:type_name -> openshell.v1.MemoryResourceCapabilities 38, // 13: openshell.v1.ResourceCapabilities.gpu:type_name -> openshell.v1.GpuResourceCapabilities - 304, // 14: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 40, // 15: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec - 51, // 16: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus - 50, // 17: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance - 276, // 18: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry - 43, // 19: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 305, // 20: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 41, // 21: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements - 17, // 22: openshell.v1.SandboxSpec.restart_policy:type_name -> openshell.v1.SandboxRestartPolicy - 42, // 23: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 277, // 24: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 278, // 25: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 279, // 26: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 306, // 27: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 306, // 28: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 304, // 29: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 45, // 30: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec - 46, // 31: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig - 306, // 32: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct - 48, // 33: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel - 280, // 34: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry - 47, // 35: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources - 42, // 36: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements - 49, // 37: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup - 307, // 38: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration - 52, // 39: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition - 1, // 40: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 272, // 41: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus - 188, // 42: openshell.v1.SandboxStatus.configuration_admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 273, // 43: openshell.v1.SandboxStatus.provisioning:type_name -> openshell.v1.SandboxProvisioning - 303, // 44: openshell.v1.SandboxStatus.next_restart_time:type_name -> google.protobuf.Timestamp - 303, // 45: openshell.v1.SandboxStatus.main_process_started_time:type_name -> google.protobuf.Timestamp - 270, // 46: openshell.v1.SandboxStatus.last_token_grant_failure:type_name -> openshell.v1.TokenGrantFailureObservation - 303, // 47: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp - 303, // 48: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp - 281, // 49: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 308, // 50: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 40, // 51: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 282, // 52: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 283, // 53: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 274, // 54: openshell.v1.CreateSandboxRequest.service_exposures:type_name -> openshell.v1.SandboxServiceExposure - 308, // 55: openshell.v1.UpdateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 56, // 56: openshell.v1.UpdateSandboxRequest.command:type_name -> openshell.v1.SandboxCommandUpdate - 57, // 57: openshell.v1.UpdateSandboxRequest.environment:type_name -> openshell.v1.SandboxMapUpdate - 57, // 58: openshell.v1.UpdateSandboxRequest.labels:type_name -> openshell.v1.SandboxMapUpdate - 57, // 59: openshell.v1.UpdateSandboxRequest.annotations:type_name -> openshell.v1.SandboxMapUpdate - 284, // 60: openshell.v1.SandboxMapUpdate.values:type_name -> openshell.v1.SandboxMapUpdate.ValuesEntry - 308, // 61: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 44, // 62: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 308, // 63: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 64: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 65: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 44, // 66: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 44, // 67: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate - 18, // 68: openshell.v1.DeleteSandboxTemplateResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 308, // 69: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 303, // 70: openshell.v1.BeginRootfsTarStagingResponse.expiration_time:type_name -> google.protobuf.Timestamp - 308, // 71: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 72: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 73: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 74: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 75: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 76: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 77: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 78: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 79: openshell.v1.SuspendSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 80: openshell.v1.ResumeSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 39, // 81: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 285, // 82: openshell.v1.SandboxResponse.service_urls:type_name -> openshell.v1.SandboxResponse.ServiceUrlsEntry - 39, // 83: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 309, // 84: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 39, // 85: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 86, // 86: openshell.v1.AttachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt - 39, // 87: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 86, // 88: openshell.v1.DetachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt - 84, // 89: openshell.v1.ConfigSnapshotRevision.sandbox_config:type_name -> openshell.v1.SandboxConfigRevision - 82, // 90: openshell.v1.ConfigSnapshotRevision.provider_target:type_name -> openshell.v1.ProviderDesiredIdentity - 310, // 91: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource - 5, // 92: openshell.v1.ConfigUpdateOperation.component:type_name -> openshell.v1.ConfigComponent - 83, // 93: openshell.v1.ConfigUpdateOperation.target_revision:type_name -> openshell.v1.ConfigSnapshotRevision - 7, // 94: openshell.v1.ConfigUpdateOperation.state:type_name -> openshell.v1.ConfigUpdateOperationState - 6, // 95: openshell.v1.ConfigUpdateOperation.outcome:type_name -> openshell.v1.ConfigApplyOutcome - 303, // 96: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp - 303, // 97: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp - 303, // 98: openshell.v1.ConfigUpdateOperation.completed_time:type_name -> google.protobuf.Timestamp - 2, // 99: openshell.v1.ProviderMutationReceipt.kind:type_name -> openshell.v1.ProviderMutationKind - 82, // 100: openshell.v1.ProviderMutationReceipt.desired:type_name -> openshell.v1.ProviderDesiredIdentity - 303, // 101: openshell.v1.ProviderMutationReceipt.persisted_time:type_name -> google.protobuf.Timestamp - 4, // 102: openshell.v1.ProviderReadinessObservation.reason:type_name -> openshell.v1.ProviderReadinessReason - 86, // 103: openshell.v1.ProviderReadinessStatus.receipt:type_name -> openshell.v1.ProviderMutationReceipt - 3, // 104: openshell.v1.ProviderReadinessStatus.state:type_name -> openshell.v1.ProviderReadinessState - 4, // 105: openshell.v1.ProviderReadinessStatus.reason:type_name -> openshell.v1.ProviderReadinessReason - 87, // 106: openshell.v1.ProviderReadinessStatus.observed:type_name -> openshell.v1.ProviderReadinessObservation - 303, // 107: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp - 303, // 108: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp - 85, // 109: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation - 308, // 110: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 88, // 111: openshell.v1.GetSandboxProviderStatusResponse.status:type_name -> openshell.v1.ProviderReadinessStatus - 87, // 112: openshell.v1.ReportProviderReadinessRequest.observation:type_name -> openshell.v1.ProviderReadinessObservation - 307, // 113: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration - 307, // 114: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration - 18, // 115: openshell.v1.DeleteSandboxResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 308, // 116: openshell.v1.CreateSshSessionRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 303, // 117: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp - 308, // 118: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 20, // 119: openshell.v1.ExposeServiceRequest.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode - 308, // 120: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 121: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 103, // 122: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 308, // 123: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 18, // 124: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 304, // 125: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 20, // 126: openshell.v1.ServiceEndpoint.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode - 102, // 127: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 18, // 128: openshell.v1.RevokeSshSessionResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 308, // 129: openshell.v1.ExecSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 286, // 130: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 307, // 131: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration - 107, // 132: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 108, // 133: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 109, // 134: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 213, // 135: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 214, // 136: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 111, // 137: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 106, // 138: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 114, // 139: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 304, // 140: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 303, // 141: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp - 308, // 142: openshell.v1.WatchSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 303, // 143: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp - 39, // 144: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 118, // 145: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 53, // 146: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 119, // 147: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 230, // 148: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 303, // 149: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp - 287, // 150: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 308, // 151: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 309, // 152: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 308, // 153: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 154: openshell.v1.GetProviderCredentialsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 307, // 155: openshell.v1.GetProviderCredentialsRequest.minimum_remaining_lifetime:type_name -> google.protobuf.Duration - 303, // 156: openshell.v1.ProviderCredentialValue.expiration_time:type_name -> google.protobuf.Timestamp - 288, // 157: openshell.v1.GetProviderCredentialsResponse.credentials:type_name -> openshell.v1.GetProviderCredentialsResponse.CredentialsEntry - 308, // 158: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 159: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 309, // 160: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 289, // 161: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry - 308, // 162: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 309, // 163: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 86, // 164: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt - 309, // 165: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 308, // 166: openshell.v1.ListProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 167: openshell.v1.GetProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 151, // 168: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 307, // 169: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration - 134, // 170: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride - 8, // 171: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType - 135, // 172: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken - 140, // 173: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 136, // 174: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant - 9, // 175: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 307, // 176: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration - 307, // 177: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration - 138, // 178: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 139, // 179: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput - 9, // 180: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 303, // 181: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp - 303, // 182: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp - 303, // 183: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp - 16, // 184: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 303, // 185: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp - 308, // 186: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 141, // 187: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 308, // 188: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 9, // 189: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 290, // 190: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 303, // 191: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp - 141, // 192: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 308, // 193: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 141, // 194: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 308, // 195: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 18, // 196: openshell.v1.DeleteProviderRefreshResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 10, // 197: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 137, // 198: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 311, // 199: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 312, // 200: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 142, // 201: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 291, // 202: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 152, // 203: openshell.v1.ProviderProfile.files:type_name -> openshell.v1.ProviderProfileFile - 151, // 204: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 151, // 205: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 308, // 206: openshell.v1.ImportProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 132, // 207: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 133, // 208: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 151, // 209: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 308, // 210: openshell.v1.UpdateProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 132, // 211: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 133, // 212: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 151, // 213: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 308, // 214: openshell.v1.LintProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 132, // 215: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 133, // 216: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 18, // 217: openshell.v1.DeleteProviderResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 308, // 218: openshell.v1.DeleteProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 18, // 219: openshell.v1.DeleteProviderProfileResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 165, // 220: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 292, // 221: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 293, // 222: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry - 294, // 223: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 295, // 224: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 4, // 225: openshell.v1.GetSandboxProviderEnvironmentResponse.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason - 296, // 226: openshell.v1.GetSandboxProviderEnvironmentResponse.files:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntry - 11, // 227: openshell.v1.ProviderEnvironmentValue.classification:type_name -> openshell.v1.ProviderEnvironmentValueClassification - 166, // 228: openshell.v1.ProviderEnvironmentValue.static_credential_binding:type_name -> openshell.v1.StaticCredentialBinding - 303, // 229: openshell.v1.ProviderEnvironmentValue.expiration_time:type_name -> google.protobuf.Timestamp - 168, // 230: openshell.v1.ProviderEnvironmentSnapshot.values:type_name -> openshell.v1.ProviderEnvironmentValue - 297, // 231: openshell.v1.ProviderEnvironmentSnapshot.dynamic_credentials:type_name -> openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry - 4, // 232: openshell.v1.ProviderEnvironmentSnapshot.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason - 298, // 233: openshell.v1.ProviderEnvironmentSnapshot.files:type_name -> openshell.v1.ProviderEnvironmentSnapshot.FilesEntry - 307, // 234: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration - 308, // 235: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 305, // 236: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 313, // 237: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 173, // 238: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 299, // 239: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 174, // 240: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 175, // 241: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 176, // 242: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 178, // 243: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 179, // 244: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 180, // 245: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 314, // 246: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 312, // 247: openshell.v1.L7RuleTarget.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 315, // 248: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 177, // 249: openshell.v1.AddDenyRules.target:type_name -> openshell.v1.L7RuleTarget - 316, // 250: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 177, // 251: openshell.v1.AddAllowRules.target:type_name -> openshell.v1.L7RuleTarget - 300, // 252: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 308, // 253: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 191, // 254: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 308, // 255: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 191, // 256: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 13, // 257: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 12, // 258: openshell.v1.SandboxConfigurationAdmission.state:type_name -> openshell.v1.ConfigurationAdmissionState - 188, // 259: openshell.v1.ReportSandboxConfigurationRequest.admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 13, // 260: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 303, // 261: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp - 303, // 262: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp - 305, // 263: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 301, // 264: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 308, // 265: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 303, // 266: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp - 118, // 267: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 118, // 268: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 198, // 269: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 206, // 270: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 223, // 271: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 224, // 272: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 202, // 273: openshell.v1.SupervisorMessage.config_update_result:type_name -> openshell.v1.ConfigUpdateResult - 199, // 274: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 204, // 275: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 207, // 276: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 212, // 277: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 224, // 278: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 205, // 279: openshell.v1.GatewayMessage.session_redirect:type_name -> openshell.v1.SessionRedirect - 200, // 280: openshell.v1.GatewayMessage.config_update:type_name -> openshell.v1.ConfigUpdate - 200, // 281: openshell.v1.SessionAccepted.bootstrap:type_name -> openshell.v1.ConfigUpdate - 307, // 282: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration - 317, // 283: openshell.v1.ConfigUpdate.sandbox_config:type_name -> openshell.sandbox.v1.SandboxConfigSnapshot - 169, // 284: openshell.v1.ConfigUpdate.provider_environment:type_name -> openshell.v1.ProviderEnvironmentSnapshot - 6, // 285: openshell.v1.ConfigUpdateResult.outcome:type_name -> openshell.v1.ConfigApplyOutcome - 201, // 286: openshell.v1.ConfigUpdateResult.failure:type_name -> openshell.v1.ConfigApplyFailure - 188, // 287: openshell.v1.ConfigUpdateResult.admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 203, // 288: openshell.v1.ConfigUpdateResult.policy_load:type_name -> openshell.v1.PolicyLoadResult - 213, // 289: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 214, // 290: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 215, // 291: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 212, // 292: openshell.v1.PeerRelayInit.relay_open:type_name -> openshell.v1.RelayOpen - 217, // 293: openshell.v1.PeerRelayFrame.init:type_name -> openshell.v1.PeerRelayInit - 219, // 294: openshell.v1.PeerNotifyConfigUpdateRequest.sandbox:type_name -> openshell.v1.PeerConfigSandboxTarget - 220, // 295: openshell.v1.PeerNotifyConfigUpdateRequest.provider:type_name -> openshell.v1.PeerConfigProviderTarget - 303, // 296: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp - 303, // 297: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp - 225, // 298: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 227, // 299: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 314, // 300: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 303, // 301: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp - 303, // 302: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp - 303, // 303: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp - 303, // 304: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp - 305, // 305: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 305, // 306: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 308, // 307: openshell.v1.SubmitPolicyAnalysisRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 226, // 308: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 229, // 309: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 228, // 310: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 308, // 311: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 229, // 312: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 303, // 313: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp - 308, // 314: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 315: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 316: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 239, // 317: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 308, // 318: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 314, // 319: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 308, // 320: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 321: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 308, // 322: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 303, // 323: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp - 249, // 324: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 302, // 325: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 318, // 326: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 318, // 327: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 318, // 328: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 18, // 329: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 304, // 330: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 15, // 331: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 308, // 332: openshell.v1.AddWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 15, // 333: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 259, // 334: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 308, // 335: openshell.v1.RemoveWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 18, // 336: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 308, // 337: openshell.v1.ListWorkspaceMembersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 259, // 338: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 303, // 339: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp - 19, // 340: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult - 275, // 341: openshell.v1.EndpointObservation.failure:type_name -> openshell.v1.EndpointFailure - 267, // 342: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation - 269, // 343: openshell.v1.ReportEndpointStatusRequest.token_grant_status:type_name -> openshell.v1.TokenGrantStatus - 270, // 344: openshell.v1.TokenGrantStatus.last_failure:type_name -> openshell.v1.TokenGrantFailureObservation - 275, // 345: openshell.v1.TokenGrantFailureObservation.failure:type_name -> openshell.v1.EndpointFailure - 19, // 346: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult - 303, // 347: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp - 275, // 348: openshell.v1.EndpointStatus.last_failure:type_name -> openshell.v1.EndpointFailure - 303, // 349: openshell.v1.SandboxProvisioning.configuration_change_time:type_name -> google.protobuf.Timestamp - 303, // 350: openshell.v1.SandboxProvisioning.first_rejection_time:type_name -> google.protobuf.Timestamp - 303, // 351: openshell.v1.SandboxProvisioning.deadline:type_name -> google.protobuf.Timestamp - 303, // 352: openshell.v1.SandboxProvisioning.timeout_time:type_name -> google.protobuf.Timestamp - 303, // 353: openshell.v1.SandboxProvisioning.cleanup_completed_time:type_name -> google.protobuf.Timestamp - 303, // 354: openshell.v1.SandboxProvisioning.cleanup_retry_time:type_name -> google.protobuf.Timestamp - 303, // 355: openshell.v1.SandboxProvisioning.attachment_change_time:type_name -> google.protobuf.Timestamp - 303, // 356: openshell.v1.SandboxProvisioning.preparation_deadline:type_name -> google.protobuf.Timestamp - 303, // 357: openshell.v1.SandboxProvisioning.admission_start_time:type_name -> google.protobuf.Timestamp - 20, // 358: openshell.v1.SandboxServiceExposure.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode - 21, // 359: openshell.v1.EndpointFailure.domain:type_name -> openshell.v1.EndpointFailureDomain - 16, // 360: openshell.v1.EndpointFailure.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 123, // 361: openshell.v1.GetProviderCredentialsResponse.CredentialsEntry.value:type_name -> openshell.v1.ProviderCredentialValue - 303, // 362: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 303, // 363: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 137, // 364: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 166, // 365: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 137, // 366: openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 26, // 367: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 28, // 368: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 30, // 369: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 54, // 370: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 65, // 371: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest - 67, // 372: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 68, // 373: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 58, // 374: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest - 59, // 375: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest - 60, // 376: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest - 61, // 377: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest - 69, // 378: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 70, // 379: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 71, // 380: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 89, // 381: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest - 72, // 382: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 73, // 383: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 74, // 384: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 75, // 385: openshell.v1.OpenShell.SuspendSandbox:input_type -> openshell.v1.SuspendSandboxRequest - 76, // 386: openshell.v1.OpenShell.ResumeSandbox:input_type -> openshell.v1.ResumeSandboxRequest - 94, // 387: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 96, // 388: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 97, // 389: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 98, // 390: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 100, // 391: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 104, // 392: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 106, // 393: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 112, // 394: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 113, // 395: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 120, // 396: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 121, // 397: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 122, // 398: openshell.v1.OpenShell.GetProviderCredentials:input_type -> openshell.v1.GetProviderCredentialsRequest - 125, // 399: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 130, // 400: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 131, // 401: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 155, // 402: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 157, // 403: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 159, // 404: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 126, // 405: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 143, // 406: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 145, // 407: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 147, // 408: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 149, // 409: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 127, // 410: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 162, // 411: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 319, // 412: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 320, // 413: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 172, // 414: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 182, // 415: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 184, // 416: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 186, // 417: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 268, // 418: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest - 91, // 419: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest - 189, // 420: openshell.v1.OpenShell.ReportSandboxConfiguration:input_type -> openshell.v1.ReportSandboxConfigurationRequest - 164, // 421: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 170, // 422: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 192, // 423: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 193, // 424: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 196, // 425: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 208, // 426: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 210, // 427: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest - 216, // 428: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 218, // 429: openshell.v1.OpenShell.PeerRelay:input_type -> openshell.v1.PeerRelayFrame - 91, // 430: openshell.v1.OpenShell.PeerReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest - 268, // 431: openshell.v1.OpenShell.PeerReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest - 89, // 432: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest - 221, // 433: openshell.v1.OpenShell.PeerNotifyConfigUpdate:input_type -> openshell.v1.PeerNotifyConfigUpdateRequest - 116, // 434: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 231, // 435: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 233, // 436: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 235, // 437: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 237, // 438: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 240, // 439: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 242, // 440: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 244, // 441: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 246, // 442: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 248, // 443: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 22, // 444: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 24, // 445: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 251, // 446: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 253, // 447: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 255, // 448: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 257, // 449: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 260, // 450: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 262, // 451: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 264, // 452: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 55, // 453: openshell.v1.SandboxUpdates.UpdateSandbox:input_type -> openshell.v1.UpdateSandboxRequest - 27, // 454: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 29, // 455: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 31, // 456: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 77, // 457: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 66, // 458: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse - 77, // 459: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 78, // 460: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 62, // 461: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 62, // 462: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 63, // 463: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse - 64, // 464: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse - 79, // 465: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 80, // 466: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 81, // 467: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 90, // 468: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse - 93, // 469: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 77, // 470: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 77, // 471: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 77, // 472: openshell.v1.OpenShell.SuspendSandbox:output_type -> openshell.v1.SandboxResponse - 77, // 473: openshell.v1.OpenShell.ResumeSandbox:output_type -> openshell.v1.SandboxResponse - 95, // 474: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 103, // 475: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 103, // 476: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 99, // 477: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 101, // 478: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 105, // 479: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 110, // 480: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 112, // 481: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 110, // 482: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 128, // 483: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 128, // 484: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 124, // 485: openshell.v1.OpenShell.GetProviderCredentials:output_type -> openshell.v1.GetProviderCredentialsResponse - 129, // 486: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 154, // 487: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 153, // 488: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 156, // 489: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 158, // 490: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 160, // 491: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 128, // 492: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 144, // 493: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 146, // 494: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 148, // 495: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 150, // 496: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 161, // 497: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 163, // 498: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 321, // 499: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 322, // 500: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 181, // 501: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 183, // 502: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 185, // 503: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 187, // 504: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 271, // 505: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse - 92, // 506: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse - 190, // 507: openshell.v1.OpenShell.ReportSandboxConfiguration:output_type -> openshell.v1.ReportSandboxConfigurationResponse - 167, // 508: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 171, // 509: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 195, // 510: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 194, // 511: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 197, // 512: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 209, // 513: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 211, // 514: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse - 216, // 515: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 218, // 516: openshell.v1.OpenShell.PeerRelay:output_type -> openshell.v1.PeerRelayFrame - 92, // 517: openshell.v1.OpenShell.PeerReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse - 271, // 518: openshell.v1.OpenShell.PeerReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse - 90, // 519: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse - 222, // 520: openshell.v1.OpenShell.PeerNotifyConfigUpdate:output_type -> openshell.v1.PeerNotifyConfigUpdateResponse - 117, // 521: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 232, // 522: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 234, // 523: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 236, // 524: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 238, // 525: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 241, // 526: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 243, // 527: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 245, // 528: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 247, // 529: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 250, // 530: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 23, // 531: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 25, // 532: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 252, // 533: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 254, // 534: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 256, // 535: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 258, // 536: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 261, // 537: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 263, // 538: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 265, // 539: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 77, // 540: openshell.v1.SandboxUpdates.UpdateSandbox:output_type -> openshell.v1.SandboxResponse - 454, // [454:541] is the sub-list for method output_type - 367, // [367:454] is the sub-list for method input_type - 367, // [367:367] is the sub-list for extension type_name - 367, // [367:367] is the sub-list for extension extendee - 0, // [0:367] is the sub-list for field type_name + 304, // 14: openshell.v1.ResourceCapabilities.sampled_budget:type_name -> openshell.sandbox.v1.SampledResourceBudgetCapabilities + 305, // 15: openshell.v1.CpuResourceCapabilities.limit_enforcement:type_name -> openshell.sandbox.v1.ResourceEnforcement + 305, // 16: openshell.v1.CpuResourceCapabilities.request_enforcement:type_name -> openshell.sandbox.v1.ResourceEnforcement + 305, // 17: openshell.v1.MemoryResourceCapabilities.limit_enforcement:type_name -> openshell.sandbox.v1.ResourceEnforcement + 305, // 18: openshell.v1.MemoryResourceCapabilities.request_enforcement:type_name -> openshell.sandbox.v1.ResourceEnforcement + 306, // 19: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 40, // 20: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec + 51, // 21: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus + 50, // 22: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance + 276, // 23: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 43, // 24: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate + 307, // 25: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 41, // 26: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements + 17, // 27: openshell.v1.SandboxSpec.restart_policy:type_name -> openshell.v1.SandboxRestartPolicy + 42, // 28: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements + 308, // 29: openshell.v1.ResourceRequirements.sampled_budget:type_name -> openshell.sandbox.v1.SampledResourceBudget + 277, // 30: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 278, // 31: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 279, // 32: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 309, // 33: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 309, // 34: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 306, // 35: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 45, // 36: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec + 46, // 37: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig + 309, // 38: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct + 48, // 39: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel + 280, // 40: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + 47, // 41: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources + 42, // 42: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements + 308, // 43: openshell.v1.SandboxResources.sampled_budget:type_name -> openshell.sandbox.v1.SampledResourceBudget + 49, // 44: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup + 310, // 45: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration + 52, // 46: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition + 1, // 47: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase + 272, // 48: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus + 188, // 49: openshell.v1.SandboxStatus.configuration_admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 273, // 50: openshell.v1.SandboxStatus.provisioning:type_name -> openshell.v1.SandboxProvisioning + 303, // 51: openshell.v1.SandboxStatus.next_restart_time:type_name -> google.protobuf.Timestamp + 303, // 52: openshell.v1.SandboxStatus.main_process_started_time:type_name -> google.protobuf.Timestamp + 270, // 53: openshell.v1.SandboxStatus.last_token_grant_failure:type_name -> openshell.v1.TokenGrantFailureObservation + 303, // 54: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp + 303, // 55: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp + 281, // 56: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 311, // 57: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 40, // 58: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec + 282, // 59: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 283, // 60: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 274, // 61: openshell.v1.CreateSandboxRequest.service_exposures:type_name -> openshell.v1.SandboxServiceExposure + 311, // 62: openshell.v1.UpdateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 56, // 63: openshell.v1.UpdateSandboxRequest.command:type_name -> openshell.v1.SandboxCommandUpdate + 57, // 64: openshell.v1.UpdateSandboxRequest.environment:type_name -> openshell.v1.SandboxMapUpdate + 57, // 65: openshell.v1.UpdateSandboxRequest.labels:type_name -> openshell.v1.SandboxMapUpdate + 57, // 66: openshell.v1.UpdateSandboxRequest.annotations:type_name -> openshell.v1.SandboxMapUpdate + 284, // 67: openshell.v1.SandboxMapUpdate.values:type_name -> openshell.v1.SandboxMapUpdate.ValuesEntry + 311, // 68: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 44, // 69: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 311, // 70: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 71: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 72: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 44, // 73: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 44, // 74: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate + 18, // 75: openshell.v1.DeleteSandboxTemplateResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 311, // 76: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 303, // 77: openshell.v1.BeginRootfsTarStagingResponse.expiration_time:type_name -> google.protobuf.Timestamp + 311, // 78: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 79: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 80: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 81: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 82: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 83: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 84: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 85: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 86: openshell.v1.SuspendSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 87: openshell.v1.ResumeSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 39, // 88: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 285, // 89: openshell.v1.SandboxResponse.service_urls:type_name -> openshell.v1.SandboxResponse.ServiceUrlsEntry + 39, // 90: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 312, // 91: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 39, // 92: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 86, // 93: openshell.v1.AttachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 39, // 94: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 86, // 95: openshell.v1.DetachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 84, // 96: openshell.v1.ConfigSnapshotRevision.sandbox_config:type_name -> openshell.v1.SandboxConfigRevision + 82, // 97: openshell.v1.ConfigSnapshotRevision.provider_target:type_name -> openshell.v1.ProviderDesiredIdentity + 313, // 98: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource + 5, // 99: openshell.v1.ConfigUpdateOperation.component:type_name -> openshell.v1.ConfigComponent + 83, // 100: openshell.v1.ConfigUpdateOperation.target_revision:type_name -> openshell.v1.ConfigSnapshotRevision + 7, // 101: openshell.v1.ConfigUpdateOperation.state:type_name -> openshell.v1.ConfigUpdateOperationState + 6, // 102: openshell.v1.ConfigUpdateOperation.outcome:type_name -> openshell.v1.ConfigApplyOutcome + 303, // 103: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp + 303, // 104: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp + 303, // 105: openshell.v1.ConfigUpdateOperation.completed_time:type_name -> google.protobuf.Timestamp + 2, // 106: openshell.v1.ProviderMutationReceipt.kind:type_name -> openshell.v1.ProviderMutationKind + 82, // 107: openshell.v1.ProviderMutationReceipt.desired:type_name -> openshell.v1.ProviderDesiredIdentity + 303, // 108: openshell.v1.ProviderMutationReceipt.persisted_time:type_name -> google.protobuf.Timestamp + 4, // 109: openshell.v1.ProviderReadinessObservation.reason:type_name -> openshell.v1.ProviderReadinessReason + 86, // 110: openshell.v1.ProviderReadinessStatus.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 3, // 111: openshell.v1.ProviderReadinessStatus.state:type_name -> openshell.v1.ProviderReadinessState + 4, // 112: openshell.v1.ProviderReadinessStatus.reason:type_name -> openshell.v1.ProviderReadinessReason + 87, // 113: openshell.v1.ProviderReadinessStatus.observed:type_name -> openshell.v1.ProviderReadinessObservation + 303, // 114: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp + 303, // 115: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp + 85, // 116: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation + 311, // 117: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 88, // 118: openshell.v1.GetSandboxProviderStatusResponse.status:type_name -> openshell.v1.ProviderReadinessStatus + 87, // 119: openshell.v1.ReportProviderReadinessRequest.observation:type_name -> openshell.v1.ProviderReadinessObservation + 310, // 120: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration + 310, // 121: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration + 18, // 122: openshell.v1.DeleteSandboxResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 311, // 123: openshell.v1.CreateSshSessionRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 303, // 124: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp + 311, // 125: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 20, // 126: openshell.v1.ExposeServiceRequest.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode + 311, // 127: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 128: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 103, // 129: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 311, // 130: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 18, // 131: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 306, // 132: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 20, // 133: openshell.v1.ServiceEndpoint.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode + 102, // 134: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 18, // 135: openshell.v1.RevokeSshSessionResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 311, // 136: openshell.v1.ExecSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 286, // 137: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 310, // 138: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration + 107, // 139: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 108, // 140: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 109, // 141: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 213, // 142: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 214, // 143: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 111, // 144: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 106, // 145: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 114, // 146: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 306, // 147: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 303, // 148: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp + 311, // 149: openshell.v1.WatchSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 303, // 150: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp + 39, // 151: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 118, // 152: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 53, // 153: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 119, // 154: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 230, // 155: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 303, // 156: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp + 287, // 157: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 311, // 158: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 312, // 159: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 311, // 160: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 161: openshell.v1.GetProviderCredentialsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 310, // 162: openshell.v1.GetProviderCredentialsRequest.minimum_remaining_lifetime:type_name -> google.protobuf.Duration + 303, // 163: openshell.v1.ProviderCredentialValue.expiration_time:type_name -> google.protobuf.Timestamp + 288, // 164: openshell.v1.GetProviderCredentialsResponse.credentials:type_name -> openshell.v1.GetProviderCredentialsResponse.CredentialsEntry + 311, // 165: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 166: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 312, // 167: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 289, // 168: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + 311, // 169: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 312, // 170: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 86, // 171: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt + 312, // 172: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 311, // 173: openshell.v1.ListProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 174: openshell.v1.GetProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 151, // 175: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 310, // 176: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration + 134, // 177: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 8, // 178: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType + 135, // 179: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 140, // 180: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 136, // 181: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 9, // 182: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 310, // 183: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration + 310, // 184: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration + 138, // 185: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 139, // 186: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 9, // 187: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 303, // 188: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp + 303, // 189: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp + 303, // 190: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp + 16, // 191: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 303, // 192: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp + 311, // 193: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 141, // 194: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 311, // 195: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 9, // 196: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 290, // 197: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 303, // 198: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp + 141, // 199: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 311, // 200: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 141, // 201: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 311, // 202: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 18, // 203: openshell.v1.DeleteProviderRefreshResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 10, // 204: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 137, // 205: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 314, // 206: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 315, // 207: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 142, // 208: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 291, // 209: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 152, // 210: openshell.v1.ProviderProfile.files:type_name -> openshell.v1.ProviderProfileFile + 151, // 211: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 151, // 212: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 311, // 213: openshell.v1.ImportProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 132, // 214: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 133, // 215: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 151, // 216: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 311, // 217: openshell.v1.UpdateProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 132, // 218: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 133, // 219: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 151, // 220: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 311, // 221: openshell.v1.LintProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 132, // 222: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 133, // 223: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 18, // 224: openshell.v1.DeleteProviderResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 311, // 225: openshell.v1.DeleteProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 18, // 226: openshell.v1.DeleteProviderProfileResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 165, // 227: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 292, // 228: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 293, // 229: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + 294, // 230: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 295, // 231: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 4, // 232: openshell.v1.GetSandboxProviderEnvironmentResponse.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason + 296, // 233: openshell.v1.GetSandboxProviderEnvironmentResponse.files:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntry + 11, // 234: openshell.v1.ProviderEnvironmentValue.classification:type_name -> openshell.v1.ProviderEnvironmentValueClassification + 166, // 235: openshell.v1.ProviderEnvironmentValue.static_credential_binding:type_name -> openshell.v1.StaticCredentialBinding + 303, // 236: openshell.v1.ProviderEnvironmentValue.expiration_time:type_name -> google.protobuf.Timestamp + 168, // 237: openshell.v1.ProviderEnvironmentSnapshot.values:type_name -> openshell.v1.ProviderEnvironmentValue + 297, // 238: openshell.v1.ProviderEnvironmentSnapshot.dynamic_credentials:type_name -> openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry + 4, // 239: openshell.v1.ProviderEnvironmentSnapshot.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason + 298, // 240: openshell.v1.ProviderEnvironmentSnapshot.files:type_name -> openshell.v1.ProviderEnvironmentSnapshot.FilesEntry + 310, // 241: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration + 311, // 242: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 307, // 243: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 316, // 244: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 173, // 245: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 299, // 246: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 174, // 247: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 175, // 248: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 176, // 249: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 178, // 250: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 179, // 251: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 180, // 252: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 317, // 253: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 315, // 254: openshell.v1.L7RuleTarget.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 318, // 255: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 177, // 256: openshell.v1.AddDenyRules.target:type_name -> openshell.v1.L7RuleTarget + 319, // 257: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 177, // 258: openshell.v1.AddAllowRules.target:type_name -> openshell.v1.L7RuleTarget + 300, // 259: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 311, // 260: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 191, // 261: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 311, // 262: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 191, // 263: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 13, // 264: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 12, // 265: openshell.v1.SandboxConfigurationAdmission.state:type_name -> openshell.v1.ConfigurationAdmissionState + 188, // 266: openshell.v1.ReportSandboxConfigurationRequest.admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 13, // 267: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 303, // 268: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp + 303, // 269: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp + 307, // 270: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 301, // 271: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 311, // 272: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 303, // 273: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp + 118, // 274: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 118, // 275: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 198, // 276: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 206, // 277: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 223, // 278: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 224, // 279: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 202, // 280: openshell.v1.SupervisorMessage.config_update_result:type_name -> openshell.v1.ConfigUpdateResult + 199, // 281: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 204, // 282: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 207, // 283: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 212, // 284: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 224, // 285: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 205, // 286: openshell.v1.GatewayMessage.session_redirect:type_name -> openshell.v1.SessionRedirect + 200, // 287: openshell.v1.GatewayMessage.config_update:type_name -> openshell.v1.ConfigUpdate + 200, // 288: openshell.v1.SessionAccepted.bootstrap:type_name -> openshell.v1.ConfigUpdate + 310, // 289: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration + 320, // 290: openshell.v1.ConfigUpdate.sandbox_config:type_name -> openshell.sandbox.v1.SandboxConfigSnapshot + 169, // 291: openshell.v1.ConfigUpdate.provider_environment:type_name -> openshell.v1.ProviderEnvironmentSnapshot + 6, // 292: openshell.v1.ConfigUpdateResult.outcome:type_name -> openshell.v1.ConfigApplyOutcome + 201, // 293: openshell.v1.ConfigUpdateResult.failure:type_name -> openshell.v1.ConfigApplyFailure + 188, // 294: openshell.v1.ConfigUpdateResult.admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 203, // 295: openshell.v1.ConfigUpdateResult.policy_load:type_name -> openshell.v1.PolicyLoadResult + 213, // 296: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 214, // 297: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 215, // 298: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 212, // 299: openshell.v1.PeerRelayInit.relay_open:type_name -> openshell.v1.RelayOpen + 217, // 300: openshell.v1.PeerRelayFrame.init:type_name -> openshell.v1.PeerRelayInit + 219, // 301: openshell.v1.PeerNotifyConfigUpdateRequest.sandbox:type_name -> openshell.v1.PeerConfigSandboxTarget + 220, // 302: openshell.v1.PeerNotifyConfigUpdateRequest.provider:type_name -> openshell.v1.PeerConfigProviderTarget + 303, // 303: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp + 303, // 304: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp + 225, // 305: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 227, // 306: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 317, // 307: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 303, // 308: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp + 303, // 309: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp + 303, // 310: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp + 303, // 311: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp + 307, // 312: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 307, // 313: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 311, // 314: openshell.v1.SubmitPolicyAnalysisRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 226, // 315: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 229, // 316: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 228, // 317: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 311, // 318: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 229, // 319: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 303, // 320: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp + 311, // 321: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 322: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 323: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 239, // 324: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 311, // 325: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 317, // 326: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 311, // 327: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 328: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 311, // 329: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 303, // 330: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp + 249, // 331: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 302, // 332: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 321, // 333: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 321, // 334: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 321, // 335: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 18, // 336: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 306, // 337: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 15, // 338: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 311, // 339: openshell.v1.AddWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 15, // 340: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 259, // 341: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 311, // 342: openshell.v1.RemoveWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 18, // 343: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 311, // 344: openshell.v1.ListWorkspaceMembersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 259, // 345: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 303, // 346: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp + 19, // 347: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult + 275, // 348: openshell.v1.EndpointObservation.failure:type_name -> openshell.v1.EndpointFailure + 267, // 349: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation + 269, // 350: openshell.v1.ReportEndpointStatusRequest.token_grant_status:type_name -> openshell.v1.TokenGrantStatus + 270, // 351: openshell.v1.TokenGrantStatus.last_failure:type_name -> openshell.v1.TokenGrantFailureObservation + 275, // 352: openshell.v1.TokenGrantFailureObservation.failure:type_name -> openshell.v1.EndpointFailure + 19, // 353: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult + 303, // 354: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp + 275, // 355: openshell.v1.EndpointStatus.last_failure:type_name -> openshell.v1.EndpointFailure + 303, // 356: openshell.v1.SandboxProvisioning.configuration_change_time:type_name -> google.protobuf.Timestamp + 303, // 357: openshell.v1.SandboxProvisioning.first_rejection_time:type_name -> google.protobuf.Timestamp + 303, // 358: openshell.v1.SandboxProvisioning.deadline:type_name -> google.protobuf.Timestamp + 303, // 359: openshell.v1.SandboxProvisioning.timeout_time:type_name -> google.protobuf.Timestamp + 303, // 360: openshell.v1.SandboxProvisioning.cleanup_completed_time:type_name -> google.protobuf.Timestamp + 303, // 361: openshell.v1.SandboxProvisioning.cleanup_retry_time:type_name -> google.protobuf.Timestamp + 303, // 362: openshell.v1.SandboxProvisioning.attachment_change_time:type_name -> google.protobuf.Timestamp + 303, // 363: openshell.v1.SandboxProvisioning.preparation_deadline:type_name -> google.protobuf.Timestamp + 303, // 364: openshell.v1.SandboxProvisioning.admission_start_time:type_name -> google.protobuf.Timestamp + 20, // 365: openshell.v1.SandboxServiceExposure.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode + 21, // 366: openshell.v1.EndpointFailure.domain:type_name -> openshell.v1.EndpointFailureDomain + 16, // 367: openshell.v1.EndpointFailure.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 123, // 368: openshell.v1.GetProviderCredentialsResponse.CredentialsEntry.value:type_name -> openshell.v1.ProviderCredentialValue + 303, // 369: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 303, // 370: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 137, // 371: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 166, // 372: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 137, // 373: openshell.v1.ProviderEnvironmentSnapshot.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 26, // 374: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 28, // 375: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 30, // 376: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 54, // 377: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 65, // 378: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest + 67, // 379: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 68, // 380: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 58, // 381: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest + 59, // 382: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest + 60, // 383: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest + 61, // 384: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest + 69, // 385: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 70, // 386: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 71, // 387: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 89, // 388: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest + 72, // 389: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 73, // 390: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 74, // 391: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 75, // 392: openshell.v1.OpenShell.SuspendSandbox:input_type -> openshell.v1.SuspendSandboxRequest + 76, // 393: openshell.v1.OpenShell.ResumeSandbox:input_type -> openshell.v1.ResumeSandboxRequest + 94, // 394: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 96, // 395: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 97, // 396: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 98, // 397: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 100, // 398: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 104, // 399: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 106, // 400: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 112, // 401: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 113, // 402: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 120, // 403: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 121, // 404: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 122, // 405: openshell.v1.OpenShell.GetProviderCredentials:input_type -> openshell.v1.GetProviderCredentialsRequest + 125, // 406: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 130, // 407: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 131, // 408: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 155, // 409: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 157, // 410: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 159, // 411: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 126, // 412: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 143, // 413: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 145, // 414: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 147, // 415: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 149, // 416: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 127, // 417: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 162, // 418: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 322, // 419: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 323, // 420: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 172, // 421: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 182, // 422: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 184, // 423: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 186, // 424: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 268, // 425: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest + 91, // 426: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest + 189, // 427: openshell.v1.OpenShell.ReportSandboxConfiguration:input_type -> openshell.v1.ReportSandboxConfigurationRequest + 164, // 428: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 170, // 429: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 192, // 430: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 193, // 431: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 196, // 432: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 208, // 433: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 210, // 434: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest + 216, // 435: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 218, // 436: openshell.v1.OpenShell.PeerRelay:input_type -> openshell.v1.PeerRelayFrame + 91, // 437: openshell.v1.OpenShell.PeerReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest + 268, // 438: openshell.v1.OpenShell.PeerReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest + 89, // 439: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest + 221, // 440: openshell.v1.OpenShell.PeerNotifyConfigUpdate:input_type -> openshell.v1.PeerNotifyConfigUpdateRequest + 116, // 441: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 231, // 442: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 233, // 443: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 235, // 444: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 237, // 445: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 240, // 446: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 242, // 447: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 244, // 448: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 246, // 449: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 248, // 450: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 22, // 451: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 24, // 452: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 251, // 453: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 253, // 454: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 255, // 455: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 257, // 456: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 260, // 457: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 262, // 458: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 264, // 459: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 55, // 460: openshell.v1.SandboxUpdates.UpdateSandbox:input_type -> openshell.v1.UpdateSandboxRequest + 27, // 461: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 29, // 462: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 31, // 463: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 77, // 464: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 66, // 465: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse + 77, // 466: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 78, // 467: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 62, // 468: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 62, // 469: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 63, // 470: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse + 64, // 471: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse + 79, // 472: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 80, // 473: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 81, // 474: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 90, // 475: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse + 93, // 476: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 77, // 477: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 77, // 478: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 77, // 479: openshell.v1.OpenShell.SuspendSandbox:output_type -> openshell.v1.SandboxResponse + 77, // 480: openshell.v1.OpenShell.ResumeSandbox:output_type -> openshell.v1.SandboxResponse + 95, // 481: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 103, // 482: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 103, // 483: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 99, // 484: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 101, // 485: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 105, // 486: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 110, // 487: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 112, // 488: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 110, // 489: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 128, // 490: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 128, // 491: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 124, // 492: openshell.v1.OpenShell.GetProviderCredentials:output_type -> openshell.v1.GetProviderCredentialsResponse + 129, // 493: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 154, // 494: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 153, // 495: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 156, // 496: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 158, // 497: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 160, // 498: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 128, // 499: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 144, // 500: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 146, // 501: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 148, // 502: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 150, // 503: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 161, // 504: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 163, // 505: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 324, // 506: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 325, // 507: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 181, // 508: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 183, // 509: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 185, // 510: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 187, // 511: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 271, // 512: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse + 92, // 513: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse + 190, // 514: openshell.v1.OpenShell.ReportSandboxConfiguration:output_type -> openshell.v1.ReportSandboxConfigurationResponse + 167, // 515: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 171, // 516: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 195, // 517: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 194, // 518: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 197, // 519: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 209, // 520: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 211, // 521: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse + 216, // 522: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 218, // 523: openshell.v1.OpenShell.PeerRelay:output_type -> openshell.v1.PeerRelayFrame + 92, // 524: openshell.v1.OpenShell.PeerReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse + 271, // 525: openshell.v1.OpenShell.PeerReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse + 90, // 526: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse + 222, // 527: openshell.v1.OpenShell.PeerNotifyConfigUpdate:output_type -> openshell.v1.PeerNotifyConfigUpdateResponse + 117, // 528: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 232, // 529: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 234, // 530: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 236, // 531: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 238, // 532: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 241, // 533: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 243, // 534: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 245, // 535: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 247, // 536: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 250, // 537: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 23, // 538: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 25, // 539: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 252, // 540: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 254, // 541: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 256, // 542: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 258, // 543: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 261, // 544: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 263, // 545: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 265, // 546: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 77, // 547: openshell.v1.SandboxUpdates.UpdateSandbox:output_type -> openshell.v1.SandboxResponse + 461, // [461:548] is the sub-list for method output_type + 374, // [374:461] is the sub-list for method input_type + 374, // [374:374] is the sub-list for extension type_name + 374, // [374:374] is the sub-list for extension extendee + 0, // [0:374] is the sub-list for field type_name } func init() { file_openshell_proto_init() } diff --git a/sdk/go/proto/sandboxv1/sandbox.pb.go b/sdk/go/proto/sandboxv1/sandbox.pb.go index 045e15fac5..dcab3a90e1 100644 --- a/sdk/go/proto/sandboxv1/sandbox.pb.go +++ b/sdk/go/proto/sandboxv1/sandbox.pb.go @@ -409,6 +409,114 @@ func (SupervisorMiddlewareBinding) EnumDescriptor() ([]byte, []int) { return file_sandbox_proto_rawDescGZIP(), []int{6} } +type ResourceEnforcementKind int32 + +const ( + ResourceEnforcementKind_RESOURCE_ENFORCEMENT_KIND_UNSPECIFIED ResourceEnforcementKind = 0 + ResourceEnforcementKind_RESOURCE_ENFORCEMENT_KIND_UNSUPPORTED ResourceEnforcementKind = 1 + ResourceEnforcementKind_RESOURCE_ENFORCEMENT_KIND_KERNEL_LIMIT ResourceEnforcementKind = 2 + // Admission by the platform scheduler, not dedicated physical capacity. + ResourceEnforcementKind_RESOURCE_ENFORCEMENT_KIND_SCHEDULER_REQUEST ResourceEnforcementKind = 3 +) + +// Enum value maps for ResourceEnforcementKind. +var ( + ResourceEnforcementKind_name = map[int32]string{ + 0: "RESOURCE_ENFORCEMENT_KIND_UNSPECIFIED", + 1: "RESOURCE_ENFORCEMENT_KIND_UNSUPPORTED", + 2: "RESOURCE_ENFORCEMENT_KIND_KERNEL_LIMIT", + 3: "RESOURCE_ENFORCEMENT_KIND_SCHEDULER_REQUEST", + } + ResourceEnforcementKind_value = map[string]int32{ + "RESOURCE_ENFORCEMENT_KIND_UNSPECIFIED": 0, + "RESOURCE_ENFORCEMENT_KIND_UNSUPPORTED": 1, + "RESOURCE_ENFORCEMENT_KIND_KERNEL_LIMIT": 2, + "RESOURCE_ENFORCEMENT_KIND_SCHEDULER_REQUEST": 3, + } +) + +func (x ResourceEnforcementKind) Enum() *ResourceEnforcementKind { + p := new(ResourceEnforcementKind) + *p = x + return p +} + +func (x ResourceEnforcementKind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ResourceEnforcementKind) Descriptor() protoreflect.EnumDescriptor { + return file_sandbox_proto_enumTypes[7].Descriptor() +} + +func (ResourceEnforcementKind) Type() protoreflect.EnumType { + return &file_sandbox_proto_enumTypes[7] +} + +func (x ResourceEnforcementKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ResourceEnforcementKind.Descriptor instead. +func (ResourceEnforcementKind) EnumDescriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{7} +} + +type ResourceEnforcementScope int32 + +const ( + ResourceEnforcementScope_RESOURCE_ENFORCEMENT_SCOPE_UNSPECIFIED ResourceEnforcementScope = 0 + ResourceEnforcementScope_RESOURCE_ENFORCEMENT_SCOPE_PROCESS ResourceEnforcementScope = 1 + ResourceEnforcementScope_RESOURCE_ENFORCEMENT_SCOPE_CONTAINER ResourceEnforcementScope = 2 + ResourceEnforcementScope_RESOURCE_ENFORCEMENT_SCOPE_SANDBOX_ACCOUNT ResourceEnforcementScope = 3 + ResourceEnforcementScope_RESOURCE_ENFORCEMENT_SCOPE_VIRTUAL_MACHINE ResourceEnforcementScope = 4 +) + +// Enum value maps for ResourceEnforcementScope. +var ( + ResourceEnforcementScope_name = map[int32]string{ + 0: "RESOURCE_ENFORCEMENT_SCOPE_UNSPECIFIED", + 1: "RESOURCE_ENFORCEMENT_SCOPE_PROCESS", + 2: "RESOURCE_ENFORCEMENT_SCOPE_CONTAINER", + 3: "RESOURCE_ENFORCEMENT_SCOPE_SANDBOX_ACCOUNT", + 4: "RESOURCE_ENFORCEMENT_SCOPE_VIRTUAL_MACHINE", + } + ResourceEnforcementScope_value = map[string]int32{ + "RESOURCE_ENFORCEMENT_SCOPE_UNSPECIFIED": 0, + "RESOURCE_ENFORCEMENT_SCOPE_PROCESS": 1, + "RESOURCE_ENFORCEMENT_SCOPE_CONTAINER": 2, + "RESOURCE_ENFORCEMENT_SCOPE_SANDBOX_ACCOUNT": 3, + "RESOURCE_ENFORCEMENT_SCOPE_VIRTUAL_MACHINE": 4, + } +) + +func (x ResourceEnforcementScope) Enum() *ResourceEnforcementScope { + p := new(ResourceEnforcementScope) + *p = x + return p +} + +func (x ResourceEnforcementScope) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ResourceEnforcementScope) Descriptor() protoreflect.EnumDescriptor { + return file_sandbox_proto_enumTypes[8].Descriptor() +} + +func (ResourceEnforcementScope) Type() protoreflect.EnumType { + return &file_sandbox_proto_enumTypes[8] +} + +func (x ResourceEnforcementScope) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ResourceEnforcementScope.Descriptor instead. +func (ResourceEnforcementScope) EnumDescriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{8} +} + // Sandbox security policy configuration. type SandboxPolicy struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2696,6 +2804,211 @@ func (x *SupervisorMiddlewareService) GetBindings() []SupervisorMiddlewareBindin return nil } +// Explicit budgets observed across a sandbox account. These are not CPU-rate +// quotas or hard memory limits. Empty budgets are invalid. +type SampledResourceBudget struct { + state protoimpl.MessageState `protogen:"open.v1"` + CpuTime *durationpb.Duration `protobuf:"bytes,1,opt,name=cpu_time,json=cpuTime,proto3" json:"cpu_time,omitempty"` + ResidentBytes *uint64 `protobuf:"varint,2,opt,name=resident_bytes,json=residentBytes,proto3,oneof" json:"resident_bytes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SampledResourceBudget) Reset() { + *x = SampledResourceBudget{} + mi := &file_sandbox_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SampledResourceBudget) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SampledResourceBudget) ProtoMessage() {} + +func (x *SampledResourceBudget) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[25] + 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 SampledResourceBudget.ProtoReflect.Descriptor instead. +func (*SampledResourceBudget) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{25} +} + +func (x *SampledResourceBudget) GetCpuTime() *durationpb.Duration { + if x != nil { + return x.CpuTime + } + return nil +} + +func (x *SampledResourceBudget) GetResidentBytes() uint64 { + if x != nil && x.ResidentBytes != nil { + return *x.ResidentBytes + } + return 0 +} + +// Mechanism and aggregation boundary for a resource request. +type ResourceEnforcement struct { + state protoimpl.MessageState `protogen:"open.v1"` + Kind ResourceEnforcementKind `protobuf:"varint,1,opt,name=kind,proto3,enum=openshell.sandbox.v1.ResourceEnforcementKind" json:"kind,omitempty"` + Scope ResourceEnforcementScope `protobuf:"varint,2,opt,name=scope,proto3,enum=openshell.sandbox.v1.ResourceEnforcementScope" json:"scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceEnforcement) Reset() { + *x = ResourceEnforcement{} + mi := &file_sandbox_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceEnforcement) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceEnforcement) ProtoMessage() {} + +func (x *ResourceEnforcement) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[26] + 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 ResourceEnforcement.ProtoReflect.Descriptor instead. +func (*ResourceEnforcement) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{26} +} + +func (x *ResourceEnforcement) GetKind() ResourceEnforcementKind { + if x != nil { + return x.Kind + } + return ResourceEnforcementKind_RESOURCE_ENFORCEMENT_KIND_UNSPECIFIED +} + +func (x *ResourceEnforcement) GetScope() ResourceEnforcementScope { + if x != nil { + return x.Scope + } + return ResourceEnforcementScope_RESOURCE_ENFORCEMENT_SCOPE_UNSPECIFIED +} + +type SampledResourceBudgetCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + CpuTimeSupported bool `protobuf:"varint,1,opt,name=cpu_time_supported,json=cpuTimeSupported,proto3" json:"cpu_time_supported,omitempty"` + ResidentBytesSupported bool `protobuf:"varint,2,opt,name=resident_bytes_supported,json=residentBytesSupported,proto3" json:"resident_bytes_supported,omitempty"` + Scope ResourceEnforcementScope `protobuf:"varint,3,opt,name=scope,proto3,enum=openshell.sandbox.v1.ResourceEnforcementScope" json:"scope,omitempty"` + // Sleep between completed observations; collection adds latency. This is + // neither an enforcement deadline nor a bound on overshoot. Short-lived + // processes may escape observation. + SamplingInterval *durationpb.Duration `protobuf:"bytes,4,opt,name=sampling_interval,json=samplingInterval,proto3" json:"sampling_interval,omitempty"` + TerminateOnExcess bool `protobuf:"varint,5,opt,name=terminate_on_excess,json=terminateOnExcess,proto3" json:"terminate_on_excess,omitempty"` + // Smallest exactly representable CPU-time request. + CpuTimeResolution *durationpb.Duration `protobuf:"bytes,6,opt,name=cpu_time_resolution,json=cpuTimeResolution,proto3" json:"cpu_time_resolution,omitempty"` + // A per-process CPU-time kernel limit also accompanies the sampled budget. + // It may use a coarser resolution than the account-wide observation. + PerProcessCpuLimit bool `protobuf:"varint,7,opt,name=per_process_cpu_limit,json=perProcessCpuLimit,proto3" json:"per_process_cpu_limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SampledResourceBudgetCapabilities) Reset() { + *x = SampledResourceBudgetCapabilities{} + mi := &file_sandbox_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SampledResourceBudgetCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SampledResourceBudgetCapabilities) ProtoMessage() {} + +func (x *SampledResourceBudgetCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[27] + 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 SampledResourceBudgetCapabilities.ProtoReflect.Descriptor instead. +func (*SampledResourceBudgetCapabilities) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{27} +} + +func (x *SampledResourceBudgetCapabilities) GetCpuTimeSupported() bool { + if x != nil { + return x.CpuTimeSupported + } + return false +} + +func (x *SampledResourceBudgetCapabilities) GetResidentBytesSupported() bool { + if x != nil { + return x.ResidentBytesSupported + } + return false +} + +func (x *SampledResourceBudgetCapabilities) GetScope() ResourceEnforcementScope { + if x != nil { + return x.Scope + } + return ResourceEnforcementScope_RESOURCE_ENFORCEMENT_SCOPE_UNSPECIFIED +} + +func (x *SampledResourceBudgetCapabilities) GetSamplingInterval() *durationpb.Duration { + if x != nil { + return x.SamplingInterval + } + return nil +} + +func (x *SampledResourceBudgetCapabilities) GetTerminateOnExcess() bool { + if x != nil { + return x.TerminateOnExcess + } + return false +} + +func (x *SampledResourceBudgetCapabilities) GetCpuTimeResolution() *durationpb.Duration { + if x != nil { + return x.CpuTimeResolution + } + return nil +} + +func (x *SampledResourceBudgetCapabilities) GetPerProcessCpuLimit() bool { + if x != nil { + return x.PerProcessCpuLimit + } + return false +} + var File_sandbox_proto protoreflect.FileDescriptor const file_sandbox_proto_rawDesc = "" + @@ -2916,7 +3229,22 @@ const file_sandbox_proto_rawDesc = "" + "\baudience\x18\x06 \x01(\tR\baudience\x128\n" + "\x18allow_insecure_transport\x18\a \x01(\bR\x16allowInsecureTransport\x12^\n" + "\x0ebinding_policy\x18\b \x01(\x0e27.openshell.sandbox.v1.SupervisorMiddlewareBindingPolicyR\rbindingPolicy\x12M\n" + - "\bbindings\x18\t \x03(\x0e21.openshell.sandbox.v1.SupervisorMiddlewareBindingR\bbindingsJ\x04\b\x04\x10\x05R\atimeout*\x97\x01\n" + + "\bbindings\x18\t \x03(\x0e21.openshell.sandbox.v1.SupervisorMiddlewareBindingR\bbindingsJ\x04\b\x04\x10\x05R\atimeout\"\x8c\x01\n" + + "\x15SampledResourceBudget\x124\n" + + "\bcpu_time\x18\x01 \x01(\v2\x19.google.protobuf.DurationR\acpuTime\x12*\n" + + "\x0eresident_bytes\x18\x02 \x01(\x04H\x00R\rresidentBytes\x88\x01\x01B\x11\n" + + "\x0f_resident_bytes\"\x9e\x01\n" + + "\x13ResourceEnforcement\x12A\n" + + "\x04kind\x18\x01 \x01(\x0e2-.openshell.sandbox.v1.ResourceEnforcementKindR\x04kind\x12D\n" + + "\x05scope\x18\x02 \x01(\x0e2..openshell.sandbox.v1.ResourceEnforcementScopeR\x05scope\"\xc7\x03\n" + + "!SampledResourceBudgetCapabilities\x12,\n" + + "\x12cpu_time_supported\x18\x01 \x01(\bR\x10cpuTimeSupported\x128\n" + + "\x18resident_bytes_supported\x18\x02 \x01(\bR\x16residentBytesSupported\x12D\n" + + "\x05scope\x18\x03 \x01(\x0e2..openshell.sandbox.v1.ResourceEnforcementScopeR\x05scope\x12F\n" + + "\x11sampling_interval\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\x10samplingInterval\x12.\n" + + "\x13terminate_on_excess\x18\x05 \x01(\bR\x11terminateOnExcess\x12I\n" + + "\x13cpu_time_resolution\x18\x06 \x01(\v2\x19.google.protobuf.DurationR\x11cpuTimeResolution\x121\n" + + "\x15per_process_cpu_limit\x18\a \x01(\bR\x12perProcessCpuLimit*\x97\x01\n" + "\x0eNetworkTlsMode\x12 \n" + "\x1cNETWORK_TLS_MODE_UNSPECIFIED\x10\x00\x12\x19\n" + "\x15NETWORK_TLS_MODE_SKIP\x10\x01\x12\"\n" + @@ -2948,7 +3276,18 @@ const file_sandbox_proto_rawDesc = "" + ")SUPERVISOR_MIDDLEWARE_BINDING_UNSPECIFIED\x10\x00\x12>\n" + ":SUPERVISOR_MIDDLEWARE_BINDING_HTTP_REQUEST_PRE_CREDENTIALS\x10\x01\x12:\n" + "6SUPERVISOR_MIDDLEWARE_BINDING_HTTP_RESPONSE_PRE_RETURN\x10\x02\x12C\n" + - "?SUPERVISOR_MIDDLEWARE_BINDING_WEBSOCKET_MESSAGE_PRE_CREDENTIALS\x10\x03b\x06proto3" + "?SUPERVISOR_MIDDLEWARE_BINDING_WEBSOCKET_MESSAGE_PRE_CREDENTIALS\x10\x03*\xcc\x01\n" + + "\x17ResourceEnforcementKind\x12)\n" + + "%RESOURCE_ENFORCEMENT_KIND_UNSPECIFIED\x10\x00\x12)\n" + + "%RESOURCE_ENFORCEMENT_KIND_UNSUPPORTED\x10\x01\x12*\n" + + "&RESOURCE_ENFORCEMENT_KIND_KERNEL_LIMIT\x10\x02\x12/\n" + + "+RESOURCE_ENFORCEMENT_KIND_SCHEDULER_REQUEST\x10\x03*\xf8\x01\n" + + "\x18ResourceEnforcementScope\x12*\n" + + "&RESOURCE_ENFORCEMENT_SCOPE_UNSPECIFIED\x10\x00\x12&\n" + + "\"RESOURCE_ENFORCEMENT_SCOPE_PROCESS\x10\x01\x12(\n" + + "$RESOURCE_ENFORCEMENT_SCOPE_CONTAINER\x10\x02\x12.\n" + + "*RESOURCE_ENFORCEMENT_SCOPE_SANDBOX_ACCOUNT\x10\x03\x12.\n" + + "*RESOURCE_ENFORCEMENT_SCOPE_VIRTUAL_MACHINE\x10\x04b\x06proto3" var ( file_sandbox_proto_rawDescOnce sync.Once @@ -2962,110 +3301,121 @@ func file_sandbox_proto_rawDescGZIP() []byte { return file_sandbox_proto_rawDescData } -var file_sandbox_proto_enumTypes = make([]protoimpl.EnumInfo, 7) -var file_sandbox_proto_msgTypes = make([]protoimpl.MessageInfo, 35) +var file_sandbox_proto_enumTypes = make([]protoimpl.EnumInfo, 9) +var file_sandbox_proto_msgTypes = make([]protoimpl.MessageInfo, 38) var file_sandbox_proto_goTypes = []any{ - (NetworkTlsMode)(0), // 0: openshell.sandbox.v1.NetworkTlsMode - (NetworkEnforcementMode)(0), // 1: openshell.sandbox.v1.NetworkEnforcementMode - (NetworkAccessPreset)(0), // 2: openshell.sandbox.v1.NetworkAccessPreset - (SettingScope)(0), // 3: openshell.sandbox.v1.SettingScope - (PolicySource)(0), // 4: openshell.sandbox.v1.PolicySource - (SupervisorMiddlewareBindingPolicy)(0), // 5: openshell.sandbox.v1.SupervisorMiddlewareBindingPolicy - (SupervisorMiddlewareBinding)(0), // 6: openshell.sandbox.v1.SupervisorMiddlewareBinding - (*SandboxPolicy)(nil), // 7: openshell.sandbox.v1.SandboxPolicy - (*FilesystemPolicy)(nil), // 8: openshell.sandbox.v1.FilesystemPolicy - (*LandlockPolicy)(nil), // 9: openshell.sandbox.v1.LandlockPolicy - (*ProcessPolicy)(nil), // 10: openshell.sandbox.v1.ProcessPolicy - (*NetworkPolicyRule)(nil), // 11: openshell.sandbox.v1.NetworkPolicyRule - (*NetworkMiddlewareConfig)(nil), // 12: openshell.sandbox.v1.NetworkMiddlewareConfig - (*MiddlewareEndpointSelector)(nil), // 13: openshell.sandbox.v1.MiddlewareEndpointSelector - (*NetworkCredentialBinding)(nil), // 14: openshell.sandbox.v1.NetworkCredentialBinding - (*NetworkEndpoint)(nil), // 15: openshell.sandbox.v1.NetworkEndpoint - (*McpOptions)(nil), // 16: openshell.sandbox.v1.McpOptions - (*GraphqlOperation)(nil), // 17: openshell.sandbox.v1.GraphqlOperation - (*L7DenyRule)(nil), // 18: openshell.sandbox.v1.L7DenyRule - (*L7Rule)(nil), // 19: openshell.sandbox.v1.L7Rule - (*L7Allow)(nil), // 20: openshell.sandbox.v1.L7Allow - (*L7QueryMatcher)(nil), // 21: openshell.sandbox.v1.L7QueryMatcher - (*NetworkBinary)(nil), // 22: openshell.sandbox.v1.NetworkBinary - (*GetSandboxConfigRequest)(nil), // 23: openshell.sandbox.v1.GetSandboxConfigRequest - (*GetGatewayConfigRequest)(nil), // 24: openshell.sandbox.v1.GetGatewayConfigRequest - (*GetGatewayConfigResponse)(nil), // 25: openshell.sandbox.v1.GetGatewayConfigResponse - (*SettingValue)(nil), // 26: openshell.sandbox.v1.SettingValue - (*EffectiveSetting)(nil), // 27: openshell.sandbox.v1.EffectiveSetting - (*SandboxConfigSnapshot)(nil), // 28: openshell.sandbox.v1.SandboxConfigSnapshot - (*GetSandboxConfigResponse)(nil), // 29: openshell.sandbox.v1.GetSandboxConfigResponse - (*SandboxServiceRoute)(nil), // 30: openshell.sandbox.v1.SandboxServiceRoute - (*SupervisorMiddlewareService)(nil), // 31: openshell.sandbox.v1.SupervisorMiddlewareService - nil, // 32: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry - nil, // 33: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry - nil, // 34: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry - nil, // 35: openshell.sandbox.v1.L7DenyRule.QueryEntry - nil, // 36: openshell.sandbox.v1.L7DenyRule.ParamsEntry - nil, // 37: openshell.sandbox.v1.L7Allow.QueryEntry - nil, // 38: openshell.sandbox.v1.L7Allow.ParamsEntry - nil, // 39: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry - nil, // 40: openshell.sandbox.v1.SandboxConfigSnapshot.SettingsEntry - nil, // 41: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry - (*structpb.Struct)(nil), // 42: google.protobuf.Struct - (*datamodelv1.WorkspaceSelector)(nil), // 43: openshell.datamodel.v1.WorkspaceSelector - (*durationpb.Duration)(nil), // 44: google.protobuf.Duration + (NetworkTlsMode)(0), // 0: openshell.sandbox.v1.NetworkTlsMode + (NetworkEnforcementMode)(0), // 1: openshell.sandbox.v1.NetworkEnforcementMode + (NetworkAccessPreset)(0), // 2: openshell.sandbox.v1.NetworkAccessPreset + (SettingScope)(0), // 3: openshell.sandbox.v1.SettingScope + (PolicySource)(0), // 4: openshell.sandbox.v1.PolicySource + (SupervisorMiddlewareBindingPolicy)(0), // 5: openshell.sandbox.v1.SupervisorMiddlewareBindingPolicy + (SupervisorMiddlewareBinding)(0), // 6: openshell.sandbox.v1.SupervisorMiddlewareBinding + (ResourceEnforcementKind)(0), // 7: openshell.sandbox.v1.ResourceEnforcementKind + (ResourceEnforcementScope)(0), // 8: openshell.sandbox.v1.ResourceEnforcementScope + (*SandboxPolicy)(nil), // 9: openshell.sandbox.v1.SandboxPolicy + (*FilesystemPolicy)(nil), // 10: openshell.sandbox.v1.FilesystemPolicy + (*LandlockPolicy)(nil), // 11: openshell.sandbox.v1.LandlockPolicy + (*ProcessPolicy)(nil), // 12: openshell.sandbox.v1.ProcessPolicy + (*NetworkPolicyRule)(nil), // 13: openshell.sandbox.v1.NetworkPolicyRule + (*NetworkMiddlewareConfig)(nil), // 14: openshell.sandbox.v1.NetworkMiddlewareConfig + (*MiddlewareEndpointSelector)(nil), // 15: openshell.sandbox.v1.MiddlewareEndpointSelector + (*NetworkCredentialBinding)(nil), // 16: openshell.sandbox.v1.NetworkCredentialBinding + (*NetworkEndpoint)(nil), // 17: openshell.sandbox.v1.NetworkEndpoint + (*McpOptions)(nil), // 18: openshell.sandbox.v1.McpOptions + (*GraphqlOperation)(nil), // 19: openshell.sandbox.v1.GraphqlOperation + (*L7DenyRule)(nil), // 20: openshell.sandbox.v1.L7DenyRule + (*L7Rule)(nil), // 21: openshell.sandbox.v1.L7Rule + (*L7Allow)(nil), // 22: openshell.sandbox.v1.L7Allow + (*L7QueryMatcher)(nil), // 23: openshell.sandbox.v1.L7QueryMatcher + (*NetworkBinary)(nil), // 24: openshell.sandbox.v1.NetworkBinary + (*GetSandboxConfigRequest)(nil), // 25: openshell.sandbox.v1.GetSandboxConfigRequest + (*GetGatewayConfigRequest)(nil), // 26: openshell.sandbox.v1.GetGatewayConfigRequest + (*GetGatewayConfigResponse)(nil), // 27: openshell.sandbox.v1.GetGatewayConfigResponse + (*SettingValue)(nil), // 28: openshell.sandbox.v1.SettingValue + (*EffectiveSetting)(nil), // 29: openshell.sandbox.v1.EffectiveSetting + (*SandboxConfigSnapshot)(nil), // 30: openshell.sandbox.v1.SandboxConfigSnapshot + (*GetSandboxConfigResponse)(nil), // 31: openshell.sandbox.v1.GetSandboxConfigResponse + (*SandboxServiceRoute)(nil), // 32: openshell.sandbox.v1.SandboxServiceRoute + (*SupervisorMiddlewareService)(nil), // 33: openshell.sandbox.v1.SupervisorMiddlewareService + (*SampledResourceBudget)(nil), // 34: openshell.sandbox.v1.SampledResourceBudget + (*ResourceEnforcement)(nil), // 35: openshell.sandbox.v1.ResourceEnforcement + (*SampledResourceBudgetCapabilities)(nil), // 36: openshell.sandbox.v1.SampledResourceBudgetCapabilities + nil, // 37: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry + nil, // 38: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry + nil, // 39: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry + nil, // 40: openshell.sandbox.v1.L7DenyRule.QueryEntry + nil, // 41: openshell.sandbox.v1.L7DenyRule.ParamsEntry + nil, // 42: openshell.sandbox.v1.L7Allow.QueryEntry + nil, // 43: openshell.sandbox.v1.L7Allow.ParamsEntry + nil, // 44: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry + nil, // 45: openshell.sandbox.v1.SandboxConfigSnapshot.SettingsEntry + nil, // 46: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry + (*structpb.Struct)(nil), // 47: google.protobuf.Struct + (*datamodelv1.WorkspaceSelector)(nil), // 48: openshell.datamodel.v1.WorkspaceSelector + (*durationpb.Duration)(nil), // 49: google.protobuf.Duration } var file_sandbox_proto_depIdxs = []int32{ - 8, // 0: openshell.sandbox.v1.SandboxPolicy.filesystem:type_name -> openshell.sandbox.v1.FilesystemPolicy - 9, // 1: openshell.sandbox.v1.SandboxPolicy.landlock:type_name -> openshell.sandbox.v1.LandlockPolicy - 10, // 2: openshell.sandbox.v1.SandboxPolicy.process:type_name -> openshell.sandbox.v1.ProcessPolicy - 32, // 3: openshell.sandbox.v1.SandboxPolicy.network_policies:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry - 33, // 4: openshell.sandbox.v1.SandboxPolicy.network_middlewares:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry - 15, // 5: openshell.sandbox.v1.NetworkPolicyRule.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 22, // 6: openshell.sandbox.v1.NetworkPolicyRule.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 42, // 7: openshell.sandbox.v1.NetworkMiddlewareConfig.config:type_name -> google.protobuf.Struct - 13, // 8: openshell.sandbox.v1.NetworkMiddlewareConfig.endpoints:type_name -> openshell.sandbox.v1.MiddlewareEndpointSelector + 10, // 0: openshell.sandbox.v1.SandboxPolicy.filesystem:type_name -> openshell.sandbox.v1.FilesystemPolicy + 11, // 1: openshell.sandbox.v1.SandboxPolicy.landlock:type_name -> openshell.sandbox.v1.LandlockPolicy + 12, // 2: openshell.sandbox.v1.SandboxPolicy.process:type_name -> openshell.sandbox.v1.ProcessPolicy + 37, // 3: openshell.sandbox.v1.SandboxPolicy.network_policies:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry + 38, // 4: openshell.sandbox.v1.SandboxPolicy.network_middlewares:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry + 17, // 5: openshell.sandbox.v1.NetworkPolicyRule.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 24, // 6: openshell.sandbox.v1.NetworkPolicyRule.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 47, // 7: openshell.sandbox.v1.NetworkMiddlewareConfig.config:type_name -> google.protobuf.Struct + 15, // 8: openshell.sandbox.v1.NetworkMiddlewareConfig.endpoints:type_name -> openshell.sandbox.v1.MiddlewareEndpointSelector 0, // 9: openshell.sandbox.v1.NetworkEndpoint.tls:type_name -> openshell.sandbox.v1.NetworkTlsMode 1, // 10: openshell.sandbox.v1.NetworkEndpoint.enforcement:type_name -> openshell.sandbox.v1.NetworkEnforcementMode 2, // 11: openshell.sandbox.v1.NetworkEndpoint.access:type_name -> openshell.sandbox.v1.NetworkAccessPreset - 19, // 12: openshell.sandbox.v1.NetworkEndpoint.rules:type_name -> openshell.sandbox.v1.L7Rule - 18, // 13: openshell.sandbox.v1.NetworkEndpoint.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 34, // 14: openshell.sandbox.v1.NetworkEndpoint.graphql_persisted_queries:type_name -> openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry - 16, // 15: openshell.sandbox.v1.NetworkEndpoint.mcp:type_name -> openshell.sandbox.v1.McpOptions - 14, // 16: openshell.sandbox.v1.NetworkEndpoint.credential_binding:type_name -> openshell.sandbox.v1.NetworkCredentialBinding - 35, // 17: openshell.sandbox.v1.L7DenyRule.query:type_name -> openshell.sandbox.v1.L7DenyRule.QueryEntry - 36, // 18: openshell.sandbox.v1.L7DenyRule.params:type_name -> openshell.sandbox.v1.L7DenyRule.ParamsEntry - 20, // 19: openshell.sandbox.v1.L7Rule.allow:type_name -> openshell.sandbox.v1.L7Allow - 37, // 20: openshell.sandbox.v1.L7Allow.query:type_name -> openshell.sandbox.v1.L7Allow.QueryEntry - 38, // 21: openshell.sandbox.v1.L7Allow.params:type_name -> openshell.sandbox.v1.L7Allow.ParamsEntry - 43, // 22: openshell.sandbox.v1.GetSandboxConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 39, // 23: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry - 26, // 24: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue + 21, // 12: openshell.sandbox.v1.NetworkEndpoint.rules:type_name -> openshell.sandbox.v1.L7Rule + 20, // 13: openshell.sandbox.v1.NetworkEndpoint.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 39, // 14: openshell.sandbox.v1.NetworkEndpoint.graphql_persisted_queries:type_name -> openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry + 18, // 15: openshell.sandbox.v1.NetworkEndpoint.mcp:type_name -> openshell.sandbox.v1.McpOptions + 16, // 16: openshell.sandbox.v1.NetworkEndpoint.credential_binding:type_name -> openshell.sandbox.v1.NetworkCredentialBinding + 40, // 17: openshell.sandbox.v1.L7DenyRule.query:type_name -> openshell.sandbox.v1.L7DenyRule.QueryEntry + 41, // 18: openshell.sandbox.v1.L7DenyRule.params:type_name -> openshell.sandbox.v1.L7DenyRule.ParamsEntry + 22, // 19: openshell.sandbox.v1.L7Rule.allow:type_name -> openshell.sandbox.v1.L7Allow + 42, // 20: openshell.sandbox.v1.L7Allow.query:type_name -> openshell.sandbox.v1.L7Allow.QueryEntry + 43, // 21: openshell.sandbox.v1.L7Allow.params:type_name -> openshell.sandbox.v1.L7Allow.ParamsEntry + 48, // 22: openshell.sandbox.v1.GetSandboxConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 44, // 23: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry + 28, // 24: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue 3, // 25: openshell.sandbox.v1.EffectiveSetting.scope:type_name -> openshell.sandbox.v1.SettingScope - 7, // 26: openshell.sandbox.v1.SandboxConfigSnapshot.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 40, // 27: openshell.sandbox.v1.SandboxConfigSnapshot.settings:type_name -> openshell.sandbox.v1.SandboxConfigSnapshot.SettingsEntry + 9, // 26: openshell.sandbox.v1.SandboxConfigSnapshot.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 45, // 27: openshell.sandbox.v1.SandboxConfigSnapshot.settings:type_name -> openshell.sandbox.v1.SandboxConfigSnapshot.SettingsEntry 4, // 28: openshell.sandbox.v1.SandboxConfigSnapshot.policy_source:type_name -> openshell.sandbox.v1.PolicySource - 31, // 29: openshell.sandbox.v1.SandboxConfigSnapshot.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService - 30, // 30: openshell.sandbox.v1.SandboxConfigSnapshot.service_routes:type_name -> openshell.sandbox.v1.SandboxServiceRoute - 7, // 31: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 41, // 32: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry + 33, // 29: openshell.sandbox.v1.SandboxConfigSnapshot.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService + 32, // 30: openshell.sandbox.v1.SandboxConfigSnapshot.service_routes:type_name -> openshell.sandbox.v1.SandboxServiceRoute + 9, // 31: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 46, // 32: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry 4, // 33: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource - 31, // 34: openshell.sandbox.v1.GetSandboxConfigResponse.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService - 30, // 35: openshell.sandbox.v1.GetSandboxConfigResponse.service_routes:type_name -> openshell.sandbox.v1.SandboxServiceRoute - 44, // 36: openshell.sandbox.v1.SupervisorMiddlewareService.request_timeout:type_name -> google.protobuf.Duration + 33, // 34: openshell.sandbox.v1.GetSandboxConfigResponse.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService + 32, // 35: openshell.sandbox.v1.GetSandboxConfigResponse.service_routes:type_name -> openshell.sandbox.v1.SandboxServiceRoute + 49, // 36: openshell.sandbox.v1.SupervisorMiddlewareService.request_timeout:type_name -> google.protobuf.Duration 5, // 37: openshell.sandbox.v1.SupervisorMiddlewareService.binding_policy:type_name -> openshell.sandbox.v1.SupervisorMiddlewareBindingPolicy 6, // 38: openshell.sandbox.v1.SupervisorMiddlewareService.bindings:type_name -> openshell.sandbox.v1.SupervisorMiddlewareBinding - 11, // 39: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 12, // 40: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig - 17, // 41: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation - 21, // 42: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 21, // 43: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 21, // 44: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 21, // 45: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 26, // 46: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue - 27, // 47: openshell.sandbox.v1.SandboxConfigSnapshot.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting - 27, // 48: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting - 49, // [49:49] is the sub-list for method output_type - 49, // [49:49] is the sub-list for method input_type - 49, // [49:49] is the sub-list for extension type_name - 49, // [49:49] is the sub-list for extension extendee - 0, // [0:49] is the sub-list for field type_name + 49, // 39: openshell.sandbox.v1.SampledResourceBudget.cpu_time:type_name -> google.protobuf.Duration + 7, // 40: openshell.sandbox.v1.ResourceEnforcement.kind:type_name -> openshell.sandbox.v1.ResourceEnforcementKind + 8, // 41: openshell.sandbox.v1.ResourceEnforcement.scope:type_name -> openshell.sandbox.v1.ResourceEnforcementScope + 8, // 42: openshell.sandbox.v1.SampledResourceBudgetCapabilities.scope:type_name -> openshell.sandbox.v1.ResourceEnforcementScope + 49, // 43: openshell.sandbox.v1.SampledResourceBudgetCapabilities.sampling_interval:type_name -> google.protobuf.Duration + 49, // 44: openshell.sandbox.v1.SampledResourceBudgetCapabilities.cpu_time_resolution:type_name -> google.protobuf.Duration + 13, // 45: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 14, // 46: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig + 19, // 47: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation + 23, // 48: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 23, // 49: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 23, // 50: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 23, // 51: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 28, // 52: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue + 29, // 53: openshell.sandbox.v1.SandboxConfigSnapshot.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting + 29, // 54: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting + 55, // [55:55] is the sub-list for method output_type + 55, // [55:55] is the sub-list for method input_type + 55, // [55:55] is the sub-list for extension type_name + 55, // [55:55] is the sub-list for extension extendee + 0, // [0:55] is the sub-list for field type_name } func init() { file_sandbox_proto_init() } @@ -3080,13 +3430,14 @@ func file_sandbox_proto_init() { (*SettingValue_IntValue)(nil), (*SettingValue_BytesValue)(nil), } + file_sandbox_proto_msgTypes[25].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_sandbox_proto_rawDesc), len(file_sandbox_proto_rawDesc)), - NumEnums: 7, - NumMessages: 35, + NumEnums: 9, + NumMessages: 38, NumExtensions: 0, NumServices: 0, }, diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 8b64c9768d..567b9c96c5 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -348,3 +348,7 @@ mise run e2e:sdk:ts:exec # public interactive helper against an isolated Dock ``` Formatting and linting are handled by [Biome](https://biomejs.dev) (`biome.json`): 2-space indent, single quotes, semicolons, 120-column width. Generated `src/gen/` is excluded. `sdk:ts:lint` runs in CI as part of `sdk:ts:ci`. + +## Next minor raw message migration + +Raw Health responses add `supportsSampledResourceBudget`. Complete object literals need the false default, or use a generated message builder. Resource requirement/capability messages also gain optional enforcement and budget fields. This change belongs to the next minor release; it is not a patch-source-compatibility claim. Upgrade gateway writers together so older protobuf writers cannot discard the new metadata. diff --git a/sdk/typescript/src/client.test.ts b/sdk/typescript/src/client.test.ts index 86fdfa3972..682351285b 100644 --- a/sdk/typescript/src/client.test.ts +++ b/sdk/typescript/src/client.test.ts @@ -761,22 +761,39 @@ describe('create', () => { }); it('sends canonical main process fields', async () => { - let created: { spec?: { command?: string[]; tty?: boolean } } = {}; + let created: { + spec?: { + command?: string[]; + tty?: boolean; + resourceRequirements?: { + sampledBudget?: { residentBytes?: bigint; cpuTime?: { seconds?: bigint; nanos?: number } }; + }; + }; + } = {}; + let supportsSampledResourceBudget = false; const sandbox = client({ + health: () => ({ supportsSampledResourceBudget }), createSandbox: (req) => { created = req; return readySandbox('sb', 'sb-id'); }, }); - await sandbox.create({ + const spec = { image: 'img', command: ['/opt/worker', '--serve'], tty: true, - }); + sampledBudget: { cpuTime: { seconds: 2n, nanos: 0 }, residentBytes: 4096n }, + }; + await expect(sandbox.create(spec)).rejects.toMatchObject({ code: 'invalid_config' }); + expect(created).toEqual({}); + supportsSampledResourceBudget = true; + await sandbox.create(spec); expect(created.spec?.command).toEqual(['/opt/worker', '--serve']); expect(created.spec?.tty).toBe(true); + expect(created.spec?.resourceRequirements?.sampledBudget?.residentBytes).toBe(4096n); + expect(created.spec?.resourceRequirements?.sampledBudget?.cpuTime?.seconds).toBe(2n); }); it('sends the restart policy', async () => { @@ -1165,6 +1182,37 @@ describe('create', () => { }); }); +describe('sampled budget compatibility', () => { + it('blocks template and raw-spec writes until the gateway acknowledges the fields', async () => { + let supported = false; + let writes = 0; + const transport = createRouterTransport((router) => { + router.service(OpenShell, { + health: () => ({ supportsSampledResourceBudget: supported }), + createSandboxTemplate: (req) => { + writes++; + return { template: req.template }; + }, + createSandbox: () => { + writes++; + return readySandbox('budget', 'sb-id'); + }, + }); + }); + const templates = new SandboxTemplateClient(transport); + const sandbox = new SandboxClient(transport); + const template = { spec: { workload: { resources: { sampledBudget: { residentBytes: 4096n } } } } }; + const spec = { rawSpec: { resourceRequirements: { sampledBudget: { residentBytes: 4096n } } } }; + await expect(templates.create(template)).rejects.toMatchObject({ code: 'invalid_config' }); + await expect(sandbox.create(spec)).rejects.toMatchObject({ code: 'invalid_config' }); + expect(writes).toBe(0); + supported = true; + await templates.create(template); + await sandbox.create(spec); + expect(writes).toBe(2); + }); +}); + describe('sandbox templates', () => { it('create sends the template resource and workspace', async () => { let observed: ScopedRequest & { diff --git a/sdk/typescript/src/client.ts b/sdk/typescript/src/client.ts index 205796f1bd..ee76fa0b47 100644 --- a/sdk/typescript/src/client.ts +++ b/sdk/typescript/src/client.ts @@ -38,7 +38,13 @@ import { type TcpForwardFrameSchema, } from './gen/openshell_pb.js'; import type { EffectiveSetting, GetSandboxConfigResponse, SandboxPolicy, SettingValue } from './gen/sandbox_pb.js'; -import { PolicySource, type SandboxPolicySchema, SettingScope, type SettingValueSchema } from './gen/sandbox_pb.js'; +import { + PolicySource, + type SampledResourceBudgetSchema, + type SandboxPolicySchema, + SettingScope, + type SettingValueSchema, +} from './gen/sandbox_pb.js'; import { ProviderClient } from './providers.js'; import { heldUntilReady, OWNER_REPLICA_HEADER, OWNER_REQUEST_HEADER, routedStream } from './routing.js'; import { validateSshResponse } from './ssh-validate.js'; @@ -80,10 +86,22 @@ export type { SandboxWorkloadTemplate, SandboxWorkloadTemplateSpec, } from './gen/openshell_pb.js'; -export type { SandboxPolicy, SettingValue } from './gen/sandbox_pb.js'; +export type { SampledResourceBudget, SandboxPolicy, SettingValue } from './gen/sandbox_pb.js'; export type { ConnectOptions }; export { errorCode }; +// Health acknowledges that the gateway preserves sampled fields on writes. +// It is available to workspace users; driver admission still decides support. +async function requireSampledBudgetSupport(grpc: Client): Promise { + const health = await grpc.health({}); + if (!health.supportsSampledResourceBudget) { + throw new SdkError( + 'invalid_config', + 'gateway does not acknowledge sampled resource budgets; upgrade it before creating this workload', + ); + } +} + // ---- Curated public types -------------------------------------------------- // The gateway enums (SandboxPhase, ServiceStatus, SettingScope, PolicySource) @@ -164,6 +182,8 @@ export interface SandboxSpec { environment?: Record; providers?: string[]; gpu?: boolean; + /** Explicit sampled budgets, distinct from hard CPU and memory quotas. */ + sampledBudget?: MessageInitShape; /** Exact canonical command. Empty selects the gateway scratch shell. */ command?: string[]; /** Allocate a retained pseudo-terminal for the canonical command. */ @@ -1118,6 +1138,9 @@ export class SandboxTemplateClient { options?: SandboxTemplateWorkspaceOptions | null, ): Promise { try { + if (template.spec?.workload?.resources?.sampledBudget !== undefined) { + await requireSampledBudgetSupport(this.grpc); + } const resp = await this.grpc.createSandboxTemplate({ workspaceScope: workspaceScope(options), template, @@ -1221,7 +1244,10 @@ export class SandboxClient { environment: spec.environment ?? {}, providers: spec.providers ?? [], template: spec.image ? { image: spec.image } : undefined, - resourceRequirements: spec.gpu ? { gpu: {} } : undefined, + resourceRequirements: + spec.gpu || spec.sampledBudget !== undefined + ? { gpu: spec.gpu ? {} : undefined, sampledBudget: spec.sampledBudget } + : undefined, policy: spec.policy, command: spec.command ?? [], tty: spec.tty ?? false, @@ -1229,6 +1255,9 @@ export class SandboxClient { }; if (spec.rawSpec) Object.assign(specInit, spec.rawSpec); + if (specInit.resourceRequirements?.sampledBudget !== undefined) { + await requireSampledBudgetSupport(this.grpc); + } const resp = await this.grpc.createSandbox({ workspaceScope: workspaceScope(spec), name: spec.name ?? '', @@ -1243,7 +1272,7 @@ export class SandboxClient { }); return sandboxRef(resp.sandbox, resp.serviceUrls); } catch (e) { - throw fromConnect(e); + throw e instanceof SdkError ? e : fromConnect(e); } } diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index 4948c13623..b041f3d320 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -29,6 +29,7 @@ export type { ProviderChange, ProviderChangeOptions, ProviderRef, + SampledResourceBudget, SandboxConfig, SandboxFromTemplateSpec, SandboxPhaseName, diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index 351d7e3b84..82307566bd 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -354,6 +354,17 @@ config opt-in. Consult the published gateway configuration reference before changing admission settings; do not recommend disabling admission to bypass a denial. Put driver config on a template only when it should be reused. +For a compute driver that advertises sampled account budgets, use +`sandbox template create` with `--cpu-time-budget 30s` and +`--resident-memory-budget-bytes 1073741824`, then create from that template. +These explicit budgets terminate on observed excess; collection and sampling +can overshoot. They are distinct from `--cpu` core-rate limits and `--memory` +hard limits. Inspect `gateway info` for driver guarantees; ordinary workspace +users do not need its administrator permission to use budgets. Budget-bearing +writes check the public gateway protocol acknowledgement and fail on older +gateways rather than discarding the fields. Unsupported drivers reject the +request before provisioning. + ### Manage sandbox workload templates ```bash From 25410ec2e8fa9131a50d4e928d58088474826925 Mon Sep 17 00:00:00 2001 From: Shiju Date: Wed, 4 Nov 2026 12:41:50 +0530 Subject: [PATCH 2/3] docs: explain resource enforcement without negotiation details Signed-off-by: Shiju --- docs/extensibility/drivers.mdx | 18 +++++------------- skills/openshell-cli/SKILL.md | 6 ++---- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/docs/extensibility/drivers.mdx b/docs/extensibility/drivers.mdx index bd894cbba1..e16e43cb6c 100644 --- a/docs/extensibility/drivers.mdx +++ b/docs/extensibility/drivers.mdx @@ -93,13 +93,11 @@ observation and overshoot has no fixed bound. These budgets do not satisfy a hard CPU-rate or memory quota. First-party container and VM drivers currently reject sampled budgets; external drivers may provide them. -Before writing budget-bearing requests or templates, curated SDKs and the CLI -require `HealthResponse.supports_sampled_resource_budget`. This public protocol -acknowledgement prevents older gateways from silently discarding the request; -it does not promise driver support. Raw API callers must check it too. Use a -homogeneous upgraded gateway deployment: a probe cannot make mixed-version -load-balancer routing atomic. The gateway and driver remain responsible for -resource admission before provisioning and on retained launches. +Raw API clients must check `HealthResponse.supports_sampled_resource_budget` +before sending sampled budgets. The CLI and curated SDKs check it automatically. +This confirms that the gateway preserves budget fields; driver support is +reported separately. Upgrade all gateway replicas before using sampled budgets, +because older versions can discard budget fields. ## Credential Drivers @@ -126,9 +124,3 @@ Drivers exchange peer metadata with the gateway and advertise their extension family capability. Upgrade both peers together when the protocol version changes. Refer to [Extension Protocol Negotiation](/extensibility/overview#protocol-negotiation) for the negotiation contract. - -## Release compatibility - -These API changes target the next minor release and must not be backported into the `0.1.x` patch series. Upgrade gateway replicas together before sending sampled budgets. An older replica can discard budget fields even if another replica acknowledged support. - -For raw Rust and TypeScript field changes and defaults, see the [SDK migration notes](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-sdk/README.md#next-minor-api-migration). diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index 82307566bd..f26c1b26ba 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -360,10 +360,8 @@ For a compute driver that advertises sampled account budgets, use These explicit budgets terminate on observed excess; collection and sampling can overshoot. They are distinct from `--cpu` core-rate limits and `--memory` hard limits. Inspect `gateway info` for driver guarantees; ordinary workspace -users do not need its administrator permission to use budgets. Budget-bearing -writes check the public gateway protocol acknowledgement and fail on older -gateways rather than discarding the fields. Unsupported drivers reject the -request before provisioning. +users do not need its administrator permission to use budgets. Budget requests +fail before provisioning if the gateway or driver does not support them. ### Manage sandbox workload templates From aa94d0289680112b230c95ec118bcbedcb4791e0 Mon Sep 17 00:00:00 2001 From: Matthew Grossman Date: Tue, 3 Nov 2026 23:14:00 +0000 Subject: [PATCH 3/3] fix(tests): pin the k3s installer to a release (#5223) get.k3s.io serves the latest install.sh from k3s master, so the pinned checksum broke when upstream changed the script. Download install.sh from the release tag matching k3s_version instead and pin its checksum. Signed-off-by: Matthew Grossman Signed-off-by: Shiju --- tests/tmachine/ansible/playbooks/k3s.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/tmachine/ansible/playbooks/k3s.yaml b/tests/tmachine/ansible/playbooks/k3s.yaml index 8666b2ac6f..d27c6c487b 100644 --- a/tests/tmachine/ansible/playbooks/k3s.yaml +++ b/tests/tmachine/ansible/playbooks/k3s.yaml @@ -21,10 +21,12 @@ state: present update_cache: true + # get.k3s.io serves the latest script, so pin the one from the k3s_version + # release tag. Update the checksum whenever k3s_version changes. - name: Download K3s installer ansible.builtin.get_url: - url: https://get.k3s.io - checksum: sha256:e5cc3b3d9dfc1662c2d9be6da5abc9a4cd317d6abc3a5ffc02e3dd3248207fee + url: "https://raw.githubusercontent.com/k3s-io/k3s/v{{ k3s_version | urlencode }}/install.sh" + checksum: sha256:3ce239d57d43b2d836d2b561043433e6decae8b9dc41f5d13908c0fafb0340cd dest: /tmp/install-k3s.sh mode: "0755"