hive-c0re: drop the subtree check from the scheduling verbs

The topology predicate `is_descendant_of` gated the four schedule-
managing verbs: a caller could only name a schedule owned by an agent at
or below itself in `topology.json`. Those gates now permit any requester,
so the predicate, its pure `_in` form and the `schedule_authorized`
wrapper built on it are gone rather than left returning a constant. The
other two wrappers went earlier with the verbs they served —
`require_descendant` with the lifecycle MCP verbs in 87970a8c, and
`resolve_agent_state_target` with `get_loose_ends`'s agent parameter.

`require_group(agent, "scheduling", ...)` is untouched and still fires at
dispatch for every one of the five scheduling verbs, so holding the tool
group remains the gate; what goes is the ownership restriction layered on
top of it.

The three schedule-mutating verbs keep their row lookup as a plain
existence check, so a caller naming a schedule that does not exist still
gets `not found` rather than a message from deeper in the cancel path.
`list_schedules` stops filtering per row: it would only have hidden rows
the requester may act on anyway.

Error messages, tool descriptions and docs that described the subtree
relation are reworded — a refusal message naming a topology that no
longer decides anything is worse than none.

The six `is_descendant_of_in` unit tests go with the function they test;
the permit behaviour they leave unasserted is picked up by the next
commit.

Refs #4472
This commit is contained in:
atlas 2026-09-17 19:27:15 +02:00 committed by mara
commit 4121e11d87
8 changed files with 67 additions and 277 deletions

View file

@ -639,8 +639,7 @@ impl AgentServer {
keep their next_fire_at unchanged (the manual fire is additive). One-shot \
schedules are CONSUMED by the manual fire (cancelled afterwards): the operator's \
intent on a one-shot is 'send this now, the scheduled time was wrong'. \n\n\
Authorization mirrors `cancel_schedule`: you can fire your own schedules + any \
owned by a sub-agent in your subtree per topology.json."
Authorization mirrors `cancel_schedule`: you can fire any schedule, whoever owns it."
)]
async fn fire_schedule_now(&self, Parameters(args): Parameters<FireScheduleNowArgs>) -> String {
let log = format!("{args:?}");
@ -662,8 +661,7 @@ impl AgentServer {
whole schedule (all recipients flipped). With a non-empty `targets` list, cancels \
just those recipients; the schedule keeps firing for any remaining active targets \
and auto-cancels its parent row when every target is cancelled. \n\n\
Authorization: the manager can cancel its own schedules + any schedule whose \
owner is one of its sub-agents per topology.json. Other owners are refused."
Authorization: you can cancel any schedule, whoever owns it."
)]
async fn cancel_schedule(&self, Parameters(args): Parameters<CancelScheduleArgs>) -> String {
let log = format!("{args:?}");
@ -693,8 +691,8 @@ impl AgentServer {
swap a target atomically. Re-adding a previously-removed target starts a fresh \
per-target history (drops the tombstone). Draining all targets auto-cancels the \
parent schedule. \n\n\
Authorization mirrors `cancel_schedule` / `fire_schedule_now`: you can edit your \
own schedules + any owned by a sub-agent in your subtree per topology.json. \
Authorization mirrors `cancel_schedule` / `fire_schedule_now`: you can edit any \
schedule, whoever owns it. \
Refuses cancelled schedules (the row's terminal submit a fresh one)."
)]
async fn edit_schedule(&self, Parameters(args): Parameters<EditScheduleArgs>) -> String {
@ -721,12 +719,11 @@ impl AgentServer {
}
#[tool(
description = "List the scheduled prompts you're authorized to see — your own, plus \
any owned by a sub-agent in your subtree (everything, for the operator) active and \
description = "List every scheduled prompt in the hive, whoever owns it — active and \
cancelled-but-not-yet-reaped. Returns schedule id, owner, body, target set with \
per-target last_fired_at + last_result, next fire time, recurring interval. \
Use this to look up an id before calling `cancel_schedule`, or to audit what \
your subtree is going to be woken up about next."
the hive is going to be woken up about next."
)]
async fn list_schedules(&self) -> String {
run_tool_envelope("list_schedules", String::new(), async move {