c0re: schedule_prompt approval kind + worker + manager surface (#444 step 2)

This commit is contained in:
damocles 2026-05-26 01:11:36 +02:00 committed by Mara
commit aa7d8d9c9a
9 changed files with 612 additions and 4 deletions

View file

@ -344,6 +344,20 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
);
ManagerResponse::Ok
}
ManagerRequest::RequestSchedulePrompt(payload) => {
handle_request_schedule_prompt(coord, hive_sh4re::MANAGER_AGENT, payload).await
}
ManagerRequest::CancelSchedule { id, targets } => {
handle_cancel_schedule(coord, hive_sh4re::MANAGER_AGENT, *id, targets.as_deref())
}
ManagerRequest::ListSchedules => match coord.scheduled_prompts.list() {
Ok(schedules) => ManagerResponse::Schedules {
schedules: schedules.into_iter().map(schedule_to_wire).collect(),
},
Err(e) => ManagerResponse::Err {
message: format!("list scheduled prompts: {e:#}"),
},
},
ManagerRequest::Ask {
question,
options,
@ -692,6 +706,172 @@ async fn submit_apply_commit(
Ok((id, sha))
}
/// Submit a `RequestSchedulePrompt` payload as an `ApprovalKind::SchedulePrompt`
/// row. Encodes the payload into the approval's `commit_ref` so the
/// approve handler can re-parse it without a side table. Validates
/// inputs (non-empty targets, non-empty body, sane interval) at
/// submit time — the operator should never see a malformed schedule
/// pending approval.
async fn handle_request_schedule_prompt(
coord: &Arc<Coordinator>,
requester: &str,
payload: &hive_sh4re::SchedulePromptPayload,
) -> ManagerResponse {
if payload.targets.is_empty() {
return ManagerResponse::Err {
message: "schedule must have at least one target".into(),
};
}
if payload.body.trim().is_empty() {
return ManagerResponse::Err {
message: "schedule body must be non-empty".into(),
};
}
if let Some(0) = payload.interval_seconds {
return ManagerResponse::Err {
message: "interval_seconds must be > 0 (use None for one-shot)".into(),
};
}
let commit_ref = match serde_json::to_string(payload) {
Ok(s) => s,
Err(e) => {
return ManagerResponse::Err {
message: format!("encode SchedulePromptPayload: {e:#}"),
}
}
};
let id = match coord.approvals.submit_kind(
requester,
hive_sh4re::ApprovalKind::SchedulePrompt,
&commit_ref,
payload.description.as_deref(),
) {
Ok(id) => id,
Err(e) => {
return ManagerResponse::Err {
message: format!("queue schedule_prompt approval: {e:#}"),
}
}
};
tracing::info!(
%id,
requester,
targets = ?payload.targets,
first_fire_at = payload.first_fire_at_unix,
interval = ?payload.interval_seconds,
"schedule_prompt approval queued"
);
coord.emit_approval_added(
id,
requester,
"schedule_prompt",
None,
None,
payload.description.clone(),
);
ManagerResponse::Ok
}
/// Cancel a schedule (whole or per-target). Manager-surface
/// authorization: a manager can cancel its own schedules + any
/// schedule whose owner is one of its sub-agents (topology-walked).
/// The operator surface bypasses this and can cancel anything;
/// agents reaching this path through the manager get the
/// topology-scoped check.
fn handle_cancel_schedule(
coord: &Arc<Coordinator>,
requester: &str,
schedule_id: i64,
targets: Option<&[String]>,
) -> ManagerResponse {
let schedule = match coord.scheduled_prompts.get(schedule_id) {
Ok(Some(s)) => s,
Ok(None) => {
return ManagerResponse::Err {
message: format!("schedule {schedule_id} not found"),
}
}
Err(e) => {
return ManagerResponse::Err {
message: format!("read schedule {schedule_id}: {e:#}"),
}
}
};
if !cancel_authorized(requester, &schedule.owner) {
return ManagerResponse::Err {
message: format!(
"not authorized: {requester} cannot cancel schedule owned by {owner}",
owner = schedule.owner
),
};
}
let result = match targets {
Some(list) if !list.is_empty() => coord
.scheduled_prompts
.cancel_targets(schedule_id, list)
.map_err(|e| format!("cancel targets: {e:#}")),
_ => coord
.scheduled_prompts
.cancel_all(schedule_id)
.map_err(|e| format!("cancel all: {e:#}")),
};
match result {
Ok(()) => ManagerResponse::Ok,
Err(message) => ManagerResponse::Err { message },
}
}
/// Permission check for `CancelSchedule` on the manager surface.
/// `requester` (always `hm1nd` here) can cancel its own schedules.
/// Sub-agent ownership is delegated to topology — see
/// `crate::topology::is_descendant_of`.
fn cancel_authorized(requester: &str, owner: &str) -> bool {
if requester == owner {
return true;
}
if requester == hive_sh4re::OPERATOR_RECIPIENT {
return true;
}
// Manager can cancel anything owned by an agent in its subtree.
// For the current single-manager topology that covers everything,
// but the check stays correct as the tree grows.
crate::topology::is_descendant_of(owner, requester)
}
/// Map a `scheduled_prompts::Schedule` to its public wire shape.
/// Field-by-field copy — the two types are intentionally identical;
/// the separation keeps hive-sh4re free of hive-c0re-internal types.
fn schedule_to_wire(s: crate::scheduled_prompts::Schedule) -> hive_sh4re::WireSchedule {
hive_sh4re::WireSchedule {
id: s.id,
owner: s.owner,
body: s.body,
interval_seconds: s.interval_seconds,
next_fire_at_unix: s.next_fire_at_unix,
created_at_unix: s.created_at_unix,
source: match s.source {
crate::scheduled_prompts::ScheduleSource::Operator => {
hive_sh4re::WireScheduleSource::Operator
}
crate::scheduled_prompts::ScheduleSource::Approval { id } => {
hive_sh4re::WireScheduleSource::Approval { id }
}
},
cancelled_at_unix: s.cancelled_at_unix,
description: s.description,
targets: s
.targets
.into_iter()
.map(|t| hive_sh4re::WireScheduleTarget {
target: t.target,
cancelled_at_unix: t.cancelled_at_unix,
last_fired_at_unix: t.last_fired_at_unix,
last_result: t.last_result,
})
.collect(),
}
}
/// On `Ask { ttl_seconds: Some(n) }`, sleep n seconds and then try to
/// resolve the question with `[expired]`. If the operator (or any
/// other path) already answered it, `answer()` returns Err and we