From c28da210b45db0289537c419f3b3cf089583aa4b Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 11 Sep 2026 16:06:20 +0200 Subject: [PATCH] scheduling: clamp get_logs host-side, and fix two authorization claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `docs/tools/scheduling.md` said three things about who may do what. Two were wrong prose; the third was the code. "All scheduling ops go through the operator approval queue" — one of the five does. Cancel, edit, list and fire are `require_group("scheduling")` and nothing else (socket_server/mod.rs:594-643), which is what the MCP tool descriptions already tell an agent. The page pushed in the cautious direction: someone watching a runaway recurring schedule would wait for an operator rather than cancel it themselves. The authorization sentence covered "read/cancel/edit". Cancel, edit and fire really do check `cancel_authorized` (self, operator, or subtree) from three call sites. `handle_list_schedules` takes no requester at all and returns every row — now stated, along with the part that matters: the snapshot includes other agents' schedule bodies. `lines` was documented as "host-capped at 500" and the 500 was in the agent's own MCP layer, not the host; `handle_get_logs` passed any u32 straight into JournalQuery. A limit in the caller is not a limit, so the host clamps instead of the sentence changing. That also makes args.rs's arg doc and the tool description agents read correct, untouched. The sibling `get_host_journal` already clamps host-side at 100, which is both the precedent and the control that the missing clamp here was real. Closes #4230. --- docs/tools/scheduling.md | 21 ++++++++++++++++----- hive-c0re/src/socket_server/mod.rs | 5 ++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/tools/scheduling.md b/docs/tools/scheduling.md index 57e00328..c10374f5 100644 --- a/docs/tools/scheduling.md +++ b/docs/tools/scheduling.md @@ -3,11 +3,18 @@ ## `scheduling` tool group Scheduled prompts fan a message body out to one or more agent inboxes -at a future time, optionally recurring. All scheduling ops go through -the operator approval queue (even self-targeted schedules — use -`remind` for unapproved self-wake). Authorization for read/cancel/edit -ops: you can act on schedules you own or any owned by a sub-agent in -your topology subtree. +at a future time, optionally recurring. + +**Creating** one goes through the operator approval queue, even when it +targets only yourself — use `remind` for an unapproved self-wake. The +other four verbs need no approval: holding the `scheduling` tool group +is the whole gate. + +Authorization for cancel / edit / fire: you can act on schedules you own +or any owned by an agent in your topology subtree. **`list_schedules` is +not scoped at all** — it returns every schedule on the hive, bodies +included, which is what makes it useful for auditing and worth knowing +before you put anything private in a schedule body. ### `request_schedule_prompt(targets, body, first_fire_at_unix, interval_seconds?, description?)` @@ -55,6 +62,10 @@ and `last_result`, `next_fire_at_unix`, `interval_seconds`. Use to look up an id before cancelling, or to audit upcoming wake-ups in your subtree. +Unscoped on purpose — unlike the verbs that _change_ a schedule, this +one applies no ownership filter, so the snapshot covers other agents' +schedules and the operator's. + ## `diagnostics` tool group ### `get_logs(agent, lines?)` diff --git a/hive-c0re/src/socket_server/mod.rs b/hive-c0re/src/socket_server/mod.rs index ec50aafe..e73a0e45 100644 --- a/hive-c0re/src/socket_server/mod.rs +++ b/hive-c0re/src/socket_server/mod.rs @@ -1121,7 +1121,10 @@ pub(crate) fn handle_send( /// `-M` read needs root). `journalctl -M` wants the `h-` machine /// name, which `container_name` derives. async fn handle_get_logs(agent: &str, lines: Option) -> Response { - let n = lines.unwrap_or(50); + // Clamped here and not only in the MCP layer that also clamps it: a + // caller-side limit is not a limit, and anything speaking this socket + // sets `lines` itself. Mirrors `handle_get_host_journal`'s own cap. + let n = lines.unwrap_or(50).min(500); let machine = crate::lifecycle::container_name(agent); tracing::info!(%agent, %machine, %n, "manager: get_logs"); match crate::priv_client::read_container_journal(