From 11097ed33645a77b85d0dc3b7c7fcfb033823ba9 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 21 Sep 2026 19:20:05 +0200 Subject: [PATCH] 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. --- docs/agent-lifecycle/persistence.md | 2 +- docs/process/conventions.md | 3 +- docs/tools/README.md | 2 +- docs/tools/scheduling.md | 25 ---- docs/turn-loop/mcp.md | 9 +- docs/web-ui/dashboard.md | 1 - hive-agent-mcp/src/mcp/args.rs | 30 ----- hive-agent-mcp/src/mcp/mod.rs | 46 +------ hive-agent/prompts/system.md | 1 - hive-agent/src/mcp_config.rs | 38 ------ hive-agent/src/stream_enrich.rs | 20 ---- hive-c0re/src/agent_config/capabilities.rs | 23 ++-- hive-c0re/src/socket_server/mod.rs | 133 --------------------- hive-core-agent-sock/src/lib.rs | 33 ----- hive-priv/src/main.rs | 4 +- hive-sh4re/src/journal.rs | 41 ------- hive-sh4re/src/lib.rs | 1 - hive-sh4re/src/permissions.rs | 10 +- 18 files changed, 21 insertions(+), 401 deletions(-) delete mode 100644 hive-sh4re/src/journal.rs diff --git a/docs/agent-lifecycle/persistence.md b/docs/agent-lifecycle/persistence.md index 3ac0ef61..e908efdd 100644 --- a/docs/agent-lifecycle/persistence.md +++ b/docs/agent-lifecycle/persistence.md @@ -399,7 +399,7 @@ Contents: `tool_groups::set_groups`; injected as `HIVE_TOOL_GROUPS` env var into each agent's container. - `capabilities.json` — per-agent capability grants - (`{ "atlas": ["read_host_journal"] }`). Written by + (`{ "ruth": ["manage_root_agent"] }`). Written by `capabilities::set_caps`; injected as `HIVE_CAPABILITIES` env var. Absent agents have no extra capabilities. - `resource-limits.json` — per-agent container resource overrides diff --git a/docs/process/conventions.md b/docs/process/conventions.md index aefacf00..fbd8e565 100644 --- a/docs/process/conventions.md +++ b/docs/process/conventions.md @@ -363,11 +363,10 @@ that allows the underlying resource access. | Capability | Effect | |---|---| | `manage_root_agent` | may manage *any* agent: bind-mounts every agent's state (rw) + config (ro) into the holder's container, plus `/applied` and `/meta` (ro). Takes effect on the holder's next container rebuild/restart | -| `read_host_journal` | registers the `get_host_journal` MCP tool + serves `GET /journal-host` requests | **Config storage** — per-agent capabilities live in `/var/lib/hyperhive/meta/capabilities.json` alongside `tool-groups.json`. -Format: `{ "atlas": ["read_host_journal"], "ruth": ["manage_root_agent"] }`. +Format: `{ "ruth": ["manage_root_agent"] }`. An absent entry means "no extra capabilities." `render_flake` in `meta.rs` reads this file and injects `HIVE_CAPABILITIES` (comma-separated `snake_case` names) into each agent's systemd service env; absent entries emit diff --git a/docs/tools/README.md b/docs/tools/README.md index f88794d8..865681b6 100644 --- a/docs/tools/README.md +++ b/docs/tools/README.md @@ -44,4 +44,4 @@ debug agent behavior. (`mcp__matrix__*`) for agents with a matrix account, multiple accounts per agent, and declaring extra MCP servers generally. - **[scheduling](scheduling.md)** — scheduled prompts (operator - approval required) and the `get_host_journal` diagnostics tool. + approval required). diff --git a/docs/tools/scheduling.md b/docs/tools/scheduling.md index 15291fb9..457b0f43 100644 --- a/docs/tools/scheduling.md +++ b/docs/tools/scheduling.md @@ -66,31 +66,6 @@ body, per-target `last_fired_at` and `last_result`, `next_fire_at_unix`, `interval_seconds`. Use to look up an id before cancelling, or to audit upcoming wake-ups across the hive. -## `read_host_journal` capability - -Capability-gated (not a tool group) — the operator enables it in the -P3RM1SS10NS C4P4B1L1T13S section. Unlike tool groups this isn't -configurable from `agent.nix`. - -### `get_host_journal(unit?, container?, lines?, priority?, grep?, since?, until?)` - -Fetch recent lines from the **host** journal (requires -`read_host_journal` capability). Useful when you need visibility -outside your own container — infrastructure services, hive-c0re -lifecycle events, or another container's boot log. - -- `unit` — filter to a systemd unit (for example `hive-c0re.service`). -- `container` — nspawn machine name verbatim. Agent containers use - the `h-` prefix (for example `h-iris`); infrastructure containers - use their full name (for example `hive-ci`, `hive-forge`, `hive-matrix`). - Omit for the host journal. The gateway has no machine — its nginx - runs on the host, so read it with `unit: nginx.service` and no - `container`. -- `lines` — how many lines to return (default 30, max 100). -- `priority` — minimum syslog level (`emerg` … `debug`). -- `grep` — regex matched against log message fields (`journalctl --grep`). -- `since` / `until` — time bounds (for example `-1h`, `2024-01-01 12:00:00`). - ## See also - `remind` (no-approval self-wake path) — documented in diff --git a/docs/turn-loop/mcp.md b/docs/turn-loop/mcp.md index bab51e46..22b0a00a 100644 --- a/docs/turn-loop/mcp.md +++ b/docs/turn-loop/mcp.md @@ -150,11 +150,10 @@ hive_name?, swarm_name?, matrix_accounts? }`. `matrix_accounts` is a and `WebSearch` tools (not MCP tools; added directly to the `--allowedTools` list). Off by default; add the group in the P3RM1SS10NS tab and rebuild to enable. -- **Capability-gated** — `get_host_journal` (requires - `read_host_journal` capability set via the P3RM1SS10NS tab; - orthogonal to tool groups). Full list of capabilities and their - effects in [`docs/process/conventions.md#capabilities`](../process/conventions.md). - Also documented in [`docs/tools/scheduling.md`](../tools/scheduling.md). +- **Capabilities** — orthogonal to tool groups, set via the P3RM1SS10NS + tab. No capability registers an MCP tool today; the full list and their + effects are in + [`docs/process/conventions.md#capabilities`](../process/conventions.md). - **Matrix MCP + extra servers** — `mcp__matrix__*` tools and per-agent extra MCP config. See [`docs/tools/matrix.md`](../tools/matrix.md). diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 7b9ba0c2..488117b8 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -441,7 +441,6 @@ The current capabilities are: | Name | Effect | |------|--------| | `manage_root_agent` | mounts every agent's state (rw) + config (ro) into this agent's container so it can manage/recover any agent | -| `read_host_journal` | unlocks `get_host_journal` to read journald from inside a container | Each row is one agent. Columns are the capability names returned by `GET /api/capabilities` as `caps: Vec`. Checking or unchecking diff --git a/hive-agent-mcp/src/mcp/args.rs b/hive-agent-mcp/src/mcp/args.rs index 4051294e..70ab70cb 100644 --- a/hive-agent-mcp/src/mcp/args.rs +++ b/hive-agent-mcp/src/mcp/args.rs @@ -216,33 +216,3 @@ pub struct EditScheduleArgs { #[serde(default)] pub targets_remove: Option>, } - -/// Arguments for `get_host_journal` (capability-gated: `read_host_journal`). -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct GetHostJournalArgs { - /// Systemd unit to filter (e.g. `hive-c0re.service`). Omit for all units. - #[serde(default)] - pub unit: Option, - /// nspawn machine name verbatim (e.g. `h-iris`). Omit for host journal. - /// Agent containers use the `h-` prefix (e.g. `h-iris`); infrastructure - /// containers use their full name (e.g. `hive-ci`, `hive-forge`, - /// `hive-matrix`). The gateway has no machine — its nginx runs on the - /// host, so read it with `unit: nginx.service` and no `container`. - #[serde(default)] - pub container: Option, - /// Number of lines to return (default 30, max 100). - #[serde(default)] - pub lines: Option, - /// Minimum syslog priority level. - #[serde(default)] - pub priority: Option, - /// Regex to match against log message fields (journalctl --grep). - #[serde(default)] - pub grep: Option, - /// Show entries on or newer than this timestamp (e.g. `-1h`). - #[serde(default)] - pub since: Option, - /// Show entries on or older than this timestamp. - #[serde(default)] - pub until: Option, -} diff --git a/hive-agent-mcp/src/mcp/mod.rs b/hive-agent-mcp/src/mcp/mod.rs index 455df407..2eb85350 100644 --- a/hive-agent-mcp/src/mcp/mod.rs +++ b/hive-agent-mcp/src/mcp/mod.rs @@ -25,8 +25,8 @@ mod render; pub use args::{ AckUntilArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs, CreateRepoArgs, - EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, GetHostJournalArgs, MarkTodosDoneArgs, - RecvArgs, RemindArgs, RequestSchedulePromptArgs, SendArgs, SetStatusArgs, + EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, MarkTodosDoneArgs, RecvArgs, + RemindArgs, RequestSchedulePromptArgs, SendArgs, SetStatusArgs, }; pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv}; @@ -547,48 +547,6 @@ impl AgentServer { .await } - // IMPORTANT: this tool is capability-gated (`read_host_journal`). - // It is added to `--allowedTools` by `allowed_capability_tools` only - // when `HIVE_CAPABILITIES` contains `read_host_journal`. hive-c0re - // 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. \ - `unit`: filter to a systemd unit (e.g. `hive-c0re.service`). \ - `container`: nspawn machine name verbatim (e.g. `h-iris`). Omit for host journal. \ - Agent containers use the `h-` prefix (e.g. `h-iris`, `h-atlas`); \ - infrastructure containers use their full name (e.g. `hive-ci`, `hive-forge`, \ - `hive-matrix`). The gateway is not a container: its nginx logs are in \ - the host journal, so omit `container` and pass `unit: nginx.service`. \ - `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, Parameters(args): Parameters) -> String { - let log = format!("{args:?}"); - run_tool_envelope("get_host_journal", log, async move { - let (resp, retries) = self - .dispatch(hive_core_agent_sock::Request::GetHostJournal { - unit: args.unit, - container: args.container, - lines: args.lines, - priority: args.priority, - grep: args.grep, - since: args.since, - until: args.until, - }) - .await; - let result = match resp { - Ok(hive_core_agent_sock::Response::HostJournal { content }) => content, - other => reply_err(other, "get_host_journal"), - }; - annotate_retries(result, retries) - }) - .await - } - #[tool( description = "Queue an approval to add a scheduled prompt — one body delivered to \ N agent inboxes at a target time, optionally recurring every `interval_seconds`. \ diff --git a/hive-agent/prompts/system.md b/hive-agent/prompts/system.md index 13221bcc..f73a1949 100644 --- a/hive-agent/prompts/system.md +++ b/hive-agent/prompts/system.md @@ -5,7 +5,6 @@ Tools (hyperhive surface). Full signature + behavior for each comes from the too - **Inbox / messaging** (always available): `mcp__hyperhive__recv`, `mcp__hyperhive__ack_until`, `mcp__hyperhive__send`, `mcp__hyperhive__get_loose_ends`, `mcp__hyperhive__cancel_loose_end`, `mcp__hyperhive__mark_todos_done`, `mcp__hyperhive__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`. One habit worth internalizing beyond the tool descriptions themselves: prefer ending the turn over repeatedly polling `recv` when idle (only turn-boundaries observe in-container todo wakes — bash-task completions, matrix unread, forge activity — and ending the turn is also your checkpoint). For a large todo backlog (`get_loose_ends` caps at 40 rows), clear reviewed ids in bulk with `mark_todos_done` rather than cancelling one at a time — there's no blind range-clear, only ids you've actually looked at. - **Extra MCP tools** (some agents only): `mcp____` — agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. First-class tools, already operator-approved at deploy time. - **Scheduling** (_requires `scheduling` tool group_): `request_schedule_prompt` (queues an approval), `cancel_schedule`, `fire_schedule_now`, `edit_schedule`, `list_schedules` (these four don't need approval — you can manage any schedule, whoever owns it). -- **Diagnostics**: `get_host_journal` (_requires `read_host_journal` capability_). Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — open a PR on your `agent-configs/` repo, or contact the operator directly. Config repos live at `/agents/{label}/config/` (read-only inside your container). All changes flow through operator-approved commits. diff --git a/hive-agent/src/mcp_config.rs b/hive-agent/src/mcp_config.rs index 84c646df..5edb9204 100644 --- a/hive-agent/src/mcp_config.rs +++ b/hive-agent/src/mcp_config.rs @@ -30,37 +30,6 @@ pub const DEFAULT_MCP_HTTP_PORT: u16 = 8790; /// drift. pub use hive_sh4re::permissions::{builtin_tools_arg, effective_tool_groups}; -/// `HIVE_CAPABILITIES` env var injected by `meta::render_flake` when the -/// operator grants capabilities to this agent. Comma-separated -/// `hive_sh4re::permissions::Capability` `snake_case` names. Absent = no extra capabilities. -const CAPABILITIES_ENV: &str = "HIVE_CAPABILITIES"; - -/// Returns the MCP tool names (without `mcp__hyperhive__` prefix) that are -/// unlocked by the agent's current capability set. These are added to the -/// `--allowedTools` list so claude can call them without prompting, and -/// hive-c0re performs a second server-side capability check before executing. -fn allowed_capability_tools() -> Vec { - let raw = match std::env::var(CAPABILITIES_ENV) { - Ok(v) if !v.trim().is_empty() => v, - _ => return vec![], - }; - let mut tools = Vec::new(); - for token in raw.split(',') { - let t = token.trim().to_ascii_lowercase(); - match t.as_str() { - "read_host_journal" => tools.push("get_host_journal".to_owned()), - // manage_root_agent exposes no MCP tool at all: it is a - // host-side grant, enforced by hive-c0re when it computes the - // container's bind mounts. Nothing for the harness to register. - "manage_root_agent" => {} - unknown => { - tracing::warn!(capability = %unknown, "unrecognised capability in HIVE_CAPABILITIES — skipped"); - } - } - } - tools -} - /// Tool group an extra (out-of-process) MCP server is gated behind, if any. /// /// Most `services.hyperhive.agent.extraMcpServers` entries are ungated — available whenever @@ -161,13 +130,6 @@ pub fn allowed_tools_arg() -> String { .map(ToOwned::to_owned) .collect(); all.extend(allowed_mcp_tools(&groups)); - // Capability-gated MCP tools: added to --allowedTools when HIVE_CAPABILITIES - // includes the corresponding capability. hive-c0re performs a second - // server-side check, so this is a usability gate (no annoying prompts), - // not the security boundary. - for tool in allowed_capability_tools() { - all.push(format!("mcp__{SERVER_NAME}__{tool}")); - } all.join(",") } diff --git a/hive-agent/src/stream_enrich.rs b/hive-agent/src/stream_enrich.rs index 2a5056b1..b73994c8 100644 --- a/hive-agent/src/stream_enrich.rs +++ b/hive-agent/src/stream_enrich.rs @@ -501,7 +501,6 @@ fn tool_icon(name: &str) -> &'static str { "mcp__matrix__list_rooms" | "mcp__matrix__list_room_members" | "mcp__matrix__list_invites" => "📋", - "mcp__hyperhive__get_host_journal" => "📜", "mcp__matrix__read_room" | "Read" => "📖", "mcp__matrix__mark_read" => "👁️", "mcp__bash__kill" => "🛑", @@ -645,25 +644,6 @@ fn fmt_hyperhive_tool(name: &str, short: &str, input: &Value) -> String { .map_or_else(|| "?".to_owned(), |n| n.to_string()); format!("{short} ≤{up_to}") } - "mcp__hyperhive__get_host_journal" => { - let mut parts = Vec::new(); - if let Some(c) = input.get("container").and_then(Value::as_str) { - parts.push(c.to_owned()); - } else if let Some(u) = input.get("unit").and_then(Value::as_str) { - parts.push(u.to_owned()); - } - if let Some(g) = input.get("grep").and_then(Value::as_str) { - parts.push(format!("/{g}/")); - } - if let Some(l) = input.get("lines").and_then(Value::as_u64) { - parts.push(format!("{l}L")); - } - if parts.is_empty() { - format!("{short}()") - } else { - format!("{short} {}", parts.join(" · ")) - } - } "mcp__hyperhive__remind" => fmt_hyperhive_remind(short, input), "mcp__hyperhive__list_schedules" | "mcp__hyperhive__cancel_schedule" diff --git a/hive-c0re/src/agent_config/capabilities.rs b/hive-c0re/src/agent_config/capabilities.rs index 8855e8fb..9e077798 100644 --- a/hive-c0re/src/agent_config/capabilities.rs +++ b/hive-c0re/src/agent_config/capabilities.rs @@ -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()]); } } diff --git a/hive-c0re/src/socket_server/mod.rs b/hive-c0re/src/socket_server/mod.rs index 6f564921..1ce0c746 100644 --- a/hive-c0re/src/socket_server/mod.rs +++ b/hive-c0re/src/socket_server/mod.rs @@ -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, - pub container: &'a Option, - pub lines: &'a Option, - pub priority: &'a Option, - pub grep: &'a Option, - pub since: &'a Option, - pub until: &'a Option, -} - -/// 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 = 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 `": "` strings for any delivery /// failures (empty = all good). diff --git a/hive-core-agent-sock/src/lib.rs b/hive-core-agent-sock/src/lib.rs index 8b8c7a47..bdbd4973 100644 --- a/hive-core-agent-sock/src/lib.rs +++ b/hive-core-agent-sock/src/lib.rs @@ -9,7 +9,6 @@ use hive_sh4re::container::MatrixIdentity; use hive_sh4re::inbox::{CancelLooseEndKind, DeliveredMessage, InboxRow, LooseEnd}; -use hive_sh4re::journal::JournalPriority; use hive_sh4re::manager::SchedulePromptPayload; use hive_sh4re::schedule::WireSchedule; use hive_types::Ident; @@ -119,34 +118,6 @@ pub enum Request { /// DAG's drain node resolve immediately instead of waiting out its /// timeout fallback — same shape as `GracefulStopComplete`. PauseAcknowledged, - /// *(capability-gated: `read_host_journal`)* Fetch recent lines - /// from the host journal. Filters are all optional; omitting all - /// returns the last `lines` entries from the global journal. - GetHostJournal { - /// Filter to a specific systemd unit (e.g. `hive-c0re.service`). - #[serde(default, skip_serializing_if = "Option::is_none")] - unit: Option, - /// 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, - /// Number of journal lines to return (default 30, max 100). - #[serde(default, skip_serializing_if = "Option::is_none")] - lines: Option, - /// Minimum syslog priority level. - #[serde(default, skip_serializing_if = "Option::is_none")] - priority: Option, - /// Regex to match against log message fields (journalctl `--grep`). - #[serde(default, skip_serializing_if = "Option::is_none")] - grep: Option, - /// 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, - /// Show entries on or older than this timestamp (journalctl `--until`). - #[serde(default, skip_serializing_if = "Option::is_none")] - until: Option, - }, // ---- privileged (manager socket only for now) --------------------------- /// *(privileged)* Queue an approval to add a scheduled prompt. @@ -247,10 +218,6 @@ pub enum Response { #[serde(default, skip_serializing_if = "Vec::is_empty")] matrix_accounts: Vec, }, - /// `GetHostJournal` result: host journal lines matching the - /// requested filters. Returned on the agent socket when the agent - /// holds the `read_host_journal` capability. - HostJournal { content: String }, /// `ListSchedules` result. Snapshot of the schedules the requester is /// authorized to see — see `ListSchedules`'s own doc comment. /// Returned on the manager socket only. diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 8eaca8d1..4831b5d2 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -2259,8 +2259,8 @@ fn validate_forge_admin_arg(arg: &str) -> Result<()> { /// ⚠️ This deliberately over-matches. A nix store hash is also a long /// opaque run and will redact its line. That is the correct direction to /// be wrong in: the cost of a false positive is one less log line, and -/// the cost of a false negative is a live credential in a journal that -/// any `read_host_journal` holder can read. +/// the cost of a false negative is a live credential sitting in the host +/// journal, readable by anything with access to it. fn redact_secret_line(line: &str) -> std::borrow::Cow<'_, str> { if line.to_ascii_lowercase().contains("password") { return std::borrow::Cow::Borrowed("[redacted: line mentions a password]"); diff --git a/hive-sh4re/src/journal.rs b/hive-sh4re/src/journal.rs deleted file mode 100644 index e140cf8a..00000000 --- a/hive-sh4re/src/journal.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Journal-priority encoding for `GetHostJournal` (`read_host_journal` -//! capability). One small enum, own topic module rather than a -//! catch-all — see `hive-sh4re/README.md`'s module list. - -use serde::{Deserialize, Serialize}; - -/// 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`. Named - /// `as_journald_str` (not `as_str`) because this is a specific - /// external-tool encoding, not the enum's general wire/db string — - /// distinct call sites shouldn't reach for this by accident when they - /// actually want the serde wire representation. - #[must_use] - pub fn as_journald_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", - } - } -} diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 2e4d2ddf..37129bbb 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -5,7 +5,6 @@ pub mod assets; pub mod bash_task; pub mod container; pub mod inbox; -pub mod journal; pub mod manager; pub mod paths; pub mod permissions; diff --git a/hive-sh4re/src/permissions.rs b/hive-sh4re/src/permissions.rs index 6c80bb7d..76d36377 100644 --- a/hive-sh4re/src/permissions.rs +++ b/hive-sh4re/src/permissions.rs @@ -354,7 +354,7 @@ impl ToolGroup { } /// 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": ["manage_root_agent"] }`). /// Capabilities control system-level access hive-c0re enforces at /// dispatch time; they are orthogonal to tool groups (which control /// which MCP tools the harness exposes to claude). @@ -382,17 +382,12 @@ pub enum Capability { /// into an unrecognised name that `capabilities::prune_unknown` /// silently drops. ManageRootAgent, - /// Agent can read the full host journal via `GetHostJournal`. - /// hive-c0re checks this capability before running journalctl. - /// MCP tool `get_host_journal` is only registered in the harness - /// when this capability is present. - ReadHostJournal, } impl Capability { /// Every known capability in a stable order. Use this to enumerate /// columns in the permissions UI or validate incoming capability strings. - pub const ALL: &'static [Self] = &[Self::ManageRootAgent, Self::ReadHostJournal]; + pub const ALL: &'static [Self] = &[Self::ManageRootAgent]; /// Short human-readable description suitable for a tooltip or help text. #[must_use] @@ -401,7 +396,6 @@ impl Capability { Self::ManageRootAgent => { "manage any agent: every agent's state (rw) and config (ro) mounted for recovery" } - Self::ReadHostJournal => "read host journald via get_host_journal MCP tool", } } }