From 1023acf69f2577395d3e7d6ecd104160e6516fc3 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 20:28:42 +0200 Subject: [PATCH] add get_logs tool to manager mcp surface --- hive-ag3nt/src/mcp.rs | 46 +++++++++++++++++++++++++++++++++ hive-c0re/src/manager_server.rs | 29 +++++++++++++++++++++ hive-sh4re/src/lib.rs | 15 +++++++++++ 3 files changed, 90 insertions(+) diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index e252d10..9fc3ddb 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -39,6 +39,7 @@ pub enum SocketReply { Status(u64), QuestionQueued(i64), Recent(Vec), + Logs(String), } impl From for SocketReply { @@ -65,6 +66,7 @@ impl From for SocketReply { hive_sh4re::ManagerResponse::Status { unread } => Self::Status(unread), hive_sh4re::ManagerResponse::QuestionQueued { id } => Self::QuestionQueued(id), hive_sh4re::ManagerResponse::Recent { rows } => Self::Recent(rows), + hive_sh4re::ManagerResponse::Logs { content } => Self::Logs(content), } } } @@ -351,6 +353,15 @@ pub struct RequestApplyCommitArgs { pub description: Option, } +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +pub struct GetLogsArgs { + /// Logical name of the sub-agent container to fetch logs for. + pub agent: String, + /// How many journal lines to return (default: 50, max: 500). + #[serde(default)] + pub lines: Option, +} + #[derive(Debug, Clone)] pub struct ManagerServer { socket: PathBuf, @@ -580,6 +591,40 @@ impl ManagerServer { }) .await } + + #[tool( + description = "Fetch recent journal log lines for a sub-agent container. Useful \ + for diagnosing MCP server registration failures, startup crashes, plugin install \ + errors, or any harness issue you can't see from inside the container. `lines` \ + defaults to 50 (max capped at 500 on the host side)." + )] + async fn get_logs(&self, Parameters(args): Parameters) -> String { + let log = format!("{args:?}"); + let agent = args.agent.clone(); + run_tool_envelope("get_logs", log, async move { + let lines = args.lines.map(|n| n.min(500)); + let (resp, retries) = self + .dispatch(hive_sh4re::ManagerRequest::GetLogs { + agent: agent.clone(), + lines, + }) + .await; + let s = match resp { + Ok(SocketReply::Logs(content)) => { + if content.is_empty() { + format!("(no journal output for {agent})") + } else { + content + } + } + Ok(SocketReply::Err(m)) => format!("get_logs failed: {m}"), + Ok(other) => format!("get_logs unexpected response: {other:?}"), + Err(e) => format!("get_logs transport error: {e:#}"), + }; + annotate_retries(s, retries) + }) + .await + } } #[tool_handler( @@ -635,6 +680,7 @@ pub fn allowed_mcp_tools(flavor: Flavor) -> Vec { "update", "request_apply_commit", "ask_operator", + "get_logs", ], }; let mut out: Vec = names diff --git a/hive-c0re/src/manager_server.rs b/hive-c0re/src/manager_server.rs index 1146b9f..5f90a57 100644 --- a/hive-c0re/src/manager_server.rs +++ b/hive-c0re/src/manager_server.rs @@ -273,6 +273,35 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc) -> ManagerResp }, } } + ManagerRequest::GetLogs { agent, lines } => { + let n = lines.unwrap_or(50); + tracing::info!(%agent, %n, "manager: get_logs"); + match tokio::process::Command::new("journalctl") + .args([ + "-M", + agent, + "-n", + &n.to_string(), + "--no-pager", + "--output=short", + ]) + .output() + .await + { + Ok(out) => { + let content = if out.status.success() || !out.stdout.is_empty() { + String::from_utf8_lossy(&out.stdout).into_owned() + } else { + let stderr = String::from_utf8_lossy(&out.stderr); + format!("journalctl exited {}: {stderr}", out.status) + }; + ManagerResponse::Logs { content } + } + Err(e) => ManagerResponse::Err { + message: format!("journalctl spawn failed: {e:#}"), + }, + } + } ManagerRequest::RequestApplyCommit { agent, commit_ref, diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 5a6fd0c..314aba6 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -475,6 +475,17 @@ pub enum ManagerRequest { #[serde(default)] ttl_seconds: Option, }, + /// Fetch recent journal lines for a sub-agent container. hive-c0re + /// runs `journalctl -M -n --no-pager` and returns + /// the output as a string. Useful for diagnosing MCP registration + /// failures, startup crashes, and harness errors. + /// + /// `lines` defaults to 50 when omitted. + GetLogs { + agent: String, + #[serde(default)] + lines: Option, + }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -502,4 +513,8 @@ pub enum ManagerResponse { Recent { rows: Vec, }, + /// `GetLogs` result: journal lines for the requested container. + Logs { + content: String, + }, }