From 5a415c7196814e6b26f665e00d3f8d5d9a219898 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Thu, 17 Sep 2026 13:16:26 -0700 Subject: [PATCH 1/8] feat(sandbox): expose services during creation Signed-off-by: Drew Newberry --- architecture/gateway.md | 9 + architecture/sandbox.md | 8 + crates/openshell-cli/src/main.rs | 83 +- crates/openshell-cli/src/run.rs | 100 +- .../tests/provider_commands_integration.rs | 1 + .../sandbox_create_lifecycle_integration.rs | 92 +- .../sandbox_name_fallback_integration.rs | 1 + .../src/proto_json.rs | 1 + .../src/runtime.rs | 1 + crates/openshell-sdk/README.md | 5 + crates/openshell-sdk/src/client.rs | 30 +- crates/openshell-sdk/src/lib.rs | 2 +- crates/openshell-sdk/src/types.rs | 17 + crates/openshell-sdk/tests/client_mock.rs | 27 +- .../src/grpc/mutation_replay/ordinary.rs | 22 +- crates/openshell-server/src/grpc/sandbox.rs | 148 ++- crates/openshell-server/src/grpc/service.rs | 40 +- crates/openshell-server/src/storage_proto.rs | 2 +- crates/openshell-tui/src/lib.rs | 1 + docs/sandboxes/manage-sandboxes.mdx | 63 + examples/codex-app-server/Dockerfile | 84 ++ examples/codex-app-server/README.md | 83 ++ proto/openshell.proto | 15 + python/openshell/__init__.py | 2 + python/openshell/sandbox.py | 51 +- python/openshell/sandbox_test.py | 31 +- sdk/go/docs/src/api/sandboxes.md | 11 +- sdk/go/openshell/v1/sandbox_client.go | 25 +- sdk/go/openshell/v1/sandbox_client_test.go | 25 +- sdk/go/openshell/v1/service.go | 3 + sdk/go/openshell/v1/types/options.go | 2 + sdk/go/openshell/v1/types/sandbox.go | 3 + sdk/go/openshell/v1/types/service.go | 6 + sdk/go/proto/openshellv1/openshell.pb.go | 1081 +++++++++-------- sdk/typescript/README.md | 2 + sdk/typescript/src/client.test.ts | 30 + sdk/typescript/src/client.ts | 30 +- sdk/typescript/src/index.ts | 1 + 38 files changed, 1570 insertions(+), 568 deletions(-) create mode 100644 examples/codex-app-server/Dockerfile create mode 100644 examples/codex-app-server/README.md diff --git a/architecture/gateway.md b/architecture/gateway.md index 45e934d610..e77ed5c5cd 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -403,6 +403,15 @@ Allow and deny append requests carry `L7RuleTarget` to declare the rule, endpoin `GetSandboxProviderStatus` and `ReportProviderReadiness` are unary public gateway RPCs. The first lets authorized users inspect a provider change; the second accepts installation reports only from the sandbox's current authenticated supervisor session. +`CreateSandboxRequest.service_exposures` is an additive public API field. Its +`SandboxServiceExposure` entries register service endpoints as part of sandbox +creation and are represented in the Rust, Python, TypeScript, and Go SDK create +options. The request-only exposure description is not durable; the gateway +persists the resulting `ServiceEndpoint` objects through the existing endpoint +store after it persists the sandbox. `SandboxResponse.service_urls` returns the +routed URLs keyed by service name for `CreateSandbox`; the empty key represents +the unnamed endpoint, and other sandbox operations leave the map empty. + The removed `NetworkBinary.harness` field remains reserved by number and name, so protobuf implementations cannot reuse its wire slot or source identifier. The durable-policy compatibility decoder reads the former boolean before Prost diff --git a/architecture/sandbox.md b/architecture/sandbox.md index cf661d32c5..96898e22d2 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -586,6 +586,14 @@ sandbox workload directly. The relay supports: `BASH_ENV` when the child environment sets it. - Tar-based file sync. - Port forwarding where supported by the CLI/TUI surface. +- Persistent HTTP and WebSocket service routing through gateway-managed + `ServiceEndpoint` records. `CreateSandboxRequest.service_exposures` registers + named or unnamed endpoints as part of sandbox creation, and the gateway + returns their routed URLs keyed by service name. The empty key identifies the + unnamed endpoint. Routing starts only while the sandbox is ready. `sandbox + create --expose PORT` uses the unnamed create-time endpoint and keeps the + sandbox. The standalone service API can add, update, or remove endpoints + later. Sandbox logs are emitted locally and can also be pushed back to the gateway. Security-relevant sandbox behavior uses OCSF structured events; internal diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index 139271ed0c..9e1beded85 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -1316,7 +1316,7 @@ enum SandboxCommands { keep: bool, /// Delete the sandbox after the initial command or shell exits. - #[arg(long, conflicts_with_all = ["keep", "editor", "forward"])] + #[arg(long, conflicts_with_all = ["keep", "editor", "forward", "expose"])] no_keep: bool, /// Launch a remote editor after the sandbox is ready. @@ -1363,6 +1363,16 @@ enum SandboxCommands { #[arg(long, conflicts_with = "no_keep")] forward: Option, + /// Expose a loopback HTTP or WebSocket service after the sandbox is ready. + /// Keeps the sandbox alive. Use `openshell service expose` to add a named service. + #[arg( + long, + value_name = "PORT", + value_parser = clap::value_parser!(u16).range(1..), + conflicts_with = "no_keep" + )] + expose: Option, + /// Allocate a pseudo-terminal for the remote command. /// Defaults to auto-detection (on when stdin and stdout are terminals). /// Use --tty to force a PTY even when auto-detection fails, or @@ -1416,7 +1426,7 @@ enum SandboxCommands { approval_mode: String, /// Output format. - #[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table, conflicts_with_all = ["editor", "command", "no_keep", "forward"])] + #[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table, conflicts_with_all = ["editor", "no_keep", "forward"])] output: OutputFormat, /// Command to run after "--" (defaults to an interactive shell). @@ -3144,6 +3154,7 @@ async fn run_async() -> Result<()> { providers, policy, forward, + expose, tty, no_tty, detach, @@ -3211,7 +3222,11 @@ async fn run_async() -> Result<()> { let forward = forward .map(|s| openshell_core::forward::ForwardSpec::parse(&s)) .transpose()?; - let keep = keep || !no_keep || editor.is_some() || forward.is_some(); + let keep = keep + || !no_keep + || editor.is_some() + || forward.is_some() + || expose.is_some(); let gpu_requirements: Option = gpu.map(Into::into); let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?; @@ -3235,6 +3250,7 @@ async fn run_async() -> Result<()> { providers: &providers, policy: policy.as_deref(), forward, + expose, command: &command, tty_override, auto_providers_override, @@ -5837,7 +5853,6 @@ mod tests { fn sandbox_create_output_conflicts_with_side_effect_args() { for (label, extra_args) in [ ("--editor", &["--editor", "code"][..]), - ("trailing command", &["--", "claude"][..]), ("--no-keep", &["--no-keep"][..]), ("--forward", &["--forward", "8080"][..]), ] { @@ -5852,6 +5867,22 @@ mod tests { } } + #[test] + fn sandbox_create_detached_command_accepts_structured_output() { + Cli::try_parse_from([ + "openshell", + "sandbox", + "create", + "--detach", + "--output", + "json", + "--", + "codex", + "app-server", + ]) + .expect("a detached command should support structured create output"); + } + #[test] fn sandbox_create_resource_flags_parse() { let cli = Cli::try_parse_from([ @@ -6239,6 +6270,50 @@ mod tests { ); } + #[test] + fn sandbox_create_accepts_service_exposure() { + let cli = Cli::try_parse_from([ + "openshell", + "sandbox", + "create", + "--expose", + "4500", + "--detach", + ]) + .expect("sandbox create --expose should parse"); + + match cli.command { + Some(Commands::Sandbox { + command: Some(SandboxCommands::Create { expose, detach, .. }), + }) => { + assert_eq!(expose, Some(4500)); + assert!(detach); + } + other => panic!("expected SandboxCommands::Create, got: {other:?}"), + } + } + + #[test] + fn sandbox_create_rejects_service_exposure_with_no_keep() { + let result = Cli::try_parse_from([ + "openshell", + "sandbox", + "create", + "--expose", + "4500", + "--no-keep", + ]); + + assert!(result.is_err()); + } + + #[test] + fn sandbox_create_rejects_zero_service_port() { + let result = Cli::try_parse_from(["openshell", "sandbox", "create", "--expose", "0"]); + + assert!(result.is_err()); + } + #[test] fn service_expose_accepts_positional_target_port_and_service() { let cli = Cli::try_parse_from([ diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index 5ac8d4b1a0..198abedb27 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -55,8 +55,8 @@ use openshell_core::proto::{ ListSandboxPoliciesRequest, ListSandboxTemplatesRequest, ListSandboxesRequest, ListServicesRequest, PolicySource, PolicyStatus, RejectDraftChunkRequest, ResourceRequirements, RevokeSshSessionRequest, Sandbox, SandboxCondition, SandboxPhase, SandboxPolicy, - SandboxResources, SandboxServiceLevel, SandboxSpec, SandboxStartup, SandboxTemplate, - SandboxWorkloadConfig, SandboxWorkloadTemplate, SandboxWorkloadTemplateSpec, + SandboxResources, SandboxServiceExposure, SandboxServiceLevel, SandboxSpec, SandboxStartup, + SandboxTemplate, SandboxWorkloadConfig, SandboxWorkloadTemplate, SandboxWorkloadTemplateSpec, ServiceEndpointResponse, SettingScope, StartSandboxRequest, StopSandboxRequest, TcpForwardFrame, TcpForwardInit, TcpRelayTarget, UpdateConfigRequest, WatchSandboxRequest, exec_sandbox_event, tcp_forward_init, @@ -249,8 +249,8 @@ pub fn doctor_check() -> Result<()> { Err(miette::miette!("docker info failed: {}", stderr.trim())) } -fn sandbox_should_persist(keep: bool, forward: Option<&ForwardSpec>) -> bool { - keep || forward.is_some() +fn sandbox_should_persist(keep: bool, forward: Option<&ForwardSpec>, expose: Option) -> bool { + keep || forward.is_some() || expose.is_some() } fn has_main_process_result(sandbox: &Sandbox) -> bool { @@ -440,6 +440,7 @@ pub struct SandboxCreateConfig<'a> { pub providers: &'a [String], pub policy: Option<&'a str>, pub forward: Option, + pub expose: Option, pub command: &'a [String], pub tty_override: Option, pub auto_providers_override: Option, @@ -467,6 +468,7 @@ impl Default for SandboxCreateConfig<'_> { providers: &[], policy: None, forward: None, + expose: None, command: &[], tty_override: None, auto_providers_override: None, @@ -502,6 +504,7 @@ pub async fn sandbox_create( providers, policy, forward, + expose, command, tty_override, auto_providers_override, @@ -528,6 +531,9 @@ pub async fn sandbox_create( "structured output cannot be combined with an attached trailing command; use table output to stream the command or add --detach" )); } + if expose == Some(0) { + return Err(miette::miette!("--expose port must be in 1..=65535")); + } // Check port availability *before* creating the sandbox so we don't // leave an orphaned sandbox behind when the forward would fail. @@ -634,7 +640,7 @@ pub async fn sandbox_create( // (bash when present, otherwise /bin/sh on minimal images like Alpine). // Baking a shell here would force a shell the image may not ship. let main_command = command.to_vec(); - let persist = sandbox_should_persist(keep, forward.as_ref()); + let persist = sandbox_should_persist(keep, forward.as_ref(), expose); let create_detaches = detach || (persist && command.is_empty() @@ -672,6 +678,13 @@ pub async fn sandbox_create( )), await_main_process_attachment, workload_template: template.unwrap_or_default().to_string(), + service_exposures: expose + .map(|target_port| SandboxServiceExposure { + service: String::new(), + target_port: u32::from(target_port), + }) + .into_iter() + .collect(), }; let response = match client.create_sandbox(request).await { @@ -684,8 +697,9 @@ pub async fn sandbox_create( } Err(status) => return Err(miette::miette!(status.to_string())), }; + let response = response.into_inner(); + let service_urls = response.service_urls; let sandbox = response - .into_inner() .sandbox .ok_or_else(|| miette::miette!("sandbox missing from response"))?; @@ -1095,8 +1109,27 @@ pub async fn sandbox_create( ); } + if let Some(target_port) = expose + && !structured_output + { + eprintln!( + " {} Exposed sandbox {sandbox_name} service on 127.0.0.1:{target_port}", + "\u{2713}".green().bold(), + ); + if let Some(url) = service_urls.get("").filter(|url| !url.is_empty()) { + eprintln!( + " Access at: {}", + service_url_for_gateway(url, &effective_server) + ); + } + } + if structured_output { - crate::output::print_output_single(output, &last_sandbox, sandbox_to_json)?; + let mut value = sandbox_to_json(&last_sandbox); + if let Some(object) = value.as_object_mut() { + object.insert("service_urls".to_string(), serde_json::json!(service_urls)); + } + crate::output::print_output_single(output, &value, Clone::clone)?; return Ok(0); } @@ -3627,21 +3660,8 @@ pub async fn service_expose( workspace: &str, tls: &TlsOptions, ) -> Result<()> { - let mut client = grpc_client(server, tls).await?; - let response = client - .expose_service(ExposeServiceRequest { - request_id: String::new(), - sandbox: (sandbox).to_string(), - workspace_scope: Some(openshell_core::proto::workspace_selector( - workspace.to_string(), - )), - name: service.to_string(), - target_port: u32::from(target_port), - domain: true, - }) - .await - .map_err(service_expose_status_error)? - .into_inner(); + let response = + expose_service_endpoint(server, sandbox, service, target_port, workspace, tls).await?; if service.is_empty() { println!( @@ -3666,6 +3686,31 @@ pub async fn service_expose( Ok(()) } +async fn expose_service_endpoint( + server: &str, + sandbox: &str, + service: &str, + target_port: u16, + workspace: &str, + tls: &TlsOptions, +) -> Result { + let mut client = grpc_client(server, tls).await?; + client + .expose_service(ExposeServiceRequest { + request_id: String::new(), + sandbox: sandbox.to_string(), + name: service.to_string(), + target_port: u32::from(target_port), + domain: true, + workspace_scope: Some(openshell_core::proto::workspace_selector( + workspace.to_string(), + )), + }) + .await + .map_err(service_expose_status_error) + .map(tonic::Response::into_inner) +} + fn service_expose_status_error(status: Status) -> miette::Report { service_status_error("expose service", "sandbox:write", status) } @@ -6745,18 +6790,23 @@ mod tests { #[test] fn sandbox_should_persist_defaults_to_persistent() { - assert!(sandbox_should_persist(true, None)); + assert!(sandbox_should_persist(true, None, None)); } #[test] fn sandbox_should_not_persist_when_no_keep_is_set() { - assert!(!sandbox_should_persist(false, None)); + assert!(!sandbox_should_persist(false, None, None)); } #[test] fn sandbox_should_persist_when_forward_is_requested() { let spec = openshell_core::forward::ForwardSpec::new(8080); - assert!(sandbox_should_persist(false, Some(&spec))); + assert!(sandbox_should_persist(false, Some(&spec), None)); + } + + #[test] + fn sandbox_should_persist_when_service_exposure_is_requested() { + assert!(sandbox_should_persist(false, None, Some(8080))); } #[test] diff --git a/crates/openshell-cli/tests/provider_commands_integration.rs b/crates/openshell-cli/tests/provider_commands_integration.rs index 4a26334c02..fac1bc7c05 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -322,6 +322,7 @@ impl OpenShell for TestOpenShell { status: None, ..Sandbox::default() }), + service_urls: HashMap::new(), })) } diff --git a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs index 9014713904..1afd9281b3 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -64,6 +64,7 @@ struct SandboxState { fail_list_provider_profiles: Arc, deleted_names: Arc>>>, create_requests: Arc>>, + expose_service_requests: Arc>>, fail_delete_sandbox_message: Arc>>, vm_error_after_started: Arc, vm_error_with_observed_exit: Arc, @@ -142,6 +143,16 @@ impl OpenShell for TestOpenShell { ) -> Result, Status> { let request = request.into_inner(); let name = request.name.clone(); + let service_urls = request + .service_exposures + .iter() + .map(|exposure| { + ( + exposure.service.clone(), + "https://default--sandbox.openshell.localhost:17670/".to_string(), + ) + }) + .collect(); self.state.create_requests.lock().await.push(request); let sandbox_name = if name.is_empty() { "test-sandbox".to_string() @@ -165,6 +176,7 @@ impl OpenShell for TestOpenShell { sandbox.set_phase(SandboxPhase::Provisioning as i32); Ok(Response::new(SandboxResponse { sandbox: Some(sandbox), + service_urls, })) } @@ -204,6 +216,7 @@ impl OpenShell for TestOpenShell { sandbox.set_phase(SandboxPhase::Ready as i32); Ok(Response::new(SandboxResponse { sandbox: Some(sandbox), + service_urls: HashMap::new(), })) } @@ -429,10 +442,18 @@ impl OpenShell for TestOpenShell { async fn expose_service( &self, - _request: tonic::Request, + request: tonic::Request, ) -> Result, Status> { + self.state + .expose_service_requests + .lock() + .await + .push(request.into_inner()); Ok(Response::new( - openshell_core::proto::ServiceEndpointResponse::default(), + openshell_core::proto::ServiceEndpointResponse { + url: "https://default--sandbox.openshell.localhost:17670/".to_string(), + ..Default::default() + }, )) } @@ -440,7 +461,12 @@ impl OpenShell for TestOpenShell { &self, _: tonic::Request, ) -> Result, Status> { - Err(Status::unimplemented("unused")) + Ok(Response::new( + openshell_core::proto::ServiceEndpointResponse { + url: "https://default--sandbox.openshell.localhost:17670/".to_string(), + ..Default::default() + }, + )) } async fn list_services( @@ -1437,6 +1463,18 @@ async fn create_requests(server: &TestServer) -> Vec { server.openshell.state.create_requests.lock().await.clone() } +async fn expose_service_requests( + server: &TestServer, +) -> Vec { + server + .openshell + .state + .expose_service_requests + .lock() + .await + .clone() +} + async fn template_create_requests(server: &TestServer) -> Vec { server .openshell @@ -2686,6 +2724,39 @@ async fn sandbox_create_keeps_sandbox_with_forwarding() { .status(); } +#[tokio::test] +async fn sandbox_create_exposes_service_after_ready_and_keeps_sandbox() { + let server = run_server().await; + let fake_ssh_dir = tempfile::tempdir().unwrap(); + let xdg_dir = tempfile::tempdir().unwrap(); + let _env = test_env(&fake_ssh_dir, &xdg_dir); + let tls = test_tls(&server); + + run::sandbox_create( + &server.endpoint, + "openshell", + run::SandboxCreateConfig { + name: Some("sandbox"), + keep: false, + expose: Some(4500), + detach: true, + ..test_config() + }, + "default", + &tls, + ) + .await + .expect("sandbox create with service exposure should succeed"); + + assert!(deleted_names(&server).await.is_empty()); + let create_requests = create_requests(&server).await; + assert_eq!(create_requests.len(), 1); + assert_eq!(create_requests[0].service_exposures.len(), 1); + assert_eq!(create_requests[0].service_exposures[0].service, ""); + assert_eq!(create_requests[0].service_exposures[0].target_port, 4500); + assert!(expose_service_requests(&server).await.is_empty()); +} + #[tokio::test] async fn sandbox_forward_background_tracks_owned_child_when_pid_discovery_fails() { let server = run_server().await; @@ -3149,15 +3220,26 @@ async fn sandbox_create_continues_with_unexpired_cached_token_when_refresh_fails async fn sandbox_create_json_stdout_is_parseable() { let server = run_server().await; - let result = run_cli_sandbox_create(&server, "json-clean", &["--output=json"]).await; + let result = run_cli_sandbox_create( + &server, + "json-clean", + &["--output=json", "--expose=4500", "--detach"], + ) + .await; assert!( result.status.success(), "sandbox create failed:\n{}", String::from_utf8_lossy(&result.stderr) ); let stdout = String::from_utf8(result.stdout).expect("stdout should be UTF-8"); - serde_json::from_str::(&stdout) + let value = serde_json::from_str::(&stdout) .unwrap_or_else(|err| panic!("stdout should contain only JSON: {err}\n{stdout}")); + assert_eq!( + value["service_urls"], + serde_json::json!({ + "": "https://default--sandbox.openshell.localhost:17670/" + }) + ); } #[tokio::test] diff --git a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs index bd295a9362..9e2412db97 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -143,6 +143,7 @@ impl OpenShell for TestOpenShell { }), ..Default::default() }), + service_urls: std::collections::HashMap::new(), })) } diff --git a/crates/openshell-gateway-interceptors/src/proto_json.rs b/crates/openshell-gateway-interceptors/src/proto_json.rs index 4bd15f7af1..a67ae30c5c 100644 --- a/crates/openshell-gateway-interceptors/src/proto_json.rs +++ b/crates/openshell-gateway-interceptors/src/proto_json.rs @@ -321,6 +321,7 @@ mod tests { )), await_main_process_attachment: false, workload_template: String::new(), + service_exposures: Vec::new(), }; let bytes = request.encode_to_vec(); let json = codec diff --git a/crates/openshell-gateway-interceptors/src/runtime.rs b/crates/openshell-gateway-interceptors/src/runtime.rs index 61f88c8a8f..0c5e1f94ef 100644 --- a/crates/openshell-gateway-interceptors/src/runtime.rs +++ b/crates/openshell-gateway-interceptors/src/runtime.rs @@ -1087,6 +1087,7 @@ mod tests { )), await_main_process_attachment: false, workload_template: String::new(), + service_exposures: Vec::new(), }; let bytes = request.encode_to_vec(); diff --git a/crates/openshell-sdk/README.md b/crates/openshell-sdk/README.md index 3758338dfc..423d04488c 100644 --- a/crates/openshell-sdk/README.md +++ b/crates/openshell-sdk/README.md @@ -55,6 +55,11 @@ integers where practical. Reusable template resources are exposed as portable workload shape and driver config. Failures map to a typed `SdkError` with a discriminable kind. +Set `SandboxSpec::service_exposures` to register named or unnamed loopback HTTP +services during creation. Each `ServiceExposure` contains a service name and a +target port; an empty name selects the unnamed endpoint. The returned +`SandboxRef::service_urls` map contains each routed URL under the same name. + Curated calls without a workspace argument explicitly select the `default` workspace. Cross-workspace listing uses the separate `*_all_workspaces` methods and requires Platform Admin access. diff --git a/crates/openshell-sdk/src/client.rs b/crates/openshell-sdk/src/client.rs index b9c98683bf..b019491d6f 100644 --- a/crates/openshell-sdk/src/client.rs +++ b/crates/openshell-sdk/src/client.rs @@ -144,7 +144,7 @@ impl OpenShellClient { async move { grpc.create_sandbox(request).await } }) .await?; - sandbox_from_response(response.sandbox) + sandbox_from_create_response(response) } /// Create a new sandbox from a workspace-scoped workload template name. @@ -159,7 +159,7 @@ impl OpenShellClient { async move { grpc.create_sandbox(request).await } }) .await?; - sandbox_from_response(response.sandbox) + sandbox_from_create_response(response) } /// Create a reusable sandbox template in the default workspace. @@ -774,7 +774,7 @@ impl WorkspaceScopedClient { async move { grpc.create_sandbox(request).await } }) .await?; - sandbox_from_response(response.sandbox) + sandbox_from_create_response(response) } /// Create a new sandbox from a template in this workspace. @@ -791,7 +791,7 @@ impl WorkspaceScopedClient { async move { grpc.create_sandbox(request).await } }) .await?; - sandbox_from_response(response.sandbox) + sandbox_from_create_response(response) } /// Create a reusable sandbox template in this workspace. @@ -1178,6 +1178,7 @@ fn create_sandbox_request(spec: SandboxSpec) -> proto::CreateSandboxRequest { gpu, command, tty, + service_exposures, } = spec; let template = image.map(|image| proto::SandboxTemplate { image, @@ -1203,6 +1204,13 @@ fn create_sandbox_request(spec: SandboxSpec) -> proto::CreateSandboxRequest { workspace_scope: Some(proto::workspace_selector("default")), await_main_process_attachment: false, workload_template: String::new(), + service_exposures: service_exposures + .into_iter() + .map(|exposure| proto::SandboxServiceExposure { + service: exposure.service, + target_port: u32::from(exposure.target_port), + }) + .collect(), } } @@ -1217,6 +1225,7 @@ fn create_sandbox_from_template_request( command, tty, policy, + service_exposures, } = spec; proto::CreateSandboxRequest { request_id: String::new(), @@ -1233,6 +1242,13 @@ fn create_sandbox_from_template_request( workspace_scope: Some(proto::workspace_selector("default")), workload_template: template_name, await_main_process_attachment: false, + service_exposures: service_exposures + .into_iter() + .map(|exposure| proto::SandboxServiceExposure { + service: exposure.service, + target_port: u32::from(exposure.target_port), + }) + .collect(), } } @@ -1242,6 +1258,12 @@ fn sandbox_from_response(sandbox: Option) -> Result .ok_or_else(|| SdkError::invalid_config("sandbox missing from gateway response")) } +fn sandbox_from_create_response(response: proto::SandboxResponse) -> Result { + let mut sandbox = sandbox_from_response(response.sandbox)?; + sandbox.service_urls = response.service_urls; + Ok(sandbox) +} + fn sandbox_template_from_response( template: Option, ) -> Result { diff --git a/crates/openshell-sdk/src/lib.rs b/crates/openshell-sdk/src/lib.rs index 78f7375f8b..eac522a187 100644 --- a/crates/openshell-sdk/src/lib.rs +++ b/crates/openshell-sdk/src/lib.rs @@ -54,5 +54,5 @@ pub use types::{ SandboxPhase, SandboxRef, SandboxResources, SandboxServiceLevel, SandboxSpec, SandboxStartup, SandboxTemplateCreateSpec, SandboxTemplateListOptions, SandboxWorkloadConfig, SandboxWorkloadTemplate, SandboxWorkloadTemplateProvenance, SandboxWorkloadTemplateSpec, - ServiceStatus, WorkspaceRef, + ServiceExposure, ServiceStatus, WorkspaceRef, }; diff --git a/crates/openshell-sdk/src/types.rs b/crates/openshell-sdk/src/types.rs index 3937462d00..d09eaabf19 100644 --- a/crates/openshell-sdk/src/types.rs +++ b/crates/openshell-sdk/src/types.rs @@ -161,6 +161,17 @@ pub struct SandboxSpec { pub command: Vec, /// Allocate a retained pseudo-terminal for the canonical command. pub tty: bool, + /// Loopback HTTP services to expose when the sandbox is created. + pub service_exposures: Vec, +} + +/// A loopback HTTP service to expose during sandbox creation. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ServiceExposure { + /// Service name. Empty selects the sandbox's unnamed endpoint. + pub service: String, + /// Loopback TCP port inside the sandbox. + pub target_port: u16, } /// Caller intent for creating a sandbox from a named workload template. @@ -178,6 +189,8 @@ pub struct SandboxTemplateCreateSpec { pub command: Vec, /// Allocate a retained pseudo-terminal for the canonical command. pub tty: bool, + /// Loopback HTTP services to expose when the sandbox is created. + pub service_exposures: Vec, /// Create-time sandbox policy. The named workload template supplies runtime /// workload fields; policy remains part of the sandbox's governance spec. pub policy: Option, @@ -227,6 +240,9 @@ pub struct SandboxRef { pub resource_version: u64, pub exit_code: Option, pub created_from_workload_template: Option, + /// Service URLs returned by sandbox creation, keyed by service name. The + /// empty key identifies the unnamed service. Non-create reads leave this empty. + pub service_urls: HashMap, } /// Reusable workload template revision used to create a sandbox. @@ -258,6 +274,7 @@ impl SandboxRef { resource_version: meta.resource_version, exit_code, created_from_workload_template, + service_urls: HashMap::new(), } } } diff --git a/crates/openshell-sdk/tests/client_mock.rs b/crates/openshell-sdk/tests/client_mock.rs index 4eab8e2313..d46ff80db4 100644 --- a/crates/openshell-sdk/tests/client_mock.rs +++ b/crates/openshell-sdk/tests/client_mock.rs @@ -13,7 +13,7 @@ use openshell_core::proto::open_shell_server::{OpenShell, OpenShellServer}; use openshell_sdk::{ AuthConfig, ClientConfig, ExecOptions, ListOptions, OpenShellClient, Refresh, RefreshError, RefreshedToken, SandboxPhase, SandboxSpec, SandboxTemplateCreateSpec, - SandboxTemplateListOptions, ServiceStatus as SdkServiceStatus, + SandboxTemplateListOptions, ServiceExposure, ServiceStatus as SdkServiceStatus, }; use std::collections::HashMap; use std::sync::Arc; @@ -246,9 +246,20 @@ impl OpenShell for TestOpenShell { } else { req.name.clone() }; + let service_urls = req + .service_exposures + .iter() + .map(|exposure| { + ( + exposure.service.clone(), + format!("https://{}.example.test/", exposure.service), + ) + }) + .collect(); *self.state.last_create.lock().await = Some(req); Ok(Response::new(proto::SandboxResponse { sandbox: Some(sandbox_with_phase(&name, proto::SandboxPhase::Provisioning)), + service_urls, })) } @@ -318,6 +329,7 @@ impl OpenShell for TestOpenShell { *self.state.last_stop.lock().await = Some(request); Ok(Response::new(proto::SandboxResponse { sandbox: Some(sandbox), + service_urls: HashMap::new(), })) } @@ -334,6 +346,7 @@ impl OpenShell for TestOpenShell { *self.state.last_start.lock().await = Some(request); Ok(Response::new(proto::SandboxResponse { sandbox: Some(sandbox), + service_urls: HashMap::new(), })) } @@ -374,6 +387,7 @@ impl OpenShell for TestOpenShell { } Ok(Response::new(proto::SandboxResponse { sandbox: Some(sandbox), + service_urls: HashMap::new(), })) } @@ -983,17 +997,28 @@ async fn create_sandbox_passes_spec_through() { image: Some("ghcr.io/foo:bar".to_string()), labels: labels.clone(), gpu: true, + service_exposures: vec![ServiceExposure { + service: "web".to_string(), + target_port: 8080, + }], ..Default::default() }; let result = client.create_sandbox(spec).await.unwrap(); assert_eq!(result.name, "my-box"); assert_eq!(result.phase, SandboxPhase::Provisioning); + assert_eq!( + result.service_urls.get("web").map(String::as_str), + Some("https://web.example.test/") + ); let observed = state.last_create.lock().await.clone().unwrap(); assert_eq!(observed.name, "my-box"); assert_eq!(observed.labels, labels); assert!(observed.annotations.is_empty()); + assert_eq!(observed.service_exposures.len(), 1); + assert_eq!(observed.service_exposures[0].service, "web"); + assert_eq!(observed.service_exposures[0].target_port, 8080); let observed_spec = observed.spec.unwrap(); assert!( observed_spec diff --git a/crates/openshell-server/src/grpc/mutation_replay/ordinary.rs b/crates/openshell-server/src/grpc/mutation_replay/ordinary.rs index 2b09ed030f..48c0146381 100644 --- a/crates/openshell-server/src/grpc/mutation_replay/ordinary.rs +++ b/crates/openshell-server/src/grpc/mutation_replay/ordinary.rs @@ -154,6 +154,8 @@ pub(in crate::grpc) enum Outcome { Sandbox { id: String, changed: bool, + #[serde(default)] + service_urls: HashMap, }, SandboxDeletion { id: String, @@ -409,10 +411,15 @@ macro_rules! sandbox_scoped_mutation { }; } -fn sandbox_receipt(sandbox: Option<&Sandbox>, changed: bool) -> Result { +fn sandbox_receipt( + sandbox: Option<&Sandbox>, + changed: bool, + service_urls: HashMap, +) -> Result { Ok(Outcome::Sandbox { id: sandbox.ok_or_else(uncertain)?.object_id().into(), changed, + service_urls, }) } @@ -426,7 +433,8 @@ macro_rules! sandbox_mutation { User, |response: &Response| sandbox_receipt( response.get_ref().sandbox.as_ref(), - false + false, + HashMap::new(), ), async |store: &Store, outcome: Outcome| { let Outcome::Sandbox { id, .. } = outcome else { @@ -434,6 +442,7 @@ macro_rules! sandbox_mutation { }; Ok(SandboxResponse { sandbox: Some(live(store, &id).await?), + service_urls: HashMap::new(), }) } ); @@ -447,14 +456,19 @@ scoped_mutation!( User, |response: &Response| sandbox_receipt( response.get_ref().sandbox.as_ref(), - false + false, + response.get_ref().service_urls.clone(), ), async |store: &Store, outcome: Outcome| { - let Outcome::Sandbox { id, .. } = outcome else { + let Outcome::Sandbox { + id, service_urls, .. + } = outcome + else { return Err(replay_unavailable()); }; Ok(SandboxResponse { sandbox: Some(live(store, &id).await?), + service_urls, }) } ); diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index f3ba04a2c8..c2b34d8abb 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -46,7 +46,7 @@ use openshell_core::telemetry::{ use openshell_core::{GetResourceVersion, ObjectId, ObjectName, ObjectWorkspace}; use prost::Message; use prost_types::{Struct, Value, value::Kind}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::net::IpAddr; use std::pin::Pin; use std::sync::Arc; @@ -75,6 +75,7 @@ use crate::persistence::current_time_ms; const TCP_FORWARD_CHUNK_SIZE: usize = 64 * 1024; const NO_LOGIN_SHELL_ENV: (&str, &str) = ("OPENSHELL_NO_LOGIN_SHELL", "1"); const MAX_TEMPLATES_PER_WORKSPACE: u32 = 1000; +const MAX_CREATE_SERVICE_EXPOSURES: usize = 32; #[derive(Debug)] pub struct WatchSandboxStream { @@ -627,6 +628,19 @@ async fn handle_create_sandbox_inner( ) .await?; + let mut service_urls = HashMap::with_capacity(request.service_exposures.len()); + for exposure in &request.service_exposures { + let endpoint = super::service::expose_service_endpoint( + state, + sandbox.object_workspace(), + &sandbox, + &exposure.service, + exposure.target_port, + ) + .await?; + service_urls.insert(exposure.service.clone(), endpoint.into_inner().url); + } + info!( sandbox_id = %id, sandbox_name = %name, @@ -634,6 +648,7 @@ async fn handle_create_sandbox_inner( ); Ok(Response::new(SandboxResponse { sandbox: Some(sandbox), + service_urls, })) } @@ -648,6 +663,22 @@ fn validate_create_sandbox_request_pre_io( } crate::grpc::validation::validate_annotations(&request.annotations, "annotations")?; + if request.service_exposures.len() > MAX_CREATE_SERVICE_EXPOSURES { + return Err(Status::invalid_argument(format!( + "service_exposures must contain at most {MAX_CREATE_SERVICE_EXPOSURES} entries" + ))); + } + let mut service_names = HashSet::with_capacity(request.service_exposures.len()); + for exposure in &request.service_exposures { + super::service::validate_service_exposure_request(&exposure.service, exposure.target_port)?; + if !service_names.insert(exposure.service.as_str()) { + return Err(Status::invalid_argument(format!( + "duplicate service exposure name: '{}'", + exposure.service + ))); + } + } + if workload_template_name.is_empty() { let spec = request .spec @@ -782,6 +813,7 @@ pub(super) async fn handle_get_sandbox( .await?; Ok(Response::new(SandboxResponse { sandbox: Some(sandbox), + service_urls: HashMap::new(), })) } @@ -1531,6 +1563,7 @@ async fn handle_stop_sandbox_inner( info!(sandbox_name = %name, "StopSandbox request completed successfully"); Ok(Response::new(SandboxResponse { sandbox: Some(sandbox), + service_urls: HashMap::new(), })) } @@ -1595,6 +1628,7 @@ async fn handle_start_sandbox_inner( info!(sandbox_name = %name, "StartSandbox request completed successfully"); Ok(Response::new(SandboxResponse { sandbox: Some(sandbox), + service_urls: HashMap::new(), })) } @@ -3342,8 +3376,10 @@ mod tests { use crate::grpc::test_support::{ authed_request, test_server_state, test_server_state_with_driver, }; - use openshell_core::proto::GpuResourceRequirements; + use crate::provider_profile_sources::ProviderProfileSources; + use openshell_core::GatewayProviderProfileSourceConfig; use openshell_core::proto::datamodel::v1::ObjectMeta; + use openshell_core::proto::{GpuResourceRequirements, SandboxServiceExposure, ServiceEndpoint}; // ---- shell_escape ---- @@ -4444,6 +4480,7 @@ mod tests { )), await_main_process_attachment: false, workload_template: String::new(), + service_exposures: Vec::new(), }), ) .await @@ -4472,6 +4509,7 @@ mod tests { )), await_main_process_attachment: false, workload_template: String::new(), + service_exposures: Vec::new(), }), ) .await @@ -4590,6 +4628,7 @@ mod tests { )), await_main_process_attachment: false, workload_template: String::new(), + service_exposures: Vec::new(), }), ) .await @@ -4965,6 +5004,7 @@ mod tests { )), await_main_process_attachment: false, workload_template: String::new(), + service_exposures: Vec::new(), }), ) .await @@ -5003,6 +5043,96 @@ mod tests { ); } + #[tokio::test] + async fn create_sandbox_registers_requested_service_exposures() { + let state = test_server_state().await; + + let response = handle_create_sandbox( + &state, + authed_request(CreateSandboxRequest { + name: "services".to_string(), + spec: Some(SandboxSpec::default()), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + service_exposures: vec![ + SandboxServiceExposure { + service: String::new(), + target_port: 4500, + }, + SandboxServiceExposure { + service: "metrics".to_string(), + target_port: 9090, + }, + ], + ..Default::default() + }), + ) + .await + .expect("sandbox with service exposures should be created") + .into_inner(); + + let sandbox = response.sandbox.expect("created sandbox"); + assert_eq!(response.service_urls.len(), 2); + assert_eq!( + response.service_urls.get("").map(String::as_str), + Some("http://default--services.openshell.localhost:17670/") + ); + assert_eq!( + response.service_urls.get("metrics").map(String::as_str), + Some("http://default--services--metrics.openshell.localhost:17670/") + ); + for (service, target_port) in [("", 4500), ("metrics", 9090)] { + let key = crate::service_routing::endpoint_key("services", service); + let endpoint = state + .store + .get_message_by_name::("default", &key) + .await + .expect("service endpoint lookup should succeed") + .expect("service endpoint should be persisted"); + assert_eq!(endpoint.sandbox_id, sandbox.object_id()); + assert_eq!(endpoint.sandbox_name, "services"); + assert_eq!(endpoint.service_name, service); + assert_eq!(endpoint.target_port, target_port); + assert!(endpoint.domain); + } + } + + #[tokio::test] + async fn create_sandbox_rejects_duplicate_service_exposures_before_persisting() { + let state = test_server_state().await; + let error = handle_create_sandbox( + &state, + authed_request(CreateSandboxRequest { + name: "duplicate-services".to_string(), + spec: Some(SandboxSpec::default()), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + service_exposures: vec![ + SandboxServiceExposure { + service: "web".to_string(), + target_port: 8080, + }, + SandboxServiceExposure { + service: "web".to_string(), + target_port: 8081, + }, + ], + ..Default::default() + }), + ) + .await + .expect_err("duplicate service names should be rejected"); + + assert_eq!(error.code(), tonic::Code::InvalidArgument); + assert!(error.message().contains("duplicate service exposure name")); + assert!( + state + .store + .get_message_by_name::("default", "duplicate-services") + .await + .expect("sandbox lookup should succeed") + .is_none() + ); + } + #[tokio::test] async fn create_and_get_preserve_partial_process_identity() { let state = test_server_state_with_driver("docker").await; @@ -5031,6 +5161,7 @@ mod tests { )), await_main_process_attachment: false, workload_template: String::new(), + service_exposures: Vec::new(), }), ) .await @@ -5101,6 +5232,7 @@ mod tests { )), await_main_process_attachment: false, workload_template: String::new(), + service_exposures: Vec::new(), }), ) .await @@ -5136,6 +5268,7 @@ mod tests { )), await_main_process_attachment: false, workload_template: String::new(), + service_exposures: Vec::new(), }), ) .await @@ -5173,6 +5306,7 @@ mod tests { )), await_main_process_attachment: false, workload_template: String::new(), + service_exposures: Vec::new(), }), ) .await @@ -5689,7 +5823,7 @@ mod tests { let message = pool .get_message_by_name(message_name) .expect("message descriptor"); - let classified: std::collections::HashSet<&str> = copied_from_create_request + let classified: HashSet<&str> = copied_from_create_request .iter() .chain(rejected_template_workload_overrides.iter()) .chain(generated_by_gateway.iter()) @@ -5832,6 +5966,7 @@ mod tests { )), workload_template: "gpu-kata".to_string(), await_main_process_attachment: false, + service_exposures: Vec::new(), }), ) .await @@ -5920,6 +6055,7 @@ mod tests { )), workload_template: "default-image".to_string(), await_main_process_attachment: false, + service_exposures: Vec::new(), }), ) .await @@ -5973,6 +6109,7 @@ mod tests { )), workload_template: "default-gpu".to_string(), await_main_process_attachment: false, + service_exposures: Vec::new(), }), ) .await @@ -6013,6 +6150,7 @@ mod tests { )), workload_template: "corrupt-template".to_string(), await_main_process_attachment: false, + service_exposures: Vec::new(), }), ) .await @@ -6054,6 +6192,7 @@ mod tests { )), workload_template: "gpu-kata".to_string(), await_main_process_attachment: false, + service_exposures: Vec::new(), }), ) .await @@ -6080,6 +6219,7 @@ mod tests { )), workload_template: "Invalid_Template_Name".to_string(), await_main_process_attachment: false, + service_exposures: Vec::new(), }), ) .await @@ -6109,6 +6249,7 @@ mod tests { )), workload_template: "missing-template".to_string(), await_main_process_attachment: false, + service_exposures: Vec::new(), }), ) .await @@ -6138,6 +6279,7 @@ mod tests { )), workload_template: String::new(), await_main_process_attachment: false, + service_exposures: Vec::new(), }), ) .await diff --git a/crates/openshell-server/src/grpc/service.rs b/crates/openshell-server/src/grpc/service.rs index 8d956761e0..675b695656 100644 --- a/crates/openshell-server/src/grpc/service.rs +++ b/crates/openshell-server/src/grpc/service.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use openshell_core::proto::datamodel::v1::ObjectMeta; use openshell_core::proto::{ DeleteServiceRequest, DeleteServiceResponse, ExposeServiceRequest, GetServiceRequest, - ListServicesRequest, ListServicesResponse, ServiceEndpoint, ServiceEndpointResponse, + ListServicesRequest, ListServicesResponse, Sandbox, ServiceEndpoint, ServiceEndpointResponse, }; use openshell_core::{GetResourceVersion, ObjectId, ObjectName, ObjectWorkspace}; use prost::Message as _; @@ -49,19 +49,37 @@ pub(super) async fn handle_expose_service( super::workspace::resolve_workspace(state.store.as_ref(), sandbox.object_workspace()) .await? .ensure_active()?; - let sandbox_name = sandbox.object_name(); - validate_optional_endpoint_name("service", &req.name, MAX_SERVICE_NAME_LEN)?; - if req.target_port == 0 || req.target_port > u32::from(u16::MAX) { + validate_service_exposure_request(&req.name, req.target_port)?; + expose_service_endpoint(state, &workspace, &sandbox, &req.name, req.target_port).await +} + +pub(super) fn validate_service_exposure_request( + service: &str, + target_port: u32, +) -> Result<(), Status> { + validate_optional_endpoint_name("service", service, MAX_SERVICE_NAME_LEN)?; + if target_port == 0 || target_port > u32::from(u16::MAX) { return Err(Status::invalid_argument("target_port must be in 1..=65535")); } + Ok(()) +} + +pub(super) async fn expose_service_endpoint( + state: &Arc, + workspace: &str, + sandbox: &Sandbox, + service: &str, + target_port: u32, +) -> Result, Status> { + let sandbox_name = sandbox.object_name(); let now = crate::persistence::current_time_ms(); - let key = service_routing::endpoint_key(sandbox_name, &req.name); + let key = service_routing::endpoint_key(sandbox_name, service); // Fetch existing endpoint to determine create vs. update path let existing = state .store - .get_message_by_name::(&workspace, &key) + .get_message_by_name::(workspace, &key) .await .map_err(|e| Status::internal(format!("fetch endpoint failed: {e}")))?; @@ -106,13 +124,13 @@ pub(super) async fn handle_expose_service( labels: HashMap::from([("sandbox".to_string(), sandbox_name.to_string())]), resource_version: 0, annotations: HashMap::new(), - workspace: workspace.clone(), + workspace: workspace.to_string(), deletion_time: None, }), sandbox_id: sandbox.object_id().to_string(), sandbox: sandbox_name.to_string(), - name: req.name.clone(), - target_port: req.target_port, + name: service.to_string(), + target_port, domain: true, }; @@ -123,7 +141,7 @@ pub(super) async fn handle_expose_service( ServiceEndpoint::object_type(), &id, &key, - &workspace, + workspace, &endpoint.encode_to_vec(), Some(&labels_json), condition, @@ -136,7 +154,7 @@ pub(super) async fn handle_expose_service( meta.resource_version = result.resource_version; } - let url = service_routing::endpoint_url(&state.config, &workspace, sandbox_name, &req.name) + let url = service_routing::endpoint_url(&state.config, workspace, sandbox_name, service) .unwrap_or_default(); service_routing::emit_service_endpoint_config_event(&endpoint, &url, created); diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index bd3f4790ac..9d303b1add 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -575,7 +575,7 @@ mod tests { overlap_hash.as_str(), ), ( - (299, 24), + (300, 24), (92, 19), (80, 19), PUBLIC_RPC_SCHEMA_SHA256, diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 1cb2b02232..ef361b8b15 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -1457,6 +1457,7 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender) { workspace_scope: Some(openshell_core::proto::workspace_selector(workspace.clone())), await_main_process_attachment: false, workload_template: String::new(), + service_exposures: Vec::new(), }; let sandbox_name = diff --git a/docs/sandboxes/manage-sandboxes.mdx b/docs/sandboxes/manage-sandboxes.mdx index 7627a87650..056332c37b 100644 --- a/docs/sandboxes/manage-sandboxes.mdx +++ b/docs/sandboxes/manage-sandboxes.mdx @@ -453,6 +453,69 @@ with SandboxClient.from_active_cluster(client_credentials=auth) as client: Service forwarding makes a long-running process inside a sandbox reachable through a gateway-managed URL. Use it for development servers, notebooks, dashboards, or other services that keep listening after the sandbox starts. Run the service on loopback inside the sandbox, expose its port, then open the URL printed by OpenShell. +Expose the unnamed service as part of sandbox creation when the sandbox's main +process starts the server: + +```shell +openshell sandbox create \ + --name my-sandbox \ + --expose 8080 \ + --detach \ + -- python -m http.server 8080 --bind 127.0.0.1 +``` + +The CLI includes the unnamed endpoint in the sandbox create request and prints +its URL after the sandbox reaches `Ready`. `--expose` keeps the sandbox after +the create command returns and cannot be combined with `--no-keep`. Use +`openshell service expose` to add or update an endpoint later. + +SDK create methods accept named or unnamed service exposures. An empty service +name selects the unnamed endpoint. The returned sandbox includes a service URL +map keyed by those names; use the empty key for the unnamed endpoint: + +```python +from openshell import SandboxClient, ServiceExposure + +with SandboxClient.from_active_cluster() as client: + sandbox = client.create( + workspace="default", + name="app-server", + service_exposures=[ServiceExposure(target_port=4500)], + ) + print(sandbox.service_urls[""]) +``` + +```ts +const sandbox = await client.sandbox.create({ + name: 'app-server', + image: 'base', + serviceExposures: [{ targetPort: 4500 }], +}) +console.log(sandbox.serviceUrls['']) +``` + +```go +sandbox, err := client.Sandboxes().Create( + ctx, "default", "app-server", spec, nil, + v1.CreateOptions{ServiceExposures: []v1.ServiceExposure{ + {TargetPort: 4500}, + }}, +) +fmt.Println(sandbox.ServiceURLs[""]) +``` + +```rust +let sandbox = client.create_sandbox(openshell_sdk::SandboxSpec { + name: Some("app-server".into()), + service_exposures: vec![openshell_sdk::ServiceExposure { + service: String::new(), + target_port: 4500, + }], + ..Default::default() +}).await?; +println!("{}", sandbox.service_urls[""]); +``` + Expose a service that listens on loopback inside the sandbox: ```shell diff --git a/examples/codex-app-server/Dockerfile b/examples/codex-app-server/Dockerfile new file mode 100644 index 0000000000..0a7adccb21 --- /dev/null +++ b/examples/codex-app-server/Dockerfile @@ -0,0 +1,84 @@ +# syntax=docker/dockerfile:1.7 + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +FROM ghcr.io/nvidia/openshell-community/sandboxes/base:latest + +USER root + +RUN npm install --global --force "@openai/codex@latest" \ + && npm cache clean --force \ + && codex --version + +COPY --chmod=0755 <<'EOF' /usr/local/bin/start-codex-app-server +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); +const { spawn } = require("child_process"); + +const required = [ + "CODEX_AUTH_ACCESS_TOKEN", + "CODEX_AUTH_REFRESH_TOKEN", + "CODEX_AUTH_ACCOUNT_ID", +]; +for (const name of required) { + if (!process.env[name]) { + throw new Error(`missing required provider credential: ${name}`); + } +} + +const b64u = (value) => Buffer.from(JSON.stringify(value)).toString("base64url"); +const now = Math.floor(Date.now() / 1000); +const idToken = [ + b64u({ alg: "none", typ: "JWT" }), + b64u({ + iss: "https://auth.openai.com", + aud: "codex", + sub: "openshell-app-server", + email: "app-server@openshell.local", + iat: now, + exp: now + 3600, + }), + "placeholder", +].join("."); + +const codexHome = path.join(process.env.HOME, ".codex"); +fs.mkdirSync(codexHome, { recursive: true }); +fs.writeFileSync(path.join(codexHome, "auth.json"), JSON.stringify({ + auth_mode: "chatgpt", + OPENAI_API_KEY: null, + tokens: { + // Provider values are opaque stable handles. Codex parses the ID token + // locally, so use a short-lived, non-secret identity placeholder for it. + id_token: idToken, + access_token: process.env.CODEX_AUTH_ACCESS_TOKEN, + refresh_token: process.env.CODEX_AUTH_REFRESH_TOKEN, + account_id: process.env.CODEX_AUTH_ACCOUNT_ID, + }, + last_refresh: new Date().toISOString(), +}, null, 2), { mode: 0o600 }); + +const port = process.env.CODEX_APP_SERVER_PORT || "4500"; +const server = spawn( + "codex", + ["app-server", "--listen", `ws://127.0.0.1:${port}`], + { stdio: "inherit" }, +); +server.on("error", (error) => { + console.error(error); + process.exit(1); +}); +server.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + } else { + process.exit(code ?? 1); + } +}); +EOF + +ENV CODEX_APP_SERVER_PORT=4500 + +USER sandbox diff --git a/examples/codex-app-server/README.md b/examples/codex-app-server/README.md new file mode 100644 index 0000000000..220a195e36 --- /dev/null +++ b/examples/codex-app-server/README.md @@ -0,0 +1,83 @@ +# Codex app server in an OpenShell sandbox + +This example builds a sandbox image with Codex, runs its app server inside an +OpenShell sandbox, exposes the WebSocket service through the local gateway, and +connects a Codex client running on the host. + +Use this example only with a local, loopback-bound OpenShell gateway. The app +server does not use its own bearer-token authentication, and the exposed URL is +reachable by other local processes. + +## Prerequisites + +- A running local OpenShell gateway backed by Docker +- `docker`, `openshell`, the latest Codex client, and `jq` on the host +- A host Codex login in `$HOME/.codex/auth.json` + +Run the following commands from the example directory: + +```shell +cd examples/codex-app-server +``` + +## 1. Build the image + +This installs the latest published Codex version in the image. Keep the host +client current as well to avoid app-server protocol incompatibilities. + +```shell +docker build --pull --no-cache --tag openshell/codex-app-server:local --file Dockerfile . +``` + +## 2. Create the provider + +This single command reads the existing host login and stores it in an +OpenShell provider named `codex`. Skip it if that provider already exists on +the gateway. + +```shell +openshell provider create \ + --name codex \ + --type codex \ + --credential "CODEX_AUTH_ACCESS_TOKEN=$(jq -er '.tokens.access_token' "$HOME/.codex/auth.json")" \ + --credential "CODEX_AUTH_REFRESH_TOKEN=$(jq -er '.tokens.refresh_token' "$HOME/.codex/auth.json")" \ + --credential "CODEX_AUTH_ACCOUNT_ID=$(jq -er '.tokens.account_id' "$HOME/.codex/auth.json")" +``` + +## 3. Launch the sandbox + +```shell +openshell sandbox create \ + --name codex-app-server \ + --from openshell/codex-app-server:local \ + --expose 4500 \ + --detach \ + --no-tty \ + --provider codex \ + --output json \ + -- start-codex-app-server +``` + +The create result includes the exposed endpoint: + +```json +{ + "service_urls": { + "": "http://default--codex-app-server.openshell.localhost:/" + } +} +``` + +Convert the returned URL to its WebSocket scheme and connect the local client, +replacing `` with the port from the create result: + +```shell +codex --remote ws://default--codex-app-server.openshell.localhost:/ --no-alt-screen +``` + +## Clean up + +```shell +openshell sandbox delete codex-app-server +docker image rm openshell/codex-app-server:local +``` diff --git a/proto/openshell.proto b/proto/openshell.proto index 50ec3c799f..8435c73350 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -1194,6 +1194,10 @@ message CreateSandboxRequest { // Optional nonzero UUID for durable at-most-once admission. Successful results // can be replayed for 24 hours; see the API errors and retries reference. string request_id = 8; + // HTTP services to expose when the sandbox is created. Endpoints are + // registered after the sandbox has been persisted and route only while the + // sandbox is ready. + repeated SandboxServiceExposure service_exposures = 9; } message CreateSandboxTemplateRequest { @@ -1378,6 +1382,9 @@ message StartSandboxRequest { // Sandbox response. message SandboxResponse { Sandbox sandbox = 1; + // Service URLs created by CreateSandbox, keyed by service name. The empty + // key identifies the unnamed service. Other sandbox RPCs return an empty map. + map service_urls = 2; } // List sandboxes response. @@ -3598,3 +3605,11 @@ message SandboxProvisioning { string attachment_change_id = 10; google.protobuf.Timestamp attachment_change_time = 11; } + +// Create-time request to expose one loopback HTTP service in a sandbox. +message SandboxServiceExposure { + // Service name within the sandbox. Empty selects the unnamed endpoint. + string service = 1; + // Loopback TCP port inside the sandbox. + uint32 target_port = 2; +} diff --git a/python/openshell/__init__.py b/python/openshell/__init__.py index 8cfc8cb338..8d0d20f578 100644 --- a/python/openshell/__init__.py +++ b/python/openshell/__init__.py @@ -21,6 +21,7 @@ SandboxStatusRef, SandboxTemplateClient, SandboxWorkloadTemplateProvenanceRef, + ServiceExposure, TlsConfig, WorkspaceClient, WorkspaceRef, @@ -52,6 +53,7 @@ "SandboxStatusRef", "SandboxTemplateClient", "SandboxWorkloadTemplateProvenanceRef", + "ServiceExposure", "TlsConfig", "WorkspaceClient", "WorkspaceRef", diff --git a/python/openshell/sandbox.py b/python/openshell/sandbox.py index ec309d87a2..e304038493 100644 --- a/python/openshell/sandbox.py +++ b/python/openshell/sandbox.py @@ -80,6 +80,18 @@ def _all_workspaces_scope() -> datamodel_pb2.WorkspaceSelector: return datamodel_pb2.WorkspaceSelector(all_workspaces=datamodel_pb2.AllWorkspaces()) +def _service_exposure_messages( + exposures: Sequence[ServiceExposure] | None, +) -> list[openshell_pb2.SandboxServiceExposure]: + return [ + openshell_pb2.SandboxServiceExposure( + service=exposure.service, + target_port=exposure.target_port, + ) + for exposure in exposures or () + ] + + class _ClientCallDetails(_ClientCallDetailsBase, grpc.ClientCallDetails): pass @@ -410,6 +422,14 @@ class SandboxStatusRef: exit_code: int | None = None +@dataclass(frozen=True) +class ServiceExposure: + """A loopback HTTP service to expose during sandbox creation.""" + + target_port: int + service: str = "" + + class _ImmutableLabels(dict[str, str]): """A read-only, copy- and pickle-safe label mapping.""" @@ -451,9 +471,14 @@ class SandboxRef: # immutable mapping remains safe for deepcopy, pickle, and asdict. labels: Mapping[str, str] = field(default_factory=_ImmutableLabels, compare=False) created_from_workload_template: SandboxWorkloadTemplateProvenanceRef | None = None + # Populated by create operations. The empty key identifies the unnamed service. + service_urls: Mapping[str, str] = field( + default_factory=_ImmutableLabels, compare=False + ) def __post_init__(self) -> None: object.__setattr__(self, "labels", _ImmutableLabels(self.labels)) + object.__setattr__(self, "service_urls", _ImmutableLabels(self.service_urls)) @property def phase(self) -> int: @@ -764,6 +789,7 @@ def create( spec: openshell_pb2.SandboxSpec | None = None, name: str | None = None, labels: Mapping[str, str] | None = None, + service_exposures: Sequence[ServiceExposure] | None = None, ) -> SandboxRef: request_spec = spec if spec is not None else _default_spec() response = self._stub.CreateSandbox( @@ -772,10 +798,11 @@ def create( spec=request_spec, name=name or "", labels=dict(labels) if labels else {}, + service_exposures=_service_exposure_messages(service_exposures), ), timeout=self._timeout, ) - sandbox_ref = _sandbox_ref(response.sandbox) + sandbox_ref = _sandbox_ref(response.sandbox, response.service_urls) if sandbox_ref.id == "": raise SandboxError("CreateSandbox returned empty sandbox id") return sandbox_ref @@ -788,6 +815,7 @@ def create_from_template( spec: openshell_pb2.SandboxSpec | None = None, name: str | None = None, labels: Mapping[str, str] | None = None, + service_exposures: Sequence[ServiceExposure] | None = None, ) -> SandboxRef: if not workload_template.strip(): raise SandboxError("workload_template is required") @@ -799,10 +827,11 @@ def create_from_template( name=name or "", labels=dict(labels) if labels else {}, workload_template=workload_template, + service_exposures=_service_exposure_messages(service_exposures), ), timeout=self._timeout, ) - sandbox_ref = _sandbox_ref(response.sandbox) + sandbox_ref = _sandbox_ref(response.sandbox, response.service_urls) if sandbox_ref.id == "": raise SandboxError("CreateSandbox returned empty sandbox id") return sandbox_ref @@ -814,9 +843,17 @@ def create_session( spec: openshell_pb2.SandboxSpec | None = None, name: str | None = None, labels: Mapping[str, str] | None = None, + service_exposures: Sequence[ServiceExposure] | None = None, ) -> SandboxSession: return SandboxSession( - self, self.create(workspace=workspace, spec=spec, name=name, labels=labels) + self, + self.create( + workspace=workspace, + spec=spec, + name=name, + labels=labels, + service_exposures=service_exposures, + ), ) def create_session_from_template( @@ -827,6 +864,7 @@ def create_session_from_template( spec: openshell_pb2.SandboxSpec | None = None, name: str | None = None, labels: Mapping[str, str] | None = None, + service_exposures: Sequence[ServiceExposure] | None = None, ) -> SandboxSession: return SandboxSession( self, @@ -836,6 +874,7 @@ def create_session_from_template( spec=spec, name=name, labels=labels, + service_exposures=service_exposures, ), ) @@ -1677,7 +1716,10 @@ def _serialize_python_callable( return base64.b64encode(payload).decode("ascii") -def _sandbox_ref(sandbox: openshell_pb2.Sandbox) -> SandboxRef: +def _sandbox_ref( + sandbox: openshell_pb2.Sandbox, + service_urls: Mapping[str, str] | None = None, +) -> SandboxRef: status = sandbox.status if sandbox.HasField("status") else None provenance = ( SandboxWorkloadTemplateProvenanceRef( @@ -1700,6 +1742,7 @@ def _sandbox_ref(sandbox: openshell_pb2.Sandbox) -> SandboxRef: ), labels=sandbox.metadata.labels if sandbox.metadata else {}, created_from_workload_template=provenance, + service_urls=service_urls or {}, ) diff --git a/python/openshell/sandbox_test.py b/python/openshell/sandbox_test.py index d06c2c2ce9..2980450b7b 100644 --- a/python/openshell/sandbox_test.py +++ b/python/openshell/sandbox_test.py @@ -32,6 +32,7 @@ SandboxRef, SandboxStatusRef, SandboxTemplateClient, + ServiceExposure, TlsConfig, _atomic_replace, _BearerAuthInterceptor, @@ -2073,7 +2074,11 @@ def CreateSandbox( request.name or "generated", dict(request.labels), workspace=_request_workspace(request) or "default", - ) + ), + service_urls={ + exposure.service: f"https://{exposure.service}.example.test/" + for exposure in request.service_exposures + }, ) def ListSandboxes( @@ -2205,6 +2210,30 @@ def test_create_forwards_name_and_labels() -> None: assert dict(ref.labels) == {"aiq": "deep-research"} +def test_create_forwards_service_exposures() -> None: + stub = _FakeSandboxStub() + client = _client_with_fake_stub(stub) + + ref = client.create( + workspace="default", + name="app-server", + service_exposures=[ + ServiceExposure(target_port=4500), + ServiceExposure(service="metrics", target_port=9090), + ], + ) + + assert stub.create_request is not None + assert [ + (exposure.service, exposure.target_port) + for exposure in stub.create_request.service_exposures + ] == [("", 4500), ("metrics", 9090)] + assert dict(ref.service_urls) == { + "": "https://.example.test/", + "metrics": "https://metrics.example.test/", + } + + def test_create_from_template_forwards_workload_template() -> None: stub = _FakeSandboxStub() client = _client_with_fake_stub(stub) diff --git a/sdk/go/docs/src/api/sandboxes.md b/sdk/go/docs/src/api/sandboxes.md index f9504227c2..6981adf65d 100644 --- a/sdk/go/docs/src/api/sandboxes.md +++ b/sdk/go/docs/src/api/sandboxes.md @@ -17,9 +17,18 @@ sb, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSp Providers: []string{"openai"}, }, map[string]string{ "team": "platform", -}) +}, v1.CreateOptions{ServiceExposures: []v1.ServiceExposure{ + {TargetPort: 8080}, +}}) +fmt.Println(sb.ServiceURLs[""]) ``` +Create-time service exposures register loopback HTTP endpoints with the +sandbox. Leave `Service` empty for the unnamed endpoint or set it to create a +named endpoint. Routing begins when the sandbox is ready. +`ServiceURLs` returns the routed URLs keyed by service name; the empty key is +the unnamed endpoint. + Set `GPU: true` to request the active driver's default GPU assignment. Set `GPUCount` when the sandbox needs a specific GPU count; a non-nil `GPUCount` also implies `GPU`. diff --git a/sdk/go/openshell/v1/sandbox_client.go b/sdk/go/openshell/v1/sandbox_client.go index 8fce5a0df0..2d22fd62fe 100644 --- a/sdk/go/openshell/v1/sandbox_client.go +++ b/sdk/go/openshell/v1/sandbox_client.go @@ -41,12 +41,17 @@ func (s *sandboxClient) Create(ctx context.Context, workspace, name string, spec } if len(opts) > 0 { req.Annotations = converter.CopyStringMap(opts[0].Annotations) + req.ServiceExposures = serviceExposuresToProto(opts[0].ServiceExposures) } resp, err := s.client.CreateSandbox(ctx, req) if err != nil { return nil, converter.FromGRPCError(err) } - return converter.SandboxFromProto(resp.GetSandbox()), nil + sandbox := converter.SandboxFromProto(resp.GetSandbox()) + if sandbox != nil { + sandbox.ServiceURLs = converter.CopyStringMap(resp.GetServiceUrls()) + } + return sandbox, nil } func (s *sandboxClient) CreateFromTemplate(ctx context.Context, workspace, name, templateName string, spec *SandboxSpec, labels map[string]string, opts ...CreateOptions) (*Sandbox, error) { @@ -69,12 +74,28 @@ func (s *sandboxClient) CreateFromTemplate(ctx context.Context, workspace, name, } if len(opts) > 0 { req.Annotations = converter.CopyStringMap(opts[0].Annotations) + req.ServiceExposures = serviceExposuresToProto(opts[0].ServiceExposures) } resp, err := s.client.CreateSandbox(ctx, req) if err != nil { return nil, converter.FromGRPCError(err) } - return converter.SandboxFromProto(resp.GetSandbox()), nil + sandbox := converter.SandboxFromProto(resp.GetSandbox()) + if sandbox != nil { + sandbox.ServiceURLs = converter.CopyStringMap(resp.GetServiceUrls()) + } + return sandbox, nil +} + +func serviceExposuresToProto(exposures []types.ServiceExposure) []*pb.SandboxServiceExposure { + result := make([]*pb.SandboxServiceExposure, 0, len(exposures)) + for _, exposure := range exposures { + result = append(result, &pb.SandboxServiceExposure{ + Service: exposure.Service, + TargetPort: exposure.TargetPort, + }) + } + return result } func validateTemplateCreateSpec(spec *SandboxSpec) error { diff --git a/sdk/go/openshell/v1/sandbox_client_test.go b/sdk/go/openshell/v1/sandbox_client_test.go index b9a63ceecf..c7c2421e0a 100644 --- a/sdk/go/openshell/v1/sandbox_client_test.go +++ b/sdk/go/openshell/v1/sandbox_client_test.go @@ -78,7 +78,11 @@ func (s *mockSandboxServer) CreateSandbox(_ context.Context, req *pb.CreateSandb Status: &pb.SandboxStatus{Phase: pb.SandboxPhase_SANDBOX_PHASE_PROVISIONING}, } s.sandboxes[req.GetName()] = sb - return &pb.SandboxResponse{Sandbox: sb}, nil + serviceURLs := make(map[string]string, len(req.GetServiceExposures())) + for _, exposure := range req.GetServiceExposures() { + serviceURLs[exposure.GetService()] = "https://" + exposure.GetService() + ".example.test/" + } + return &pb.SandboxResponse{Sandbox: sb, ServiceUrls: serviceURLs}, nil } func (s *mockSandboxServer) GetSandbox(_ context.Context, req *pb.GetSandboxRequest) (*pb.SandboxResponse, error) { @@ -287,7 +291,17 @@ func TestSandboxCreate(t *testing.T) { } labels := map[string]string{"env": "dev"} - result, err := client.Create(context.Background(), "default", "my-sandbox", spec, labels) + result, err := client.Create( + context.Background(), + "default", + "my-sandbox", + spec, + labels, + CreateOptions{ServiceExposures: []ServiceExposure{ + {TargetPort: 4500}, + {Service: "metrics", TargetPort: 9090}, + }}, + ) require.NoError(t, err) require.NotNil(t, result) @@ -295,6 +309,13 @@ func TestSandboxCreate(t *testing.T) { assert.Equal(t, "sb-my-sandbox", result.ID) assert.Equal(t, map[string]string{"env": "dev"}, result.Labels) assert.Equal(t, SandboxProvisioning, result.Status.Phase) + assert.Equal(t, map[string]string{ + "": "https://.example.test/", + "metrics": "https://metrics.example.test/", + }, result.ServiceURLs) + require.Len(t, mock.createRequest.GetServiceExposures(), 2) + assert.Equal(t, uint32(4500), mock.createRequest.GetServiceExposures()[0].GetTargetPort()) + assert.Equal(t, "metrics", mock.createRequest.GetServiceExposures()[1].GetService()) } func TestSandboxCreate_DefaultGPURequest(t *testing.T) { diff --git a/sdk/go/openshell/v1/service.go b/sdk/go/openshell/v1/service.go index 3d53069afa..3ef3bea2fc 100644 --- a/sdk/go/openshell/v1/service.go +++ b/sdk/go/openshell/v1/service.go @@ -12,6 +12,9 @@ import ( // ServiceEndpoint represents an exposed HTTP service endpoint within a sandbox. type ServiceEndpoint = types.ServiceEndpoint +// ServiceExposure describes a loopback HTTP service to expose during sandbox creation. +type ServiceExposure = types.ServiceExposure + // ServiceInterface defines operations for managing sandbox service endpoints. type ServiceInterface interface { Expose(ctx context.Context, workspace, sandboxName, serviceName string, targetPort uint32, domain bool) (*ServiceEndpoint, error) diff --git a/sdk/go/openshell/v1/types/options.go b/sdk/go/openshell/v1/types/options.go index def5ca0fff..d514b8d125 100644 --- a/sdk/go/openshell/v1/types/options.go +++ b/sdk/go/openshell/v1/types/options.go @@ -8,6 +8,8 @@ import "time" // CreateOptions configures resource creation. type CreateOptions struct { Annotations map[string]string + // ServiceExposures are loopback HTTP services registered with the sandbox. + ServiceExposures []ServiceExposure } // ListOptions configures resource listing with pagination and filtering. diff --git a/sdk/go/openshell/v1/types/sandbox.go b/sdk/go/openshell/v1/types/sandbox.go index 4b945a4ec2..1218bb6224 100644 --- a/sdk/go/openshell/v1/types/sandbox.go +++ b/sdk/go/openshell/v1/types/sandbox.go @@ -18,6 +18,9 @@ type Sandbox struct { CreatedFromWorkloadTemplate *SandboxWorkloadTemplateProvenance Spec SandboxSpec Status SandboxStatus + // ServiceURLs is populated by create operations and keyed by service name. + // The empty key identifies the unnamed service. + ServiceURLs map[string]string } // SandboxSpec holds the desired state of a sandbox. diff --git a/sdk/go/openshell/v1/types/service.go b/sdk/go/openshell/v1/types/service.go index 37b6ddf2fb..9cccff8258 100644 --- a/sdk/go/openshell/v1/types/service.go +++ b/sdk/go/openshell/v1/types/service.go @@ -3,6 +3,12 @@ package types +// ServiceExposure describes a loopback HTTP service to expose during sandbox creation. +type ServiceExposure struct { + Service string + TargetPort uint32 +} + // ServiceEndpoint represents an exposed HTTP service on a sandbox. type ServiceEndpoint struct { ID string diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index aab21beff6..4fafa9c31a 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -3142,9 +3142,13 @@ type CreateSandboxRequest struct { WorkloadTemplate string `protobuf:"bytes,6,opt,name=workload_template,json=workloadTemplate,proto3" json:"workload_template,omitempty"` // Optional nonzero UUID for durable at-most-once admission. Successful results // can be replayed for 24 hours; see the API errors and retries reference. - RequestId string `protobuf:"bytes,8,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + RequestId string `protobuf:"bytes,8,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // HTTP services to expose when the sandbox is created. Endpoints are + // registered after the sandbox has been persisted and route only while the + // sandbox is ready. + ServiceExposures []*SandboxServiceExposure `protobuf:"bytes,9,rep,name=service_exposures,json=serviceExposures,proto3" json:"service_exposures,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateSandboxRequest) Reset() { @@ -3233,6 +3237,13 @@ func (x *CreateSandboxRequest) GetRequestId() string { return "" } +func (x *CreateSandboxRequest) GetServiceExposures() []*SandboxServiceExposure { + if x != nil { + return x.ServiceExposures + } + return nil +} + type CreateSandboxTemplateRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Workspace scope. Only a named workspace selection is accepted. @@ -4335,8 +4346,11 @@ func (x *StartSandboxRequest) GetRequestId() string { // Sandbox response. type SandboxResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Service URLs created by CreateSandbox, keyed by service name. The empty + // key identifies the unnamed service. Other sandbox RPCs return an empty map. + ServiceUrls map[string]string `protobuf:"bytes,2,rep,name=service_urls,json=serviceUrls,proto3" json:"service_urls,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4378,6 +4392,13 @@ func (x *SandboxResponse) GetSandbox() *Sandbox { return nil } +func (x *SandboxResponse) GetServiceUrls() map[string]string { + if x != nil { + return x.ServiceUrls + } + return nil +} + // List sandboxes response. type ListSandboxesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -17072,6 +17093,61 @@ func (x *SandboxProvisioning) GetAttachmentChangeTime() *timestamppb.Timestamp { return nil } +// Create-time request to expose one loopback HTTP service in a sandbox. +type SandboxServiceExposure struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Service name within the sandbox. Empty selects the unnamed endpoint. + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + // Loopback TCP port inside the sandbox. + TargetPort uint32 `protobuf:"varint,2,opt,name=target_port,json=targetPort,proto3" json:"target_port,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxServiceExposure) Reset() { + *x = SandboxServiceExposure{} + mi := &file_openshell_proto_msgTypes[227] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxServiceExposure) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxServiceExposure) ProtoMessage() {} + +func (x *SandboxServiceExposure) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[227] + 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 SandboxServiceExposure.ProtoReflect.Descriptor instead. +func (*SandboxServiceExposure) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{227} +} + +func (x *SandboxServiceExposure) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *SandboxServiceExposure) GetTargetPort() uint32 { + if x != nil { + return x.TargetPort + } + return 0 +} + var File_openshell_proto protoreflect.FileDescriptor const file_openshell_proto_rawDesc = "" + @@ -17236,7 +17312,7 @@ const file_openshell_proto_rawDesc = "" + "\bmetadata\x18\x06 \x03(\v2).openshell.v1.PlatformEvent.MetadataEntryR\bmetadata\x1a;\n" + "\rMetadataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x01\x10\x02R\ftimestamp_ms\"\xd6\x04\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x01\x10\x02R\ftimestamp_ms\"\xa9\x05\n" + "\x14CreateSandboxRequest\x12R\n" + "\x0fworkspace_scope\x18\a \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12-\n" + "\x04spec\x18\x01 \x01(\v2\x19.openshell.v1.SandboxSpecR\x04spec\x12\x12\n" + @@ -17246,7 +17322,8 @@ const file_openshell_proto_rawDesc = "" + "\x1dawait_main_process_attachment\x18\x05 \x01(\bR\x1aawaitMainProcessAttachment\x12+\n" + "\x11workload_template\x18\x06 \x01(\tR\x10workloadTemplate\x12\x1d\n" + "\n" + - "request_id\x18\b \x01(\tR\trequestId\x1a9\n" + + "request_id\x18\b \x01(\tR\trequestId\x12Q\n" + + "\x11service_exposures\x18\t \x03(\v2$.openshell.v1.SandboxServiceExposureR\x10serviceExposures\x1a9\n" + "\vLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + @@ -17332,9 +17409,13 @@ const file_openshell_proto_rawDesc = "" + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + "\n" + - "request_id\x18\x03 \x01(\tR\trequestId\"B\n" + + "request_id\x18\x03 \x01(\tR\trequestId\"\xd5\x01\n" + "\x0fSandboxResponse\x12/\n" + - "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\"t\n" + + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\x12Q\n" + + "\fservice_urls\x18\x02 \x03(\v2..openshell.v1.SandboxResponse.ServiceUrlsEntryR\vserviceUrls\x1a>\n" + + "\x10ServiceUrlsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"t\n" + "\x15ListSandboxesResponse\x123\n" + "\tsandboxes\x18\x01 \x03(\v2\x15.openshell.v1.SandboxR\tsandboxes\x12&\n" + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"^\n" + @@ -18351,7 +18432,11 @@ const file_openshell_proto_rawDesc = "" + "\x12cleanup_retry_time\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\x10cleanupRetryTime\x120\n" + "\x14attachment_change_id\x18\n" + " \x01(\tR\x12attachmentChangeId\x12P\n" + - "\x16attachment_change_time\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\x14attachmentChangeTime*\xa6\x02\n" + + "\x16attachment_change_time\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\x14attachmentChangeTime\"S\n" + + "\x16SandboxServiceExposure\x12\x18\n" + + "\aservice\x18\x01 \x01(\tR\aservice\x12\x1f\n" + + "\vtarget_port\x18\x02 \x01(\rR\n" + + "targetPort*\xa6\x02\n" + "\fSandboxPhase\x12\x1d\n" + "\x19SANDBOX_PHASE_UNSPECIFIED\x10\x00\x12\x1e\n" + "\x1aSANDBOX_PHASE_PROVISIONING\x10\x01\x12\x17\n" + @@ -18653,7 +18738,7 @@ func file_openshell_proto_rawDescGZIP() []byte { } var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 17) -var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 248) +var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 250) var file_openshell_proto_goTypes = []any{ (SandboxPhase)(0), // 0: openshell.v1.SandboxPhase (ProviderMutationKind)(0), // 1: openshell.v1.ProviderMutationKind @@ -18899,52 +18984,54 @@ var file_openshell_proto_goTypes = []any{ (*ReportEndpointStatusResponse)(nil), // 241: openshell.v1.ReportEndpointStatusResponse (*EndpointStatus)(nil), // 242: openshell.v1.EndpointStatus (*SandboxProvisioning)(nil), // 243: openshell.v1.SandboxProvisioning - nil, // 244: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 245: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 246: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 247: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 248: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry - nil, // 249: openshell.v1.PlatformEvent.MetadataEntry - nil, // 250: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 251: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 252: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 253: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 254: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry - nil, // 255: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 256: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 257: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 258: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry - nil, // 259: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 260: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 261: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 262: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 263: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 264: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*timestamppb.Timestamp)(nil), // 265: google.protobuf.Timestamp - (*datamodelv1.ObjectMeta)(nil), // 266: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 267: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 268: google.protobuf.Struct - (*durationpb.Duration)(nil), // 269: google.protobuf.Duration - (*datamodelv1.WorkspaceSelector)(nil), // 270: openshell.datamodel.v1.WorkspaceSelector - (*datamodelv1.Provider)(nil), // 271: openshell.datamodel.v1.Provider - (sandboxv1.PolicySource)(0), // 272: openshell.sandbox.v1.PolicySource - (*sandboxv1.NetworkEndpoint)(nil), // 273: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 274: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 275: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 276: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 277: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 278: openshell.sandbox.v1.L7Rule - (*datamodelv1.Workspace)(nil), // 279: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 280: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 281: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 282: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 283: openshell.sandbox.v1.GetGatewayConfigResponse + (*SandboxServiceExposure)(nil), // 244: openshell.v1.SandboxServiceExposure + nil, // 245: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 246: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 247: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 248: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 249: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + nil, // 250: openshell.v1.PlatformEvent.MetadataEntry + nil, // 251: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 252: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 253: openshell.v1.SandboxResponse.ServiceUrlsEntry + nil, // 254: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 255: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 256: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + nil, // 257: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 258: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 259: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 260: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + nil, // 261: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 262: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 263: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 264: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 265: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 266: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*timestamppb.Timestamp)(nil), // 267: google.protobuf.Timestamp + (*datamodelv1.ObjectMeta)(nil), // 268: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 269: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 270: google.protobuf.Struct + (*durationpb.Duration)(nil), // 271: google.protobuf.Duration + (*datamodelv1.WorkspaceSelector)(nil), // 272: openshell.datamodel.v1.WorkspaceSelector + (*datamodelv1.Provider)(nil), // 273: openshell.datamodel.v1.Provider + (sandboxv1.PolicySource)(0), // 274: openshell.sandbox.v1.PolicySource + (*sandboxv1.NetworkEndpoint)(nil), // 275: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 276: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 277: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 278: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 279: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 280: openshell.sandbox.v1.L7Rule + (*datamodelv1.Workspace)(nil), // 281: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 282: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 283: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 284: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 285: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 265, // 0: openshell.v1.IssueSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp - 265, // 1: openshell.v1.RefreshSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp + 267, // 0: openshell.v1.IssueSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp + 267, // 1: openshell.v1.RefreshSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp 238, // 2: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential - 265, // 3: openshell.v1.RefreshSandboxTokenResponse.sandbox_expiration_time:type_name -> google.protobuf.Timestamp + 267, // 3: openshell.v1.RefreshSandboxTokenResponse.sandbox_expiration_time:type_name -> google.protobuf.Timestamp 12, // 4: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus 12, // 5: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus 27, // 6: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo @@ -18953,465 +19040,467 @@ var file_openshell_proto_depIdxs = []int32{ 30, // 9: openshell.v1.ResourceCapabilities.cpu:type_name -> openshell.v1.CpuResourceCapabilities 31, // 10: openshell.v1.ResourceCapabilities.memory:type_name -> openshell.v1.MemoryResourceCapabilities 32, // 11: openshell.v1.ResourceCapabilities.gpu:type_name -> openshell.v1.GpuResourceCapabilities - 266, // 12: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 268, // 12: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 34, // 13: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec 45, // 14: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus 44, // 15: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance - 244, // 16: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 245, // 16: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry 37, // 17: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 267, // 18: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 269, // 18: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy 35, // 19: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements 36, // 20: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 245, // 21: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 246, // 22: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 247, // 23: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 268, // 24: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 268, // 25: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 266, // 26: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 246, // 21: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 247, // 22: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 248, // 23: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 270, // 24: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 270, // 25: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 268, // 26: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 39, // 27: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec 40, // 28: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig - 268, // 29: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct + 270, // 29: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct 42, // 30: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel - 248, // 31: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + 249, // 31: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry 41, // 32: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources 36, // 33: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements 43, // 34: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup - 269, // 35: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration + 271, // 35: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration 46, // 36: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition 0, // 37: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase 242, // 38: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus 171, // 39: openshell.v1.SandboxStatus.configuration_admission:type_name -> openshell.v1.SandboxConfigurationAdmission 243, // 40: openshell.v1.SandboxStatus.provisioning:type_name -> openshell.v1.SandboxProvisioning - 265, // 41: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp - 265, // 42: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp - 249, // 43: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 270, // 44: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 267, // 41: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp + 267, // 42: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp + 250, // 43: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 272, // 44: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 34, // 45: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 250, // 46: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 251, // 47: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 270, // 48: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 38, // 49: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 270, // 50: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 51: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 52: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 38, // 53: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 38, // 54: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate - 15, // 55: openshell.v1.DeleteSandboxTemplateResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 270, // 56: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 57: openshell.v1.BeginRootfsTarStagingResponse.expiration_time:type_name -> google.protobuf.Timestamp - 270, // 58: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 59: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 60: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 61: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 62: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 63: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 64: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 65: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 33, // 66: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 33, // 67: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 271, // 68: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 33, // 69: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 75, // 70: openshell.v1.AttachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt - 33, // 71: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 75, // 72: openshell.v1.DetachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt - 73, // 73: openshell.v1.ConfigSnapshotRevision.sandbox_config:type_name -> openshell.v1.SandboxConfigRevision - 71, // 74: openshell.v1.ConfigSnapshotRevision.provider_target:type_name -> openshell.v1.ProviderDesiredIdentity - 272, // 75: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource - 4, // 76: openshell.v1.ConfigUpdateOperation.component:type_name -> openshell.v1.ConfigComponent - 72, // 77: openshell.v1.ConfigUpdateOperation.target_revision:type_name -> openshell.v1.ConfigSnapshotRevision - 6, // 78: openshell.v1.ConfigUpdateOperation.state:type_name -> openshell.v1.ConfigUpdateOperationState - 5, // 79: openshell.v1.ConfigUpdateOperation.outcome:type_name -> openshell.v1.ConfigApplyOutcome - 265, // 80: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp - 265, // 81: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp - 265, // 82: openshell.v1.ConfigUpdateOperation.completed_time:type_name -> google.protobuf.Timestamp - 1, // 83: openshell.v1.ProviderMutationReceipt.kind:type_name -> openshell.v1.ProviderMutationKind - 71, // 84: openshell.v1.ProviderMutationReceipt.desired:type_name -> openshell.v1.ProviderDesiredIdentity - 265, // 85: openshell.v1.ProviderMutationReceipt.persisted_time:type_name -> google.protobuf.Timestamp - 3, // 86: openshell.v1.ProviderReadinessObservation.reason:type_name -> openshell.v1.ProviderReadinessReason - 75, // 87: openshell.v1.ProviderReadinessStatus.receipt:type_name -> openshell.v1.ProviderMutationReceipt - 2, // 88: openshell.v1.ProviderReadinessStatus.state:type_name -> openshell.v1.ProviderReadinessState - 3, // 89: openshell.v1.ProviderReadinessStatus.reason:type_name -> openshell.v1.ProviderReadinessReason - 76, // 90: openshell.v1.ProviderReadinessStatus.observed:type_name -> openshell.v1.ProviderReadinessObservation - 265, // 91: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp - 265, // 92: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp - 74, // 93: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation - 270, // 94: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 77, // 95: openshell.v1.GetSandboxProviderStatusResponse.status:type_name -> openshell.v1.ProviderReadinessStatus - 76, // 96: openshell.v1.ReportProviderReadinessRequest.observation:type_name -> openshell.v1.ProviderReadinessObservation - 269, // 97: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration - 269, // 98: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration - 15, // 99: openshell.v1.DeleteSandboxResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 270, // 100: openshell.v1.CreateSshSessionRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 101: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp - 270, // 102: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 103: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 104: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 92, // 105: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 270, // 106: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 15, // 107: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 266, // 108: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 91, // 109: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 15, // 110: openshell.v1.RevokeSshSessionResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 270, // 111: openshell.v1.ExecSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 252, // 112: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 269, // 113: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration - 96, // 114: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 97, // 115: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 98, // 116: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 191, // 117: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 192, // 118: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 100, // 119: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 95, // 120: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 103, // 121: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 266, // 122: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 265, // 123: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp - 270, // 124: openshell.v1.WatchSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 125: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp - 33, // 126: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 107, // 127: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 47, // 128: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 108, // 129: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 202, // 130: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 265, // 131: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp - 253, // 132: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 270, // 133: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 271, // 134: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 270, // 135: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 136: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 137: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 271, // 138: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 254, // 139: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry - 270, // 140: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 271, // 141: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 75, // 142: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt - 271, // 143: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 270, // 144: openshell.v1.ListProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 145: openshell.v1.GetProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 137, // 146: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 269, // 147: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration - 120, // 148: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride - 7, // 149: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType - 121, // 150: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken - 126, // 151: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 122, // 152: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant - 8, // 153: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 269, // 154: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration - 269, // 155: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration - 124, // 156: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 125, // 157: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput - 8, // 158: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 265, // 159: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp - 265, // 160: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp - 265, // 161: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp - 14, // 162: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 265, // 163: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp - 270, // 164: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 127, // 165: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 270, // 166: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 8, // 167: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 255, // 168: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 265, // 169: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp - 127, // 170: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 270, // 171: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 127, // 172: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 270, // 173: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 15, // 174: openshell.v1.DeleteProviderRefreshResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 9, // 175: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 123, // 176: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 273, // 177: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 274, // 178: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 128, // 179: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 256, // 180: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 137, // 181: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 137, // 182: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 270, // 183: openshell.v1.ImportProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 118, // 184: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 119, // 185: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 137, // 186: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 270, // 187: openshell.v1.UpdateProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 118, // 188: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 119, // 189: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 137, // 190: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 270, // 191: openshell.v1.LintProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 118, // 192: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 119, // 193: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 15, // 194: openshell.v1.DeleteProviderResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 270, // 195: openshell.v1.DeleteProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 15, // 196: openshell.v1.DeleteProviderProfileResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 150, // 197: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 257, // 198: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 258, // 199: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry - 259, // 200: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 260, // 201: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 3, // 202: openshell.v1.GetSandboxProviderEnvironmentResponse.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason - 269, // 203: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration - 270, // 204: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 267, // 205: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 275, // 206: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 156, // 207: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 261, // 208: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 157, // 209: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 158, // 210: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 159, // 211: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 161, // 212: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 162, // 213: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 163, // 214: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 276, // 215: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 274, // 216: openshell.v1.L7RuleTarget.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 277, // 217: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 160, // 218: openshell.v1.AddDenyRules.target:type_name -> openshell.v1.L7RuleTarget - 278, // 219: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 160, // 220: openshell.v1.AddAllowRules.target:type_name -> openshell.v1.L7RuleTarget - 262, // 221: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 270, // 222: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 174, // 223: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 270, // 224: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 174, // 225: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 11, // 226: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 10, // 227: openshell.v1.SandboxConfigurationAdmission.state:type_name -> openshell.v1.ConfigurationAdmissionState - 171, // 228: openshell.v1.ReportSandboxConfigurationRequest.admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 11, // 229: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 265, // 230: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp - 265, // 231: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp - 267, // 232: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 263, // 233: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 270, // 234: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 235: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp - 107, // 236: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 107, // 237: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 181, // 238: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 184, // 239: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 195, // 240: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 196, // 241: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 182, // 242: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 183, // 243: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 185, // 244: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 190, // 245: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 196, // 246: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 269, // 247: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration - 191, // 248: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 192, // 249: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 193, // 250: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 265, // 251: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp - 265, // 252: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp - 197, // 253: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 199, // 254: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 276, // 255: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 265, // 256: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp - 265, // 257: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp - 265, // 258: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp - 265, // 259: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp - 267, // 260: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 267, // 261: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 270, // 262: openshell.v1.SubmitPolicyAnalysisRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 198, // 263: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 201, // 264: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 200, // 265: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 270, // 266: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 201, // 267: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 265, // 268: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp - 270, // 269: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 270: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 271: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 211, // 272: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 270, // 273: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 276, // 274: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 270, // 275: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 276: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 270, // 277: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 265, // 278: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp - 221, // 279: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 264, // 280: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 279, // 281: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 279, // 282: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 279, // 283: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 15, // 284: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 266, // 285: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 13, // 286: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 270, // 287: openshell.v1.AddWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 13, // 288: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 231, // 289: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 270, // 290: openshell.v1.RemoveWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 15, // 291: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 270, // 292: openshell.v1.ListWorkspaceMembersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 231, // 293: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 265, // 294: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp - 16, // 295: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult - 239, // 296: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation - 16, // 297: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult - 265, // 298: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp - 265, // 299: openshell.v1.SandboxProvisioning.configuration_change_time:type_name -> google.protobuf.Timestamp - 265, // 300: openshell.v1.SandboxProvisioning.first_rejection_time:type_name -> google.protobuf.Timestamp - 265, // 301: openshell.v1.SandboxProvisioning.deadline:type_name -> google.protobuf.Timestamp - 265, // 302: openshell.v1.SandboxProvisioning.timeout_time:type_name -> google.protobuf.Timestamp - 265, // 303: openshell.v1.SandboxProvisioning.cleanup_completed_time:type_name -> google.protobuf.Timestamp - 265, // 304: openshell.v1.SandboxProvisioning.cleanup_retry_time:type_name -> google.protobuf.Timestamp - 265, // 305: openshell.v1.SandboxProvisioning.attachment_change_time:type_name -> google.protobuf.Timestamp - 265, // 306: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 265, // 307: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 123, // 308: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 151, // 309: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 21, // 310: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 23, // 311: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 25, // 312: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 48, // 313: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 56, // 314: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest - 58, // 315: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 59, // 316: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 49, // 317: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest - 50, // 318: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest - 51, // 319: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest - 52, // 320: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest - 60, // 321: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 61, // 322: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 62, // 323: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 78, // 324: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest - 63, // 325: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 64, // 326: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 65, // 327: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 83, // 328: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 85, // 329: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 86, // 330: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 87, // 331: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 89, // 332: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 93, // 333: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 95, // 334: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 101, // 335: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 102, // 336: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 109, // 337: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 110, // 338: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 111, // 339: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 116, // 340: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 117, // 341: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 140, // 342: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 142, // 343: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 144, // 344: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 112, // 345: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 129, // 346: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 131, // 347: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 133, // 348: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 135, // 349: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 113, // 350: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 147, // 351: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 280, // 352: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 281, // 353: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 155, // 354: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 165, // 355: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 167, // 356: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 169, // 357: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 240, // 358: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest - 80, // 359: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest - 172, // 360: openshell.v1.OpenShell.ReportSandboxConfiguration:input_type -> openshell.v1.ReportSandboxConfigurationRequest - 149, // 361: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 153, // 362: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 175, // 363: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 176, // 364: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 179, // 365: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 186, // 366: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 188, // 367: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest - 194, // 368: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 105, // 369: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 203, // 370: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 205, // 371: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 207, // 372: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 209, // 373: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 212, // 374: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 214, // 375: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 216, // 376: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 218, // 377: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 220, // 378: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 17, // 379: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 19, // 380: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 223, // 381: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 225, // 382: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 227, // 383: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 229, // 384: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 232, // 385: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 234, // 386: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 236, // 387: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 22, // 388: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 24, // 389: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 26, // 390: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 66, // 391: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 57, // 392: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse - 66, // 393: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 67, // 394: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 53, // 395: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 53, // 396: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 54, // 397: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse - 55, // 398: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse - 68, // 399: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 69, // 400: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 70, // 401: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 79, // 402: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse - 82, // 403: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 66, // 404: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 66, // 405: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 84, // 406: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 92, // 407: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 92, // 408: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 88, // 409: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 90, // 410: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 94, // 411: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 99, // 412: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 101, // 413: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 99, // 414: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 114, // 415: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 114, // 416: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 115, // 417: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 139, // 418: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 138, // 419: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 141, // 420: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 143, // 421: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 145, // 422: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 114, // 423: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 130, // 424: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 132, // 425: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 134, // 426: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 136, // 427: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 146, // 428: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 148, // 429: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 282, // 430: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 283, // 431: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 164, // 432: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 166, // 433: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 168, // 434: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 170, // 435: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 241, // 436: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse - 81, // 437: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse - 173, // 438: openshell.v1.OpenShell.ReportSandboxConfiguration:output_type -> openshell.v1.ReportSandboxConfigurationResponse - 152, // 439: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 154, // 440: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 178, // 441: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 177, // 442: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 180, // 443: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 187, // 444: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 189, // 445: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse - 194, // 446: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 106, // 447: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 204, // 448: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 206, // 449: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 208, // 450: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 210, // 451: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 213, // 452: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 215, // 453: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 217, // 454: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 219, // 455: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 222, // 456: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 18, // 457: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 20, // 458: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 224, // 459: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 226, // 460: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 228, // 461: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 230, // 462: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 233, // 463: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 235, // 464: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 237, // 465: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 388, // [388:466] is the sub-list for method output_type - 310, // [310:388] is the sub-list for method input_type - 310, // [310:310] is the sub-list for extension type_name - 310, // [310:310] is the sub-list for extension extendee - 0, // [0:310] is the sub-list for field type_name + 251, // 46: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 252, // 47: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 244, // 48: openshell.v1.CreateSandboxRequest.service_exposures:type_name -> openshell.v1.SandboxServiceExposure + 272, // 49: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 38, // 50: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 272, // 51: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 52: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 53: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 38, // 54: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 38, // 55: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate + 15, // 56: openshell.v1.DeleteSandboxTemplateResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 272, // 57: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 267, // 58: openshell.v1.BeginRootfsTarStagingResponse.expiration_time:type_name -> google.protobuf.Timestamp + 272, // 59: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 60: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 61: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 62: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 63: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 64: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 65: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 66: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 33, // 67: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 253, // 68: openshell.v1.SandboxResponse.service_urls:type_name -> openshell.v1.SandboxResponse.ServiceUrlsEntry + 33, // 69: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 273, // 70: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 33, // 71: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 75, // 72: openshell.v1.AttachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 33, // 73: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 75, // 74: openshell.v1.DetachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 73, // 75: openshell.v1.ConfigSnapshotRevision.sandbox_config:type_name -> openshell.v1.SandboxConfigRevision + 71, // 76: openshell.v1.ConfigSnapshotRevision.provider_target:type_name -> openshell.v1.ProviderDesiredIdentity + 274, // 77: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource + 4, // 78: openshell.v1.ConfigUpdateOperation.component:type_name -> openshell.v1.ConfigComponent + 72, // 79: openshell.v1.ConfigUpdateOperation.target_revision:type_name -> openshell.v1.ConfigSnapshotRevision + 6, // 80: openshell.v1.ConfigUpdateOperation.state:type_name -> openshell.v1.ConfigUpdateOperationState + 5, // 81: openshell.v1.ConfigUpdateOperation.outcome:type_name -> openshell.v1.ConfigApplyOutcome + 267, // 82: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp + 267, // 83: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp + 267, // 84: openshell.v1.ConfigUpdateOperation.completed_time:type_name -> google.protobuf.Timestamp + 1, // 85: openshell.v1.ProviderMutationReceipt.kind:type_name -> openshell.v1.ProviderMutationKind + 71, // 86: openshell.v1.ProviderMutationReceipt.desired:type_name -> openshell.v1.ProviderDesiredIdentity + 267, // 87: openshell.v1.ProviderMutationReceipt.persisted_time:type_name -> google.protobuf.Timestamp + 3, // 88: openshell.v1.ProviderReadinessObservation.reason:type_name -> openshell.v1.ProviderReadinessReason + 75, // 89: openshell.v1.ProviderReadinessStatus.receipt:type_name -> openshell.v1.ProviderMutationReceipt + 2, // 90: openshell.v1.ProviderReadinessStatus.state:type_name -> openshell.v1.ProviderReadinessState + 3, // 91: openshell.v1.ProviderReadinessStatus.reason:type_name -> openshell.v1.ProviderReadinessReason + 76, // 92: openshell.v1.ProviderReadinessStatus.observed:type_name -> openshell.v1.ProviderReadinessObservation + 267, // 93: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp + 267, // 94: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp + 74, // 95: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation + 272, // 96: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 77, // 97: openshell.v1.GetSandboxProviderStatusResponse.status:type_name -> openshell.v1.ProviderReadinessStatus + 76, // 98: openshell.v1.ReportProviderReadinessRequest.observation:type_name -> openshell.v1.ProviderReadinessObservation + 271, // 99: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration + 271, // 100: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration + 15, // 101: openshell.v1.DeleteSandboxResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 272, // 102: openshell.v1.CreateSshSessionRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 267, // 103: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp + 272, // 104: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 105: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 106: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 92, // 107: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 272, // 108: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 15, // 109: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 268, // 110: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 91, // 111: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 15, // 112: openshell.v1.RevokeSshSessionResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 272, // 113: openshell.v1.ExecSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 254, // 114: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 271, // 115: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration + 96, // 116: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 97, // 117: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 98, // 118: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 191, // 119: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 192, // 120: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 100, // 121: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 95, // 122: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 103, // 123: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 268, // 124: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 267, // 125: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp + 272, // 126: openshell.v1.WatchSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 267, // 127: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp + 33, // 128: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 107, // 129: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 47, // 130: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 108, // 131: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 202, // 132: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 267, // 133: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp + 255, // 134: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 272, // 135: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 273, // 136: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 272, // 137: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 138: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 139: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 273, // 140: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 256, // 141: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + 272, // 142: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 273, // 143: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 75, // 144: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt + 273, // 145: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 272, // 146: openshell.v1.ListProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 147: openshell.v1.GetProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 137, // 148: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 271, // 149: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration + 120, // 150: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 7, // 151: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType + 121, // 152: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 126, // 153: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 122, // 154: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 8, // 155: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 271, // 156: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration + 271, // 157: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration + 124, // 158: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 125, // 159: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 8, // 160: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 267, // 161: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp + 267, // 162: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp + 267, // 163: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp + 14, // 164: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 267, // 165: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp + 272, // 166: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 127, // 167: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 272, // 168: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 8, // 169: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 257, // 170: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 267, // 171: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp + 127, // 172: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 272, // 173: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 127, // 174: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 272, // 175: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 15, // 176: openshell.v1.DeleteProviderRefreshResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 9, // 177: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 123, // 178: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 275, // 179: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 276, // 180: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 128, // 181: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 258, // 182: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 137, // 183: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 137, // 184: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 272, // 185: openshell.v1.ImportProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 118, // 186: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 119, // 187: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 137, // 188: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 272, // 189: openshell.v1.UpdateProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 118, // 190: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 119, // 191: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 137, // 192: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 272, // 193: openshell.v1.LintProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 118, // 194: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 119, // 195: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 15, // 196: openshell.v1.DeleteProviderResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 272, // 197: openshell.v1.DeleteProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 15, // 198: openshell.v1.DeleteProviderProfileResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 150, // 199: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 259, // 200: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 260, // 201: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + 261, // 202: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 262, // 203: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 3, // 204: openshell.v1.GetSandboxProviderEnvironmentResponse.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason + 271, // 205: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration + 272, // 206: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 269, // 207: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 277, // 208: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 156, // 209: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 263, // 210: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 157, // 211: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 158, // 212: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 159, // 213: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 161, // 214: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 162, // 215: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 163, // 216: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 278, // 217: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 276, // 218: openshell.v1.L7RuleTarget.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 279, // 219: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 160, // 220: openshell.v1.AddDenyRules.target:type_name -> openshell.v1.L7RuleTarget + 280, // 221: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 160, // 222: openshell.v1.AddAllowRules.target:type_name -> openshell.v1.L7RuleTarget + 264, // 223: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 272, // 224: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 174, // 225: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 272, // 226: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 174, // 227: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 11, // 228: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 10, // 229: openshell.v1.SandboxConfigurationAdmission.state:type_name -> openshell.v1.ConfigurationAdmissionState + 171, // 230: openshell.v1.ReportSandboxConfigurationRequest.admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 11, // 231: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 267, // 232: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp + 267, // 233: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp + 269, // 234: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 265, // 235: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 272, // 236: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 267, // 237: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp + 107, // 238: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 107, // 239: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 181, // 240: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 184, // 241: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 195, // 242: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 196, // 243: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 182, // 244: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 183, // 245: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 185, // 246: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 190, // 247: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 196, // 248: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 271, // 249: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration + 191, // 250: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 192, // 251: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 193, // 252: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 267, // 253: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp + 267, // 254: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp + 197, // 255: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 199, // 256: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 278, // 257: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 267, // 258: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp + 267, // 259: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp + 267, // 260: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp + 267, // 261: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp + 269, // 262: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 269, // 263: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 272, // 264: openshell.v1.SubmitPolicyAnalysisRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 198, // 265: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 201, // 266: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 200, // 267: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 272, // 268: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 201, // 269: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 267, // 270: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp + 272, // 271: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 272: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 273: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 211, // 274: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 272, // 275: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 278, // 276: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 272, // 277: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 278: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 272, // 279: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 267, // 280: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp + 221, // 281: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 266, // 282: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 281, // 283: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 281, // 284: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 281, // 285: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 15, // 286: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 268, // 287: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 13, // 288: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 272, // 289: openshell.v1.AddWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 13, // 290: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 231, // 291: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 272, // 292: openshell.v1.RemoveWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 15, // 293: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 272, // 294: openshell.v1.ListWorkspaceMembersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 231, // 295: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 267, // 296: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp + 16, // 297: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult + 239, // 298: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation + 16, // 299: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult + 267, // 300: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp + 267, // 301: openshell.v1.SandboxProvisioning.configuration_change_time:type_name -> google.protobuf.Timestamp + 267, // 302: openshell.v1.SandboxProvisioning.first_rejection_time:type_name -> google.protobuf.Timestamp + 267, // 303: openshell.v1.SandboxProvisioning.deadline:type_name -> google.protobuf.Timestamp + 267, // 304: openshell.v1.SandboxProvisioning.timeout_time:type_name -> google.protobuf.Timestamp + 267, // 305: openshell.v1.SandboxProvisioning.cleanup_completed_time:type_name -> google.protobuf.Timestamp + 267, // 306: openshell.v1.SandboxProvisioning.cleanup_retry_time:type_name -> google.protobuf.Timestamp + 267, // 307: openshell.v1.SandboxProvisioning.attachment_change_time:type_name -> google.protobuf.Timestamp + 267, // 308: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 267, // 309: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 123, // 310: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 151, // 311: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 21, // 312: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 23, // 313: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 25, // 314: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 48, // 315: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 56, // 316: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest + 58, // 317: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 59, // 318: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 49, // 319: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest + 50, // 320: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest + 51, // 321: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest + 52, // 322: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest + 60, // 323: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 61, // 324: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 62, // 325: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 78, // 326: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest + 63, // 327: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 64, // 328: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 65, // 329: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 83, // 330: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 85, // 331: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 86, // 332: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 87, // 333: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 89, // 334: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 93, // 335: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 95, // 336: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 101, // 337: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 102, // 338: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 109, // 339: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 110, // 340: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 111, // 341: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 116, // 342: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 117, // 343: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 140, // 344: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 142, // 345: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 144, // 346: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 112, // 347: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 129, // 348: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 131, // 349: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 133, // 350: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 135, // 351: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 113, // 352: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 147, // 353: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 282, // 354: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 283, // 355: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 155, // 356: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 165, // 357: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 167, // 358: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 169, // 359: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 240, // 360: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest + 80, // 361: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest + 172, // 362: openshell.v1.OpenShell.ReportSandboxConfiguration:input_type -> openshell.v1.ReportSandboxConfigurationRequest + 149, // 363: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 153, // 364: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 175, // 365: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 176, // 366: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 179, // 367: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 186, // 368: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 188, // 369: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest + 194, // 370: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 105, // 371: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 203, // 372: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 205, // 373: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 207, // 374: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 209, // 375: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 212, // 376: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 214, // 377: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 216, // 378: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 218, // 379: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 220, // 380: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 17, // 381: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 19, // 382: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 223, // 383: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 225, // 384: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 227, // 385: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 229, // 386: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 232, // 387: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 234, // 388: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 236, // 389: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 22, // 390: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 24, // 391: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 26, // 392: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 66, // 393: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 57, // 394: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse + 66, // 395: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 67, // 396: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 53, // 397: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 53, // 398: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 54, // 399: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse + 55, // 400: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse + 68, // 401: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 69, // 402: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 70, // 403: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 79, // 404: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse + 82, // 405: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 66, // 406: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 66, // 407: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 84, // 408: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 92, // 409: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 92, // 410: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 88, // 411: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 90, // 412: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 94, // 413: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 99, // 414: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 101, // 415: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 99, // 416: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 114, // 417: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 114, // 418: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 115, // 419: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 139, // 420: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 138, // 421: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 141, // 422: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 143, // 423: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 145, // 424: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 114, // 425: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 130, // 426: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 132, // 427: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 134, // 428: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 136, // 429: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 146, // 430: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 148, // 431: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 284, // 432: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 285, // 433: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 164, // 434: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 166, // 435: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 168, // 436: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 170, // 437: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 241, // 438: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse + 81, // 439: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse + 173, // 440: openshell.v1.OpenShell.ReportSandboxConfiguration:output_type -> openshell.v1.ReportSandboxConfigurationResponse + 152, // 441: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 154, // 442: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 178, // 443: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 177, // 444: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 180, // 445: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 187, // 446: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 189, // 447: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse + 194, // 448: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 106, // 449: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 204, // 450: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 206, // 451: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 208, // 452: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 210, // 453: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 213, // 454: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 215, // 455: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 217, // 456: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 219, // 457: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 222, // 458: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 18, // 459: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 20, // 460: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 224, // 461: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 226, // 462: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 228, // 463: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 230, // 464: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 233, // 465: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 235, // 466: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 237, // 467: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 390, // [390:468] is the sub-list for method output_type + 312, // [312:390] is the sub-list for method input_type + 312, // [312:312] is the sub-list for extension type_name + 312, // [312:312] is the sub-list for extension extendee + 0, // [0:312] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -19488,7 +19577,7 @@ func file_openshell_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_openshell_proto_rawDesc), len(file_openshell_proto_rawDesc)), NumEnums: 17, - NumMessages: 248, + NumMessages: 250, NumExtensions: 0, NumServices: 1, }, diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 26ac0738a7..e563485773 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -35,7 +35,9 @@ const client = await OpenShellClient.connect({ const sandbox = await client.sandbox.create({ image: 'ghcr.io/nvidia/openshell-community/sandboxes/python:latest', + serviceExposures: [{ targetPort: 8080 }], }) +console.log(sandbox.serviceUrls['']) await client.sandbox.waitReady(sandbox.name, 120) const result = await client.sandbox.exec(sandbox.name, ['/bin/sh', '-c', 'echo hello']) diff --git a/sdk/typescript/src/client.test.ts b/sdk/typescript/src/client.test.ts index 356ce0d591..f5f3dcf534 100644 --- a/sdk/typescript/src/client.test.ts +++ b/sdk/typescript/src/client.test.ts @@ -274,6 +274,36 @@ describe('exec / execStream', () => { }); describe('create', () => { + it('sends create-time service exposures', async () => { + let created: { serviceExposures?: Array<{ service?: string; targetPort?: number }> } = {}; + const sandbox = client({ + createSandbox: (req) => { + created = req; + return { + ...readySandbox('sb', 'sb-id'), + serviceUrls: { + '': 'https://sb.example.test/', + metrics: 'https://metrics.sb.example.test/', + }, + }; + }, + }); + + const result = await sandbox.create({ + image: 'img', + serviceExposures: [{ targetPort: 4500 }, { service: 'metrics', targetPort: 9090 }], + }); + + expect(created.serviceExposures?.map(({ service, targetPort }) => ({ service, targetPort }))).toEqual([ + { service: '', targetPort: 4500 }, + { service: 'metrics', targetPort: 9090 }, + ]); + expect(result.serviceUrls).toEqual({ + '': 'https://sb.example.test/', + metrics: 'https://metrics.sb.example.test/', + }); + }); + it('sends the curated policy through spec.policy', async () => { let created: { spec?: { policy?: { version?: number } } } = {}; const sandbox = client({ diff --git a/sdk/typescript/src/client.ts b/sdk/typescript/src/client.ts index 546f151e4e..549a5e6021 100644 --- a/sdk/typescript/src/client.ts +++ b/sdk/typescript/src/client.ts @@ -139,6 +139,8 @@ export interface SandboxSpec { command?: string[]; /** Allocate a retained pseudo-terminal for the canonical command. */ tty?: boolean; + /** Loopback HTTP services to expose when the sandbox is created. */ + serviceExposures?: ServiceExposure[]; /** * Create-time sandbox policy (the safety boundary). Sandbox-scoped * `setPolicy` cannot introduce static fields later, so express filesystem, @@ -155,6 +157,13 @@ export interface SandboxSpec { rawSpec?: MessageInitShape; } +export interface ServiceExposure { + /** Service name. Empty or omitted selects the unnamed endpoint. */ + service?: string; + /** Loopback TCP port inside the sandbox. */ + targetPort: number; +} + export interface SandboxFromTemplateSpec { name?: string; /** Workspace name. Omit for `default`; empty strings are invalid. */ @@ -166,6 +175,8 @@ export interface SandboxFromTemplateSpec { command?: string[]; /** Allocate a retained pseudo-terminal for the canonical command. */ tty?: boolean; + /** Loopback HTTP services to expose when the sandbox is created. */ + serviceExposures?: ServiceExposure[]; /** * Create-time sandbox policy (the safety boundary). The named workload * template supplies runtime workload fields. @@ -184,6 +195,8 @@ export interface SandboxRef { mainProcessInstanceId?: string; exitCode?: number; createdFromWorkloadTemplate?: SandboxWorkloadTemplateProvenance; + /** Service URLs returned by creation, keyed by service name. */ + serviceUrls: Record; } export interface SandboxWorkloadTemplateProvenance { @@ -449,7 +462,7 @@ function policySourceName(s: PolicySource): PolicySourceName { return POLICY_SOURCE_NAMES[s] ?? 'unspecified'; } -function sandboxRef(sandbox: Sandbox | undefined): SandboxRef { +function sandboxRef(sandbox: Sandbox | undefined, serviceUrls: Record = {}): SandboxRef { if (!sandbox) throw new SdkError('invalid_config', 'sandbox missing from gateway response'); const meta = sandbox.metadata; if (!meta?.id || !meta.name) { @@ -470,6 +483,7 @@ function sandboxRef(sandbox: Sandbox | undefined): SandboxRef { resourceVersion: sandbox.createdFromWorkloadTemplate.resourceVersion, } : undefined, + serviceUrls, }; } @@ -877,8 +891,13 @@ export class SandboxClient { name: spec.name ?? '', labels: spec.labels ?? {}, spec: specInit, + serviceExposures: + spec.serviceExposures?.map((exposure) => ({ + service: exposure.service ?? '', + targetPort: exposure.targetPort, + })) ?? [], }); - return sandboxRef(resp.sandbox); + return sandboxRef(resp.sandbox, resp.serviceUrls); } catch (e) { throw fromConnect(e); } @@ -898,8 +917,13 @@ export class SandboxClient { policy: spec.policy, }, workloadTemplate: spec.workloadTemplate, + serviceExposures: + spec.serviceExposures?.map((exposure) => ({ + service: exposure.service ?? '', + targetPort: exposure.targetPort, + })) ?? [], }); - return sandboxRef(resp.sandbox); + return sandboxRef(resp.sandbox, resp.serviceUrls); } catch (e) { throw fromConnect(e); } diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index d858cd75b6..3902f9135a 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -41,6 +41,7 @@ export type { SandboxWorkloadTemplate, SandboxWorkloadTemplateProvenance, SandboxWorkloadTemplateSpec, + ServiceExposure, SetPolicyOptions, SettingScopeName, SettingValue, From 07c3c5ad7915a6412669b69965c093bff1402822 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Thu, 17 Sep 2026 14:15:06 -0700 Subject: [PATCH 2/8] feat(providers): refresh Codex credentials in gateway Signed-off-by: Drew Newberry --- examples/codex-app-server/Dockerfile | 87 ++++--------------- examples/codex-app-server/README.md | 19 ++-- .../codex-app-server/start-codex-app-server | 68 +++++++++++++++ providers/codex.yaml | 20 ++++- 4 files changed, 117 insertions(+), 77 deletions(-) create mode 100644 examples/codex-app-server/start-codex-app-server diff --git a/examples/codex-app-server/Dockerfile b/examples/codex-app-server/Dockerfile index 0a7adccb21..1956665729 100644 --- a/examples/codex-app-server/Dockerfile +++ b/examples/codex-app-server/Dockerfile @@ -3,82 +3,31 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -FROM ghcr.io/nvidia/openshell-community/sandboxes/base:latest +FROM ubuntu:24.04 -USER root +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install --yes --no-install-recommends \ + ca-certificates \ + nodejs \ + npm \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd \ + --create-home \ + --home-dir /sandbox \ + --shell /bin/bash \ + sandbox RUN npm install --global --force "@openai/codex@latest" \ && npm cache clean --force \ && codex --version -COPY --chmod=0755 <<'EOF' /usr/local/bin/start-codex-app-server -#!/usr/bin/env node - -const fs = require("fs"); -const path = require("path"); -const { spawn } = require("child_process"); - -const required = [ - "CODEX_AUTH_ACCESS_TOKEN", - "CODEX_AUTH_REFRESH_TOKEN", - "CODEX_AUTH_ACCOUNT_ID", -]; -for (const name of required) { - if (!process.env[name]) { - throw new Error(`missing required provider credential: ${name}`); - } -} - -const b64u = (value) => Buffer.from(JSON.stringify(value)).toString("base64url"); -const now = Math.floor(Date.now() / 1000); -const idToken = [ - b64u({ alg: "none", typ: "JWT" }), - b64u({ - iss: "https://auth.openai.com", - aud: "codex", - sub: "openshell-app-server", - email: "app-server@openshell.local", - iat: now, - exp: now + 3600, - }), - "placeholder", -].join("."); - -const codexHome = path.join(process.env.HOME, ".codex"); -fs.mkdirSync(codexHome, { recursive: true }); -fs.writeFileSync(path.join(codexHome, "auth.json"), JSON.stringify({ - auth_mode: "chatgpt", - OPENAI_API_KEY: null, - tokens: { - // Provider values are opaque stable handles. Codex parses the ID token - // locally, so use a short-lived, non-secret identity placeholder for it. - id_token: idToken, - access_token: process.env.CODEX_AUTH_ACCESS_TOKEN, - refresh_token: process.env.CODEX_AUTH_REFRESH_TOKEN, - account_id: process.env.CODEX_AUTH_ACCOUNT_ID, - }, - last_refresh: new Date().toISOString(), -}, null, 2), { mode: 0o600 }); - -const port = process.env.CODEX_APP_SERVER_PORT || "4500"; -const server = spawn( - "codex", - ["app-server", "--listen", `ws://127.0.0.1:${port}`], - { stdio: "inherit" }, -); -server.on("error", (error) => { - console.error(error); - process.exit(1); -}); -server.on("exit", (code, signal) => { - if (signal) { - process.kill(process.pid, signal); - } else { - process.exit(code ?? 1); - } -}); -EOF +COPY --chmod=0755 start-codex-app-server /usr/local/bin/start-codex-app-server ENV CODEX_APP_SERVER_PORT=4500 +ENV HOME=/sandbox USER sandbox +WORKDIR /sandbox diff --git a/examples/codex-app-server/README.md b/examples/codex-app-server/README.md index 220a195e36..7a5b93ee23 100644 --- a/examples/codex-app-server/README.md +++ b/examples/codex-app-server/README.md @@ -31,17 +31,25 @@ docker build --pull --no-cache --tag openshell/codex-app-server:local --file Doc ## 2. Create the provider -This single command reads the existing host login and stores it in an -OpenShell provider named `codex`. Skip it if that provider already exists on -the gateway. +This single command creates a provider from the current host login, moves the +refresh token into gateway-only refresh material, and rotates the access token +once. The sandbox receives opaque handles for only the access token and account +ID; it never receives the refresh token. Run this command once per gateway. ```shell openshell provider create \ --name codex \ --type codex \ --credential "CODEX_AUTH_ACCESS_TOKEN=$(jq -er '.tokens.access_token' "$HOME/.codex/auth.json")" \ - --credential "CODEX_AUTH_REFRESH_TOKEN=$(jq -er '.tokens.refresh_token' "$HOME/.codex/auth.json")" \ - --credential "CODEX_AUTH_ACCOUNT_ID=$(jq -er '.tokens.account_id' "$HOME/.codex/auth.json")" + --credential "CODEX_AUTH_ACCOUNT_ID=$(jq -er '.tokens.account_id' "$HOME/.codex/auth.json")" && \ +env "CODEX_REFRESH_TOKEN=$(jq -er '.tokens.refresh_token' "$HOME/.codex/auth.json")" \ + openshell provider refresh configure codex \ + --credential-key CODEX_AUTH_ACCESS_TOKEN \ + --strategy oauth2-refresh-token \ + --material client_id=app_EMoamEEZ73f0CkXaXp7hrann \ + --secret-material-env refresh_token=CODEX_REFRESH_TOKEN && \ +openshell provider refresh rotate codex \ + --credential-key CODEX_AUTH_ACCESS_TOKEN ``` ## 3. Launch the sandbox @@ -79,5 +87,6 @@ codex --remote ws://default--codex-app-server.openshell.localhost: ```shell openshell sandbox delete codex-app-server +openshell provider delete codex docker image rm openshell/codex-app-server:local ``` diff --git a/examples/codex-app-server/start-codex-app-server b/examples/codex-app-server/start-codex-app-server new file mode 100644 index 0000000000..ddc5aa2654 --- /dev/null +++ b/examples/codex-app-server/start-codex-app-server @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const fs = require("fs"); +const path = require("path"); +const { spawn } = require("child_process"); + +const required = [ + "CODEX_AUTH_ACCESS_TOKEN", + "CODEX_AUTH_ACCOUNT_ID", +]; +for (const name of required) { + if (!process.env[name]) { + throw new Error(`missing required provider credential: ${name}`); + } +} + +const b64u = (value) => Buffer.from(JSON.stringify(value)).toString("base64url"); +const now = Math.floor(Date.now() / 1000); +const idToken = [ + b64u({ alg: "none", typ: "JWT" }), + b64u({ + iss: "https://auth.openai.com", + aud: "codex", + sub: "openshell-app-server", + email: "app-server@openshell.local", + iat: now, + exp: now + 3600, + }), + "placeholder", +].join("."); + +const codexHome = path.join(process.env.HOME, ".codex"); +fs.mkdirSync(codexHome, { recursive: true }); +fs.writeFileSync(path.join(codexHome, "auth.json"), JSON.stringify({ + auth_mode: "chatgpt", + OPENAI_API_KEY: null, + tokens: { + // Provider values are opaque stable handles. Codex parses the ID token + // locally, so use a short-lived, non-secret identity placeholder for it. + id_token: idToken, + access_token: process.env.CODEX_AUTH_ACCESS_TOKEN, + // The gateway owns refresh material and rotates the access-token handle. + refresh_token: "gateway-managed-refresh-token", + account_id: process.env.CODEX_AUTH_ACCOUNT_ID, + }, + last_refresh: new Date().toISOString(), +}, null, 2), { mode: 0o600 }); + +const port = process.env.CODEX_APP_SERVER_PORT || "4500"; +const server = spawn( + "codex", + ["app-server", "--listen", `ws://127.0.0.1:${port}`], + { stdio: "inherit" }, +); +server.on("error", (error) => { + console.error(error); + process.exit(1); +}); +server.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + } else { + process.exit(code ?? 1); + } +}); diff --git a/providers/codex.yaml b/providers/codex.yaml index c9572a0a1b..531cf73356 100644 --- a/providers/codex.yaml +++ b/providers/codex.yaml @@ -30,13 +30,27 @@ category: agent inference_capable: true credentials: - name: access_token - description: Codex OAuth access token + description: Codex OAuth access token refreshed by the gateway env_vars: [CODEX_AUTH_ACCESS_TOKEN] required: true + auth_style: bearer + header_name: authorization + refresh: + strategy: oauth2_refresh_token + token_url: https://auth.openai.com/oauth/token + refresh_before_seconds: 300 + max_lifetime_seconds: 3600 + material: + - name: client_id + description: Codex OAuth client ID + required: true + - name: refresh_token + description: Codex OAuth refresh token + required: true + secret: true - name: refresh_token - description: Codex OAuth refresh token + description: Codex OAuth refresh token for clients that manage their own refresh env_vars: [CODEX_AUTH_REFRESH_TOKEN] - required: true - name: account_id description: Codex account identifier env_vars: [CODEX_AUTH_ACCOUNT_ID] From 76ed0f005675ebfece5ac56d1bb9fa3c3482d679 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Thu, 17 Sep 2026 17:31:59 -0700 Subject: [PATCH 3/8] fix(cli): normalize create-time service URLs Signed-off-by: Drew Newberry --- crates/openshell-cli/src/run.rs | 6 +++++- .../tests/sandbox_create_lifecycle_integration.rs | 6 +++++- skills/openshell-cli/SKILL.md | 13 +++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index 198abedb27..a00f6b9dfe 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -698,7 +698,11 @@ pub async fn sandbox_create( Err(status) => return Err(miette::miette!(status.to_string())), }; let response = response.into_inner(); - let service_urls = response.service_urls; + let service_urls = response + .service_urls + .into_iter() + .map(|(service, url)| (service, service_url_for_gateway(&url, &effective_server))) + .collect::>(); let sandbox = response .sandbox .ok_or_else(|| miette::miette!("sandbox missing from response"))?; diff --git a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs index 1afd9281b3..25bc9ca6e5 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -3234,10 +3234,14 @@ async fn sandbox_create_json_stdout_is_parseable() { let stdout = String::from_utf8(result.stdout).expect("stdout should be UTF-8"); let value = serde_json::from_str::(&stdout) .unwrap_or_else(|err| panic!("stdout should contain only JSON: {err}\n{stdout}")); + let gateway_port = url::Url::parse(&server.endpoint) + .expect("test gateway endpoint should be a URL") + .port() + .expect("test gateway endpoint should include a port"); assert_eq!( value["service_urls"], serde_json::json!({ - "": "https://default--sandbox.openshell.localhost:17670/" + "": format!("https://default--sandbox.openshell.localhost:{gateway_port}/") }) ); } diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index 708c856087..b2387365c9 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -822,6 +822,14 @@ openshell forward start 127.0.0.1:8080 my-app -d # gRPC relay to a loopback TCP service, with an optional dynamic local port. openshell forward service my-app --target-port 8000 --local 127.0.0.1:0 +# Create a sandbox with its unnamed HTTP or WebSocket service exposed. +openshell sandbox create \ + --name my-app \ + --from my-app:latest \ + --expose 8080 \ + --detach \ + -- ./start-server.sh + # Expose and manage an HTTP service through the gateway. openshell service expose my-app 8080 web openshell service list my-app @@ -833,6 +841,11 @@ openshell service delete my-app web Use `openshell service list --all-workspaces` for a Platform Admin view across workspaces. A sandbox name and `--all-workspaces` are mutually exclusive. +`sandbox create --expose PORT` registers the unnamed endpoint in the create +request and keeps the sandbox running. Add `--output json` for automation; the +result contains a `service_urls` map whose empty key is the unnamed endpoint. +Use `openshell service expose` after creation to add or update named endpoints. + Prefer loopback binds unless the user explicitly needs LAN-visible local access. --- From e329b7699837afbc860526888e64dd9c57f9556c Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Thu, 17 Sep 2026 18:18:48 -0700 Subject: [PATCH 4/8] fix(server): roll back failed service exposure Signed-off-by: Drew Newberry --- crates/openshell-server/src/grpc/sandbox.rs | 87 ++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index c2b34d8abb..6394a27a8a 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -630,14 +630,41 @@ async fn handle_create_sandbox_inner( let mut service_urls = HashMap::with_capacity(request.service_exposures.len()); for exposure in &request.service_exposures { - let endpoint = super::service::expose_service_endpoint( + let endpoint = match super::service::expose_service_endpoint( state, sandbox.object_workspace(), &sandbox, &exposure.service, exposure.target_port, ) - .await?; + .await + { + Ok(endpoint) => endpoint, + Err(exposure_error) => { + let rollback = state + .compute + .delete_sandbox(sandbox.object_workspace(), sandbox.object_name()) + .await; + if let Err(rollback_error) = rollback { + warn!( + sandbox_id = %sandbox.object_id(), + sandbox_name = %sandbox.object_name(), + service_name = %exposure.service, + exposure_error = %exposure_error, + rollback_error = %rollback_error, + "Failed to roll back sandbox after service exposure failed" + ); + return Err(Status::internal(format!( + "create sandbox failed while exposing service '{}': {}; rollback failed: {}; sandbox '{}' may require manual deletion", + exposure.service, + exposure_error.message(), + rollback_error.message(), + sandbox.object_name(), + ))); + } + return Err(exposure_error); + } + }; service_urls.insert(exposure.service.clone(), endpoint.into_inner().url); } @@ -5096,6 +5123,62 @@ mod tests { } } + #[tokio::test] + async fn create_sandbox_begins_rollback_when_service_exposure_fails() { + let state = test_server_state().await; + let corrupt_service_key = + crate::service_routing::endpoint_key("rollback-services", "metrics"); + state + .store + .put_if( + ServiceEndpoint::object_type(), + "corrupt-service-endpoint", + &corrupt_service_key, + "default", + b"not-a-service-endpoint", + None, + WriteCondition::MustCreate, + ) + .await + .expect("corrupt service endpoint fixture should be stored"); + + let error = handle_create_sandbox( + &state, + authed_request(CreateSandboxRequest { + name: "rollback-services".to_string(), + spec: Some(SandboxSpec::default()), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + service_exposures: vec![ + SandboxServiceExposure { + service: "web".to_string(), + target_port: 8080, + }, + SandboxServiceExposure { + service: "metrics".to_string(), + target_port: 9090, + }, + ], + ..Default::default() + }), + ) + .await + .expect_err("corrupt endpoint should fail sandbox creation"); + + assert_eq!(error.code(), tonic::Code::Internal); + assert!(error.message().contains("fetch endpoint failed")); + let sandbox = state + .store + .get_message_by_name::("default", "rollback-services") + .await + .expect("sandbox lookup should succeed") + .expect("asynchronous driver cleanup retains a deleting record"); + assert_eq!( + SandboxPhase::try_from(sandbox.phase()).ok(), + Some(SandboxPhase::Deleting), + "failed create must begin sandbox cleanup" + ); + } + #[tokio::test] async fn create_sandbox_rejects_duplicate_service_exposures_before_persisting() { let state = test_server_state().await; From 45312bd23113d6059d180bea0f442b6de23f900f Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Thu, 17 Sep 2026 19:18:15 -0700 Subject: [PATCH 5/8] docs(example): simplify Codex provider setup Signed-off-by: Drew Newberry --- examples/codex-app-server/README.md | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/examples/codex-app-server/README.md b/examples/codex-app-server/README.md index 7a5b93ee23..dd38ab9911 100644 --- a/examples/codex-app-server/README.md +++ b/examples/codex-app-server/README.md @@ -37,19 +37,28 @@ once. The sandbox receives opaque handles for only the access token and account ID; it never receives the refresh token. Run this command once per gateway. ```shell -openshell provider create \ - --name codex \ - --type codex \ - --credential "CODEX_AUTH_ACCESS_TOKEN=$(jq -er '.tokens.access_token' "$HOME/.codex/auth.json")" \ - --credential "CODEX_AUTH_ACCOUNT_ID=$(jq -er '.tokens.account_id' "$HOME/.codex/auth.json")" && \ -env "CODEX_REFRESH_TOKEN=$(jq -er '.tokens.refresh_token' "$HOME/.codex/auth.json")" \ +( + set -e + + export CODEX_AUTH_ACCESS_TOKEN="$(jq -er '.tokens.access_token' "$HOME/.codex/auth.json")" + export CODEX_AUTH_ACCOUNT_ID="$(jq -er '.tokens.account_id' "$HOME/.codex/auth.json")" + export CODEX_AUTH_REFRESH_TOKEN="$(jq -er '.tokens.refresh_token' "$HOME/.codex/auth.json")" + + openshell provider create \ + --name codex \ + --type codex \ + --credential CODEX_AUTH_ACCESS_TOKEN \ + --credential CODEX_AUTH_ACCOUNT_ID + openshell provider refresh configure codex \ --credential-key CODEX_AUTH_ACCESS_TOKEN \ --strategy oauth2-refresh-token \ --material client_id=app_EMoamEEZ73f0CkXaXp7hrann \ - --secret-material-env refresh_token=CODEX_REFRESH_TOKEN && \ -openshell provider refresh rotate codex \ - --credential-key CODEX_AUTH_ACCESS_TOKEN + --secret-material-env refresh_token=CODEX_AUTH_REFRESH_TOKEN + + openshell provider refresh rotate codex \ + --credential-key CODEX_AUTH_ACCESS_TOKEN +) ``` ## 3. Launch the sandbox From 266a4f95005b028c1286639a5bde24b4a60b8540 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Thu, 17 Sep 2026 19:22:36 -0700 Subject: [PATCH 6/8] docs(example): separate provider setup commands Signed-off-by: Drew Newberry --- examples/codex-app-server/README.md | 35 +++++++++++++---------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/examples/codex-app-server/README.md b/examples/codex-app-server/README.md index dd38ab9911..36964d3067 100644 --- a/examples/codex-app-server/README.md +++ b/examples/codex-app-server/README.md @@ -31,34 +31,31 @@ docker build --pull --no-cache --tag openshell/codex-app-server:local --file Doc ## 2. Create the provider -This single command creates a provider from the current host login, moves the -refresh token into gateway-only refresh material, and rotates the access token -once. The sandbox receives opaque handles for only the access token and account -ID; it never receives the refresh token. Run this command once per gateway. +These commands create a provider from the current host login, move the refresh +token into gateway-only refresh material, and rotate the access token once. The +sandbox receives opaque handles for only the access token and account ID; it +never receives the refresh token. Run these commands once per gateway. ```shell -( - set -e - - export CODEX_AUTH_ACCESS_TOKEN="$(jq -er '.tokens.access_token' "$HOME/.codex/auth.json")" - export CODEX_AUTH_ACCOUNT_ID="$(jq -er '.tokens.account_id' "$HOME/.codex/auth.json")" - export CODEX_AUTH_REFRESH_TOKEN="$(jq -er '.tokens.refresh_token' "$HOME/.codex/auth.json")" - - openshell provider create \ - --name codex \ - --type codex \ - --credential CODEX_AUTH_ACCESS_TOKEN \ - --credential CODEX_AUTH_ACCOUNT_ID +openshell provider create \ + --name codex \ + --type codex \ + --credential "CODEX_AUTH_ACCESS_TOKEN=$(jq -er '.tokens.access_token' "$HOME/.codex/auth.json")" \ + --credential "CODEX_AUTH_ACCOUNT_ID=$(jq -er '.tokens.account_id' "$HOME/.codex/auth.json")" +``` +```shell +env "CODEX_AUTH_REFRESH_TOKEN=$(jq -er '.tokens.refresh_token' "$HOME/.codex/auth.json")" \ openshell provider refresh configure codex \ --credential-key CODEX_AUTH_ACCESS_TOKEN \ --strategy oauth2-refresh-token \ --material client_id=app_EMoamEEZ73f0CkXaXp7hrann \ --secret-material-env refresh_token=CODEX_AUTH_REFRESH_TOKEN +``` - openshell provider refresh rotate codex \ - --credential-key CODEX_AUTH_ACCESS_TOKEN -) +```shell +openshell provider refresh rotate codex \ + --credential-key CODEX_AUTH_ACCESS_TOKEN ``` ## 3. Launch the sandbox From 683d6f3978202db01dd4128573f5603aa928e6e1 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 18 Sep 2026 19:38:11 -0700 Subject: [PATCH 7/8] fix(sandbox): harden create-time service exposure Signed-off-by: Drew Newberry --- crates/openshell-server/src/compute/mod.rs | 19 ++++++ .../src/grpc/mutation_replay/tests.rs | 46 +++++++++++++- crates/openshell-server/src/grpc/sandbox.rs | 60 ++++++++++++++++--- crates/openshell-server/src/storage_proto.rs | 4 +- .../codex-app-server/start-codex-app-server | 7 ++- 5 files changed, 122 insertions(+), 14 deletions(-) diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 8e2b5cb462..3c6fbc6017 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -1618,6 +1618,25 @@ impl ComputeRuntime { sandbox_id: candidate.object_id().to_string(), sandbox_name: candidate.object_name().to_string(), }; + self.delete_sandbox_target(target).await + } + + pub(crate) async fn delete_sandbox_by_id( + &self, + sandbox_id: &str, + sandbox_name: &str, + ) -> Result { + self.delete_sandbox_target(SandboxDeleteTarget { + sandbox_id: sandbox_id.to_string(), + sandbox_name: sandbox_name.to_string(), + }) + .await + } + + async fn delete_sandbox_target( + &self, + target: SandboxDeleteTarget, + ) -> Result { let delete_guard = self.lifecycle_gates.lock_for(&target.sandbox_id).await; let global_guard = self.lock_global_for_lifecycle(&delete_guard).await; diff --git a/crates/openshell-server/src/grpc/mutation_replay/tests.rs b/crates/openshell-server/src/grpc/mutation_replay/tests.rs index d2689e3be8..fa1a54d7b0 100644 --- a/crates/openshell-server/src/grpc/mutation_replay/tests.rs +++ b/crates/openshell-server/src/grpc/mutation_replay/tests.rs @@ -6,8 +6,8 @@ use crate::grpc::test_support::{authed_request, test_server_state}; use openshell_core::proto::datamodel::v1::ObjectMeta; use openshell_core::proto::open_shell_server::OpenShell; use openshell_core::proto::{ - SandboxWorkloadConfig, SandboxWorkloadTemplate, SandboxWorkloadTemplateSpec, WorkspaceMember, - WorkspaceRole, + CreateSandboxRequest, SandboxServiceExposure, SandboxSpec, SandboxWorkloadConfig, + SandboxWorkloadTemplate, SandboxWorkloadTemplateSpec, WorkspaceMember, WorkspaceRole, }; use openshell_core::rpc_error::StatusExt; use std::collections::HashMap; @@ -91,6 +91,48 @@ fn canonical_payload_ignores_map_order_and_id_but_preserves_presence() { assert_ne!(absent, fingerprint(&template).unwrap()); } +#[tokio::test] +async fn create_sandbox_replay_preserves_service_urls() { + let directory = tempfile::tempdir().unwrap(); + let key = directory.path().join("private-key"); + std::fs::write(&key, b"test-only stable private fingerprint material").unwrap(); + let mut state = test_server_state().await; + Arc::get_mut(&mut state).unwrap().config.gateway_jwt = + Some(openshell_core::config::GatewayJwtConfig { + signing_key_path: key, + public_key_path: directory.path().join("public"), + kid_path: directory.path().join("kid"), + gateway_id: "test".into(), + ttl_secs: None, + }); + let service = crate::grpc::OpenShellService::new(state); + let request = CreateSandboxRequest { + name: "replay-services".into(), + spec: Some(SandboxSpec::default()), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + service_exposures: vec![SandboxServiceExposure { + service: "web".into(), + target_port: 8080, + }], + request_id: uuid::Uuid::new_v4().to_string(), + ..Default::default() + }; + + let original = service + .create_sandbox(authed_request(request.clone())) + .await + .unwrap() + .into_inner(); + let replay = service + .create_sandbox(authed_request(request)) + .await + .unwrap(); + + assert_eq!(replay.metadata().get("openshell-replayed").unwrap(), "true"); + assert_eq!(replay.get_ref().service_urls, original.service_urls); + assert_eq!(replay.into_inner(), original); +} + async fn exercise_backend(url: &str) { let first = state_for(Store::connect(url).await.unwrap()).await; let second = state_for(Store::connect(url).await.unwrap()).await; diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 6394a27a8a..4169bac95b 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -643,7 +643,7 @@ async fn handle_create_sandbox_inner( Err(exposure_error) => { let rollback = state .compute - .delete_sandbox(sandbox.object_workspace(), sandbox.object_name()) + .delete_sandbox_by_id(sandbox.object_id(), sandbox.object_name()) .await; if let Err(rollback_error) = rollback { warn!( @@ -3403,8 +3403,6 @@ mod tests { use crate::grpc::test_support::{ authed_request, test_server_state, test_server_state_with_driver, }; - use crate::provider_profile_sources::ProviderProfileSources; - use openshell_core::GatewayProviderProfileSourceConfig; use openshell_core::proto::datamodel::v1::ObjectMeta; use openshell_core::proto::{GpuResourceRequirements, SandboxServiceExposure, ServiceEndpoint}; @@ -4574,6 +4572,7 @@ mod tests { )), await_main_process_attachment: false, workload_template: String::new(), + service_exposures: Vec::new(), }), ) .await @@ -5116,8 +5115,8 @@ mod tests { .expect("service endpoint lookup should succeed") .expect("service endpoint should be persisted"); assert_eq!(endpoint.sandbox_id, sandbox.object_id()); - assert_eq!(endpoint.sandbox_name, "services"); - assert_eq!(endpoint.service_name, service); + assert_eq!(endpoint.sandbox, "services"); + assert_eq!(endpoint.name, service); assert_eq!(endpoint.target_port, target_port); assert!(endpoint.domain); } @@ -5179,6 +5178,53 @@ mod tests { ); } + #[tokio::test] + async fn create_rollback_by_id_preserves_same_name_replacement() { + let state = test_server_state().await; + let original = handle_create_sandbox( + &state, + authed_request(CreateSandboxRequest { + name: "rollback-replace".to_string(), + spec: Some(SandboxSpec::default()), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + ..Default::default() + }), + ) + .await + .unwrap() + .into_inner() + .sandbox + .unwrap(); + let original_id = original.object_id().to_string(); + let original_name = original.object_name().to_string(); + + state + .store + .delete(Sandbox::object_type(), &original_id) + .await + .unwrap(); + let mut replacement = original; + let replacement_id = uuid::Uuid::new_v4().to_string(); + let metadata = replacement.metadata.as_mut().unwrap(); + metadata.id.clone_from(&replacement_id); + metadata.resource_version = 0; + state.store.put_message(&replacement).await.unwrap(); + + state + .compute + .delete_sandbox_by_id(&original_id, &original_name) + .await + .unwrap(); + + let stored = state + .store + .get_message_by_name::("default", &original_name) + .await + .unwrap() + .expect("replacement must survive rollback for the original ID"); + assert_eq!(stored.object_id(), replacement_id); + } + #[tokio::test] async fn create_sandbox_rejects_duplicate_service_exposures_before_persisting() { let state = test_server_state().await; @@ -5919,7 +5965,7 @@ mod tests { + generated_by_gateway.len(), "every field must have exactly one create-time owner" ); - let actual: std::collections::HashSet = message + let actual: HashSet = message .fields() .map(|field| field.name().to_string()) .collect(); @@ -5959,7 +6005,7 @@ mod tests { .await .unwrap(); let supplied_epoch = uuid::Uuid::new_v4().to_string(); - let mut generated_epochs = std::collections::HashSet::new(); + let mut generated_epochs = HashSet::new(); for (name, workload_template_name) in [("direct-epoch", ""), ("template-epoch", "epoch-template")] { diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index 9d303b1add..e89a21bcb5 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -119,7 +119,7 @@ mod tests { const STORAGE_V1_SCHEMA_SHA256: &str = "d68401809d8cea445c35233ef32412bbd041cb2ac5acaf368a0d0bf74d2ddf17"; const PUBLIC_RPC_SCHEMA_SHA256: &str = - "5e0cddacd16cbbc28d0a3b209bd9158f1ad3a0affd3fead80dae66bf08a13d86"; + "8c1c3677130e3d42314ac7ad8dc76460690560f02a6108bd913d614608156da3"; const DURABLE_SCHEMA_SHA256: &str = "9eeaa29dfba187bff69fb7bc4f9a13a0f1d7be3f7049a38c8f0e20ce77ec7d8b"; const PUBLIC_DURABLE_OVERLAP_SHA256: &str = @@ -575,7 +575,7 @@ mod tests { overlap_hash.as_str(), ), ( - (300, 24), + (301, 24), (92, 19), (80, 19), PUBLIC_RPC_SCHEMA_SHA256, diff --git a/examples/codex-app-server/start-codex-app-server b/examples/codex-app-server/start-codex-app-server index ddc5aa2654..00522856e6 100644 --- a/examples/codex-app-server/start-codex-app-server +++ b/examples/codex-app-server/start-codex-app-server @@ -35,15 +35,16 @@ const idToken = [ const codexHome = path.join(process.env.HOME, ".codex"); fs.mkdirSync(codexHome, { recursive: true }); fs.writeFileSync(path.join(codexHome, "auth.json"), JSON.stringify({ - auth_mode: "chatgpt", + // The gateway owns token refresh; Codex must not attempt to refresh these + // externally managed tokens itself. + auth_mode: "chatgptAuthTokens", OPENAI_API_KEY: null, tokens: { // Provider values are opaque stable handles. Codex parses the ID token // locally, so use a short-lived, non-secret identity placeholder for it. id_token: idToken, access_token: process.env.CODEX_AUTH_ACCESS_TOKEN, - // The gateway owns refresh material and rotates the access-token handle. - refresh_token: "gateway-managed-refresh-token", + refresh_token: "", account_id: process.env.CODEX_AUTH_ACCOUNT_ID, }, last_refresh: new Date().toISOString(), From 8733df2c8db75379b512062df0129f96937721b3 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Sat, 19 Sep 2026 21:38:20 -0700 Subject: [PATCH 8/8] docs(example): bundle Codex provider profile Signed-off-by: Drew Newberry --- examples/codex-app-server/README.md | 20 +++++++-- examples/codex-app-server/codex.yaml | 65 ++++++++++++++++++++++++++++ providers/codex.yaml | 20 ++------- 3 files changed, 85 insertions(+), 20 deletions(-) create mode 100644 examples/codex-app-server/codex.yaml diff --git a/examples/codex-app-server/README.md b/examples/codex-app-server/README.md index 36964d3067..706d94870f 100644 --- a/examples/codex-app-server/README.md +++ b/examples/codex-app-server/README.md @@ -29,12 +29,25 @@ client current as well to avoid app-server protocol incompatibilities. docker build --pull --no-cache --tag openshell/codex-app-server:local --file Dockerfile . ``` -## 2. Create the provider +## 2. Import the provider profile + +The gateway starts with an empty provider-profile catalog. Validate and import +the profile included with this example before creating the provider: + +```shell +openshell provider profile lint --file codex.yaml +``` + +```shell +openshell provider profile import --file codex.yaml +``` + +## 3. Create the provider These commands create a provider from the current host login, move the refresh token into gateway-only refresh material, and rotate the access token once. The sandbox receives opaque handles for only the access token and account ID; it -never receives the refresh token. Run these commands once per gateway. +never receives the refresh token. Run these commands once per workspace. ```shell openshell provider create \ @@ -58,7 +71,7 @@ openshell provider refresh rotate codex \ --credential-key CODEX_AUTH_ACCESS_TOKEN ``` -## 3. Launch the sandbox +## 4. Launch the sandbox ```shell openshell sandbox create \ @@ -94,5 +107,6 @@ codex --remote ws://default--codex-app-server.openshell.localhost: ```shell openshell sandbox delete codex-app-server openshell provider delete codex +openshell provider profile delete codex docker image rm openshell/codex-app-server:local ``` diff --git a/examples/codex-app-server/codex.yaml b/examples/codex-app-server/codex.yaml new file mode 100644 index 0000000000..bd500c5d06 --- /dev/null +++ b/examples/codex-app-server/codex.yaml @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Provider profile for the Codex app-server example. The gateway owns refresh +# material and injects opaque handles for the access token and account ID. + +id: codex +display_name: Codex +description: OpenAI Codex CLI with gateway-managed token refresh +category: agent +inference_capable: true +credentials: + - name: access_token + description: Codex OAuth access token refreshed by the gateway + env_vars: [CODEX_AUTH_ACCESS_TOKEN] + required: true + auth_style: bearer + header_name: authorization + refresh: + strategy: oauth2_refresh_token + token_url: https://auth.openai.com/oauth/token + refresh_before_seconds: 300 + max_lifetime_seconds: 3600 + material: + - name: client_id + description: Codex OAuth client ID + required: true + - name: refresh_token + description: Codex OAuth refresh token + required: true + secret: true + - name: refresh_token + description: Codex OAuth refresh token for clients that manage their own refresh + env_vars: [CODEX_AUTH_REFRESH_TOKEN] + - name: account_id + description: Codex account identifier + env_vars: [CODEX_AUTH_ACCOUNT_ID] + required: true + - name: id_token + description: Codex OAuth ID token + env_vars: [CODEX_AUTH_ID_TOKEN] +discovery: + credentials: [access_token, refresh_token, account_id, id_token] +endpoints: + - host: api.openai.com + port: 443 + protocol: rest + access: read-write + enforcement: enforce + - host: auth.openai.com + port: 443 + protocol: rest + access: read-write + enforcement: enforce + - host: chatgpt.com + port: 443 + protocol: rest + access: read-write + enforcement: enforce + - host: ab.chatgpt.com + port: 443 + protocol: rest + access: read-write + enforcement: enforce +binaries: [/usr/bin/codex, /usr/local/bin/codex, /usr/lib/node_modules/@openai/**] diff --git a/providers/codex.yaml b/providers/codex.yaml index 531cf73356..c9572a0a1b 100644 --- a/providers/codex.yaml +++ b/providers/codex.yaml @@ -30,27 +30,13 @@ category: agent inference_capable: true credentials: - name: access_token - description: Codex OAuth access token refreshed by the gateway + description: Codex OAuth access token env_vars: [CODEX_AUTH_ACCESS_TOKEN] required: true - auth_style: bearer - header_name: authorization - refresh: - strategy: oauth2_refresh_token - token_url: https://auth.openai.com/oauth/token - refresh_before_seconds: 300 - max_lifetime_seconds: 3600 - material: - - name: client_id - description: Codex OAuth client ID - required: true - - name: refresh_token - description: Codex OAuth refresh token - required: true - secret: true - name: refresh_token - description: Codex OAuth refresh token for clients that manage their own refresh + description: Codex OAuth refresh token env_vars: [CODEX_AUTH_REFRESH_TOKEN] + required: true - name: account_id description: Codex account identifier env_vars: [CODEX_AUTH_ACCOUNT_ID]