1use crate::control::command::CommandResult;
8use crate::control::handle::SupervisorHandle;
9use crate::dashboard::config::ValidatedDashboardIpcConfig;
10use crate::dashboard::error::DashboardError;
11use crate::dashboard::model::{
12 ControlCommandKind, ControlCommandRequest, ControlCommandResult, DashboardCurrentState,
13 DashboardState, TargetProcessRegistration, dashboard_command_result_value,
14 runtime_state_from_child_runtime_record,
15};
16use crate::dashboard::protocol::{
17 DASHBOARD_IPC_PROTOCOL_VERSION, IpcMethod, IpcRequest, IpcResponse, IpcResult,
18 decode_command_params,
19};
20use crate::dashboard::registration::build_registration_payload;
21use crate::dashboard::runtime::DEFAULT_MAX_FRAME_BYTES;
22use crate::dashboard::state::{DashboardStateInput, build_dashboard_state};
23use crate::id::types::{ChildId, SupervisorPath};
24use crate::ipc::security::peer_identity::PeerIdentity;
25use crate::ipc::security::{CheckOutcome, IpcSecurityPipeline};
26use crate::journal::ring::EventJournal;
27use crate::spec::supervisor::SupervisorSpec;
28use crate::state::supervisor::SupervisorState;
29use std::os::unix::fs::{FileTypeExt, PermissionsExt};
30use std::os::unix::net::UnixStream as StdUnixStream;
31use std::sync::atomic::{AtomicU64, Ordering};
32use std::sync::{Arc, Mutex};
33use tokio::net::UnixListener;
34
35pub struct DashboardIpcService {
37 config: ValidatedDashboardIpcConfig,
39 spec: SupervisorSpec,
41 state: SupervisorState,
43 journal: EventJournal,
45 handle: Option<SupervisorHandle>,
47 state_generation: AtomicU64,
52 security_pipeline: Option<Arc<Mutex<IpcSecurityPipeline>>>,
54}
55
56impl DashboardIpcService {
57 pub fn new(
70 config: ValidatedDashboardIpcConfig,
71 spec: SupervisorSpec,
72 state: SupervisorState,
73 journal: EventJournal,
74 ) -> Self {
75 Self {
76 config,
77 spec,
78 state,
79 journal,
80 handle: None,
81 state_generation: AtomicU64::new(1),
82 security_pipeline: None,
83 }
84 }
85
86 pub fn with_handle(mut self, handle: SupervisorHandle) -> Self {
96 self.handle = Some(handle);
97 self
98 }
99
100 pub fn with_security_pipeline(mut self, pipeline: IpcSecurityPipeline) -> Self {
110 self.security_pipeline = Some(Arc::new(Mutex::new(pipeline)));
111 self
112 }
113
114 pub fn max_frame_bytes(&self) -> usize {
124 self.security_pipeline
125 .as_ref()
126 .and_then(|p| p.lock().ok())
127 .and_then(|guard| guard.request_size_limit_bytes())
128 .unwrap_or(DEFAULT_MAX_FRAME_BYTES)
129 }
130
131 pub fn registration_payload(&self) -> Result<TargetProcessRegistration, DashboardError> {
141 build_registration_payload(&self.config)
142 }
143
144 pub async fn handle_request(
177 &self,
178 request: IpcRequest,
179 peer: &PeerIdentity,
180 connection_id: &str,
181 raw_body_len: usize,
182 ) -> IpcResponse {
183 let method = request.method.clone();
184 let request_id = request.request_id.clone();
185 let is_high_risk = is_high_risk_command(&method, self);
186
187 if let Some(ref pipeline) = self.security_pipeline {
188 let mut guard = match pipeline.lock() {
191 Ok(guard) => guard,
192 Err(poisoned) => {
193 tracing::error!(
194 target: "rust_supervisor::ipc::security",
195 %method,
196 "pipeline mutex poisoned, recovering"
197 );
198 poisoned.into_inner()
199 }
200 };
201
202 match guard.check(&method, raw_body_len, peer, connection_id) {
204 CheckOutcome::Denied(err) => {
205 let err_code = err.code.clone();
206 let _ = self.audit_or_fail(
208 &mut guard,
209 &method,
210 peer,
211 false,
212 Some(&err),
213 &err_code,
214 is_high_risk,
215 &request_id,
216 );
217 return IpcResponse::error(request.request_id.clone(), err);
218 }
219 CheckOutcome::Passed => {}
220 }
221
222 if let Some(cached_json) = guard.check_idempotency(&request_id) {
226 let method = method.clone();
227 let peer_clone = peer.clone();
228 drop(guard);
229 if let Some(ref pipeline) = self.security_pipeline {
231 let mut guard = match pipeline.lock() {
232 Ok(guard) => guard,
233 Err(poisoned) => {
234 tracing::error!(
235 target: "rust_supervisor::ipc::security",
236 %method,
237 "pipeline mutex poisoned during C8 audit"
238 );
239 poisoned.into_inner()
240 }
241 };
242 let _ = self.audit_or_fail(
243 &mut guard,
244 &method,
245 &peer_clone,
246 true,
247 None,
248 "c8_idempotency_cache_hit",
249 is_high_risk,
250 &request_id,
251 );
252 }
253 return serde_json::from_str(&cached_json).unwrap_or_else(|_| {
255 IpcResponse::error(
256 request_id,
257 DashboardError::new(
258 "idempotency_cache_corrupted",
259 "c8_idempotency",
260 Some(self.config.target_id.clone()),
261 "cached response failed to deserialize".to_owned(),
262 false,
263 ),
264 )
265 });
266 }
267
268 if let Err(err) = guard.check_replay_and_record(&request_id) {
271 let err_code = err.code.clone();
272 let _ = self.audit_or_fail(
274 &mut guard,
275 &method,
276 peer,
277 false,
278 Some(&err),
279 &err_code,
280 is_high_risk,
281 &request_id,
282 );
283 return IpcResponse::error(request.request_id.clone(), err);
284 }
285 drop(guard);
286 }
287
288 let dispatch_result = self.dispatch(&request).await;
290 let mut response = match &dispatch_result {
291 Ok(result) => IpcResponse::ok(request.request_id.clone(), result.clone()),
292 Err(error) => IpcResponse::error(request.request_id.clone(), error.clone()),
293 };
294
295 if let Some(ref pipeline) = self.security_pipeline {
297 let mut guard = match pipeline.lock() {
298 Ok(guard) => guard,
299 Err(poisoned) => {
300 tracing::error!(
301 target: "rust_supervisor::ipc::security",
302 %method,
303 "pipeline mutex poisoned during post-dispatch"
304 );
305 poisoned.into_inner()
306 }
307 };
308
309 if let Ok(response_json) = serde_json::to_string(&response) {
311 guard.cache_result(&request_id, &response_json);
312 }
313
314 let (allowed, denial_error, denial_code): (bool, Option<&DashboardError>, &str) =
316 match &dispatch_result {
317 Ok(_) => (true, None, "dispatch_ok"),
318 Err(err) => (false, Some(err), err.code.as_str()),
319 };
320 if let Err(audit_err) = self.audit_or_fail(
323 &mut guard,
324 &method,
325 peer,
326 allowed,
327 denial_error,
328 denial_code,
329 is_high_risk,
330 &request_id,
331 ) && is_high_risk
332 {
333 tracing::error!(
334 target: "rust_supervisor::ipc::security::audit",
335 %method,
336 %request_id,
337 ?audit_err,
338 "HIGH-RISK command denied because audit write failed (fail-closed)"
339 );
340 response = IpcResponse::error(request.request_id.clone(), audit_err);
341 }
342 }
343
344 response
345 }
346
347 #[allow(clippy::too_many_arguments)]
354 fn audit_or_fail(
355 &self,
356 guard: &mut std::sync::MutexGuard<'_, IpcSecurityPipeline>,
357 method: &str,
358 peer: &PeerIdentity,
359 allowed: bool,
360 denial_error: Option<&DashboardError>,
361 denial_code: &str,
362 is_high_risk: bool,
363 request_id: &str,
364 ) -> Result<(), DashboardError> {
365 guard
366 .write_audit(
367 method,
368 peer,
369 allowed,
370 denial_error,
371 denial_code,
372 is_high_risk,
373 )
374 .map_err(|err| {
375 tracing::error!(
376 target: "rust_supervisor::ipc::security::audit",
377 %method,
378 %request_id,
379 high_risk = is_high_risk,
380 ?err,
381 "audit write failed"
382 );
383 err
384 })
385 }
386
387 async fn dispatch(&self, request: &IpcRequest) -> Result<IpcResult, DashboardError> {
397 let method = IpcMethod::parse(&request.method)?;
398 match method {
399 IpcMethod::Hello => Ok(IpcResult::Hello {
400 protocol_version: DASHBOARD_IPC_PROTOCOL_VERSION.to_owned(),
401 registration: self.registration_payload()?,
402 }),
403 IpcMethod::CurrentState => {
404 let state = self.current_dashboard_state().await?;
405 Ok(IpcResult::State {
406 target_id: state.target.target_id.clone(),
407 state: Box::new(state),
408 })
409 }
410 IpcMethod::EventsSubscribe => {
411 require_session_trigger(request, &self.config.target_id, self)?;
412 Ok(IpcResult::Subscription {
413 target_id: self.config.target_id.clone(),
414 subscription: "events".to_owned(),
415 })
416 }
417 IpcMethod::LogsTail => {
418 require_session_trigger(request, &self.config.target_id, self)?;
419 Ok(IpcResult::Subscription {
420 target_id: self.config.target_id.clone(),
421 subscription: "logs".to_owned(),
422 })
423 }
424 IpcMethod::CommandRestartChild
425 | IpcMethod::CommandPauseChild
426 | IpcMethod::CommandResumeChild
427 | IpcMethod::CommandQuarantineChild
428 | IpcMethod::CommandRemoveChild
429 | IpcMethod::CommandAddChild
430 | IpcMethod::CommandShutdownTree => self.command_result(request).await,
431 }
432 }
433
434 pub async fn current_dashboard_state(&self) -> Result<DashboardState, DashboardError> {
450 let generation = self.state_generation.load(Ordering::Relaxed);
453 let registration = self.registration_payload().ok();
454 let mut state = build_dashboard_state(
455 DashboardStateInput {
456 target_id: self.config.target_id.clone(),
457 display_name: registration
458 .as_ref()
459 .map(|registration| registration.display_name.clone())
460 .unwrap_or_else(|| self.config.target_id.clone()),
461 state_generation: generation,
462 recent_limit: 128,
463 },
464 &self.spec,
465 &self.state,
466 &self.journal,
467 );
468 if let Some(handle) = self.handle.as_ref() {
469 let result = handle.current_state().await.map_err(|error| {
470 DashboardError::new(
471 "current_state_failed",
472 "state",
473 Some(self.config.target_id.clone()),
474 error.to_string(),
475 true,
476 )
477 })?;
478 if let CommandResult::CurrentState {
479 state: runtime_state,
480 } = result
481 {
482 let dashboard_state = DashboardCurrentState::from_current_state(&runtime_state);
483 state.runtime_state = runtime_state
485 .child_runtime_records
486 .iter()
487 .map(|record| {
488 runtime_state_from_child_runtime_record(
489 record,
490 runtime_state.shutdown_completed,
491 )
492 })
493 .collect();
494 state.child_runtime_records = dashboard_state.child_runtime_records;
495 }
496 }
497 Ok(state)
498 }
499
500 async fn command_result(&self, request: &IpcRequest) -> Result<IpcResult, DashboardError> {
510 let command = decode_command_params(request)?;
511 validate_command(&command)?;
512 if command.target_id != self.config.target_id {
513 return Err(DashboardError::validation(
514 "command_validate",
515 Some(self.config.target_id.clone()),
516 "command target_id must match target process",
517 ));
518 }
519 let result = if let Some(handle) = self.handle.as_ref() {
520 execute_command(handle, &command).await
521 } else {
522 Err(DashboardError::target_unavailable(
523 "command_dispatch",
524 command.target_id.clone(),
525 "runtime control handle is not attached",
526 ))
527 };
528 let _ = self.state_generation.fetch_add(1, Ordering::Relaxed);
533 let result = match result {
534 Ok(result) => {
535 let state_delta = dashboard_command_result_value(&result).map_err(|error| {
536 DashboardError::new(
537 "command_result_model_failed",
538 "command_dispatch",
539 Some(command.target_id.clone()),
540 format!("failed to map command result: {error}"),
541 false,
542 )
543 })?;
544 ControlCommandResult {
545 command_id: command.command_id.clone(),
546 target_id: command.target_id.clone(),
547 accepted: true,
548 status: "completed".to_owned(),
549 error: None,
550 state_delta: Some(state_delta),
551 completed_at_unix_nanos: Some(unix_nanos_now()),
552 }
553 }
554 Err(error) => ControlCommandResult {
555 command_id: command.command_id.clone(),
556 target_id: command.target_id.clone(),
557 accepted: false,
558 status: "failed".to_owned(),
559 error: Some(error),
560 state_delta: None,
561 completed_at_unix_nanos: Some(unix_nanos_now()),
562 },
563 };
564 Ok(IpcResult::CommandResult {
565 target_id: command.target_id,
566 result,
567 })
568 }
569}
570
571pub fn bind_dashboard_listener(
581 config: &ValidatedDashboardIpcConfig,
582) -> Result<UnixListener, DashboardError> {
583 prepare_socket_path(config)?;
584 if let Some(parent) = config.path.parent() {
586 std::fs::create_dir_all(parent).map_err(|error| {
587 DashboardError::new(
588 "ipc_parent_dir_creation_failed",
589 "ipc_bind",
590 Some(config.target_id.clone()),
591 format!("failed to create IPC parent directory: {error}"),
592 false,
593 )
594 })?;
595 }
596 let listener = UnixListener::bind(&config.path).map_err(|error| {
597 DashboardError::new(
598 "ipc_bind_failed",
599 "ipc_bind",
600 Some(config.target_id.clone()),
601 format!("failed to bind target IPC socket: {error}"),
602 true,
603 )
604 })?;
605
606 let mode = parse_permissions_string(&config.permissions, &config.target_id)?;
608 set_socket_permissions(&config.path, mode, &config.target_id)?;
609
610 Ok(listener)
611}
612
613fn parse_permissions_string(perm_str: &str, target_id: &str) -> Result<u32, DashboardError> {
625 if perm_str.len() != 4 || !perm_str.bytes().all(|b| b.is_ascii_digit()) {
627 return Err(DashboardError::validation(
628 "ipc_bind",
629 Some(target_id.to_owned()),
630 format!("dashboard.permissions must be a 4-digit octal string, got \"{perm_str}\""),
631 ));
632 }
633 let mode = u32::from_str_radix(perm_str, 8).map_err(|_| {
634 DashboardError::validation(
635 "ipc_bind",
636 Some(target_id.to_owned()),
637 format!("dashboard.permissions \"{perm_str}\" is not valid octal"),
638 )
639 })?;
640 if mode & 0o002 != 0 {
642 return Err(DashboardError::validation(
643 "ipc_bind",
644 Some(target_id.to_owned()),
645 format!(
646 "dashboard.permissions \"{perm_str}\" grants world-write access, \
647 which is not allowed for Unix domain sockets"
648 ),
649 ));
650 }
651 Ok(mode)
652}
653
654fn set_socket_permissions(
666 path: &std::path::Path,
667 mode: u32,
668 target_id: &str,
669) -> Result<(), DashboardError> {
670 let permissions = std::fs::Permissions::from_mode(mode);
671 std::fs::set_permissions(path, permissions).map_err(|error| {
672 DashboardError::new(
673 "ipc_set_permissions_failed",
674 "ipc_bind",
675 Some(target_id.to_owned()),
676 format!("failed to set socket permissions to {mode:#o}: {error}"),
677 true,
678 )
679 })
680}
681
682fn prepare_socket_path(config: &ValidatedDashboardIpcConfig) -> Result<(), DashboardError> {
692 let metadata = match std::fs::symlink_metadata(&config.path) {
693 Ok(metadata) => metadata,
694 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
695 Err(error) => {
696 return Err(DashboardError::new(
697 "ipc_path_metadata_failed",
698 "ipc_bind",
699 Some(config.target_id.clone()),
700 format!("failed to inspect IPC path: {error}"),
701 false,
702 ));
703 }
704 };
705 match config.bind_mode {
706 crate::config::configurable::DashboardIpcBindMode::CreateNew => {
707 Err(DashboardError::validation(
708 "ipc_bind",
709 Some(config.target_id.clone()),
710 "IPC path already exists and bind_mode is create_new",
711 ))
712 }
713 crate::config::configurable::DashboardIpcBindMode::ReplaceStale => {
714 if metadata.file_type().is_symlink() {
715 return Err(DashboardError::validation(
716 "ipc_bind",
717 Some(config.target_id.clone()),
718 "IPC path must not be a symlink",
719 ));
720 }
721 if !metadata.file_type().is_socket() {
722 return Err(DashboardError::validation(
723 "ipc_bind",
724 Some(config.target_id.clone()),
725 "IPC path must be a Unix socket before stale replacement",
726 ));
727 }
728 if StdUnixStream::connect(&config.path).is_ok() {
729 return Err(DashboardError::validation(
730 "ipc_bind",
731 Some(config.target_id.clone()),
732 "IPC path is served by a live process",
733 ));
734 }
735 crate::ipc::security::peer_identity::prepare_socket_path_for_bind(&config.path)?;
737 std::fs::remove_file(&config.path).map_err(|error| {
738 DashboardError::new(
739 "ipc_stale_remove_failed",
740 "ipc_bind",
741 Some(config.target_id.clone()),
742 format!("failed to remove stale IPC path: {error}"),
743 true,
744 )
745 })
746 }
747 }
748}
749
750fn require_session_trigger(
775 request: &IpcRequest,
776 target_id: &str,
777 service: &DashboardIpcService,
778) -> Result<(), DashboardError> {
779 if service.security_pipeline.is_none() {
783 return Err(DashboardError::new(
784 "session_required",
785 "subscription",
786 Some(target_id.to_owned()),
787 "event and log subscription require a security pipeline; \
788 without one the session_established flag cannot be verified",
789 false,
790 ));
791 }
792
793 let established = request
794 .params
795 .get("session_established")
796 .and_then(serde_json::Value::as_bool)
797 .unwrap_or(false);
798 if established {
799 Ok(())
800 } else {
801 Err(DashboardError::new(
802 "session_required",
803 "subscription",
804 Some(target_id.to_owned()),
805 "event and log subscription must be triggered by an established dashboard session",
806 false,
807 ))
808 }
809}
810
811pub fn validate_command(command: &ControlCommandRequest) -> Result<(), DashboardError> {
821 if command.reason.trim().is_empty() {
822 return Err(DashboardError::validation(
823 "command_validate",
824 Some(command.target_id.clone()),
825 "command reason must not be empty",
826 ));
827 }
828 if command.requested_by.trim().is_empty() {
829 return Err(DashboardError::validation(
830 "command_validate",
831 Some(command.target_id.clone()),
832 "requested_by must be derived by relay",
833 ));
834 }
835 if uuid::Uuid::parse_str(&command.command_id).is_err() {
839 return Err(DashboardError::validation(
840 "command_validate",
841 Some(command.target_id.clone()),
842 format!(
843 "command_id is not a valid UUID: {id}",
844 id = command.command_id
845 ),
846 ));
847 }
848 if matches!(command.command, ControlCommandKind::AddChild) {
850 let has_manifest = command
851 .target
852 .child_manifest
853 .as_ref()
854 .is_some_and(|m| !m.trim().is_empty());
855 if !has_manifest {
856 return Err(DashboardError::validation(
857 "command_validate",
858 Some(command.target_id.clone()),
859 "add_child command requires a non-empty child_manifest",
860 ));
861 }
862 }
863 if matches!(
865 command.command,
866 ControlCommandKind::RestartChild
867 | ControlCommandKind::PauseChild
868 | ControlCommandKind::ResumeChild
869 | ControlCommandKind::QuarantineChild
870 | ControlCommandKind::RemoveChild
871 ) && command
872 .target
873 .child_path
874 .as_ref()
875 .is_none_or(|p| p.trim().is_empty())
876 {
877 return Err(DashboardError::validation(
878 "command_validate",
879 Some(command.target_id.clone()),
880 format!(
881 "{:?} command requires a non-empty child_path",
882 command.command
883 ),
884 ));
885 }
886 if command.requested_at_unix_nanos == 0 {
889 return Err(DashboardError::validation(
890 "command_validate",
891 Some(command.target_id.clone()),
892 "requested_at_unix_nanos must not be 0",
893 ));
894 }
895 if matches!(
896 command.command,
897 ControlCommandKind::ShutdownTree
898 | ControlCommandKind::RemoveChild
899 | ControlCommandKind::AddChild
900 ) && !command.confirmed
901 {
902 return Err(DashboardError::validation(
903 "command_validate",
904 Some(command.target_id.clone()),
905 "dangerous command requires confirmation",
906 ));
907 }
908 Ok(())
909}
910
911const COMMAND_TIMEOUT_SECS: u64 = 30;
915
916async fn execute_command(
928 handle: &SupervisorHandle,
929 command: &ControlCommandRequest,
930) -> Result<CommandResult, DashboardError> {
931 tokio::time::timeout(
935 std::time::Duration::from_secs(COMMAND_TIMEOUT_SECS),
936 execute_command_inner(handle, command),
937 )
938 .await
939 .map_err(|_elapsed| {
940 DashboardError::new(
941 "command_timeout",
942 "command_dispatch",
943 Some(command.target_id.clone()),
944 format!(
945 "command {:?} timed out after {}s",
946 command.command, COMMAND_TIMEOUT_SECS,
947 ),
948 true,
949 )
950 })?
951}
952
953async fn execute_command_inner(
955 handle: &SupervisorHandle,
956 command: &ControlCommandRequest,
957) -> Result<CommandResult, DashboardError> {
958 let command_id = command
963 .command_id
964 .parse::<uuid::Uuid>()
965 .expect("command_id already validated as legal UUID");
966 let meta = crate::control::command::CommandMeta::with_id(
967 crate::control::command::CommandId::from_uuid(command_id),
968 &command.requested_by,
969 &command.reason,
970 );
971
972 let result = match command.command {
973 ControlCommandKind::RestartChild => {
974 handle
975 .execute_with_command_id(crate::control::command::ControlCommand::RestartChild {
976 meta: meta.clone(),
977 child_id: child_id(command)?,
978 })
979 .await
980 }
981 ControlCommandKind::PauseChild => {
982 handle
983 .execute_with_command_id(crate::control::command::ControlCommand::PauseChild {
984 meta: meta.clone(),
985 child_id: child_id(command)?,
986 })
987 .await
988 }
989 ControlCommandKind::ResumeChild => {
990 handle
991 .execute_with_command_id(crate::control::command::ControlCommand::ResumeChild {
992 meta: meta.clone(),
993 child_id: child_id(command)?,
994 })
995 .await
996 }
997 ControlCommandKind::QuarantineChild => {
998 handle
999 .execute_with_command_id(crate::control::command::ControlCommand::QuarantineChild {
1000 meta: meta.clone(),
1001 child_id: child_id(command)?,
1002 })
1003 .await
1004 }
1005 ControlCommandKind::RemoveChild => {
1006 handle
1007 .execute_with_command_id(crate::control::command::ControlCommand::RemoveChild {
1008 meta: meta.clone(),
1009 child_id: child_id(command)?,
1010 })
1011 .await
1012 }
1013 ControlCommandKind::AddChild => {
1014 handle
1015 .execute_with_command_id(crate::control::command::ControlCommand::AddChild {
1016 meta: meta.clone(),
1017 target: SupervisorPath::root(),
1018 child_manifest: command.target.child_manifest.clone().unwrap_or_default(),
1019 })
1020 .await
1021 }
1022 ControlCommandKind::ShutdownTree => {
1023 handle
1024 .execute_with_command_id(crate::control::command::ControlCommand::ShutdownTree {
1025 meta: meta.clone(),
1026 })
1027 .await
1028 }
1029 };
1030 result.map_err(|error| {
1031 DashboardError::new(
1032 "command_failed",
1033 "command_dispatch",
1034 Some(command.target_id.clone()),
1035 error.to_string(),
1036 true,
1037 )
1038 })
1039}
1040
1041fn child_id(command: &ControlCommandRequest) -> Result<ChildId, DashboardError> {
1051 let child_path = command.target.child_path.as_deref().ok_or_else(|| {
1052 DashboardError::validation(
1053 "command_validate",
1054 Some(command.target_id.clone()),
1055 "child_path is required for child command",
1056 )
1057 })?;
1058 let value = child_path
1059 .rsplit('/')
1060 .find(|segment| !segment.is_empty())
1061 .unwrap_or(child_path);
1062 Ok(ChildId::new(value))
1063}
1064
1065fn unix_nanos_now() -> u128 {
1075 std::time::SystemTime::now()
1076 .duration_since(std::time::UNIX_EPOCH)
1077 .unwrap_or(std::time::Duration::ZERO)
1078 .as_nanos()
1079}
1080
1081fn is_high_risk_command(method: &str, service: &DashboardIpcService) -> bool {
1089 if let Some(ref pipeline) = service.security_pipeline
1091 && let Ok(guard) = pipeline.lock()
1092 {
1093 let configured = guard.high_risk_methods();
1094 if !configured.is_empty() {
1095 return configured.iter().any(|m| m == method);
1096 }
1097 }
1098 matches!(
1100 method,
1101 "command.restart_child"
1102 | "command.pause_child"
1103 | "command.resume_child"
1104 | "command.quarantine_child"
1105 | "command.remove_child"
1106 | "command.shutdown_tree"
1107 | "command.add_child"
1108 )
1109}