Watch
0
0
Fork
You've already forked hyperhive
0

hive-c0re: remove the approvals subsystem

Nothing submits an approval since scheduled prompts left hive-c0re, so the
queue only acted on rows written before that.

- hive-c0re: the approvals store and its migrations, approve/deny/
  resolve actions, the ResolveApproval tail node (and meta_update's
  approval_id), POST /api/approve and /api/deny, the approval views on
  /api/state, the approval_resolved dashboard event, the orphan GC,
  destroy/purge failing pending rows, and the approval rows in
  get_loose_ends. DestroyBookkeeping loses the purge flag, which only
  worded the approval-failure note.
- The CancelLooseEnd request goes: its only hive-c0re arm was the
  approval one. cancel_loose_end handles reminder and todo in the harness.
- Wire types: hive_sh4re::approvals (ReminderStats moves to inbox),
  LooseEnd::Approval, CancelLooseEndKind, HelperEvent::ApprovalResolved,
  ToolGroup::Approvals, HostRequest::{Pending,Approve,Deny} and
  HostResponse.approvals. HOST_SOCK_VERSION goes to 2 for the removed
  field.
- hivectl loses the approvals verbs; docs/tools/hivectl-cli.md is
  regenerated.
- The system prompt drops approval_resolved.
- The approvals table stays in broker.sqlite, unread, the same way the
  operator_questions table was left when its store was removed.
- An `approvals` entry still in tool-groups.json is skipped with a warning
  by ToolGroup::parse_list, like any unknown group.
- docs/agent-lifecycle/approvals.md is renamed to config-changes.md so the
  code's doc pointers resolve; its content follows in the docs commit.
This commit is contained in:
atlas 2026-10-04 23:55:03 +02:00
commit 58ef299c3c
60 changed files with 153 additions and 1828 deletions

View file

@ -94,7 +94,7 @@ pub struct CompactArgs {
}
// -----------------------------------------------------------------------------
// Privileged tool arg types (lifecycle, approvals)
// Privileged tool arg types (lifecycle)
// -----------------------------------------------------------------------------
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
@ -115,8 +115,7 @@ pub struct GetAgentMetaArgs {
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CancelLooseEndArgs {
/// Which kind of thread to cancel — `"reminder"` for a scheduled
/// `remind` that hasn't fired yet, `"approval"` for a pending
/// approval you submitted, or `"todo"` for a loose-ends-v2 todo
/// `remind` that hasn't fired yet, or `"todo"` for a loose-ends-v2 todo
/// (bash/matrix/forge). Use the `kind` field straight off the
/// `get_loose_ends` row.
pub kind: String,

View file

@ -30,9 +30,8 @@ pub use args::{
pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv};
use render::{
dial_agent_socket, format_matrix_summary, local_reminders, local_todos, loose_end_kind_label,
mark_local_todo_done, mark_local_todos_done, matrix_unread_summary, parse_loose_end_kind,
render_loose_ends, reply_err,
dial_agent_socket, format_matrix_summary, local_reminders, local_todos, mark_local_todo_done,
mark_local_todos_done, matrix_unread_summary, render_loose_ends, reply_err,
};
/// Write (or remove) the status file in the agent's own `state/` directory.
@ -231,9 +230,7 @@ impl AgentServer {
#[tool(
description = "List loose ends pending against this agent: pending reminders \
you scheduled, plus — for the manager only — pending \
approvals you submitted that the operator hasn't acted on yet (agents with the \
`approvals` tool group also see their own pending approvals). Also lists active \
you scheduled. Also lists active \
local tasks published by external MCP daemons (e.g. running bash tasks). Cheap sweep, no args. Useful \
at turn start to remember what you owe / what's owed to you without scrolling \
inbox history. Output is a short bulleted list with ids, ages in seconds, and \
@ -346,9 +343,6 @@ impl AgentServer {
you scheduled (hard-deleted before it fires). `kind` is `\"reminder\"`; \
`id` is the row id from the matching `get_loose_ends` entry. Auth: you can only \
cancel rows where you're the owner. Returns `ok` or an error string.\n\
`kind` may also be `\"approval\"` to withdraw a pending approval you submitted \
(before the operator acts on it) — root agent (`ruth`) only; the server rejects \
`approval` kind for all other callers.\n\
`kind` may also be `\"todo\"` to clear one of your own loose-ends-v2 todos (the id \
from a `get_loose_ends` `todo #N` line) — dials the in-agent socket directly, safe \
to call repeatedly.")]
@ -373,42 +367,26 @@ impl AgentServer {
.to_owned(),
};
}
let kind = match parse_loose_end_kind(&args.kind) {
Ok(k) => k,
Err(e) => return e,
};
let kind_label = loose_end_kind_label(kind);
// Reminders are harness-local — dial the in-agent socket
// directly instead of the broker; the other kind (approval)
// still lives in c0re.
if kind == hive_sh4re::inbox::CancelLooseEndKind::Reminder {
return match dial_agent_socket(&hive_agent_sock::Request::CancelReminder { id })
.await
{
Some(hive_agent_sock::Response::Acked { count }) if count > 0 => {
format!("cancelled {kind_label} {id}")
}
Some(hive_agent_sock::Response::Acked { .. }) => {
format!("cancel_loose_end failed: no pending {kind_label} {id}")
}
Some(hive_agent_sock::Response::Err { message }) => {
format!("cancel_loose_end failed: {message}")
}
Some(other) => format!("cancel_loose_end unexpected response: {other:?}"),
None => "cancel_loose_end failed: in-agent socket unavailable".to_owned(),
};
let kind = args.kind.trim().to_ascii_lowercase();
if !matches!(kind.as_str(), "reminder" | "r") {
return format!(
"cancel_loose_end: unknown kind '{kind}' (expected \"reminder\" or \"todo\")"
);
}
// Reminders are harness-local — dial the in-agent socket directly.
match dial_agent_socket(&hive_agent_sock::Request::CancelReminder { id }).await {
Some(hive_agent_sock::Response::Acked { count }) if count > 0 => {
format!("cancelled reminder {id}")
}
Some(hive_agent_sock::Response::Acked { .. }) => {
format!("cancel_loose_end failed: no pending reminder {id}")
}
Some(hive_agent_sock::Response::Err { message }) => {
format!("cancel_loose_end failed: {message}")
}
Some(other) => format!("cancel_loose_end unexpected response: {other:?}"),
None => "cancel_loose_end failed: in-agent socket unavailable".to_owned(),
}
let (resp, retries) = self
.dispatch(hive_core_agent_sock::Request::CancelLooseEnd { kind, id })
.await;
annotate_retries(
format_ack(
resp,
"cancel_loose_end",
format!("cancelled {kind_label} {id}"),
),
retries,
)
})
.await
}

