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
|
|
@ -399,7 +399,7 @@ Contents:
|
||||||
`tool_groups::set_groups`; injected as `HIVE_TOOL_GROUPS` env
|
`tool_groups::set_groups`; injected as `HIVE_TOOL_GROUPS` env
|
||||||
var into each agent's container.
|
var into each agent's container.
|
||||||
- `capabilities.json` — per-agent capability grants
|
- `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
|
`capabilities::set_caps`; injected as `HIVE_CAPABILITIES` env
|
||||||
var. Absent agents have no extra capabilities.
|
var. Absent agents have no extra capabilities.
|
||||||
- `resource-limits.json` — per-agent container resource overrides
|
- `resource-limits.json` — per-agent container resource overrides
|
||||||
|
|
|
||||||
|
|
@ -363,11 +363,10 @@ that allows the underlying resource access.
|
||||||
| Capability | Effect |
|
| 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 |
|
| `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
|
**Config storage** — per-agent capabilities live in
|
||||||
`/var/lib/hyperhive/meta/capabilities.json` alongside `tool-groups.json`.
|
`/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`
|
An absent entry means "no extra capabilities." `render_flake` in `meta.rs`
|
||||||
reads this file and injects `HIVE_CAPABILITIES` (comma-separated
|
reads this file and injects `HIVE_CAPABILITIES` (comma-separated
|
||||||
`snake_case` names) into each agent's systemd service env; absent entries emit
|
`snake_case` names) into each agent's systemd service env; absent entries emit
|
||||||
|
|
|
||||||
|
|
@ -44,4 +44,4 @@ debug agent behavior.
|
||||||
(`mcp__matrix__*`) for agents with a matrix account, multiple
|
(`mcp__matrix__*`) for agents with a matrix account, multiple
|
||||||
accounts per agent, and declaring extra MCP servers generally.
|
accounts per agent, and declaring extra MCP servers generally.
|
||||||
- **[scheduling](scheduling.md)** — scheduled prompts (operator
|
- **[scheduling](scheduling.md)** — scheduled prompts (operator
|
||||||
approval required) and the `get_host_journal` diagnostics tool.
|
approval required).
|
||||||
|
|
|
||||||
|
|
@ -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
|
`next_fire_at_unix`, `interval_seconds`. Use to look up an id before
|
||||||
cancelling, or to audit upcoming wake-ups across the hive.
|
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-<name>` 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
|
## See also
|
||||||
|
|
||||||
- `remind` (no-approval self-wake path) — documented in
|
- `remind` (no-approval self-wake path) — documented in
|
||||||
|
|
|
||||||
|
|
@ -150,11 +150,10 @@ hive_name?, swarm_name?, matrix_accounts? }`. `matrix_accounts` is a
|
||||||
and `WebSearch` tools (not MCP tools; added directly to the
|
and `WebSearch` tools (not MCP tools; added directly to the
|
||||||
`--allowedTools` list). Off by default; add the group in the
|
`--allowedTools` list). Off by default; add the group in the
|
||||||
P3RM1SS10NS tab and rebuild to enable.
|
P3RM1SS10NS tab and rebuild to enable.
|
||||||
- **Capability-gated** — `get_host_journal` (requires
|
- **Capabilities** — orthogonal to tool groups, set via the P3RM1SS10NS
|
||||||
`read_host_journal` capability set via the P3RM1SS10NS tab;
|
tab. No capability registers an MCP tool today; the full list and their
|
||||||
orthogonal to tool groups). Full list of capabilities and their
|
effects are in
|
||||||
effects in [`docs/process/conventions.md#capabilities`](../process/conventions.md).
|
[`docs/process/conventions.md#capabilities`](../process/conventions.md).
|
||||||
Also documented in [`docs/tools/scheduling.md`](../tools/scheduling.md).
|
|
||||||
- **Matrix MCP + extra servers** — `mcp__matrix__*` tools and
|
- **Matrix MCP + extra servers** — `mcp__matrix__*` tools and
|
||||||
per-agent extra MCP config. See
|
per-agent extra MCP config. See
|
||||||
[`docs/tools/matrix.md`](../tools/matrix.md).
|
[`docs/tools/matrix.md`](../tools/matrix.md).
|
||||||
|
|
|
||||||
|
|
@ -441,7 +441,6 @@ The current capabilities are:
|
||||||
| Name | Effect |
|
| 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 |
|
| `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
|
Each row is one agent. Columns are the capability names returned by
|
||||||
`GET /api/capabilities` as `caps: Vec<String>`. Checking or unchecking
|
`GET /api/capabilities` as `caps: Vec<String>`. Checking or unchecking
|
||||||
|
|
|
||||||
|
|
@ -216,33 +216,3 @@ pub struct EditScheduleArgs {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub targets_remove: Option<Vec<String>>,
|
pub targets_remove: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<String>,
|
|
||||||
/// 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<String>,
|
|
||||||
/// Number of lines to return (default 30, max 100).
|
|
||||||
#[serde(default)]
|
|
||||||
pub lines: Option<u32>,
|
|
||||||
/// Minimum syslog priority level.
|
|
||||||
#[serde(default)]
|
|
||||||
pub priority: Option<hive_sh4re::journal::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>,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ mod render;
|
||||||
|
|
||||||
pub use args::{
|
pub use args::{
|
||||||
AckUntilArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs, CreateRepoArgs,
|
AckUntilArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs, CreateRepoArgs,
|
||||||
EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, GetHostJournalArgs, MarkTodosDoneArgs,
|
EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, MarkTodosDoneArgs, RecvArgs,
|
||||||
RecvArgs, RemindArgs, RequestSchedulePromptArgs, SendArgs, SetStatusArgs,
|
RemindArgs, RequestSchedulePromptArgs, SendArgs, SetStatusArgs,
|
||||||
};
|
};
|
||||||
pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv};
|
pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv};
|
||||||
|
|
||||||
|
|
@ -547,48 +547,6 @@ impl AgentServer {
|
||||||
.await
|
.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<GetHostJournalArgs>) -> 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(
|
#[tool(
|
||||||
description = "Queue an approval to add a scheduled prompt — one body delivered to \
|
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`. \
|
N agent inboxes at a target time, optionally recurring every `interval_seconds`. \
|
||||||
|
|
|
||||||
|
|
@ -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.
|
- **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__<server>__<tool>` — 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.
|
- **Extra MCP tools** (some agents only): `mcp__<server>__<tool>` — 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).
|
- **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/<name>` 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.
|
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/<name>` 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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,37 +30,6 @@ pub const DEFAULT_MCP_HTTP_PORT: u16 = 8790;
|
||||||
/// drift.
|
/// drift.
|
||||||
pub use hive_sh4re::permissions::{builtin_tools_arg, effective_tool_groups};
|
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<String> {
|
|
||||||
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.
|
/// Tool group an extra (out-of-process) MCP server is gated behind, if any.
|
||||||
///
|
///
|
||||||
/// Most `services.hyperhive.agent.extraMcpServers` entries are ungated — available whenever
|
/// Most `services.hyperhive.agent.extraMcpServers` entries are ungated — available whenever
|
||||||
|
|
@ -161,13 +130,6 @@ pub fn allowed_tools_arg() -> String {
|
||||||
.map(ToOwned::to_owned)
|
.map(ToOwned::to_owned)
|
||||||
.collect();
|
.collect();
|
||||||
all.extend(allowed_mcp_tools(&groups));
|
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(",")
|
all.join(",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -501,7 +501,6 @@ fn tool_icon(name: &str) -> &'static str {
|
||||||
"mcp__matrix__list_rooms"
|
"mcp__matrix__list_rooms"
|
||||||
| "mcp__matrix__list_room_members"
|
| "mcp__matrix__list_room_members"
|
||||||
| "mcp__matrix__list_invites" => "📋",
|
| "mcp__matrix__list_invites" => "📋",
|
||||||
"mcp__hyperhive__get_host_journal" => "📜",
|
|
||||||
"mcp__matrix__read_room" | "Read" => "📖",
|
"mcp__matrix__read_room" | "Read" => "📖",
|
||||||
"mcp__matrix__mark_read" => "👁️",
|
"mcp__matrix__mark_read" => "👁️",
|
||||||
"mcp__bash__kill" => "🛑",
|
"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());
|
.map_or_else(|| "?".to_owned(), |n| n.to_string());
|
||||||
format!("{short} ≤{up_to}")
|
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__remind" => fmt_hyperhive_remind(short, input),
|
||||||
"mcp__hyperhive__list_schedules"
|
"mcp__hyperhive__list_schedules"
|
||||||
| "mcp__hyperhive__cancel_schedule"
|
| "mcp__hyperhive__cancel_schedule"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
//!
|
//!
|
||||||
//! ```json
|
//! ```json
|
||||||
//! {
|
//! {
|
||||||
//! "atlas": ["read_host_journal"]
|
//! "atlas": ["manage_root_agent"]
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
|
@ -167,9 +167,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn prune_unknown_keeps_known_name() {
|
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!(!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]
|
#[test]
|
||||||
|
|
@ -183,23 +183,16 @@ mod tests {
|
||||||
fn prune_unknown_keeps_known_and_drops_unknown_in_same_entry() {
|
fn prune_unknown_keeps_known_and_drops_unknown_in_same_entry() {
|
||||||
let mut map = BTreeMap::from([(
|
let mut map = BTreeMap::from([(
|
||||||
"atlas".to_owned(),
|
"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!(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()]);
|
assert_eq!(map["atlas"], vec!["manage_root_agent".to_owned()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_known_caps_removes_entry_on_empty_input() {
|
fn apply_known_caps_removes_entry_on_empty_input() {
|
||||||
let mut current =
|
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", &[]);
|
apply_known_caps(&mut current, "atlas", &[]);
|
||||||
assert!(!current.contains_key("atlas"));
|
assert!(!current.contains_key("atlas"));
|
||||||
}
|
}
|
||||||
|
|
@ -207,7 +200,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_known_caps_removes_entry_when_only_unknown_names_given() {
|
fn apply_known_caps_removes_entry_when_only_unknown_names_given() {
|
||||||
let mut current =
|
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()]);
|
apply_known_caps(&mut current, "atlas", &["fly_to_the_moon".to_owned()]);
|
||||||
assert!(!current.contains_key("atlas"));
|
assert!(!current.contains_key("atlas"));
|
||||||
}
|
}
|
||||||
|
|
@ -218,8 +211,8 @@ mod tests {
|
||||||
apply_known_caps(
|
apply_known_caps(
|
||||||
&mut current,
|
&mut current,
|
||||||
"atlas",
|
"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);
|
coord.clear_pause_pending(agent);
|
||||||
hive_core_agent_sock::Response::Ok
|
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.
|
// Not a shared variant.
|
||||||
_ => return None,
|
_ => 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
|
/// Fan out one message to each recipient in `targets`. Skips the sender
|
||||||
/// itself. Returns a list of `"<agent>: <error>"` strings for any delivery
|
/// itself. Returns a list of `"<agent>: <error>"` strings for any delivery
|
||||||
/// failures (empty = all good).
|
/// failures (empty = all good).
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
|
|
||||||
use hive_sh4re::container::MatrixIdentity;
|
use hive_sh4re::container::MatrixIdentity;
|
||||||
use hive_sh4re::inbox::{CancelLooseEndKind, DeliveredMessage, InboxRow, LooseEnd};
|
use hive_sh4re::inbox::{CancelLooseEndKind, DeliveredMessage, InboxRow, LooseEnd};
|
||||||
use hive_sh4re::journal::JournalPriority;
|
|
||||||
use hive_sh4re::manager::SchedulePromptPayload;
|
use hive_sh4re::manager::SchedulePromptPayload;
|
||||||
use hive_sh4re::schedule::WireSchedule;
|
use hive_sh4re::schedule::WireSchedule;
|
||||||
use hive_types::Ident;
|
use hive_types::Ident;
|
||||||
|
|
@ -119,34 +118,6 @@ pub enum Request {
|
||||||
/// DAG's drain node resolve immediately instead of waiting out its
|
/// DAG's drain node resolve immediately instead of waiting out its
|
||||||
/// timeout fallback — same shape as `GracefulStopComplete`.
|
/// timeout fallback — same shape as `GracefulStopComplete`.
|
||||||
PauseAcknowledged,
|
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<String>,
|
|
||||||
/// 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 30, max 100).
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
lines: Option<u32>,
|
|
||||||
/// Minimum syslog priority level.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
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) ---------------------------
|
||||||
/// *(privileged)* Queue an approval to add a scheduled prompt.
|
/// *(privileged)* Queue an approval to add a scheduled prompt.
|
||||||
|
|
@ -247,10 +218,6 @@ pub enum Response {
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
matrix_accounts: Vec<MatrixIdentity>,
|
matrix_accounts: Vec<MatrixIdentity>,
|
||||||
},
|
},
|
||||||
/// `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
|
/// `ListSchedules` result. Snapshot of the schedules the requester is
|
||||||
/// authorized to see — see `ListSchedules`'s own doc comment.
|
/// authorized to see — see `ListSchedules`'s own doc comment.
|
||||||
/// Returned on the manager socket only.
|
/// Returned on the manager socket only.
|
||||||
|
|
|
||||||
|
|
@ -2259,8 +2259,8 @@ fn validate_forge_admin_arg(arg: &str) -> Result<()> {
|
||||||
/// ⚠️ This deliberately over-matches. A nix store hash is also a long
|
/// ⚠️ 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
|
/// 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
|
/// 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
|
/// the cost of a false negative is a live credential sitting in the host
|
||||||
/// any `read_host_journal` holder can read.
|
/// journal, readable by anything with access to it.
|
||||||
fn redact_secret_line(line: &str) -> std::borrow::Cow<'_, str> {
|
fn redact_secret_line(line: &str) -> std::borrow::Cow<'_, str> {
|
||||||
if line.to_ascii_lowercase().contains("password") {
|
if line.to_ascii_lowercase().contains("password") {
|
||||||
return std::borrow::Cow::Borrowed("[redacted: line mentions a password]");
|
return std::borrow::Cow::Borrowed("[redacted: line mentions a password]");
|
||||||
|
|
|
||||||
|
|
@ -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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,6 @@ pub mod assets;
|
||||||
pub mod bash_task;
|
pub mod bash_task;
|
||||||
pub mod container;
|
pub mod container;
|
||||||
pub mod inbox;
|
pub mod inbox;
|
||||||
pub mod journal;
|
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
pub mod paths;
|
pub mod paths;
|
||||||
pub mod permissions;
|
pub mod permissions;
|
||||||
|
|
|
||||||
|
|
@ -354,7 +354,7 @@ 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": ["manage_root_agent"] }`).
|
||||||
/// Capabilities control system-level access hive-c0re enforces at
|
/// Capabilities control system-level access hive-c0re enforces at
|
||||||
/// dispatch time; they are orthogonal to tool groups (which control
|
/// 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).
|
||||||
|
|
@ -382,17 +382,12 @@ pub enum Capability {
|
||||||
/// into an unrecognised name that `capabilities::prune_unknown`
|
/// into an unrecognised name that `capabilities::prune_unknown`
|
||||||
/// silently drops.
|
/// silently drops.
|
||||||
ManageRootAgent,
|
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 {
|
impl Capability {
|
||||||
/// Every known capability in a stable order. Use this to enumerate
|
/// Every known capability in a stable order. Use this to enumerate
|
||||||
/// columns in the permissions UI or validate incoming capability strings.
|
/// 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.
|
/// Short human-readable description suitable for a tooltip or help text.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
@ -401,7 +396,6 @@ impl Capability {
|
||||||
Self::ManageRootAgent => {
|
Self::ManageRootAgent => {
|
||||||
"manage any agent: every agent's state (rw) and config (ro) mounted for recovery"
|
"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",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue