Skip to main content

degenbot_cli_core/
fleet.rs

1//! The `fleet` command arms (ADR-051 D6;).
2//!
3//! Ports `cli/fleet.py`: both verbs are thin clients of a LIVE bot's
4//! `OperatorServer` over the JSON-lines wire protocol in
5//! [`crate::operator`]. The socket is resolved through the same cascade the
6//! rest of the console uses (`--socket` > `DEGENBOT_OPERATOR_SOCKET` > the
7//! `~/.config/degenbot/operator.sock` plugin default); no local config or
8//! database work happens here.
9//!
10//! - `fleet posture show` sends `get_fleet_posture` and renders the echoed
11//!   effective policy as one sorted JSON line.
12//! - `fleet posture set` is a PARTIAL patch: it validates wire hygiene
13//!   (unknown key / empty patch, in [`crate::operator::validate_posture_patch`])
14//!   BEFORE the socket is touched, sends `set_fleet_posture`, and renders the
15//!   echoed effective policy. Value validation stays server-side.
16
17use crate::context::CliContext;
18use crate::error::CliError;
19use crate::operator::{
20    render_json_sorted, resolve_socket, send_request, validate_posture_patch, PosturePatchEntry,
21    WireRequest, WireResponse,
22};
23use crate::prompt::PromptPlan;
24use crate::report::FleetReport;
25
26/// The `fleet` command group.
27#[derive(Debug, Clone, PartialEq)]
28pub enum FleetCommand {
29    /// `fleet posture show`: read the live cordon posture.
30    PostureShow {
31        /// The operator socket path override (`--socket`).
32        socket: Option<String>,
33    },
34    /// `fleet posture set`: re-tune a subset of the six cordon thresholds.
35    PostureSet {
36        /// The operator socket path override (`--socket`).
37        socket: Option<String>,
38        /// The partial patch (at least one entry; known keys only).
39        patch: Vec<PosturePatchEntry>,
40    },
41}
42
43impl FleetCommand {
44    /// Neither fleet arm prompts.
45    #[must_use]
46    pub const fn prompt_plan(&self, _ctx: &CliContext<'_>) -> PromptPlan {
47        PromptPlan::None
48    }
49}
50
51/// Execute a `fleet` command.
52///
53/// # Errors
54///
55/// [`CliError::OperatorHygiene`] for a client-side wire-hygiene refusal (an
56/// empty or unknown-key patch, raised before any socket work);
57/// [`CliError::OperatorRefused`] for a `{"ok": false}` host reply;
58/// [`CliError::OperatorProtocol`] for an unreachable socket or a malformed
59/// response.
60pub(crate) fn execute(
61    command: &FleetCommand,
62    ctx: &CliContext<'_>,
63) -> Result<FleetReport, CliError> {
64    match command {
65        FleetCommand::PostureShow { socket } => Ok(FleetReport::Posture {
66            effective: call(ctx, socket.as_deref(), &WireRequest::GetFleetPosture)?,
67        }),
68        FleetCommand::PostureSet { socket, patch } => {
69            validate_posture_patch(patch)?;
70            let request = WireRequest::SetFleetPosture {
71                patch: patch.clone(),
72            };
73            Ok(FleetReport::Posture {
74                effective: call(ctx, socket.as_deref(), &request)?,
75            })
76        }
77    }
78}
79
80/// Send one fleet request and render the echoed effective policy.
81fn call(
82    ctx: &CliContext<'_>,
83    socket: Option<&str>,
84    request: &WireRequest,
85) -> Result<String, CliError> {
86    let socket = resolve_socket(ctx.env(), socket);
87    match send_request(&socket, request)? {
88        WireResponse::Ok { effective, .. } => Ok(render_json_sorted(effective.as_ref())),
89        WireResponse::Err { error } => Err(CliError::OperatorRefused(error)),
90    }
91}