feat(#1106): split bash mcp into hive-bash-daemon + hive-bash-mcp bridge
- new hive-bash-mcp crate: daemon (subprocess runner, wake signals) +
stdio bridge (mcp tools). mirrors hive-matrix-mcp architecture
- hive-ag3nt: remove bash_runner.rs and bash_run/bash_status mcp tools;
get_loose_ends uses hive_bash_mcp:🏃:active_tasks() via crate dep
- harness-base.nix: add hive-bash-daemon systemd service + auto-inject
bash extraMcpServer into every agent (socket: /run/hive-bash/socket)
This commit is contained in:
parent
9aa624d310
commit
e86160820a
15 changed files with 811 additions and 373 deletions
110
hive-bash-mcp/src/protocol.rs
Normal file
110
hive-bash-mcp/src/protocol.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
//! 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 `<id>.json` under the tasks dir.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskFile {
|
||||
pub id: String,
|
||||
pub cmd: String,
|
||||
pub timeout_secs: u64,
|
||||
pub status: TaskStatus,
|
||||
pub created_at: i64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub started_at: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub completed_at: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub exit_code: Option<i32>,
|
||||
/// Last [`crate::runner::SUMMARY_BYTES`] of stdout.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub stdout_tail: Option<String>,
|
||||
/// Last [`crate::runner::SUMMARY_BYTES`] of stderr.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub stderr_tail: Option<String>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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<u64>,
|
||||
/// Inline wait cap: 30s. Pass `None` or `0` to get the
|
||||
/// task-started-id response immediately.
|
||||
#[serde(default)]
|
||||
wait_seconds: Option<u64>,
|
||||
},
|
||||
|
||||
/// 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<u64>,
|
||||
},
|
||||
|
||||
/// 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<T: Serialize>(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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue