fix(#1004,#1006): get_host_journal - JournalPriority enum, grep/since/until, default 30/max 100, verbatim container, fix doc comment
This commit is contained in:
parent
dc8a4e2baf
commit
b9ecacaafe
6 changed files with 96 additions and 30 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1260,6 +1260,7 @@ dependencies = [
|
|||
name = "hive-sh4re"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -863,11 +863,14 @@ impl AgentServer {
|
|||
// performs a second capability check server-side before running journalctl.
|
||||
#[tool(
|
||||
description = "Fetch recent lines from the host journal (requires `read_host_journal` \
|
||||
capability). All filters are optional — omit to get the last N host journal lines. \
|
||||
capability). All filters are optional - omit to get the last N host journal lines. \
|
||||
`unit`: filter to a systemd unit (e.g. `hive-c0re.service`). \
|
||||
`container`: logical agent name (e.g. `iris`) — adds `-M h-iris`. \
|
||||
`lines`: how many lines (default 100, max 500). \
|
||||
`priority`: minimum syslog level (`err`, `warning`, `info`, `debug`)."
|
||||
`container`: nspawn machine name verbatim (e.g. `h-iris`). Omit for host journal. \
|
||||
`lines`: how many lines (default 30, max 100). \
|
||||
`priority`: minimum syslog level enum. \
|
||||
`grep`: regex matched against log message fields (journalctl --grep). \
|
||||
`since`: show entries on or newer than this (e.g. `-1h`, `2024-01-01 12:00:00`). \
|
||||
`until`: show entries on or older than this."
|
||||
)]
|
||||
async fn get_host_journal(
|
||||
&self,
|
||||
|
|
@ -881,6 +884,9 @@ impl AgentServer {
|
|||
container: args.container,
|
||||
lines: args.lines,
|
||||
priority: args.priority,
|
||||
grep: args.grep,
|
||||
since: args.since,
|
||||
until: args.until,
|
||||
})
|
||||
.await;
|
||||
let result = match resp {
|
||||
|
|
@ -1177,16 +1183,24 @@ pub struct GetHostJournalArgs {
|
|||
/// Systemd unit to filter (e.g. `hive-c0re.service`). Omit for all units.
|
||||
#[serde(default)]
|
||||
pub unit: Option<String>,
|
||||
/// Logical agent name (e.g. `iris`) — gets that container's journal via `-M h-iris`.
|
||||
/// Omit for the host journal.
|
||||
/// nspawn machine name verbatim (e.g. `h-iris`). Omit for host journal.
|
||||
#[serde(default)]
|
||||
pub container: Option<String>,
|
||||
/// Number of lines to return (default 100, max 500).
|
||||
/// Number of lines to return (default 30, max 100).
|
||||
#[serde(default)]
|
||||
pub lines: Option<u32>,
|
||||
/// Minimum syslog priority: `err`, `warning`, `info`, `debug`.
|
||||
/// Minimum syslog priority level.
|
||||
#[serde(default)]
|
||||
pub priority: Option<String>,
|
||||
pub priority: Option<hive_sh4re::JournalPriority>,
|
||||
/// Regex to match against log message fields (journalctl --grep).
|
||||
#[serde(default)]
|
||||
pub grep: Option<String>,
|
||||
/// Show entries on or newer than this timestamp (e.g. `-1h`).
|
||||
#[serde(default)]
|
||||
pub since: Option<String>,
|
||||
/// Show entries on or older than this timestamp.
|
||||
#[serde(default)]
|
||||
pub until: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
|||
|
|
@ -310,8 +310,8 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
|
|||
message: format!("{e:#}"),
|
||||
},
|
||||
},
|
||||
AgentRequest::GetHostJournal { unit, container, lines, priority } => {
|
||||
dispatch_host_journal(agent, unit, container, lines, priority).await
|
||||
AgentRequest::GetHostJournal { unit, container, lines, priority, grep, since, until } => {
|
||||
dispatch_host_journal(agent, unit, container, lines, priority, grep, since, until).await
|
||||
}
|
||||
// Manager-only variants are not valid on the agent socket.
|
||||
_ => AgentResponse::Err {
|
||||
|
|
@ -320,28 +320,29 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
|
|||
}
|
||||
}
|
||||
|
||||
/// Handle `GetHostJournal` from the agent socket. Capability-gated:
|
||||
/// the calling agent must hold `read_host_journal` in
|
||||
/// Handle `GetHostJournal` from both the agent and manager sockets.
|
||||
/// Capability-gated: the calling agent must hold `read_host_journal` in
|
||||
/// `meta/capabilities.json`. Runs `journalctl` host-side and returns
|
||||
/// the output as a `HostJournal` response.
|
||||
///
|
||||
/// Also called from `manager_server` so the manager can use the same
|
||||
/// tool without duplicating the journalctl invocation. The manager
|
||||
/// is exempt from the capability check (it holds all capabilities
|
||||
/// implicitly until the capability system enforcement is complete).
|
||||
/// The manager is not exempt - grant `read_host_journal` in
|
||||
/// `meta/capabilities.json` to enable it for any agent including the manager.
|
||||
pub async fn dispatch_host_journal(
|
||||
agent: &str,
|
||||
unit: &Option<String>,
|
||||
container: &Option<String>,
|
||||
lines: &Option<u32>,
|
||||
priority: &Option<String>,
|
||||
priority: &Option<hive_sh4re::JournalPriority>,
|
||||
grep: &Option<String>,
|
||||
since: &Option<String>,
|
||||
until: &Option<String>,
|
||||
) -> AgentResponse {
|
||||
if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::ReadHostJournal) {
|
||||
return AgentResponse::Err {
|
||||
message: "agent does not have the read_host_journal capability".to_owned(),
|
||||
};
|
||||
}
|
||||
let n = lines.unwrap_or(100).min(500);
|
||||
let n = lines.unwrap_or(30).min(100);
|
||||
let mut args: Vec<String> = vec![
|
||||
"--no-pager".to_owned(),
|
||||
"--output=short".to_owned(),
|
||||
|
|
@ -353,13 +354,21 @@ pub async fn dispatch_host_journal(
|
|||
args.push(u.clone());
|
||||
}
|
||||
if let Some(c) = container {
|
||||
let machine = crate::lifecycle::container_name(c);
|
||||
args.push("-M".to_owned());
|
||||
args.push(machine);
|
||||
args.push(c.clone());
|
||||
}
|
||||
if let Some(p) = priority {
|
||||
args.push("-p".to_owned());
|
||||
args.push(p.clone());
|
||||
args.push(p.as_str().to_owned());
|
||||
}
|
||||
if let Some(g) = grep {
|
||||
args.push(format!("--grep={g}"));
|
||||
}
|
||||
if let Some(s) = since {
|
||||
args.push(format!("--since={s}"));
|
||||
}
|
||||
if let Some(u) = until {
|
||||
args.push(format!("--until={u}"));
|
||||
}
|
||||
tracing::info!(%agent, ?args, "get_host_journal");
|
||||
match tokio::process::Command::new("journalctl")
|
||||
|
|
|
|||
|
|
@ -582,9 +582,9 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
|||
// GetHostJournal is an agent-socket-only capability-gated variant.
|
||||
// The manager can use the existing GetLogs tool for per-container
|
||||
// logs. Route to the agent_server handler for consistency.
|
||||
ManagerRequest::GetHostJournal { unit, container, lines, priority } => {
|
||||
ManagerRequest::GetHostJournal { unit, container, lines, priority, grep, since, until } => {
|
||||
crate::agent_server::dispatch_host_journal(
|
||||
MANAGER_AGENT, unit, container, lines, priority,
|
||||
MANAGER_AGENT, unit, container, lines, priority, grep, since, until,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ version.workspace = true
|
|||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
|
|
|||
|
|
@ -435,17 +435,26 @@ pub enum Request {
|
|||
/// Filter to a specific systemd unit (e.g. `hive-c0re.service`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
unit: Option<String>,
|
||||
/// Filter to a specific nspawn container by logical agent name
|
||||
/// (e.g. `"iris"` → machine `h-iris`). Adds `-M` flag.
|
||||
/// Machine name to pass to journalctl `-M` verbatim (e.g. `h-iris`).
|
||||
/// The caller is responsible for the correct nspawn machine name.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
container: Option<String>,
|
||||
/// Number of journal lines to return (default 100, max 500).
|
||||
/// Number of journal lines to return (default 30, max 100).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
lines: Option<u32>,
|
||||
/// Minimum syslog priority level: `err`, `warning`, `info`, `debug`.
|
||||
/// Corresponds to journalctl `-p LEVEL`.
|
||||
/// Minimum syslog priority level.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
priority: Option<String>,
|
||||
priority: Option<JournalPriority>,
|
||||
/// Regex to match against log message fields (journalctl `--grep`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
grep: Option<String>,
|
||||
/// Show entries on or newer than this timestamp (journalctl `--since`).
|
||||
/// ISO 8601 or journalctl-accepted relative strings (e.g. `"-1h"`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
since: Option<String>,
|
||||
/// Show entries on or older than this timestamp (journalctl `--until`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
until: Option<String>,
|
||||
},
|
||||
|
||||
// ---- privileged (manager socket only for now) ---------------------------
|
||||
|
|
@ -847,6 +856,38 @@ impl ToolGroup {
|
|||
/// Per-agent capability grants. Stored in `meta/capabilities.json`
|
||||
/// (same shape as `tool-groups.json`: `{ "alice": ["read_host_journal"] }`).
|
||||
/// Capabilities control system-level access that hive-c0re enforces
|
||||
/// Syslog priority levels for `GetHostJournal`. Serialised as lowercase
|
||||
/// strings matching journalctl `-p` accepted values.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum JournalPriority {
|
||||
Emerg,
|
||||
Alert,
|
||||
Crit,
|
||||
Err,
|
||||
Warning,
|
||||
Notice,
|
||||
Info,
|
||||
Debug,
|
||||
}
|
||||
|
||||
impl JournalPriority {
|
||||
/// Returns the lowercase string journalctl expects for `-p`.
|
||||
#[must_use]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Emerg => "emerg",
|
||||
Self::Alert => "alert",
|
||||
Self::Crit => "crit",
|
||||
Self::Err => "err",
|
||||
Self::Warning => "warning",
|
||||
Self::Notice => "notice",
|
||||
Self::Info => "info",
|
||||
Self::Debug => "debug",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// at dispatch time; they are orthogonal to tool groups (which control
|
||||
/// which MCP tools the harness exposes to claude).
|
||||
///
|
||||
|
|
|
|||
Loading…
Reference in a new issue