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"
|
name = "hive-sh4re"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -863,11 +863,14 @@ impl AgentServer {
|
||||||
// performs a second capability check server-side before running journalctl.
|
// performs a second capability check server-side before running journalctl.
|
||||||
#[tool(
|
#[tool(
|
||||||
description = "Fetch recent lines from the host journal (requires `read_host_journal` \
|
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`). \
|
`unit`: filter to a systemd unit (e.g. `hive-c0re.service`). \
|
||||||
`container`: logical agent name (e.g. `iris`) — adds `-M h-iris`. \
|
`container`: nspawn machine name verbatim (e.g. `h-iris`). Omit for host journal. \
|
||||||
`lines`: how many lines (default 100, max 500). \
|
`lines`: how many lines (default 30, max 100). \
|
||||||
`priority`: minimum syslog level (`err`, `warning`, `info`, `debug`)."
|
`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(
|
async fn get_host_journal(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -881,6 +884,9 @@ impl AgentServer {
|
||||||
container: args.container,
|
container: args.container,
|
||||||
lines: args.lines,
|
lines: args.lines,
|
||||||
priority: args.priority,
|
priority: args.priority,
|
||||||
|
grep: args.grep,
|
||||||
|
since: args.since,
|
||||||
|
until: args.until,
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
let result = match resp {
|
let result = match resp {
|
||||||
|
|
@ -1177,16 +1183,24 @@ pub struct GetHostJournalArgs {
|
||||||
/// Systemd unit to filter (e.g. `hive-c0re.service`). Omit for all units.
|
/// Systemd unit to filter (e.g. `hive-c0re.service`). Omit for all units.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub unit: Option<String>,
|
pub unit: Option<String>,
|
||||||
/// Logical agent name (e.g. `iris`) — gets that container's journal via `-M h-iris`.
|
/// nspawn machine name verbatim (e.g. `h-iris`). Omit for host journal.
|
||||||
/// Omit for the host journal.
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub container: Option<String>,
|
pub container: Option<String>,
|
||||||
/// Number of lines to return (default 100, max 500).
|
/// Number of lines to return (default 30, max 100).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub lines: Option<u32>,
|
pub lines: Option<u32>,
|
||||||
/// Minimum syslog priority: `err`, `warning`, `info`, `debug`.
|
/// Minimum syslog priority level.
|
||||||
#[serde(default)]
|
#[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)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -310,8 +310,8 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
|
||||||
message: format!("{e:#}"),
|
message: format!("{e:#}"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AgentRequest::GetHostJournal { unit, container, lines, priority } => {
|
AgentRequest::GetHostJournal { unit, container, lines, priority, grep, since, until } => {
|
||||||
dispatch_host_journal(agent, unit, container, lines, priority).await
|
dispatch_host_journal(agent, unit, container, lines, priority, grep, since, until).await
|
||||||
}
|
}
|
||||||
// Manager-only variants are not valid on the agent socket.
|
// Manager-only variants are not valid on the agent socket.
|
||||||
_ => AgentResponse::Err {
|
_ => AgentResponse::Err {
|
||||||
|
|
@ -320,28 +320,29 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle `GetHostJournal` from the agent socket. Capability-gated:
|
/// Handle `GetHostJournal` from both the agent and manager sockets.
|
||||||
/// the calling agent must hold `read_host_journal` in
|
/// Capability-gated: the calling agent must hold `read_host_journal` in
|
||||||
/// `meta/capabilities.json`. Runs `journalctl` host-side and returns
|
/// `meta/capabilities.json`. Runs `journalctl` host-side and returns
|
||||||
/// the output as a `HostJournal` response.
|
/// the output as a `HostJournal` response.
|
||||||
///
|
///
|
||||||
/// Also called from `manager_server` so the manager can use the same
|
/// The manager is not exempt - grant `read_host_journal` in
|
||||||
/// tool without duplicating the journalctl invocation. The manager
|
/// `meta/capabilities.json` to enable it for any agent including the manager.
|
||||||
/// is exempt from the capability check (it holds all capabilities
|
|
||||||
/// implicitly until the capability system enforcement is complete).
|
|
||||||
pub async fn dispatch_host_journal(
|
pub async fn dispatch_host_journal(
|
||||||
agent: &str,
|
agent: &str,
|
||||||
unit: &Option<String>,
|
unit: &Option<String>,
|
||||||
container: &Option<String>,
|
container: &Option<String>,
|
||||||
lines: &Option<u32>,
|
lines: &Option<u32>,
|
||||||
priority: &Option<String>,
|
priority: &Option<hive_sh4re::JournalPriority>,
|
||||||
|
grep: &Option<String>,
|
||||||
|
since: &Option<String>,
|
||||||
|
until: &Option<String>,
|
||||||
) -> AgentResponse {
|
) -> AgentResponse {
|
||||||
if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::ReadHostJournal) {
|
if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::ReadHostJournal) {
|
||||||
return AgentResponse::Err {
|
return AgentResponse::Err {
|
||||||
message: "agent does not have the read_host_journal capability".to_owned(),
|
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![
|
let mut args: Vec<String> = vec![
|
||||||
"--no-pager".to_owned(),
|
"--no-pager".to_owned(),
|
||||||
"--output=short".to_owned(),
|
"--output=short".to_owned(),
|
||||||
|
|
@ -353,13 +354,21 @@ pub async fn dispatch_host_journal(
|
||||||
args.push(u.clone());
|
args.push(u.clone());
|
||||||
}
|
}
|
||||||
if let Some(c) = container {
|
if let Some(c) = container {
|
||||||
let machine = crate::lifecycle::container_name(c);
|
|
||||||
args.push("-M".to_owned());
|
args.push("-M".to_owned());
|
||||||
args.push(machine);
|
args.push(c.clone());
|
||||||
}
|
}
|
||||||
if let Some(p) = priority {
|
if let Some(p) = priority {
|
||||||
args.push("-p".to_owned());
|
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");
|
tracing::info!(%agent, ?args, "get_host_journal");
|
||||||
match tokio::process::Command::new("journalctl")
|
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.
|
// GetHostJournal is an agent-socket-only capability-gated variant.
|
||||||
// The manager can use the existing GetLogs tool for per-container
|
// The manager can use the existing GetLogs tool for per-container
|
||||||
// logs. Route to the agent_server handler for consistency.
|
// 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(
|
crate::agent_server::dispatch_host_journal(
|
||||||
MANAGER_AGENT, unit, container, lines, priority,
|
MANAGER_AGENT, unit, container, lines, priority, grep, since, until,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,5 @@ version.workspace = true
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -435,17 +435,26 @@ pub enum Request {
|
||||||
/// Filter to a specific systemd unit (e.g. `hive-c0re.service`).
|
/// Filter to a specific systemd unit (e.g. `hive-c0re.service`).
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
unit: Option<String>,
|
unit: Option<String>,
|
||||||
/// Filter to a specific nspawn container by logical agent name
|
/// Machine name to pass to journalctl `-M` verbatim (e.g. `h-iris`).
|
||||||
/// (e.g. `"iris"` → machine `h-iris`). Adds `-M` flag.
|
/// The caller is responsible for the correct nspawn machine name.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
container: Option<String>,
|
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")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
lines: Option<u32>,
|
lines: Option<u32>,
|
||||||
/// Minimum syslog priority level: `err`, `warning`, `info`, `debug`.
|
/// Minimum syslog priority level.
|
||||||
/// Corresponds to journalctl `-p LEVEL`.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[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) ---------------------------
|
// ---- privileged (manager socket only for now) ---------------------------
|
||||||
|
|
@ -847,6 +856,38 @@ impl ToolGroup {
|
||||||
/// Per-agent capability grants. Stored in `meta/capabilities.json`
|
/// Per-agent capability grants. Stored in `meta/capabilities.json`
|
||||||
/// (same shape as `tool-groups.json`: `{ "alice": ["read_host_journal"] }`).
|
/// (same shape as `tool-groups.json`: `{ "alice": ["read_host_journal"] }`).
|
||||||
/// Capabilities control system-level access that hive-c0re enforces
|
/// 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
|
/// at dispatch time; they are orthogonal to tool groups (which control
|
||||||
/// which MCP tools the harness exposes to claude).
|
/// which MCP tools the harness exposes to claude).
|
||||||
///
|
///
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue