Skip to main content

ashlar_control/
lib.rs

1//! Agent control port: a hardened loopback JSON-lines server, its client, and the standard method
2//! set every app answers.
3//!
4//! ```text
5//!  agent / MCP / CLI ──TCP 127.0.0.1──▶ Server (connection threads) ──Request──▶ app thread
6//!                                                                          │ handle(req, dispatch, ui, info)
7//!                      ◀────────── {"id","ok","result"|"error"} ◀──────────┘
8//! ```
9//!
10//! The server never touches app state: it parses, authenticates and queues [`Request`]s; the app
11//! thread answers them with [`handle`] (standard `app.*`/`engine.*` methods from any
12//! [`ashlar_command::Dispatch`], everything else from the app's [`UiHandler`]). The protocol is
13//! documented in `docs/control-protocol.md`.
14#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
15
16mod client;
17mod clock;
18mod explain;
19pub mod key;
20mod lines;
21mod methods;
22mod protocol;
23mod server;
24
25/// `ui.scene {name?}`: apply one of the app's named scenes, or with no name list them
26/// (`{scenes: [..]}`). Answered only by apps that declare scenes.
27pub const UI_SCENE: &str = "ui.scene";
28
29pub use client::{Client, MAX_REPLY};
30pub use clock::{APP_CLOCK, ClockOp, MAX_STEP_MS};
31pub use explain::{explain, explain_reply};
32pub use lines::{DEFAULT_MAX_LINE, serve_stream};
33pub use methods::{AppInfo, DEBUG_METHODS, MAX_BATCH, NoUi, STANDARD_METHODS, UI_METHODS, UiHandler, answer, handle, matches_query};
34pub use protocol::{PROTOCOL_VERSION, Reply, Request};
35pub use server::{Config, Server, Wake};
36
37use serde_json::Value;
38
39/// Why a control-port operation failed.
40#[derive(Debug, Clone, PartialEq, thiserror::Error)]
41pub enum ControlError {
42    /// Binding, connecting, reading or writing failed.
43    #[error("{0}")]
44    Io(String),
45    /// The key could not be made, written or read.
46    #[error("control key: {0}")]
47    Key(String),
48    /// The app refused the key (it restarted with a new one, or this is someone else's port).
49    #[error("unauthorized: the app refused the control key")]
50    Unauthorized,
51    /// The peer did not speak the protocol.
52    #[error("bad reply from the app: {0}")]
53    Protocol(String),
54    /// The app answered with an error.
55    #[error("{error}")]
56    Remote { error: String, detail: Option<Value> },
57}