//! Wire types for the unix socket protocol between `hive-bash-daemon` //! and `hive-bash-mcp`. One JSON request line in, one JSON response line //! out per connection. Connections are short-lived (per tool call). use serde::{Deserialize, Serialize}; // --------------------------------------------------------------------------- // Task state (shared between runner and protocol) // --------------------------------------------------------------------------- /// Lifecycle state of a bash task. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum TaskStatus { Pending, Running, Done, TimedOut, /// Daemon was restarted while the task was running; process is gone. Interrupted, } /// Task metadata + result written to `.json` under the tasks dir. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TaskFile { pub id: String, pub cmd: String, /// Kill timeout in seconds. `None` means no timeout — task runs until /// natural exit. Old task files with a numeric value are still readable /// (serde coerces `u64` → `Some(u64)` is handled by the caller). #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout_secs: Option, pub status: TaskStatus, pub created_at: i64, #[serde(default, skip_serializing_if = "Option::is_none")] pub started_at: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub completed_at: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub exit_code: Option, /// Last [`crate::runner::SUMMARY_BYTES`] of stdout. #[serde(default, skip_serializing_if = "Option::is_none")] pub stdout_tail: Option, /// Last [`crate::runner::SUMMARY_BYTES`] of stderr. #[serde(default, skip_serializing_if = "Option::is_none")] pub stderr_tail: Option, } // --------------------------------------------------------------------------- // Request / response // --------------------------------------------------------------------------- /// Requests the MCP bridge sends to the daemon. #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum DaemonRequest { /// Liveness probe — fast round-trip that doesn't touch any subprocess. Ping, /// Submit a new bash task. Returns the task ID on success. /// `wait_seconds`: optional inline poll (capped at 30s); when the /// task finishes within the window the response carries the full /// status payload. When it doesn't, the response carries just the /// task ID so the caller can check back with `BashStatus`. BashRun { cmd: String, #[serde(default)] timeout_secs: Option, /// Inline wait cap: 30s. Pass `None` or `0` to get the /// task-started-id response immediately. #[serde(default)] wait_seconds: Option, }, /// Query the current status of a task. Returns the full `TaskFile` /// (formatted as text by the bridge). `wait_seconds`: optional /// inline poll (capped at 30s) — daemon returns as soon as the /// task reaches a terminal state or the window expires. BashStatus { id: String, #[serde(default)] wait_seconds: Option, }, /// Return all tasks currently in `Pending` or `Running` state. /// Used by the harness `get_loose_ends` to surface active /// background work. ActiveTasks, } /// Response shape from the daemon. `Ok` carries a JSON payload; `Error` /// carries a human-readable message. #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum DaemonResponse { Ok { payload: serde_json::Value }, Error { message: String }, } impl DaemonResponse { /// Build an Ok response from any serialisable value. pub fn ok(payload: &T) -> Self { let payload = serde_json::to_value(payload) .unwrap_or_else(|e| serde_json::json!({ "serialise_error": e.to_string() })); Self::Ok { payload } } /// Build an Error response from any `Display` value. pub fn error(msg: impl std::fmt::Display) -> Self { Self::Error { message: msg.to_string(), } } }