From 126e97ea52fa347f053e5fc79b3be0307434692e Mon Sep 17 00:00:00 2001 From: Taylor Mutch Date: Mon, 4 May 2026 12:18:31 -0700 Subject: [PATCH 1/3] feat: Update kubernetes driver to side-load supervisor binary --- .../openshell-driver-kubernetes/src/config.rs | 7 + .../openshell-driver-kubernetes/src/driver.rs | 160 +++++++++++++----- .../openshell-driver-kubernetes/src/main.rs | 10 ++ crates/openshell-server/src/lib.rs | 65 ++++--- 4 files changed, 171 insertions(+), 71 deletions(-) diff --git a/crates/openshell-driver-kubernetes/src/config.rs b/crates/openshell-driver-kubernetes/src/config.rs index be3666130..838262c77 100644 --- a/crates/openshell-driver-kubernetes/src/config.rs +++ b/crates/openshell-driver-kubernetes/src/config.rs @@ -6,6 +6,13 @@ pub struct KubernetesComputeConfig { pub namespace: String, pub default_image: String, pub image_pull_policy: String, + /// Image that provides the `openshell-sandbox` supervisor binary. + /// An init container copies the binary from this image into a shared + /// emptyDir volume before the sandbox container starts. + pub supervisor_image: String, + /// Kubernetes `imagePullPolicy` for the supervisor init container. + /// Empty string delegates to the Kubernetes default. + pub supervisor_image_pull_policy: String, pub grpc_endpoint: String, pub ssh_socket_path: String, pub ssh_handshake_secret: String, diff --git a/crates/openshell-driver-kubernetes/src/driver.rs b/crates/openshell-driver-kubernetes/src/driver.rs index 360a94152..0b0920554 100644 --- a/crates/openshell-driver-kubernetes/src/driver.rs +++ b/crates/openshell-driver-kubernetes/src/driver.rs @@ -312,6 +312,8 @@ impl KubernetesComputeDriver { sandbox.spec.as_ref(), &self.config.default_image, &self.config.image_pull_policy, + &self.config.supervisor_image, + &self.config.supervisor_image_pull_policy, &sandbox.id, &sandbox.name, &self.config.grpc_endpoint, @@ -657,27 +659,21 @@ fn map_kube_event_to_platform( } /// Path where the supervisor binary is mounted inside the agent container. -/// The supervisor is always side-loaded from the k3s node filesystem via a -/// read-only hostPath volume — it is never baked into sandbox images. const SUPERVISOR_MOUNT_PATH: &str = "/opt/openshell/bin"; /// Name of the volume used to side-load the supervisor binary. const SUPERVISOR_VOLUME_NAME: &str = "openshell-supervisor-bin"; -/// Path on the k3s node filesystem where the supervisor binary lives. -/// This is baked into the cluster image at build time and can be updated -/// via `docker cp` during local development. -const SUPERVISOR_HOST_PATH: &str = "/opt/openshell/bin"; +/// Name of the init container that installs the supervisor binary. +const SUPERVISOR_INIT_CONTAINER_NAME: &str = "openshell-supervisor-install"; -/// Build the hostPath volume definition that exposes the supervisor binary -/// from the k3s node filesystem. +/// Build the emptyDir volume that holds the supervisor binary. +/// +/// The init container writes the binary here; the agent container reads it. fn supervisor_volume() -> serde_json::Value { serde_json::json!({ "name": SUPERVISOR_VOLUME_NAME, - "hostPath": { - "path": SUPERVISOR_HOST_PATH, - "type": "DirectoryOrCreate" - } + "emptyDir": {} }) } @@ -690,26 +686,58 @@ fn supervisor_volume_mount() -> serde_json::Value { }) } -/// Apply supervisor side-load transforms to an already-built pod template JSON. +/// Build the init container that copies the supervisor binary into the emptyDir. /// -/// This injects the hostPath volume, volume mount, command override, and -/// `runAsUser: 0` into the pod template, targeting the `agent` container -/// (or the first container if no `agent` is found). +/// The supervisor image is expected to have `openshell-sandbox` on its PATH +/// (e.g. at `/usr/local/bin/openshell-sandbox`). The init container resolves +/// the binary via `command -v` and copies it into the shared emptyDir volume +/// so the agent container can execute it from a fixed, writable path. +fn supervisor_init_container(supervisor_image: &str, supervisor_image_pull_policy: &str) -> serde_json::Value { + let copy_cmd = format!( + "set -e && \ + mkdir -p {SUPERVISOR_MOUNT_PATH} && \ + SUPERVISOR=$(command -v openshell-sandbox) && \ + cp \"$SUPERVISOR\" {SUPERVISOR_MOUNT_PATH}/openshell-sandbox && \ + chmod +x {SUPERVISOR_MOUNT_PATH}/openshell-sandbox" + ); + let mut spec = serde_json::json!({ + "name": SUPERVISOR_INIT_CONTAINER_NAME, + "image": supervisor_image, + "command": ["sh", "-c", copy_cmd], + "securityContext": {"runAsUser": 0}, + "volumeMounts": [{ + "name": SUPERVISOR_VOLUME_NAME, + "mountPath": SUPERVISOR_MOUNT_PATH, + "readOnly": false + }] + }); + if !supervisor_image_pull_policy.is_empty() { + spec["imagePullPolicy"] = serde_json::json!(supervisor_image_pull_policy); + } + spec +} + +/// Apply supervisor side-load transforms to an already-built pod template JSON. /// -/// The supervisor binary is always side-loaded from the k3s node filesystem -/// via a read-only hostPath volume. No init container is needed. +/// Injects an emptyDir volume, an init container that copies the supervisor +/// binary from the supervisor image into that volume, and a read-only volume +/// mount + command override on the agent container. /// /// The `runAsUser: 0` override ensures the supervisor binary runs as root /// regardless of the image's `USER` directive. The supervisor needs root for /// network namespace creation, proxy setup, and Landlock/seccomp configuration. /// It drops to the appropriate non-root user for child processes via the /// policy's `run_as_user`/`run_as_group`. -fn apply_supervisor_sideload(pod_template: &mut serde_json::Value) { +fn apply_supervisor_sideload( + pod_template: &mut serde_json::Value, + supervisor_image: &str, + supervisor_image_pull_policy: &str, +) { let Some(spec) = pod_template.get_mut("spec").and_then(|v| v.as_object_mut()) else { return; }; - // 1. Add the hostPath volume to spec.volumes + // 1. Add the emptyDir volume to spec.volumes let volumes = spec .entry("volumes") .or_insert_with(|| serde_json::json!([])) @@ -718,7 +746,16 @@ fn apply_supervisor_sideload(pod_template: &mut serde_json::Value) { volumes.push(supervisor_volume()); } - // 2. Find the agent container and add volume mount + command override + // 2. Add the init container that copies the binary into the emptyDir + let init_containers = spec + .entry("initContainers") + .or_insert_with(|| serde_json::json!([])) + .as_array_mut(); + if let Some(init_containers) = init_containers { + init_containers.push(supervisor_init_container(supervisor_image, supervisor_image_pull_policy)); + } + + // 3. Find the agent container and add volume mount + command override let Some(containers) = spec.get_mut("containers").and_then(|v| v.as_array_mut()) else { return; }; @@ -881,6 +918,8 @@ fn sandbox_to_k8s_spec( spec: Option<&SandboxSpec>, default_image: &str, image_pull_policy: &str, + supervisor_image: &str, + supervisor_image_pull_policy: &str, sandbox_id: &str, sandbox_name: &str, grpc_endpoint: &str, @@ -921,6 +960,8 @@ fn sandbox_to_k8s_spec( spec.gpu, default_image, image_pull_policy, + supervisor_image, + supervisor_image_pull_policy, sandbox_id, sandbox_name, grpc_endpoint, @@ -967,6 +1008,8 @@ fn sandbox_to_k8s_spec( spec.as_ref().is_some_and(|s| s.gpu), default_image, image_pull_policy, + supervisor_image, + supervisor_image_pull_policy, sandbox_id, sandbox_name, grpc_endpoint, @@ -992,6 +1035,8 @@ fn sandbox_template_to_k8s( gpu: bool, default_image: &str, image_pull_policy: &str, + supervisor_image: &str, + supervisor_image_pull_policy: &str, sandbox_id: &str, sandbox_name: &str, grpc_endpoint: &str, @@ -1003,8 +1048,6 @@ fn sandbox_template_to_k8s( host_gateway_ip: &str, inject_workspace: bool, ) -> serde_json::Value { - // The supervisor binary is always side-loaded from the node filesystem - // via a hostPath volume, regardless of which sandbox image is used. let mut metadata = serde_json::Map::new(); if !template.labels.is_empty() { @@ -1122,8 +1165,9 @@ fn sandbox_template_to_k8s( let mut result = serde_json::Value::Object(template_value); - // Always side-load the supervisor binary from the node filesystem - apply_supervisor_sideload(&mut result); + // Side-load the supervisor binary via an init container that copies it + // from the supervisor image into a shared emptyDir volume. + apply_supervisor_sideload(&mut result, supervisor_image, supervisor_image_pull_policy); // Inject workspace persistence (init container + PVC volume mount) so // that /sandbox data survives pod rescheduling. Skipped when the user @@ -1437,7 +1481,7 @@ mod tests { } }); - apply_supervisor_sideload(&mut pod_template); + apply_supervisor_sideload(&mut pod_template, "custom-image:latest", "IfNotPresent"); let sc = &pod_template["spec"]["containers"][0]["securityContext"]; assert_eq!(sc["runAsUser"], 0, "runAsUser must be 0 for supervisor"); @@ -1461,7 +1505,7 @@ mod tests { } }); - apply_supervisor_sideload(&mut pod_template); + apply_supervisor_sideload(&mut pod_template, "supervisor-image:latest", "IfNotPresent"); let sc = &pod_template["spec"]["containers"][0]["securityContext"]; assert_eq!( @@ -1471,7 +1515,7 @@ mod tests { } #[test] - fn supervisor_sideload_injects_hostpath_volume_and_mount() { + fn supervisor_sideload_injects_emptydir_volume_init_container_and_mount() { let mut pod_template = serde_json::json!({ "spec": { "containers": [{ @@ -1481,24 +1525,32 @@ mod tests { } }); - apply_supervisor_sideload(&mut pod_template); + apply_supervisor_sideload(&mut pod_template, "supervisor-image:latest", "IfNotPresent"); - // No init containers should be present (hostPath, not emptyDir+init) - assert!( - pod_template["spec"]["initContainers"].is_null(), - "hostPath sideload should not create init containers" - ); - - // Volume should be a hostPath volume + // Volume should be an emptyDir let volumes = pod_template["spec"]["volumes"] .as_array() .expect("volumes should exist"); assert_eq!(volumes.len(), 1); assert_eq!(volumes[0]["name"], SUPERVISOR_VOLUME_NAME); - assert_eq!(volumes[0]["hostPath"]["path"], SUPERVISOR_HOST_PATH); - assert_eq!(volumes[0]["hostPath"]["type"], "DirectoryOrCreate"); + assert!( + volumes[0]["emptyDir"].is_object(), + "volume should be emptyDir, not hostPath" + ); + + // Init container should use the supervisor image, not the sandbox image + let init_containers = pod_template["spec"]["initContainers"] + .as_array() + .expect("initContainers should exist"); + assert_eq!(init_containers.len(), 1); + assert_eq!( + init_containers[0]["name"], + SUPERVISOR_INIT_CONTAINER_NAME + ); + assert_eq!(init_containers[0]["image"], "supervisor-image:latest"); + assert_eq!(init_containers[0]["imagePullPolicy"], "IfNotPresent"); - // Agent container command should be overridden + // Agent container command should be overridden to the emptyDir path let command = pod_template["spec"]["containers"][0]["command"] .as_array() .expect("command should be set"); @@ -1507,7 +1559,7 @@ mod tests { format!("{SUPERVISOR_MOUNT_PATH}/openshell-sandbox") ); - // Volume mount should be read-only + // Agent volume mount should be read-only let mounts = pod_template["spec"]["containers"][0]["volumeMounts"] .as_array() .expect("volumeMounts should exist"); @@ -1572,6 +1624,8 @@ mod tests { true, "openshell/sandbox:latest", "", + "openshell/supervisor:latest", + "", "sandbox-id", "sandbox-name", "https://gateway.example.com", @@ -1614,6 +1668,8 @@ mod tests { true, "openshell/sandbox:latest", "", + "openshell/supervisor:latest", + "", "sandbox-id", "sandbox-name", "https://gateway.example.com", @@ -1652,6 +1708,8 @@ mod tests { false, "openshell/sandbox:latest", "", + "openshell/supervisor:latest", + "", "sandbox-id", "sandbox-name", "https://gateway.example.com", @@ -1686,6 +1744,8 @@ mod tests { true, "openshell/sandbox:latest", "", + "openshell/supervisor:latest", + "", "sandbox-id", "sandbox-name", "https://gateway.example.com", @@ -1713,6 +1773,8 @@ mod tests { false, "openshell/sandbox:latest", "", + "openshell/supervisor:latest", + "", "sandbox-id", "sandbox-name", "https://gateway.example.com", @@ -1744,6 +1806,8 @@ mod tests { false, "openshell/sandbox:latest", "", + "openshell/supervisor:latest", + "", "sandbox-id", "sandbox-name", "https://gateway.example.com", @@ -1770,6 +1834,8 @@ mod tests { false, "openshell/sandbox:latest", "", + "openshell/supervisor:latest", + "", "sandbox-id", "sandbox-name", "https://gateway.example.com", @@ -1913,6 +1979,8 @@ mod tests { false, "openshell/sandbox:latest", "", + "openshell/supervisor:latest", + "", "sandbox-id", "sandbox-name", "https://gateway.example.com", @@ -1925,12 +1993,14 @@ mod tests { false, // user provided custom VCTs ); - // No init container should be present + // Only the supervisor init container should be present — no workspace init container + let init_containers = pod_template["spec"]["initContainers"] + .as_array() + .expect("supervisor init container should always be present"); assert!( - pod_template["spec"]["initContainers"].is_null() - || pod_template["spec"]["initContainers"] - .as_array() - .is_none_or(Vec::is_empty), + !init_containers + .iter() + .any(|c| c["name"] == WORKSPACE_INIT_CONTAINER_NAME), "workspace init container must NOT be present when inject_workspace is false" ); diff --git a/crates/openshell-driver-kubernetes/src/main.rs b/crates/openshell-driver-kubernetes/src/main.rs index 4b871d77f..818055efc 100644 --- a/crates/openshell-driver-kubernetes/src/main.rs +++ b/crates/openshell-driver-kubernetes/src/main.rs @@ -57,6 +57,12 @@ struct Args { #[arg(long, env = "OPENSHELL_HOST_GATEWAY_IP")] host_gateway_ip: Option, + + #[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE")] + supervisor_image: Option, + + #[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE_PULL_POLICY")] + supervisor_image_pull_policy: Option, } #[tokio::main] @@ -72,6 +78,10 @@ async fn main() -> Result<()> { namespace: args.sandbox_namespace, default_image: args.sandbox_image.unwrap_or_default(), image_pull_policy: args.sandbox_image_pull_policy.unwrap_or_default(), + supervisor_image: args.supervisor_image.unwrap_or_else(|| { + openshell_core::config::DEFAULT_SUPERVISOR_IMAGE.to_string() + }), + supervisor_image_pull_policy: args.supervisor_image_pull_policy.unwrap_or_default(), grpc_endpoint: args.grpc_endpoint.unwrap_or_default(), ssh_socket_path: args.sandbox_ssh_socket_path, ssh_handshake_secret: args.ssh_handshake_secret, diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 8e480c34d..c024310b2 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -469,32 +469,45 @@ async fn build_compute_runtime( info!(driver = %driver, "Using compute driver"); match driver { - ComputeDriverKind::Kubernetes => ComputeRuntime::new_kubernetes( - KubernetesComputeConfig { - namespace: config.sandbox_namespace.clone(), - default_image: config.sandbox_image.clone(), - image_pull_policy: config.sandbox_image_pull_policy.clone(), - grpc_endpoint: config.grpc_endpoint.clone(), - // Filesystem path to the supervisor's Unix-socket SSH daemon. - // The path lives in a root-only directory so only the - // supervisor can connect; the gateway reaches it through the - // RelayStream bridge, not directly. Override via - // `sandbox_ssh_socket_path` in the config for deployments - // where multiple supervisors share a filesystem. - ssh_socket_path: config.sandbox_ssh_socket_path.clone(), - ssh_handshake_secret: config.ssh_handshake_secret.clone(), - ssh_handshake_skew_secs: config.ssh_handshake_skew_secs, - client_tls_secret_name: config.client_tls_secret_name.clone(), - host_gateway_ip: config.host_gateway_ip.clone(), - }, - store, - sandbox_index, - sandbox_watch_bus, - tracing_log_bus, - supervisor_sessions.clone(), - ) - .await - .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}"))), + ComputeDriverKind::Kubernetes => { + let supervisor_image = std::env::var("OPENSHELL_SUPERVISOR_IMAGE") + .ok() + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| openshell_core::config::DEFAULT_SUPERVISOR_IMAGE.to_string()); + let supervisor_image_pull_policy = + std::env::var("OPENSHELL_SUPERVISOR_IMAGE_PULL_POLICY") + .ok() + .filter(|s| !s.is_empty()) + .unwrap_or_default(); + ComputeRuntime::new_kubernetes( + KubernetesComputeConfig { + namespace: config.sandbox_namespace.clone(), + default_image: config.sandbox_image.clone(), + image_pull_policy: config.sandbox_image_pull_policy.clone(), + supervisor_image, + supervisor_image_pull_policy, + grpc_endpoint: config.grpc_endpoint.clone(), + // Filesystem path to the supervisor's Unix-socket SSH daemon. + // The path lives in a root-only directory so only the + // supervisor can connect; the gateway reaches it through the + // RelayStream bridge, not directly. Override via + // `sandbox_ssh_socket_path` in the config for deployments + // where multiple supervisors share a filesystem. + ssh_socket_path: config.sandbox_ssh_socket_path.clone(), + ssh_handshake_secret: config.ssh_handshake_secret.clone(), + ssh_handshake_skew_secs: config.ssh_handshake_skew_secs, + client_tls_secret_name: config.client_tls_secret_name.clone(), + host_gateway_ip: config.host_gateway_ip.clone(), + }, + store, + sandbox_index, + sandbox_watch_bus, + tracing_log_bus, + supervisor_sessions.clone(), + ) + .await + .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}"))) + } ComputeDriverKind::Docker => ComputeRuntime::new_docker( config.clone(), docker_config.clone(), From a40ed0fc9ab79c1f3cceb442c1b62645fa499054 Mon Sep 17 00:00:00 2001 From: Taylor Mutch Date: Mon, 4 May 2026 12:20:16 -0700 Subject: [PATCH 2/3] Update helm chart with supervisor image config --- deploy/helm/openshell/templates/statefulset.yaml | 6 ++++++ deploy/helm/openshell/values.yaml | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/deploy/helm/openshell/templates/statefulset.yaml b/deploy/helm/openshell/templates/statefulset.yaml index b87eebad6..d4c7ee606 100644 --- a/deploy/helm/openshell/templates/statefulset.yaml +++ b/deploy/helm/openshell/templates/statefulset.yaml @@ -70,6 +70,12 @@ spec: - name: OPENSHELL_SANDBOX_IMAGE_PULL_POLICY value: {{ .Values.server.sandboxImagePullPolicy | quote }} {{- end }} + - name: OPENSHELL_SUPERVISOR_IMAGE + value: {{ .Values.server.supervisorImage | quote }} + {{- if .Values.server.supervisorImagePullPolicy }} + - name: OPENSHELL_SUPERVISOR_IMAGE_PULL_POLICY + value: {{ .Values.server.supervisorImagePullPolicy | quote }} + {{- end }} - name: OPENSHELL_GRPC_ENDPOINT value: {{ if .Values.server.disableTls }}{{ .Values.server.grpcEndpoint | replace "https://" "http://" | quote }}{{ else }}{{ .Values.server.grpcEndpoint | quote }}{{ end }} {{- if .Values.server.sshGatewayHost }} diff --git a/deploy/helm/openshell/values.yaml b/deploy/helm/openshell/values.yaml index 18e671375..1c820c8b1 100644 --- a/deploy/helm/openshell/values.yaml +++ b/deploy/helm/openshell/values.yaml @@ -78,6 +78,14 @@ server: # (Always for :latest, IfNotPresent otherwise). Set to "Always" for dev # clusters so new images are picked up without manual eviction. sandboxImagePullPolicy: "" + # Image that provides the openshell-sandbox supervisor binary. An init + # container copies the binary from this image into a shared emptyDir volume + # before the sandbox container starts. Should match the gateway image tag + # so the supervisor and gateway versions stay in sync. + supervisorImage: "ghcr.io/nvidia/openshell/supervisor:latest" + # Kubernetes imagePullPolicy for the supervisor init container. + # Empty = Kubernetes default. + supervisorImagePullPolicy: "" # gRPC endpoint for sandboxes to callback to OpenShell (must be reachable from pods) grpcEndpoint: "https://openshell.openshell.svc.cluster.local:8080" # Public host/port returned to CLI clients for SSH proxy CONNECT requests. From 1258b024118d68b23535fc5709c180c0faca0f3a Mon Sep 17 00:00:00 2001 From: Taylor Mutch Date: Mon, 4 May 2026 12:26:47 -0700 Subject: [PATCH 3/3] chore(driver-kubernetes): fix rustfmt formatting --- crates/openshell-driver-kubernetes/src/driver.rs | 16 +++++++++------- crates/openshell-driver-kubernetes/src/main.rs | 6 +++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/openshell-driver-kubernetes/src/driver.rs b/crates/openshell-driver-kubernetes/src/driver.rs index 0b0920554..b0c2e5917 100644 --- a/crates/openshell-driver-kubernetes/src/driver.rs +++ b/crates/openshell-driver-kubernetes/src/driver.rs @@ -692,7 +692,10 @@ fn supervisor_volume_mount() -> serde_json::Value { /// (e.g. at `/usr/local/bin/openshell-sandbox`). The init container resolves /// the binary via `command -v` and copies it into the shared emptyDir volume /// so the agent container can execute it from a fixed, writable path. -fn supervisor_init_container(supervisor_image: &str, supervisor_image_pull_policy: &str) -> serde_json::Value { +fn supervisor_init_container( + supervisor_image: &str, + supervisor_image_pull_policy: &str, +) -> serde_json::Value { let copy_cmd = format!( "set -e && \ mkdir -p {SUPERVISOR_MOUNT_PATH} && \ @@ -752,7 +755,10 @@ fn apply_supervisor_sideload( .or_insert_with(|| serde_json::json!([])) .as_array_mut(); if let Some(init_containers) = init_containers { - init_containers.push(supervisor_init_container(supervisor_image, supervisor_image_pull_policy)); + init_containers.push(supervisor_init_container( + supervisor_image, + supervisor_image_pull_policy, + )); } // 3. Find the agent container and add volume mount + command override @@ -1048,7 +1054,6 @@ fn sandbox_template_to_k8s( host_gateway_ip: &str, inject_workspace: bool, ) -> serde_json::Value { - let mut metadata = serde_json::Map::new(); if !template.labels.is_empty() { metadata.insert("labels".to_string(), serde_json::json!(template.labels)); @@ -1543,10 +1548,7 @@ mod tests { .as_array() .expect("initContainers should exist"); assert_eq!(init_containers.len(), 1); - assert_eq!( - init_containers[0]["name"], - SUPERVISOR_INIT_CONTAINER_NAME - ); + assert_eq!(init_containers[0]["name"], SUPERVISOR_INIT_CONTAINER_NAME); assert_eq!(init_containers[0]["image"], "supervisor-image:latest"); assert_eq!(init_containers[0]["imagePullPolicy"], "IfNotPresent"); diff --git a/crates/openshell-driver-kubernetes/src/main.rs b/crates/openshell-driver-kubernetes/src/main.rs index 818055efc..26d323f56 100644 --- a/crates/openshell-driver-kubernetes/src/main.rs +++ b/crates/openshell-driver-kubernetes/src/main.rs @@ -78,9 +78,9 @@ async fn main() -> Result<()> { namespace: args.sandbox_namespace, default_image: args.sandbox_image.unwrap_or_default(), image_pull_policy: args.sandbox_image_pull_policy.unwrap_or_default(), - supervisor_image: args.supervisor_image.unwrap_or_else(|| { - openshell_core::config::DEFAULT_SUPERVISOR_IMAGE.to_string() - }), + supervisor_image: args + .supervisor_image + .unwrap_or_else(|| openshell_core::config::DEFAULT_SUPERVISOR_IMAGE.to_string()), supervisor_image_pull_policy: args.supervisor_image_pull_policy.unwrap_or_default(), grpc_endpoint: args.grpc_endpoint.unwrap_or_default(), ssh_socket_path: args.sandbox_ssh_socket_path,