remove the get_host_journal MCP tool and its capability
swarm-logs covers every host-tier unit this tool could reach, so the second, capability-gated path into host journald earns nothing and is removed outright rather than disabled behind a flag. Removed end to end: the MCP tool definition + handler, the GetHostJournal/HostJournal wire variants, hive-c0re's dispatch_host_journal handler, the ReadHostJournal capability, and the harness-side capability->--allowedTools gate. get_host_journal was the only capability that mapped to an MCP tool, so allowed_capability_tools could only ever return an empty vec; it goes too rather than linger as a function that provably does nothing. capabilities::has_cap/caps_for stay: #4624 gave ManageRootAgent's bind-mount enforcement (hive-c0re/src/lifecycle/host_config.rs) a second caller of has_cap, so they're no longer callerless once this lands on top of it. hive-sh4re's journal module (JournalPriority) had no consumer outside this tool and is deleted. An existing capabilities.json still naming read_host_journal does not error: capabilities::prune_unknown drops unrecognised names with a warn!, and an agent left with no capabilities has its entry removed. No migration step is needed. Untouched: hive-c0re/src/dashboard/journal.rs's read_host_journal_response, which matches the name but is the private helper behind the operator-only GET /api/journal-host dashboard route and carries no capability check.
This commit is contained in:
parent
eec908bdb7
commit
11097ed336
18 changed files with 21 additions and 401 deletions
|
|
@ -7,7 +7,7 @@
|
|||
//!
|
||||
//! ```json
|
||||
//! {
|
||||
//! "atlas": ["read_host_journal"]
|
||||
//! "atlas": ["manage_root_agent"]
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
|
|
@ -167,9 +167,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn prune_unknown_keeps_known_name() {
|
||||
let mut map = BTreeMap::from([("atlas".to_owned(), vec!["read_host_journal".to_owned()])]);
|
||||
let mut map = BTreeMap::from([("atlas".to_owned(), vec!["manage_root_agent".to_owned()])]);
|
||||
assert!(!prune_unknown(&mut map));
|
||||
assert_eq!(map["atlas"], vec!["read_host_journal".to_owned()]);
|
||||
assert_eq!(map["atlas"], vec!["manage_root_agent".to_owned()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -183,23 +183,16 @@ mod tests {
|
|||
fn prune_unknown_keeps_known_and_drops_unknown_in_same_entry() {
|
||||
let mut map = BTreeMap::from([(
|
||||
"atlas".to_owned(),
|
||||
vec!["read_host_journal".to_owned(), "fly_to_the_moon".to_owned()],
|
||||
vec!["manage_root_agent".to_owned(), "fly_to_the_moon".to_owned()],
|
||||
)]);
|
||||
assert!(prune_unknown(&mut map));
|
||||
assert_eq!(map["atlas"], vec!["read_host_journal".to_owned()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manage_root_agent_is_still_a_known_name() {
|
||||
let mut map = BTreeMap::from([("atlas".to_owned(), vec!["manage_root_agent".to_owned()])]);
|
||||
assert!(!prune_unknown(&mut map));
|
||||
assert_eq!(map["atlas"], vec!["manage_root_agent".to_owned()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_known_caps_removes_entry_on_empty_input() {
|
||||
let mut current =
|
||||
BTreeMap::from([("atlas".to_owned(), vec!["read_host_journal".to_owned()])]);
|
||||
BTreeMap::from([("atlas".to_owned(), vec!["manage_root_agent".to_owned()])]);
|
||||
apply_known_caps(&mut current, "atlas", &[]);
|
||||
assert!(!current.contains_key("atlas"));
|
||||
}
|
||||
|
|
@ -207,7 +200,7 @@ mod tests {
|
|||
#[test]
|
||||
fn apply_known_caps_removes_entry_when_only_unknown_names_given() {
|
||||
let mut current =
|
||||
BTreeMap::from([("atlas".to_owned(), vec!["read_host_journal".to_owned()])]);
|
||||
BTreeMap::from([("atlas".to_owned(), vec!["manage_root_agent".to_owned()])]);
|
||||
apply_known_caps(&mut current, "atlas", &["fly_to_the_moon".to_owned()]);
|
||||
assert!(!current.contains_key("atlas"));
|
||||
}
|
||||
|
|
@ -218,8 +211,8 @@ mod tests {
|
|||
apply_known_caps(
|
||||
&mut current,
|
||||
"atlas",
|
||||
&["read_host_journal".to_owned(), "fly_to_the_moon".to_owned()],
|
||||
&["manage_root_agent".to_owned(), "fly_to_the_moon".to_owned()],
|
||||
);
|
||||
assert_eq!(current["atlas"], vec!["read_host_journal".to_owned()]);
|
||||
assert_eq!(current["atlas"], vec!["manage_root_agent".to_owned()]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,29 +231,6 @@ pub(crate) async fn dispatch_shared(
|
|||
coord.clear_pause_pending(agent);
|
||||
hive_core_agent_sock::Response::Ok
|
||||
}
|
||||
hive_core_agent_sock::Request::GetHostJournal {
|
||||
unit,
|
||||
container,
|
||||
lines,
|
||||
priority,
|
||||
grep,
|
||||
since,
|
||||
until,
|
||||
} => {
|
||||
dispatch_host_journal(
|
||||
agent,
|
||||
HostJournalArgs {
|
||||
unit,
|
||||
container,
|
||||
lines,
|
||||
priority,
|
||||
grep,
|
||||
since,
|
||||
until,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
// Not a shared variant.
|
||||
_ => return None,
|
||||
})
|
||||
|
|
@ -735,116 +712,6 @@ fn check_can_cancel_approval(canceller: &str) -> Result<(), String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Field-named journal-query knobs for [`dispatch_host_journal`].
|
||||
/// Borrows straight from the matched `GetHostJournal` request variant.
|
||||
pub struct HostJournalArgs<'a> {
|
||||
pub unit: &'a Option<String>,
|
||||
pub container: &'a Option<String>,
|
||||
pub lines: &'a Option<u32>,
|
||||
pub priority: &'a Option<hive_sh4re::journal::JournalPriority>,
|
||||
pub grep: &'a Option<String>,
|
||||
pub since: &'a Option<String>,
|
||||
pub until: &'a Option<String>,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// 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, args: HostJournalArgs<'_>) -> Response {
|
||||
let HostJournalArgs {
|
||||
unit,
|
||||
container,
|
||||
lines,
|
||||
priority,
|
||||
grep,
|
||||
since,
|
||||
until,
|
||||
} = args;
|
||||
if !crate::capabilities::has_cap(agent, hive_sh4re::permissions::Capability::ReadHostJournal) {
|
||||
return Response::Err {
|
||||
message: "agent does not have the read_host_journal capability".to_owned(),
|
||||
};
|
||||
}
|
||||
let n = lines.unwrap_or(30).min(100);
|
||||
|
||||
// A container (`-M`) read enters the container namespace and needs
|
||||
// root, so it's delegated to hive-priv. A host read (no container)
|
||||
// the unprivileged hive-core user can do directly via its
|
||||
// systemd-journal group membership.
|
||||
if let Some(c) = container {
|
||||
tracing::info!(%agent, machine = %c, %n, "get_host_journal (container)");
|
||||
return match crate::priv_client::read_container_journal(
|
||||
c,
|
||||
hive_priv_sock::JournalQuery {
|
||||
lines: n,
|
||||
unit: unit.clone(),
|
||||
priority: priority.as_ref().map(|p| p.as_journald_str().to_owned()),
|
||||
grep: grep.clone(),
|
||||
since: since.clone(),
|
||||
until: until.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((stdout, stderr)) => {
|
||||
let content = if stdout.is_empty() { stderr } else { stdout };
|
||||
Response::HostJournal { content }
|
||||
}
|
||||
Err(e) => Response::Err {
|
||||
message: format!("journal read: {e:#}"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let mut args: Vec<String> = vec![
|
||||
"--no-pager".to_owned(),
|
||||
"--output=short".to_owned(),
|
||||
"-n".to_owned(),
|
||||
n.to_string(),
|
||||
];
|
||||
if let Some(u) = unit {
|
||||
args.push("-u".to_owned());
|
||||
args.push(u.clone());
|
||||
}
|
||||
if let Some(p) = priority {
|
||||
args.push("-p".to_owned());
|
||||
args.push(p.as_journald_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")
|
||||
.args(&args)
|
||||
.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)
|
||||
};
|
||||
Response::HostJournal { content }
|
||||
}
|
||||
Err(e) => Response::Err {
|
||||
message: format!("journalctl spawn failed: {e:#}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Fan out one message to each recipient in `targets`. Skips the sender
|
||||
/// itself. Returns a list of `"<agent>: <error>"` strings for any delivery
|
||||
/// failures (empty = all good).
|
||||
|
|
|
|||
Loading…
Reference in a new issue