Skip to main content

horto_os_ui_mcp/
embedded_ops.rs

1//! Privileged ops in-process when MCP runs on the box (`HORTO_MCP_MODE=box`).
2
3use anyhow::{bail, Result};
4use horto_os_ui_shared::{
5    backup_status, docker_rebuild, doctor, list_containers, list_timestamped_etc_backups,
6    setup_run, setup_status as engine_setup_status, setup_step, ApplyMode, HostContext,
7    NonInteractivePrompts, SetupKind,
8};
9
10const CONFIRM_DOCKER_REBUILD: &str = "docker-rebuild";
11
12fn ctx(apply: bool) -> HostContext {
13    let mode = if apply {
14        ApplyMode::Apply
15    } else {
16        ApplyMode::DryRun
17    };
18    HostContext::new(mode, SetupKind::Full).with_prompts(Box::new(NonInteractivePrompts))
19}
20
21const fn kind(full: bool) -> SetupKind {
22    if full {
23        SetupKind::Full
24    } else {
25        SetupKind::Minimal
26    }
27}
28
29fn capture_logs(host: &HostContext) -> String {
30    let mut out = host.logs.join("\n");
31    if !host.planned.is_empty() {
32        if !out.is_empty() {
33            out.push('\n');
34        }
35        for p in &host.planned {
36            out.push_str("[plan] ");
37            out.push_str(&p.summary);
38            out.push('\n');
39        }
40    }
41    if out.trim().is_empty() {
42        out = "ok".into();
43    }
44    out
45}
46
47/// Embedded setup status (resume report).
48///
49/// # Errors
50///
51/// Returns on serialize failure.
52pub fn setup_status_report(apply: bool, full: bool) -> Result<String> {
53    let c = ctx(apply);
54    let report = engine_setup_status(&c, kind(full));
55    Ok(serde_json::to_string_pretty(&report)?)
56}
57
58/// Embedded `setup run`.
59///
60/// # Errors
61///
62/// Returns engine errors (root required on apply, step failures).
63pub fn setup_run_embedded(apply: bool, full: bool, skip_piper: bool) -> Result<String> {
64    let mut c = ctx(apply);
65    c.skip_piper = skip_piper;
66    setup_run(&mut c, kind(full)).map_err(|e| anyhow::anyhow!("{e}"))?;
67    Ok(capture_logs(&c))
68}
69
70/// Embedded `setup step`.
71///
72/// # Errors
73///
74/// Returns engine errors.
75pub fn setup_step_embedded(step_id: &str, apply: bool, full: bool) -> Result<String> {
76    let mut c = ctx(apply);
77    setup_step(&mut c, kind(full), step_id).map_err(|e| anyhow::anyhow!("{e}"))?;
78    Ok(capture_logs(&c))
79}
80
81/// Embedded doctor.
82///
83/// # Errors
84///
85/// Returns on serialize failure.
86pub fn doctor_embedded() -> Result<String> {
87    let c = ctx(false);
88    let report = doctor(&c);
89    Ok(serde_json::to_string_pretty(&report)?)
90}
91
92/// Embedded docker status.
93///
94/// # Errors
95///
96/// Returns when listing containers fails.
97pub fn docker_status_embedded() -> Result<String> {
98    let list = list_containers().map_err(|e| anyhow::anyhow!("{e}"))?;
99    Ok(serde_json::to_string_pretty(&list)?)
100}
101
102/// Embedded docker rebuild.
103///
104/// # Errors
105///
106/// Returns when confirm is wrong or rebuild fails.
107pub fn docker_rebuild_embedded(confirm: &str) -> Result<String> {
108    if confirm != CONFIRM_DOCKER_REBUILD {
109        bail!("confirm must be exactly `{CONFIRM_DOCKER_REBUILD}`");
110    }
111    let assets = std::path::Path::new("/srv/docker");
112    docker_rebuild(assets).map_err(|e| anyhow::anyhow!("{e}"))?;
113    Ok("docker rebuild finished".into())
114}
115
116/// List timestamped etc backups.
117///
118/// # Errors
119///
120/// Returns on serialize failure.
121pub fn backup_list_embedded() -> Result<String> {
122    let c = ctx(false);
123    let list = list_timestamped_etc_backups(&c);
124    Ok(serde_json::to_string_pretty(&list)?)
125}
126
127/// Disk backup probe / status.
128///
129/// # Errors
130///
131/// Returns on serialize failure.
132pub fn backup_disk_status_embedded() -> Result<String> {
133    let c = ctx(false);
134    let status = backup_status(&c);
135    Ok(serde_json::to_string_pretty(&status)?)
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn docker_rebuild_rejects_bad_confirm() {
144        let err = docker_rebuild_embedded("nope").expect_err("confirm");
145        assert!(err.to_string().contains("docker-rebuild"));
146    }
147
148    #[test]
149    fn setup_status_plan_serializes() {
150        let text = setup_status_report(false, true).expect("status");
151        assert!(text.contains('{') || text.contains("steps") || !text.is_empty());
152    }
153}