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

@ -1,7 +1,7 @@
# hive-sh4re
The **shared payload vocabulary** between `hive-c0re` and the in-container
harness — the common types (`Message`, `Approval`, `LooseEnd`, `HelperEvent`, …)
harness — the common types (`Message`, `LooseEnd`, `HelperEvent`, …)
that the per-socket wire protocols are built from. The request/response
_envelopes_ themselves now live in the per-socket crates (below); this crate
holds the payloads they carry.
@ -17,7 +17,7 @@ in all of `hive-sh4re`:
- `hive-priv-sock` — the privileged-helper socket
Those crates re-export or reference the payload types that still live here
(`Approval`, `Message`, `LooseEnd`, …).
(`Message`, `LooseEnd`, …).
## Modules

View file

@ -1,68 +0,0 @@
//! The approval queue wire shape: one row (`Approval`) per pending/resolved
//! operator decision, its `kind` discriminator, and the terminal-state enum.
//! `ReminderStats` lives here too — small enough not to earn its own file,
//! and unrelated to any other topic module.
use chrono::{DateTime, Utc};
use hive_types::Ident;
use serde::{Deserialize, Serialize};
/// One row in the approval queue. `commit_ref` is overloaded per
/// `kind` — see `docs/agent-lifecycle/approvals.md::Approval kinds (wire shapes)`
/// for the encoding table and lifecycle.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Approval {
pub id: i64,
pub agent: Ident,
pub kind: ApprovalKind,
/// Kind-specific payload (git sha / inputs array / empty). See the
/// Approval struct doc.
pub commit_ref: String,
pub requested_at: DateTime<Utc>,
pub status: ApprovalStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolved_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
/// Free-text description the manager attached at submission time;
/// shown on the dashboard approval card.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
/// What action the approval, when granted, will trigger.
/// Variant-specific payload encoding + flow lives in
/// `docs/agent-lifecycle/approvals.md::Approval kinds (wire shapes)`.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, strum::IntoStaticStr)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum ApprovalKind {
/// Run `nix flake update [inputs...]` on the meta flake and commit
/// the resulting lock changes.
UpdateMetaInputs,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalStatus {
Pending,
Approved,
Denied,
Failed,
/// Manager withdrew the request before the operator acted on it.
/// Distinct from `Denied` (operator decision) and `Failed`
/// (post-approval lifecycle error). See
/// `docs/agent-lifecycle/approvals.md::Withdrawing a pending approval`.
Cancelled,
}
/// Reminder activity statistics for an agent over a time window.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReminderStats {
/// Total reminders scheduled in the window (`created_at` >= cutoff).
pub scheduled: u64,
/// Reminders that have been delivered in the window (`sent_at` IS NOT NULL).
pub delivered: u64,
/// Reminders still pending in the window (`sent_at` IS NULL).
pub pending: u64,
}

View file

