diff --git a/Cargo.lock b/Cargo.lock index 76b760f40..9e6be276f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1358,6 +1358,7 @@ dependencies = [ "openshell-sdk", "serde", "serde_json", + "sha2", "thiserror 2.0.18", "tokio", "tonic", diff --git a/environment-providers/openshell/Cargo.toml b/environment-providers/openshell/Cargo.toml index d7618b894..d713f4c55 100644 --- a/environment-providers/openshell/Cargo.toml +++ b/environment-providers/openshell/Cargo.toml @@ -20,6 +20,7 @@ openshell-policy = { git = "https://github.com/NVIDIA/OpenShell", rev = "6648bd0 openshell-sdk = { git = "https://github.com/NVIDIA/OpenShell", rev = "6648bd0c290efbc41ba131ee9831ee45cd431f94" } serde = { workspace = true } serde_json = { workspace = true } +sha2 = "0.10" thiserror = { workspace = true } tokio = { version = "1.43", features = ["macros", "rt-multi-thread"] } tonic = "0.14" diff --git a/environment-providers/openshell/README.md b/environment-providers/openshell/README.md index 34de2e06e..7c9352b3b 100644 --- a/environment-providers/openshell/README.md +++ b/environment-providers/openshell/README.md @@ -55,7 +55,7 @@ Use a registered OpenShell gateway and match the CLI, gateway, and runtime compo Environment configuration uses `provider="openshell"` and `control_location="in_env_control"`. The gateway connection selects a named workspace or explicitly uses OpenShell's default workspace. Workspace and artifact roots are absolute POSIX paths inside the sandbox; `runtime.artifacts` remains the host collection destination. Select an immutable image digest or local Docker image ID. Task images contain the SDK, `fabric-agent-ctl`, the selected adapter, and the agent/harness. -When `settings.sandbox_name` is omitted, the OpenShell provider derives a stable name from the Fabric environment ID so it can reconcile a create request whose response times out. +When `settings.sandbox_name` is omitted, the OpenShell provider derives a stable 19-character name from the Fabric environment ID, within OpenShell's sandbox-name limit. This lets it reconcile a create request whose response times out. With an existing Fabric-ready configuration, use the normal SDK lifecycle: diff --git a/environment-providers/openshell/src/lib.rs b/environment-providers/openshell/src/lib.rs index 7a252f6dd..e4357c3ce 100644 --- a/environment-providers/openshell/src/lib.rs +++ b/environment-providers/openshell/src/lib.rs @@ -21,6 +21,7 @@ use openshell_sdk::{ }; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value, json}; +use sha2::{Digest, Sha256}; use thiserror::Error; const PROTOCOL_VERSION: &str = "fabric.environment-provider.v1alpha1"; @@ -32,6 +33,15 @@ const DEFAULT_EXEC_TIMEOUT_SECONDS: u64 = 30; const GATEWAY_CALL_TIMEOUT_SECONDS: u64 = 30; const MAX_OPERATION_TIMEOUT_SECONDS: u64 = 600; +// OpenShell 0.1.2 limits DNS-routable sandbox names to 19 bytes. Keep the +// environment ID in the label for exact identity checks during reconciliation. +fn generated_sandbox_name(environment_id: &str) -> String { + let digest = Sha256::digest(environment_id.as_bytes()); + let mut prefix = [0_u8; 8]; + prefix.copy_from_slice(&digest[..8]); + format!("nf-{:016x}", u64::from_be_bytes(prefix)) +} + /// Provider process failure. #[derive(Debug, Error)] pub enum ProviderError { @@ -602,7 +612,7 @@ where let sandbox_name = settings .sandbox_name .clone() - .unwrap_or_else(|| format!("nemo-fabric-{environment_id}")); + .unwrap_or_else(|| generated_sandbox_name(environment_id)); let sandbox = match bounded_gateway_call( "create", Duration::from_secs(GATEWAY_CALL_TIMEOUT_SECONDS), @@ -2054,6 +2064,20 @@ mod tests { use super::*; use std::sync::{Arc, Mutex}; + #[test] + fn generated_sandbox_names_fit_the_openshell_gateway_contract() { + let id = "nemo-fabric-environment-1760559084-12345-678"; + let name = generated_sandbox_name(id); + assert_eq!(name.len(), 19); + assert!(name.starts_with("nf-")); + assert!( + name.bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') + ); + assert_eq!(name, generated_sandbox_name(id)); + assert_ne!(name, generated_sandbox_name("another-environment-id")); + } + #[tokio::test] async fn bounded_gateway_call_reports_timeouts_without_sdk_details() { let error = bounded_gateway_call( @@ -2993,6 +3017,15 @@ mod tests { workspace: Option<&str>, request: SandboxCreate, ) -> Result { + let name = request.name.as_deref().expect("sandbox name"); + assert!( + name.len() <= 19, + "OpenShell rejects sandbox names longer than 19 bytes: {name}" + ); + assert!( + name.bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') + ); assert_eq!(request.environment["FABRIC_VISIBLE"], "yes"); assert_eq!(request.command, ["fabric-agent-ctl", "serve"]); self.calls.lock().expect("calls").push(format!(