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}