@ -117,6 +117,17 @@ pub enum ReminderTiming {
At { unix_timestamp: i64 },
}
/// Reminder activity statistics for an agent over a time window.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReminderStats {
/// Total reminders scheduled in the window (`created_at` >= cutoff).
pub scheduled: u64,
/// Reminders that have been delivered in the window (`sent_at` IS NOT NULL).
pub delivered: u64,
/// Reminders still pending in the window (`sent_at` IS NULL).
pub pending: u64,
}
/// One row in the response to `GetLooseEnds`. Tagged enum so new
/// thread kinds can land without breaking existing handlers.
/// Per-flavour scoping + per-variant fields + clock-anomaly
@ -125,15 +136,6 @@ pub enum ReminderTiming {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum LooseEnd {
/// A pending approval row.
Approval {
id: i64,
agent: String,
commit_ref: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
age_seconds: u64,
},
/// A scheduled but un-delivered reminder row.
Reminder {
id: i64,
@ -190,9 +192,8 @@ pub enum LooseEnd {
/// Age in seconds between two unix timestamps, clamped to 0 when `then`
/// is in the future. This is what backs the `age_seconds` promise on
/// every [`LooseEnd`] variant above, so it lives beside the enum rather
/// than in either producer: hive-c0re derives approval ages here, and
/// hive-agent's in-agent socket server derives reminder + todo ages.
/// every [`LooseEnd`] variant above, so it lives beside the enum;
/// hive-agent's in-agent socket server derives reminder + todo ages with it.
///
/// Both clamps are load-bearing and neither substitutes for the other:
/// `saturating_sub` keeps the subtraction itself from overflowing on
@ -205,17 +206,6 @@ pub fn saturating_age(now: i64, then: i64) -> u64 {
u64::try_from(delta).unwrap_or(0)
}
/// Kind discriminator for `CancelLooseEnd`. Per-kind store +
/// authorisation rules live in
/// `docs/process/conventions.md::Loose-ends wire shape`.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CancelLooseEndKind {
Reminder,
/// Withdraw a pending approval (manager surface only).
Approval,
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -1,6 +1,5 @@
//! Wire types shared between `hive-c0re` and the in-container harness.
pub mod approvals;
pub mod assets;
pub mod bash_task;
pub mod container;

View file

@ -6,8 +6,6 @@
use hive_types::Ident;
use serde::{Deserialize, Serialize};
use crate::approvals::ApprovalStatus;
/// Logical name the broker uses for the manager.
pub const MANAGER_AGENT: &str = "ruth";
@ -42,19 +40,10 @@ pub fn trusted_sender(s: &str) -> Ident {
/// Out-of-band events the host-side daemon pushes to the manager's inbox.
/// Serialised as JSON in `Message::body` (sender = `SYSTEM_SENDER`).
/// Per-variant triggers + the optional `sha`/`tag` semantics live in
/// `docs/agent-lifecycle/approvals.md::Helper events to the submitting agent`.
/// `docs/agent-lifecycle/config-changes.md::Helper events`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum HelperEvent {
/// An approval transitioned to a terminal state.
ApprovalResolved {
id: i64,
agent: String,
commit_ref: String,
status: ApprovalStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
note: Option<String>,
},
/// A sub-agent's recorded flake rev is stale relative to hyperhive.
NeedsUpdate { agent: String },
/// Container exited without an operator-initiated stop (crash).

View file

@ -157,11 +157,6 @@ pub enum ToolGroup {
/// `meta/capabilities.json` grants still parse; `tools()` returns
/// `&[]`.
Lifecycle,
/// Grants no MCP tool today — `request_update_meta_inputs`, its only
/// member, was removed with no replacement. Still a live
/// server-side gate: `cancel_loose_end`'s approval-cancel arm
/// requires it (`socket_server::check_can_cancel_approval`).
Approvals,
/// Gates no tool today — `create_repo`, its only member, was removed
/// with no replacement. Kept so existing `meta/capabilities.json`
/// grants still parse; `tools()` returns `&[]`.
@ -219,15 +214,15 @@ impl ToolGroup {
/// The MCP tool names (without the `mcp__hyperhive__` prefix) in this group.
/// Returns `&[]` for `WebTools` (it enables Claude built-in tools, not MCP
/// tools — see `builtin_tools()`) and for `Lifecycle` / `Approvals` /
/// `Forge` (their tools were removed); see each variant's doc comment.
/// tools — see `builtin_tools()`) and for `Lifecycle` / `Forge` (their
/// tools were removed); see each variant's doc comment.
#[must_use]
pub fn tools(self) -> &'static [&'static str] {
match self {
Self::Messaging => &["send", "recv", "ack_until"],
Self::Meta => &["get_agent_meta"],
Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"],
// All five empty, for different reasons — see each
// All four empty, for different reasons — see each
// variant's own doc comment above. `Execution` grants the
// out-of-process `bash` MCP server
// (`mcp__bash__run`/`status`/`kill`), gated at config-render
@ -236,15 +231,11 @@ impl ToolGroup {
// out-of-process server has no later enforcement point, so
// that gate is the actual security boundary. `WebTools`
// grants Claude built-in tools, not MCP ones; see
// `builtin_tools()`. `Lifecycle`, `Approvals` and `Forge` each
// listed exactly one tool — `list_containers`,
// `request_update_meta_inputs` and `create_repo` respectively
// — and all three tools were removed outright; the variants
// stay so existing grants parse, and `Approvals` still gates
// `cancel_loose_end`'s approval-cancel arm server-side.
Self::Lifecycle | Self::Approvals | Self::Forge | Self::Execution | Self::WebTools => {
&[]
}
// `builtin_tools()`. `Lifecycle` and `Forge` each listed
// exactly one tool — `list_containers` and `create_repo`
// respectively — and both tools were removed outright; the
// variants stay so existing grants parse.
Self::Lifecycle | Self::Forge | Self::Execution | Self::WebTools => &[],
}
}
@ -297,7 +288,6 @@ impl ToolGroup {
Self::Meta,
Self::Inbox,
Self::Lifecycle,
Self::Approvals,
Self::Execution,
];
@ -309,7 +299,6 @@ impl ToolGroup {
Self::Meta,
Self::Inbox,
Self::Lifecycle,
Self::Approvals,
Self::Forge,
Self::Execution,
Self::WebTools,
@ -325,9 +314,6 @@ impl ToolGroup {
}
Self::Inbox => "get_loose_ends, cancel_loose_end, remind — self-scheduling",
Self::Lifecycle => "no tools — vestigial since list_containers was removed",
Self::Approvals => {
"no tools — grants cancel_loose_end's approval-cancel arm (privileged)"
}
Self::Forge => "no tools — vestigial since create_repo was removed",
Self::Execution => {
"run, status — run shell commands via mcp__bash__run / mcp__bash__status"