managed.rs (12167B)
1 use core::fmt; 2 3 use radroots_runtime_distribution::HardenedServiceTarget; 4 use radroots_runtime_paths::{InstanceId, RadrootsPathProfile, RuntimeContext, ServiceId}; 5 #[cfg(any(target_os = "linux", target_os = "macos"))] 6 use radroots_service_host::AdminTransportLimits; 7 8 #[cfg(any(target_os = "linux", target_os = "macos"))] 9 use crate::ManagedRuntimeStatusClient; 10 use crate::{ 11 ManagedCliCommand, ManagedCliInvocation, ManagementModeContract, 12 RadrootsRuntimeManagementContract, RadrootsRuntimeManagerError, 13 }; 14 15 /// Validated frozen runtime-management contract. 16 /// 17 /// Instance identity, profile, and canonical paths are deliberately absent; 18 /// those values enter only through a sealed [`RuntimeContext`] when a target is 19 /// resolved. 20 /// 21 /// ```compile_fail 22 /// use radroots_runtime_manager::ManagedRuntimeContext; 23 /// 24 /// let _ = ManagedRuntimeContext { contract: todo!() }; 25 /// ``` 26 #[derive(Clone)] 27 pub struct ManagedRuntimeContext { 28 contract: RadrootsRuntimeManagementContract, 29 } 30 31 impl ManagedRuntimeContext { 32 pub fn new( 33 contract: RadrootsRuntimeManagementContract, 34 ) -> Result<Self, RadrootsRuntimeManagerError> { 35 crate::validate_hardened_management_contract(&contract)?; 36 Ok(Self { contract }) 37 } 38 39 #[must_use] 40 pub fn contract(&self) -> &RadrootsRuntimeManagementContract { 41 &self.contract 42 } 43 } 44 45 impl fmt::Debug for ManagedRuntimeContext { 46 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 47 formatter 48 .debug_struct("ManagedRuntimeContext") 49 .field("schema", &self.contract.schema) 50 .field("schema_version", &self.contract.schema_version) 51 .finish_non_exhaustive() 52 } 53 } 54 55 /// Sealed management capability for one validated Myc or RHI runtime context. 56 /// 57 /// The target owns the sole service-instance identity/profile/path authority. 58 /// It exposes typed CLI-v1 and status-v1 integration but no filesystem, 59 /// process, PID, log, credential, or distribution-artifact mutation surface. 60 /// 61 /// ```compile_fail 62 /// use radroots_runtime_manager::ManagedRuntimeTarget; 63 /// 64 /// let _ = ManagedRuntimeTarget { 65 /// context: todo!(), 66 /// service_target: todo!(), 67 /// management_mode: String::new(), 68 /// mode_contract: todo!(), 69 /// }; 70 /// ``` 71 #[derive(Clone)] 72 pub struct ManagedRuntimeTarget { 73 context: RuntimeContext, 74 service_target: HardenedServiceTarget, 75 management_mode: String, 76 mode_contract: ManagementModeContract, 77 } 78 79 impl ManagedRuntimeTarget { 80 #[must_use] 81 pub fn runtime_context(&self) -> &RuntimeContext { 82 &self.context 83 } 84 85 #[must_use] 86 pub fn service_id(&self) -> &ServiceId { 87 self.context.service() 88 } 89 90 #[must_use] 91 pub fn instance_id(&self) -> &InstanceId { 92 self.context.instance() 93 } 94 95 #[must_use] 96 pub fn profile(&self) -> RadrootsPathProfile { 97 self.context.profile() 98 } 99 100 #[must_use] 101 pub fn service_target(&self) -> &HardenedServiceTarget { 102 &self.service_target 103 } 104 105 #[must_use] 106 pub fn management_mode(&self) -> &str { 107 &self.management_mode 108 } 109 110 #[must_use] 111 pub fn mode_contract(&self) -> &ManagementModeContract { 112 &self.mode_contract 113 } 114 115 pub fn cli_invocation( 116 &self, 117 command: ManagedCliCommand, 118 ) -> Result<ManagedCliInvocation, RadrootsRuntimeManagerError> { 119 ManagedCliInvocation::for_context(&self.context, command) 120 } 121 122 #[cfg(any(target_os = "linux", target_os = "macos"))] 123 pub fn status_client( 124 &self, 125 limits: AdminTransportLimits, 126 ) -> Result<ManagedRuntimeStatusClient, RadrootsRuntimeManagerError> { 127 ManagedRuntimeStatusClient::for_context(&self.context, limits) 128 } 129 } 130 131 impl fmt::Debug for ManagedRuntimeTarget { 132 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 133 formatter 134 .debug_struct("ManagedRuntimeTarget") 135 .field("service_id", self.context.service()) 136 .field("instance_id", self.context.instance()) 137 .field("profile", &self.context.profile()) 138 .field("management_mode", &self.management_mode) 139 .field("paths", &"[redacted]") 140 .finish_non_exhaustive() 141 } 142 } 143 144 pub fn resolve_runtime_target( 145 context: &ManagedRuntimeContext, 146 runtime_context: RuntimeContext, 147 ) -> Result<ManagedRuntimeTarget, RadrootsRuntimeManagerError> { 148 let service_target = context 149 .contract 150 .service_targets 151 .get(runtime_context.service()) 152 .cloned() 153 .ok_or(RadrootsRuntimeManagerError::UnsupportedServiceTarget)?; 154 let management_mode = 155 active_management_mode_for_profile(&context.contract, runtime_context.profile())?; 156 let mode_contract = context 157 .contract 158 .mode 159 .get(management_mode) 160 .cloned() 161 .ok_or(RadrootsRuntimeManagerError::InvalidContract)?; 162 Ok(ManagedRuntimeTarget { 163 context: runtime_context, 164 service_target, 165 management_mode: management_mode.to_owned(), 166 mode_contract, 167 }) 168 } 169 170 fn active_management_mode_for_profile( 171 contract: &RadrootsRuntimeManagementContract, 172 profile: RadrootsPathProfile, 173 ) -> Result<&str, RadrootsRuntimeManagerError> { 174 let profile_id = profile.to_string(); 175 contract 176 .mode 177 .iter() 178 .find(|(_, mode)| { 179 mode.contract_state == "active" 180 && mode 181 .supported_profiles 182 .iter() 183 .any(|entry| entry == &profile_id) 184 }) 185 .map(|(mode_id, _)| mode_id.as_str()) 186 .ok_or(RadrootsRuntimeManagerError::UnsupportedProfile) 187 } 188 189 #[cfg(test)] 190 mod tests { 191 use std::path::PathBuf; 192 193 use radroots_runtime_paths::{ 194 InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, 195 RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId, 196 }; 197 #[cfg(any(target_os = "linux", target_os = "macos"))] 198 use radroots_service_host::AdminTransportLimits; 199 200 use super::{ 201 ManagedRuntimeContext, active_management_mode_for_profile, resolve_runtime_target, 202 }; 203 use crate::{ 204 HARDENED_MANAGEMENT_CONTRACT, ManagedCliCommand, RadrootsRuntimeManagerError, 205 parse_contract_str, 206 }; 207 208 fn management_context() -> ManagedRuntimeContext { 209 ManagedRuntimeContext::new( 210 parse_contract_str(HARDENED_MANAGEMENT_CONTRACT).expect("contract"), 211 ) 212 .expect("management context") 213 } 214 215 fn runtime_context( 216 profile: RadrootsPathProfile, 217 service: &str, 218 instance: &str, 219 ) -> RuntimeContext { 220 let root = (profile == RadrootsPathProfile::RepoLocal) 221 .then(|| PathBuf::from("/sensitive/project-root")); 222 RuntimeContext::resolve( 223 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 224 RuntimeContextBootstrap::new( 225 profile, 226 root, 227 if profile == RadrootsPathProfile::RepoLocal { 228 RuntimeContextSource::BootstrapCli 229 } else { 230 RuntimeContextSource::SafeDefault 231 }, 232 RuntimeContextSource::BootstrapCli, 233 ) 234 .expect("bootstrap"), 235 ServiceId::new(service).expect("service"), 236 InstanceId::new(instance).expect("instance"), 237 ) 238 .expect("runtime context") 239 } 240 241 #[test] 242 fn context_accepts_only_the_exact_contract() { 243 let context = management_context(); 244 assert_eq!(context.contract().service_targets.len(), 2); 245 246 let mut direct = parse_contract_str(HARDENED_MANAGEMENT_CONTRACT).expect("contract"); 247 direct.lifecycle.actions.push("start".to_owned()); 248 assert!(matches!( 249 ManagedRuntimeContext::new(direct), 250 Err(RadrootsRuntimeManagerError::InvalidContract) 251 )); 252 } 253 254 #[test] 255 fn exact_myc_and_rhi_contexts_resolve_without_identity_duplication() { 256 let management = management_context(); 257 let myc = resolve_runtime_target( 258 &management, 259 runtime_context(RadrootsPathProfile::RepoLocal, "myc", "primary"), 260 ) 261 .expect("myc target"); 262 let rhi = resolve_runtime_target( 263 &management, 264 runtime_context(RadrootsPathProfile::ServiceHost, "rhi", "secondary"), 265 ) 266 .expect("rhi target"); 267 268 assert_eq!(myc.service_id().as_str(), "myc"); 269 assert_eq!(myc.instance_id().as_str(), "primary"); 270 assert_eq!(myc.profile(), RadrootsPathProfile::RepoLocal); 271 assert_eq!(myc.service_target().service_id(), myc.service_id()); 272 assert_eq!(myc.management_mode(), "interactive_user_managed"); 273 assert!(!myc.mode_contract().service_manager_integration); 274 let invocation = myc 275 .cli_invocation(ManagedCliCommand::Doctor) 276 .expect("Myc doctor invocation"); 277 assert_eq!(invocation.profile(), RadrootsPathProfile::RepoLocal); 278 assert_eq!(invocation.command(), ManagedCliCommand::Doctor); 279 280 #[cfg(any(target_os = "linux", target_os = "macos"))] 281 { 282 let status_client = myc 283 .status_client(AdminTransportLimits::DEFAULT) 284 .expect("Myc status client"); 285 assert!(!format!("{status_client:?}").contains("sensitive")); 286 } 287 288 assert_eq!(rhi.service_id().as_str(), "rhi"); 289 assert_eq!(rhi.instance_id().as_str(), "secondary"); 290 assert_eq!(rhi.management_mode(), "service_host_managed"); 291 assert!(rhi.mode_contract().service_manager_integration); 292 assert_eq!(rhi.runtime_context().service(), rhi.service_id()); 293 294 for rendered in [ 295 format!("{management:?}"), 296 format!("{myc:?}"), 297 format!("{rhi:?}"), 298 ] { 299 assert!(!rendered.contains("sensitive")); 300 assert!(!rendered.contains("state.sqlite")); 301 assert!(!rendered.contains("admin.sock")); 302 } 303 } 304 305 #[test] 306 fn unsupported_service_and_profile_fail_without_fallback() { 307 let management = management_context(); 308 let unsupported = runtime_context(RadrootsPathProfile::ServiceHost, "radrootsd", "default"); 309 assert!(matches!( 310 resolve_runtime_target(&management, unsupported), 311 Err(RadrootsRuntimeManagerError::UnsupportedServiceTarget) 312 )); 313 314 let mobile = RuntimeContextBootstrap::new( 315 RadrootsPathProfile::MobileNative, 316 None, 317 RuntimeContextSource::SafeDefault, 318 RuntimeContextSource::BootstrapCli, 319 ); 320 assert!(mobile.is_err()); 321 } 322 323 #[test] 324 fn inactive_management_mode_never_matches_a_supported_profile() { 325 let mut contract = parse_contract_str(HARDENED_MANAGEMENT_CONTRACT).expect("contract"); 326 contract 327 .mode 328 .get_mut("interactive_user_managed") 329 .expect("interactive mode") 330 .contract_state = "inactive".to_owned(); 331 332 assert_eq!( 333 active_management_mode_for_profile(&contract, RadrootsPathProfile::RepoLocal), 334 Err(RadrootsRuntimeManagerError::UnsupportedProfile) 335 ); 336 } 337 338 #[test] 339 fn production_manager_has_no_direct_io_process_or_artifact_authority() { 340 let source = include_str!("managed.rs") 341 .split("\n#[cfg(test)]") 342 .next() 343 .expect("production source"); 344 for forbidden in [ 345 "std::fs", 346 "std::process", 347 "load_registry", 348 "save_registry", 349 "register_instance", 350 "remove_instance", 351 "ManagedRuntimeInstancePaths", 352 "ManagedRuntimeArtifactName", 353 "inspect_runtime_", 354 "start_process", 355 "stop_process", 356 "extract_binary_archive", 357 ] { 358 assert!( 359 !source.contains(forbidden), 360 "manager retained `{forbidden}`" 361 ); 362 } 363 } 364 }