View file

@ -152,8 +152,8 @@ fn msg_id_tag(id: i64) -> String {
}
/// Hard cap on how many individual `Todo` lines `render_loose_ends` will
/// emit. Approvals/reminders stay naturally bounded (they're
/// triaged interactively and don't self-multiply), but todos are pushed by
/// emit. Reminders stay naturally bounded (they're triaged interactively
/// and don't self-multiply), but todos are pushed by
/// unattended producers — matrix/bash/forge are the built-in ones, but any
/// user-configured MCP server can push its own too — an agent that goes a long
/// stretch without calling `get_loose_ends`, or whose producers outpace its
@ -178,22 +178,6 @@ const MAX_RENDERED_TODOS: usize = 40;
fn render_one_loose_end(out: &mut String, t: &hive_sh4re::inbox::LooseEnd) {
use std::fmt::Write as _;
match t {
hive_sh4re::inbox::LooseEnd::Approval {
id,
agent,
commit_ref,
description,
age_seconds,
} => {
let desc = description
.as_deref()
.map(|d| format!(" — {d}"))
.unwrap_or_default();
let _ = writeln!(
out,
"- approval #{id} ({agent} @ {commit_ref}, {age_seconds}s old){desc}"
);
}
hive_sh4re::inbox::LooseEnd::Reminder {
id,
owner,
@ -438,33 +422,6 @@ pub(super) fn format_matrix_summary(rooms: &[MatrixRoomUnread]) -> String {
out
}
/// Parse the user-facing `kind` string for `cancel_loose_end` into the
/// wire enum. Accepts a small alias set so claude doesn't have to
/// remember the exact spelling (`"q"` / `"r"` shorthand falls out
/// for free).
pub(super) fn parse_loose_end_kind(
raw: &str,
) -> Result<hive_sh4re::inbox::CancelLooseEndKind, String> {
match raw.trim().to_ascii_lowercase().as_str() {
"reminder" | "r" => Ok(hive_sh4re::inbox::CancelLooseEndKind::Reminder),
"approval" | "a" => Ok(hive_sh4re::inbox::CancelLooseEndKind::Approval),
other => Err(format!(
"cancel_loose_end: unknown kind '{other}' \
(expected \"reminder\", \"approval\", or \"todo\")"
)),
}
}
/// Canonical user-facing label for a `CancelLooseEndKind` — used in
/// the success ack so the caller always sees `"reminder"` instead of
/// whatever alias they passed in (`"r"`).
pub(super) fn loose_end_kind_label(kind: hive_sh4re::inbox::CancelLooseEndKind) -> &'static str {
match kind {
hive_sh4re::inbox::CancelLooseEndKind::Reminder => "reminder",
hive_sh4re::inbox::CancelLooseEndKind::Approval => "approval",
}
}
/// Format helper for `get_agent_meta`: renders an agent's identity +
/// current status as a short human-readable block. `name`,
/// `hyperhive_rev`, and `running` are always shown; `status` only