Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
43 changed files with 677 additions and 732 deletions

View file

@ -643,7 +643,7 @@ live dial of the target agent's in-container todo socket (same
`UpsertTodo` request in-container producers use); the `Spawn` approval `UpsertTodo` request in-container producers use); the `Spawn` approval
uses this path, not a `HelperEvent`. Legacy approval rows that predate the uses this path, not a `HelperEvent`. Legacy approval rows that predate the
submitter column fall back to the submitter column fall back to the
root agent. Variants (`hive_sh4re::manager::HelperEvent`): root agent. Variants (`hive_sh4re::HelperEvent`):
- `ApprovalResolved { id, agent, commit_ref, status, note }` — - `ApprovalResolved { id, agent, commit_ref, status, note }` —
fired by `actions::approve` + `actions::deny` whenever an fired by `actions::approve` + `actions::deny` whenever an

View file

@ -318,7 +318,7 @@ straight to `new Date(s)` for display.
## Tool groups ## Tool groups
The MCP tool surface an agent receives is derived from a set of named The MCP tool surface an agent receives is derived from a set of named
`ToolGroup` values (`hive_sh4re::permissions::ToolGroup`), not from a hardcoded `ToolGroup` values (`hive_sh4re::ToolGroup`), not from a hardcoded
binary flavor. binary flavor.
| Group | Tools | | Group | Tools |

View file

@ -975,7 +975,7 @@ impl AgentServer {
let target_count = args.targets.len(); let target_count = args.targets.len();
let (resp, retries) = self let (resp, retries) = self
.dispatch(hive_core_agent_sock::Request::RequestSchedulePrompt( .dispatch(hive_core_agent_sock::Request::RequestSchedulePrompt(
hive_sh4re::manager::SchedulePromptPayload { hive_sh4re::SchedulePromptPayload {
targets: args.targets, targets: args.targets,
body: args.body, body: args.body,
first_fire_at_unix: args.first_fire_at_unix, first_fire_at_unix: args.first_fire_at_unix,

View file

@ -18,7 +18,7 @@ const SEND_ALLOW_PATH: &str = "/etc/hyperhive/send-allow.json";
/// the refusal as the tool result so claude knows the message didn't /// the refusal as the tool result so claude knows the message didn't
/// land and can react (e.g. route via `<parent>` instead). /// land and can react (e.g. route via `<parent>` instead).
pub fn check_send_allowed(to: &str) -> Result<(), String> { pub fn check_send_allowed(to: &str) -> Result<(), String> {
if to == hive_sh4re::manager::PARENT_RECIPIENT { if to == hive_sh4re::PARENT_RECIPIENT {
// Always allow `<parent>` — the allow-list constrains peer // Always allow `<parent>` — the allow-list constrains peer
// chatter, not the structural reporting line; the operator // chatter, not the structural reporting line; the operator
// can rewire who the parent IS via `set_parent` without // can rewire who the parent IS via `set_parent` without
@ -53,6 +53,6 @@ pub fn check_send_allowed(to: &str) -> Result<(), String> {
(configured in agent.nix). Allowed: {allow:?}. Your structural \ (configured in agent.nix). Allowed: {allow:?}. Your structural \
parent is always reachable — route through `send(to: \"{}\", …)` \ parent is always reachable — route through `send(to: \"{}\", …)` \
if you need to reach someone outside the allow-list.", if you need to reach someone outside the allow-list.",
hive_sh4re::manager::PARENT_RECIPIENT hive_sh4re::PARENT_RECIPIENT
)) ))
} }

View file

@ -147,9 +147,7 @@ pub enum Response {
/// `CountPendingReminders` result. /// `CountPendingReminders` result.
PendingRemindersCount { count: u64 }, PendingRemindersCount { count: u64 },
/// `ReminderRollup` result. /// `ReminderRollup` result.
ReminderRollup { ReminderRollup { stats: hive_sh4re::ReminderStats },
stats: hive_sh4re::approvals::ReminderStats,
},
/// Op failed; `message` is operator-facing. /// Op failed; `message` is operator-facing.
Err { message: String }, Err { message: String },
} }

View file

@ -71,7 +71,7 @@ use crate::turn_stats::TurnStats;
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
use hive_core_agent_sock::{Request, Response}; use hive_core_agent_sock::{Request, Response};
use hive_sh4re::manager::{HelperEvent, SYSTEM_SENDER}; use hive_sh4re::{HelperEvent, SYSTEM_SENDER};
use hive_sock_client::Retry; use hive_sock_client::Retry;
#[derive(Parser)] #[derive(Parser)]
@ -406,7 +406,7 @@ impl Surface for AgentSurface {
let res = hive_sock_client::request::<_, Response>( let res = hive_sock_client::request::<_, Response>(
socket, socket,
&Request::Send { &Request::Send {
to: hive_sh4re::manager::PARENT_RECIPIENT.into(), to: hive_sh4re::PARENT_RECIPIENT.into(),
body, body,
in_reply_to: None, in_reply_to: None,
}, },

View file

@ -35,14 +35,14 @@ pub const DEFAULT_MCP_HTTP_PORT: u16 = 8790;
pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &["Edit", "Glob", "Grep", "Read", "Skill", "Write"]; pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &["Edit", "Glob", "Grep", "Read", "Skill", "Write"];
/// Env var written by the meta renderer with a comma-separated list of /// Env var written by the meta renderer with a comma-separated list of
/// `hive_sh4re::permissions::ToolGroup` `snake_case` names (e.g. `"messaging,inbox,meta"`). /// `hive_sh4re::ToolGroup` `snake_case` names (e.g. `"messaging,inbox,meta"`).
/// When present, the harness expands the groups into per-tool allow entries /// When present, the harness expands the groups into per-tool allow entries
/// instead of using the hardcoded flavor default. See `docs/conventions.md::Tool groups`. /// instead of using the hardcoded flavor default. See `docs/conventions.md::Tool groups`.
const TOOL_GROUPS_ENV: &str = "HIVE_TOOL_GROUPS"; const TOOL_GROUPS_ENV: &str = "HIVE_TOOL_GROUPS";
/// `HIVE_CAPABILITIES` env var injected by `meta::render_flake` when the /// `HIVE_CAPABILITIES` env var injected by `meta::render_flake` when the
/// operator grants capabilities to this agent. Comma-separated /// operator grants capabilities to this agent. Comma-separated
/// `hive_sh4re::permissions::Capability` `snake_case` names. Absent = no extra capabilities. /// `hive_sh4re::Capability` `snake_case` names. Absent = no extra capabilities.
const CAPABILITIES_ENV: &str = "HIVE_CAPABILITIES"; const CAPABILITIES_ENV: &str = "HIVE_CAPABILITIES";
/// Returns the MCP tool names (without `mcp__hyperhive__` prefix) that are /// Returns the MCP tool names (without `mcp__hyperhive__` prefix) that are
@ -87,18 +87,18 @@ fn allowed_capability_tools() -> Vec<String> {
/// (`messaging`, `meta`, `inbox`, `lifecycle`, `approvals`, `scheduling`, /// (`messaging`, `meta`, `inbox`, `lifecycle`, `approvals`, `scheduling`,
/// `diagnostics`, `execution`). Unrecognised tokens are logged and skipped. /// `diagnostics`, `execution`). Unrecognised tokens are logged and skipped.
/// Falls back to `AGENT_DEFAULT` when the env var is absent or empty. /// Falls back to `AGENT_DEFAULT` when the env var is absent or empty.
fn effective_tool_groups() -> Vec<hive_sh4re::permissions::ToolGroup> { fn effective_tool_groups() -> Vec<hive_sh4re::ToolGroup> {
let raw = match std::env::var(TOOL_GROUPS_ENV) { let raw = match std::env::var(TOOL_GROUPS_ENV) {
Ok(v) if !v.trim().is_empty() => v, Ok(v) if !v.trim().is_empty() => v,
_ => return hive_sh4re::permissions::ToolGroup::AGENT_DEFAULT.to_vec(), _ => return hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec(),
}; };
let mut groups = Vec::new(); let mut groups = Vec::new();
for token in raw.split(',') { for token in raw.split(',') {
let t = token.trim().to_ascii_lowercase(); let t = token.trim().to_ascii_lowercase();
// Parse via serde_json (the canonical deserialization path). // Parse via serde_json (the canonical deserialization path).
if let Ok(g) = serde_json::from_value::<hive_sh4re::permissions::ToolGroup>( if let Ok(g) =
serde_json::Value::String(t.clone()), serde_json::from_value::<hive_sh4re::ToolGroup>(serde_json::Value::String(t.clone()))
) { {
groups.push(g); groups.push(g);
} else { } else {
tracing::warn!(token = %t, "{TOOL_GROUPS_ENV}: unknown tool group, skipping"); tracing::warn!(token = %t, "{TOOL_GROUPS_ENV}: unknown tool group, skipping");
@ -109,7 +109,7 @@ fn effective_tool_groups() -> Vec<hive_sh4re::permissions::ToolGroup> {
"{TOOL_GROUPS_ENV} set but contained no recognised groups; \ "{TOOL_GROUPS_ENV} set but contained no recognised groups; \
falling back to AGENT_DEFAULT" falling back to AGENT_DEFAULT"
); );
return hive_sh4re::permissions::ToolGroup::AGENT_DEFAULT.to_vec(); return hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec();
} }
groups groups
} }
@ -124,9 +124,9 @@ fn effective_tool_groups() -> Vec<hive_sh4re::permissions::ToolGroup> {
/// an out-of-process server has **no** later enforcement point — once it is /// an out-of-process server has **no** later enforcement point — once it is
/// in the claude MCP config the agent can call it. So this gate, applied at /// in the claude MCP config the agent can call it. So this gate, applied at
/// config-render time, is the security boundary for those servers. /// config-render time, is the security boundary for those servers.
fn extra_server_required_group(server: &str) -> Option<hive_sh4re::permissions::ToolGroup> { fn extra_server_required_group(server: &str) -> Option<hive_sh4re::ToolGroup> {
match server { match server {
"bash" => Some(hive_sh4re::permissions::ToolGroup::Execution), "bash" => Some(hive_sh4re::ToolGroup::Execution),
_ => None, _ => None,
} }
} }
@ -134,14 +134,14 @@ fn extra_server_required_group(server: &str) -> Option<hive_sh4re::permissions::
/// Whether an extra MCP server should be exposed to claude given the active /// Whether an extra MCP server should be exposed to claude given the active
/// tool `groups`. A gated server (see [`extra_server_required_group`]) is /// tool `groups`. A gated server (see [`extra_server_required_group`]) is
/// suppressed when the agent lacks its required group. /// suppressed when the agent lacks its required group.
fn extra_server_enabled(server: &str, groups: &[hive_sh4re::permissions::ToolGroup]) -> bool { fn extra_server_enabled(server: &str, groups: &[hive_sh4re::ToolGroup]) -> bool {
extra_server_required_group(server).is_none_or(|required| groups.contains(&required)) extra_server_required_group(server).is_none_or(|required| groups.contains(&required))
} }
#[cfg(test)] #[cfg(test)]
mod extra_server_gate_tests { mod extra_server_gate_tests {
use super::{extra_server_enabled, extra_server_required_group}; use super::{extra_server_enabled, extra_server_required_group};
use hive_sh4re::permissions::ToolGroup; use hive_sh4re::ToolGroup;
#[test] #[test]
fn bash_is_gated_behind_execution() { fn bash_is_gated_behind_execution() {
@ -173,13 +173,13 @@ mod extra_server_gate_tests {
/// requires updating the matching `ToolGroup::tools()` slice in hive-sh4re /// requires updating the matching `ToolGroup::tools()` slice in hive-sh4re
/// (single source of truth). See `docs/conventions.md::Tool groups`. /// (single source of truth). See `docs/conventions.md::Tool groups`.
#[must_use] #[must_use]
pub fn allowed_mcp_tools(groups: &[hive_sh4re::permissions::ToolGroup]) -> Vec<String> { pub fn allowed_mcp_tools(groups: &[hive_sh4re::ToolGroup]) -> Vec<String> {
// Collect all tool names, deduplicating while preserving order. // Collect all tool names, deduplicating while preserving order.
// Always-on tools (e.g. `set_status`) come first so they're present // Always-on tools (e.g. `set_status`) come first so they're present
// regardless of which groups the agent is granted — a misconfigured // regardless of which groups the agent is granted — a misconfigured
// agent still has to be able to report its dashboard status. // agent still has to be able to report its dashboard status.
let mut seen = std::collections::HashSet::new(); let mut seen = std::collections::HashSet::new();
let mut out: Vec<String> = hive_sh4re::permissions::ToolGroup::ALWAYS_ON_TOOLS let mut out: Vec<String> = hive_sh4re::ToolGroup::ALWAYS_ON_TOOLS
.iter() .iter()
.copied() .copied()
.chain(groups.iter().flat_map(|g| g.tools().iter().copied())) .chain(groups.iter().flat_map(|g| g.tools().iter().copied()))
@ -406,7 +406,7 @@ fn build_mcp_servers() -> serde_json::Map<String, serde_json::Value> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{SERVER_NAME, allowed_mcp_tools}; use super::{SERVER_NAME, allowed_mcp_tools};
use hive_sh4re::permissions::ToolGroup; use hive_sh4re::ToolGroup;
fn qualified(tool: &str) -> String { fn qualified(tool: &str) -> String {
format!("mcp__{SERVER_NAME}__{tool}") format!("mcp__{SERVER_NAME}__{tool}")

View file

@ -18,7 +18,7 @@ use std::sync::Mutex;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use hive_sh4re::approvals::ReminderStats; use hive_sh4re::ReminderStats;
use hive_sh4re::wire_time; use hive_sh4re::wire_time;
use rusqlite::{Connection, params}; use rusqlite::{Connection, params};

View file

@ -15,7 +15,7 @@ use chrono::Utc;
use rusqlite::{Connection, OpenFlags}; use rusqlite::{Connection, OpenFlags};
use serde::Serialize; use serde::Serialize;
use hive_sh4re::approvals::ReminderStats; use hive_sh4re::ReminderStats;
/// Window param accepted by `/api/stats?window=`. Each maps to a /// Window param accepted by `/api/stats?window=`. Each maps to a
/// total span + the bucket width used to roll up trend series. /// total span + the bucket width used to roll up trend series.

View file

@ -29,7 +29,7 @@ pub(super) async fn api_stats(
/// `HIVE_AGENT_SOCKET` — was a broker RPC before reminders moved /// `HIVE_AGENT_SOCKET` — was a broker RPC before reminders moved
/// in-container. Returns `None` on any transport / decode failure or /// in-container. Returns `None` on any transport / decode failure or
/// when the socket is unset — the stats are decorative, not authoritative. /// when the socket is unset — the stats are decorative, not authoritative.
async fn fetch_reminder_stats(window_secs: u64) -> Option<hive_sh4re::approvals::ReminderStats> { async fn fetch_reminder_stats(window_secs: u64) -> Option<hive_sh4re::ReminderStats> {
match crate::todo_server::dial(&hive_agent_sock::Request::ReminderRollup { match crate::todo_server::dial(&hive_agent_sock::Request::ReminderRollup {
since_secs: window_secs, since_secs: window_secs,
}) })

View file

@ -6,8 +6,7 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Context as _, Result, bail}; use anyhow::{Context as _, Result, bail};
use hive_sh4re::approvals::{ApprovalKind, ApprovalStatus}; use hive_sh4re::{ApprovalKind, ApprovalStatus, HelperEvent};
use hive_sh4re::manager::HelperEvent;
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
use crate::lifecycle; use crate::lifecycle;
@ -143,7 +142,7 @@ fn rollback_ref(approval_id: i64) -> String {
/// `pr` and `reviewed` are fields of the approval row the operator signed off /// `pr` and `reviewed` are fields of the approval row the operator signed off
/// on — so re-reading is both cheap and the authoritative source of truth. /// on — so re-reading is both cheap and the authoritative source of truth.
struct DeployCtx { struct DeployCtx {
approval: hive_sh4re::approvals::Approval, approval: hive_sh4re::Approval,
/// PR number, parsed from `approval.commit_ref`. /// PR number, parsed from `approval.commit_ref`.
pr: u64, pr: u64,
/// The PR head sha the operator reviewed (`approval.fetched_sha`). /// The PR head sha the operator reviewed (`approval.fetched_sha`).
@ -411,7 +410,7 @@ const PR_FAIL_LOG_TAIL_BYTES: usize = 4000;
/// only the error text. /// only the error text.
async fn post_merge_failure_to_pr( async fn post_merge_failure_to_pr(
coord: &Arc<Coordinator>, coord: &Arc<Coordinator>,
approval: &hive_sh4re::approvals::Approval, approval: &hive_sh4re::Approval,
err: &anyhow::Error, err: &anyhow::Error,
) { ) {
let Ok(pr) = approval.commit_ref.parse::<u64>() else { let Ok(pr) = approval.commit_ref.parse::<u64>() else {
@ -473,11 +472,10 @@ fn tail_bytes(s: &str, max_bytes: usize) -> String {
/// The worker takes over from here — fan-out at fire time. /// The worker takes over from here — fan-out at fire time.
async fn run_approval_schedule_prompt( async fn run_approval_schedule_prompt(
coord: &Coordinator, coord: &Coordinator,
approval: hive_sh4re::approvals::Approval, approval: hive_sh4re::Approval,
) -> Result<()> { ) -> Result<()> {
let result: Result<()> = async { let result: Result<()> = async {
let payload: hive_sh4re::manager::SchedulePromptPayload = let payload: hive_sh4re::SchedulePromptPayload = serde_json::from_str(&approval.commit_ref)
serde_json::from_str(&approval.commit_ref)
.context("decode SchedulePromptPayload from approval.commit_ref")?; .context("decode SchedulePromptPayload from approval.commit_ref")?;
coord coord
.scheduled_prompts .scheduled_prompts
@ -600,7 +598,7 @@ fn fetch_approval_for_worker(
coord: &Coordinator, coord: &Coordinator,
approval_id: i64, approval_id: i64,
expected_kind: ApprovalKind, expected_kind: ApprovalKind,
) -> Result<hive_sh4re::approvals::Approval> { ) -> Result<hive_sh4re::Approval> {
let approval = coord let approval = coord
.approvals .approvals
.get(approval_id) .get(approval_id)
@ -643,7 +641,7 @@ async fn forge_after_first_spawn(coord: &Arc<Coordinator>, agent: &str) {
/// work that doesn't justify a queue card. /// work that doesn't justify a queue card.
async fn run_approval_init_config( async fn run_approval_init_config(
coord: &Coordinator, coord: &Coordinator,
approval: hive_sh4re::approvals::Approval, approval: hive_sh4re::Approval,
proposed_dir: std::path::PathBuf, proposed_dir: std::path::PathBuf,
claude_dir: std::path::PathBuf, claude_dir: std::path::PathBuf,
notes_dir: std::path::PathBuf, notes_dir: std::path::PathBuf,
@ -683,7 +681,7 @@ async fn run_approval_init_config(
async fn finish_approval( async fn finish_approval(
coord: &Coordinator, coord: &Coordinator,
approval: &hive_sh4re::approvals::Approval, approval: &hive_sh4re::Approval,
result: Result<()>, result: Result<()>,
terminal_tag: Option<String>, terminal_tag: Option<String>,
) -> Result<()> { ) -> Result<()> {
@ -962,7 +960,7 @@ pub async fn destroy(coord: &Arc<Coordinator>, name: &str, purge: bool) -> Resul
drop(guard); drop(guard);
let _ = coord let _ = coord
.push_todo( .push_todo(
hive_sh4re::manager::MANAGER_AGENT, hive_sh4re::MANAGER_AGENT,
"core", "core",
Some(format!("destroyed:{name}")), Some(format!("destroyed:{name}")),
format!("agent '{name}' destroyed"), format!("agent '{name}' destroyed"),

View file

@ -3,7 +3,7 @@
//! and `tool-groups.json`. //! and `tool-groups.json`.
//! //!
//! Format: a JSON object mapping agent name to an array of //! Format: a JSON object mapping agent name to an array of
//! `hive_sh4re::permissions::Capability` `snake_case` strings: //! `hive_sh4re::Capability` `snake_case` strings:
//! //!
//! ```json //! ```json
//! { //! {
@ -52,7 +52,7 @@ pub fn caps_for(name: &str) -> Vec<String> {
/// Check whether an agent holds a specific capability. /// Check whether an agent holds a specific capability.
#[must_use] #[must_use]
pub fn has_cap(name: &str, cap: hive_sh4re::permissions::Capability) -> bool { pub fn has_cap(name: &str, cap: hive_sh4re::Capability) -> bool {
caps_for(name) caps_for(name)
.iter() .iter()
.any(|s| s.eq_ignore_ascii_case(cap.as_str())) .any(|s| s.eq_ignore_ascii_case(cap.as_str()))

View file

@ -3,7 +3,7 @@
//! and the meta `flake.nix`. //! and the meta `flake.nix`.
//! //!
//! Format: a JSON object mapping agent name to an array of //! Format: a JSON object mapping agent name to an array of
//! `hive_sh4re::permissions::ToolGroup` `snake_case` strings: //! `hive_sh4re::ToolGroup` `snake_case` strings:
//! //!
//! ```json //! ```json
//! { //! {
@ -70,7 +70,7 @@ fn write(map: &BTreeMap<String, Vec<String>>) -> std::io::Result<()> {
/// Returns `Ok(())` when all names are known, or `Err` listing the /// Returns `Ok(())` when all names are known, or `Err` listing the
/// unrecognised names so callers can surface a useful error message. /// unrecognised names so callers can surface a useful error message.
pub fn validate_groups(groups: &[String]) -> anyhow::Result<()> { pub fn validate_groups(groups: &[String]) -> anyhow::Result<()> {
let valid: std::collections::BTreeSet<&str> = hive_sh4re::permissions::ToolGroup::ALL let valid: std::collections::BTreeSet<&str> = hive_sh4re::ToolGroup::ALL
.iter() .iter()
.map(|g| g.as_str()) .map(|g| g.as_str())
.collect(); .collect();
@ -85,7 +85,7 @@ pub fn validate_groups(groups: &[String]) -> anyhow::Result<()> {
anyhow::bail!( anyhow::bail!(
"unknown tool group(s): {}; valid names are: {}", "unknown tool group(s): {}; valid names are: {}",
unknown.join(", "), unknown.join(", "),
hive_sh4re::permissions::ToolGroup::ALL hive_sh4re::ToolGroup::ALL
.iter() .iter()
.map(|g| g.as_str()) .map(|g| g.as_str())
.collect::<Vec<_>>() .collect::<Vec<_>>()

View file

@ -104,7 +104,7 @@ pub fn top_level_agents_in(topo: &BTreeMap<String, Option<String>>) -> Vec<Strin
} }
/// Resolve a magic recipient sentinel (currently just /// Resolve a magic recipient sentinel (currently just
/// [`hive_sh4re::manager::PARENT_RECIPIENT`]) to a real broker recipient at /// [`hive_sh4re::PARENT_RECIPIENT`]) to a real broker recipient at
/// send time. Returns an owned `String` so callers can plug it /// send time. Returns an owned `String` so callers can plug it
/// straight into [`crate::broker::Broker::send`] without /// straight into [`crate::broker::Broker::send`] without
/// borrow-juggling around the temporary lookup. /// borrow-juggling around the temporary lookup.
@ -116,7 +116,7 @@ pub fn top_level_agents_in(topo: &BTreeMap<String, Option<String>>) -> Vec<Strin
pub fn resolve_recipient(sender: &str, to: &str) -> String { pub fn resolve_recipient(sender: &str, to: &str) -> String {
// Early exit: only sentinel recipients need topology lookup. This // Early exit: only sentinel recipients need topology lookup. This
// keeps the cost of a normal `send` at one string comparison. // keeps the cost of a normal `send` at one string comparison.
if to != hive_sh4re::manager::PARENT_RECIPIENT { if to != hive_sh4re::PARENT_RECIPIENT {
return to.to_owned(); return to.to_owned();
} }
resolve_recipient_in(&read(), sender, to) resolve_recipient_in(&read(), sender, to)
@ -131,11 +131,11 @@ pub fn resolve_recipient_in(
sender: &str, sender: &str,
to: &str, to: &str,
) -> String { ) -> String {
if to == hive_sh4re::manager::PARENT_RECIPIENT { if to == hive_sh4re::PARENT_RECIPIENT {
topo.get(sender) topo.get(sender)
.cloned() .cloned()
.flatten() .flatten()
.unwrap_or_else(|| hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned()) .unwrap_or_else(|| hive_sh4re::OPERATOR_RECIPIENT.to_owned())
} else { } else {
to.to_owned() to.to_owned()
} }
@ -722,8 +722,8 @@ mod tests {
assert_eq!(resolve_recipient_in(&topo, "bob", "alice"), "alice"); assert_eq!(resolve_recipient_in(&topo, "bob", "alice"), "alice");
assert_eq!(resolve_recipient_in(&topo, "bob", "*"), "*"); assert_eq!(resolve_recipient_in(&topo, "bob", "*"), "*");
assert_eq!( assert_eq!(
resolve_recipient_in(&topo, "bob", hive_sh4re::manager::OPERATOR_RECIPIENT), resolve_recipient_in(&topo, "bob", hive_sh4re::OPERATOR_RECIPIENT),
hive_sh4re::manager::OPERATOR_RECIPIENT hive_sh4re::OPERATOR_RECIPIENT
); );
} }
@ -732,12 +732,12 @@ mod tests {
let topo = topo_three_level(); let topo = topo_three_level();
// bob's parent is alice → `<parent>` from bob goes to alice. // bob's parent is alice → `<parent>` from bob goes to alice.
assert_eq!( assert_eq!(
resolve_recipient_in(&topo, "bob", hive_sh4re::manager::PARENT_RECIPIENT), resolve_recipient_in(&topo, "bob", hive_sh4re::PARENT_RECIPIENT),
"alice" "alice"
); );
// alice's parent is the manager — same one-hop rewrite. // alice's parent is the manager — same one-hop rewrite.
assert_eq!( assert_eq!(
resolve_recipient_in(&topo, "alice", hive_sh4re::manager::PARENT_RECIPIENT), resolve_recipient_in(&topo, "alice", hive_sh4re::PARENT_RECIPIENT),
crate::lifecycle::MANAGER_NAME crate::lifecycle::MANAGER_NAME
); );
} }
@ -752,9 +752,9 @@ mod tests {
resolve_recipient_in( resolve_recipient_in(
&topo, &topo,
crate::lifecycle::MANAGER_NAME, crate::lifecycle::MANAGER_NAME,
hive_sh4re::manager::PARENT_RECIPIENT hive_sh4re::PARENT_RECIPIENT
), ),
hive_sh4re::manager::OPERATOR_RECIPIENT hive_sh4re::OPERATOR_RECIPIENT
); );
} }
@ -766,8 +766,8 @@ mod tests {
// its row. // its row.
let topo = topo_three_level(); let topo = topo_three_level();
assert_eq!( assert_eq!(
resolve_recipient_in(&topo, "nobody", hive_sh4re::manager::PARENT_RECIPIENT), resolve_recipient_in(&topo, "nobody", hive_sh4re::PARENT_RECIPIENT),
hive_sh4re::manager::OPERATOR_RECIPIENT hive_sh4re::OPERATOR_RECIPIENT
); );
} }

View file

@ -696,7 +696,7 @@ impl Coordinator {
/// rebuild-queue worker after a `PermChange` / Capabilities entry /// rebuild-queue worker after a `PermChange` / Capabilities entry
/// commits the JSON file, so the P3RM1SS10NS tab updates live. /// commits the JSON file, so the P3RM1SS10NS tab updates live.
pub fn emit_capabilities_snapshot(self: &Arc<Self>) { pub fn emit_capabilities_snapshot(self: &Arc<Self>) {
use hive_sh4re::permissions::Capability; use hive_sh4re::Capability;
let caps = Capability::ALL.iter().map(|c| c.as_str()).collect(); let caps = Capability::ALL.iter().map(|c| c.as_str()).collect();
let descriptions = Capability::ALL let descriptions = Capability::ALL
.iter() .iter()
@ -723,7 +723,7 @@ impl Coordinator {
/// rebuild-queue worker after a `PermChange` / `ToolGroups` entry /// rebuild-queue worker after a `PermChange` / `ToolGroups` entry
/// commits the JSON file, so the P3RM1SS10NS tab updates live. /// commits the JSON file, so the P3RM1SS10NS tab updates live.
pub fn emit_tool_groups_snapshot(self: &Arc<Self>) { pub fn emit_tool_groups_snapshot(self: &Arc<Self>) {
use hive_sh4re::permissions::ToolGroup; use hive_sh4re::ToolGroup;
let groups = ToolGroup::ALL.iter().map(|g| g.as_str()).collect(); let groups = ToolGroup::ALL.iter().map(|g| g.as_str()).collect();
let descriptions = ToolGroup::ALL let descriptions = ToolGroup::ALL
.iter() .iter()
@ -1025,7 +1025,7 @@ impl Coordinator {
/// `<parent>` sentinel's "root → operator" routing (see /// `<parent>` sentinel's "root → operator" routing (see
/// `docs/conventions.md::Recipient sentinels`). The /// `docs/conventions.md::Recipient sentinels`). The
/// notifications fire as ordinary broker messages with /// notifications fire as ordinary broker messages with
/// `from = hive_sh4re::manager::SYSTEM_SENDER` so the dashboard renders /// `from = hive_sh4re::SYSTEM_SENDER` so the dashboard renders
/// them under the existing system-source styling. /// them under the existing system-source styling.
/// ///
/// First validation failure aborts the whole batch with no disk writes. /// First validation failure aborts the whole batch with no disk writes.
@ -1056,7 +1056,7 @@ impl Coordinator {
let new_label = new_parent.unwrap_or("<root>"); let new_label = new_parent.unwrap_or("<root>");
if let Some(op) = old_parent.as_deref() { if let Some(op) = old_parent.as_deref() {
let _ = self.broker.send(&hive_sh4re::Message { let _ = self.broker.send(&hive_sh4re::Message {
from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::SYSTEM_SENDER), from: hive_sh4re::trusted_sender(hive_sh4re::SYSTEM_SENDER),
to: op.to_owned(), to: op.to_owned(),
body: format!("{child} moved out of your subtree to {new_label}"), body: format!("{child} moved out of your subtree to {new_label}"),
in_reply_to: None, in_reply_to: None,
@ -1064,7 +1064,7 @@ impl Coordinator {
} }
if let Some(np) = new_parent { if let Some(np) = new_parent {
let _ = self.broker.send(&hive_sh4re::Message { let _ = self.broker.send(&hive_sh4re::Message {
from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::SYSTEM_SENDER), from: hive_sh4re::trusted_sender(hive_sh4re::SYSTEM_SENDER),
to: np.to_owned(), to: np.to_owned(),
body: format!( body: format!(
"{child} just moved into your subtree (was previously under {old_label})" "{child} just moved into your subtree (was previously under {old_label})"
@ -1345,7 +1345,7 @@ impl Coordinator {
still in your window." still in your window."
); );
if let Err(e) = self.broker.send(&hive_sh4re::Message { if let Err(e) = self.broker.send(&hive_sh4re::Message {
from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::SYSTEM_SENDER), from: hive_sh4re::trusted_sender(hive_sh4re::SYSTEM_SENDER),
to: name.to_owned(), to: name.to_owned(),
body, body,
in_reply_to: None, in_reply_to: None,
@ -1358,8 +1358,8 @@ impl Coordinator {
/// `Message::body`; sender = `SYSTEM_SENDER`. The manager harness /// `Message::body`; sender = `SYSTEM_SENDER`. The manager harness
/// recognises the sender and parses the body. Best-effort: a serde or /// recognises the sender and parses the body. Best-effort: a serde or
/// broker error is logged but does not propagate. /// broker error is logged but does not propagate.
pub fn notify_manager(&self, event: &hive_sh4re::manager::HelperEvent) { pub fn notify_manager(&self, event: &hive_sh4re::HelperEvent) {
self.notify_agent(hive_sh4re::manager::MANAGER_AGENT, event); self.notify_agent(hive_sh4re::MANAGER_AGENT, event);
} }
/// Route an approval-scoped helper event to the agent that submitted /// Route an approval-scoped helper event to the agent that submitted
@ -1367,7 +1367,7 @@ impl Coordinator {
/// time). Legacy rows with no recorded submitter — and any lookup /// time). Legacy rows with no recorded submitter — and any lookup
/// failure — fall back to the root agent, preserving the prior /// failure — fall back to the root agent, preserving the prior
/// always-root behaviour. /// always-root behaviour.
pub fn notify_submitter(&self, approval_id: i64, event: &hive_sh4re::manager::HelperEvent) { pub fn notify_submitter(&self, approval_id: i64, event: &hive_sh4re::HelperEvent) {
let target = self.submitter_or_manager(approval_id); let target = self.submitter_or_manager(approval_id);
self.notify_agent(&target, event); self.notify_agent(&target, event);
} }
@ -1381,7 +1381,7 @@ impl Coordinator {
self.approvals self.approvals
.submitter_of(approval_id) .submitter_of(approval_id)
.unwrap_or_default() .unwrap_or_default()
.unwrap_or_else(|| hive_sh4re::manager::MANAGER_AGENT.to_owned()) .unwrap_or_else(|| hive_sh4re::MANAGER_AGENT.to_owned())
} }
/// Push a todo directly into `agent`'s in-container todo store — a /// Push a todo directly into `agent`'s in-container todo store — a
@ -1471,20 +1471,15 @@ impl Coordinator {
/// body = JSON-encoded event). Used to route `QuestionAnswered` /// body = JSON-encoded event). Used to route `QuestionAnswered`
/// events back to the agent that called `ask`, `QuestionAsked` /// events back to the agent that called `ask`, `QuestionAsked`
/// events to the target of a peer question, etc. /// events to the target of a peer question, etc.
pub fn notify_agent(&self, agent: &str, event: &hive_sh4re::manager::HelperEvent) { pub fn notify_agent(&self, agent: &str, event: &hive_sh4re::HelperEvent) {
self.notify_agent_from(hive_sh4re::manager::SYSTEM_SENDER, agent, event); self.notify_agent_from(hive_sh4re::SYSTEM_SENDER, agent, event);
} }
/// Same as `notify_agent` but with an explicit sender. Use this /// Same as `notify_agent` but with an explicit sender. Use this
/// when the event originates from a known agent or the operator /// when the event originates from a known agent or the operator
/// (e.g. `QuestionAnswered` — the answerer should be the `from`, /// (e.g. `QuestionAnswered` — the answerer should be the `from`,
/// not `system`) so the recipient's terminal shows the right name. /// not `system`) so the recipient's terminal shows the right name.
pub fn notify_agent_from( pub fn notify_agent_from(&self, from: &str, agent: &str, event: &hive_sh4re::HelperEvent) {
&self,
from: &str,
agent: &str,
event: &hive_sh4re::manager::HelperEvent,
) {
let body = match serde_json::to_string(event) { let body = match serde_json::to_string(event) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
@ -1493,7 +1488,7 @@ impl Coordinator {
} }
}; };
if let Err(e) = self.broker.send(&hive_sh4re::Message { if let Err(e) = self.broker.send(&hive_sh4re::Message {
from: hive_sh4re::manager::trusted_sender(from), from: hive_sh4re::trusted_sender(from),
to: agent.to_owned(), to: agent.to_owned(),
body, body,
in_reply_to: None, in_reply_to: None,
@ -1515,7 +1510,7 @@ impl Coordinator {
continue; continue;
} }
if let Err(e) = self.broker.send(&hive_sh4re::Message { if let Err(e) = self.broker.send(&hive_sh4re::Message {
from: hive_sh4re::manager::trusted_sender(from), from: hive_sh4re::trusted_sender(from),
to: agent_name.clone(), to: agent_name.clone(),
body: broadcast_body.clone(), body: broadcast_body.clone(),
in_reply_to: None, in_reply_to: None,

View file

@ -8,7 +8,7 @@ use axum::{
http::StatusCode, http::StatusCode,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use hive_sh4re::approvals::Approval; use hive_sh4re::Approval;
use serde::Deserialize; use serde::Deserialize;
use utoipa::ToSchema; use utoipa::ToSchema;
@ -87,8 +87,7 @@ pub(super) fn gc_orphans(coord: &Coordinator, approvals: Vec<Approval>) -> Vec<A
// the proposed dir is supposed to be missing. // the proposed dir is supposed to be missing.
if matches!( if matches!(
a.kind, a.kind,
hive_sh4re::approvals::ApprovalKind::Spawn hive_sh4re::ApprovalKind::Spawn | hive_sh4re::ApprovalKind::InitConfig
| hive_sh4re::approvals::ApprovalKind::InitConfig
) { ) {
return true; return true;
} }

View file

@ -220,7 +220,7 @@ pub(super) async fn post_op_send(
if to == "*" { if to == "*" {
let errors = state let errors = state
.coord .coord
.broadcast_send(hive_sh4re::manager::OPERATOR_RECIPIENT, &body); .broadcast_send(hive_sh4re::OPERATOR_RECIPIENT, &body);
if !errors.is_empty() { if !errors.is_empty() {
return error_response(&format!( return error_response(&format!(
"op-send broadcast partial fail: {}", "op-send broadcast partial fail: {}",
@ -228,7 +228,7 @@ pub(super) async fn post_op_send(
)); ));
} }
} else if let Err(e) = state.coord.broker.send(&hive_sh4re::Message { } else if let Err(e) = state.coord.broker.send(&hive_sh4re::Message {
from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::OPERATOR_RECIPIENT), from: hive_sh4re::trusted_sender(hive_sh4re::OPERATOR_RECIPIENT),
to: to.clone(), to: to.clone(),
body, body,
in_reply_to: None, in_reply_to: None,
@ -270,7 +270,7 @@ pub(super) async fn post_request_spawn(
} }
match state.coord.approvals.submit_kind( match state.coord.approvals.submit_kind(
&name, &name,
hive_sh4re::approvals::ApprovalKind::Spawn, hive_sh4re::ApprovalKind::Spawn,
"", "",
None, None,
"operator", "operator",

View file

@ -48,11 +48,11 @@ pub(super) struct ToolGroupsSnapshot {
pub(super) async fn get_tool_groups( pub(super) async fn get_tool_groups(
State(state): State<AppState>, State(state): State<AppState>,
) -> axum::Json<ToolGroupsSnapshot> { ) -> axum::Json<ToolGroupsSnapshot> {
let groups = hive_sh4re::permissions::ToolGroup::ALL let groups = hive_sh4re::ToolGroup::ALL
.iter() .iter()
.map(|g| g.as_str()) .map(|g| g.as_str())
.collect(); .collect();
let descriptions = hive_sh4re::permissions::ToolGroup::ALL let descriptions = hive_sh4re::ToolGroup::ALL
.iter() .iter()
.map(|g| (g.as_str(), g.description())) .map(|g| (g.as_str(), g.description()))
.collect(); .collect();
@ -80,7 +80,7 @@ pub(super) async fn get_tool_groups(
/// the container actually runs with. /// the container actually runs with.
#[must_use] #[must_use]
pub(crate) fn tool_group_default_names() -> Vec<&'static str> { pub(crate) fn tool_group_default_names() -> Vec<&'static str> {
hive_sh4re::permissions::ToolGroup::AGENT_DEFAULT hive_sh4re::ToolGroup::AGENT_DEFAULT
.iter() .iter()
.map(|g| g.as_str()) .map(|g| g.as_str())
.collect() .collect()
@ -204,7 +204,7 @@ pub(super) struct CapabilitiesSnapshot {
pub(super) async fn get_capabilities( pub(super) async fn get_capabilities(
State(state): State<AppState>, State(state): State<AppState>,
) -> axum::Json<CapabilitiesSnapshot> { ) -> axum::Json<CapabilitiesSnapshot> {
use hive_sh4re::permissions::Capability; use hive_sh4re::Capability;
let caps = Capability::ALL.iter().map(|c| c.as_str()).collect(); let caps = Capability::ALL.iter().map(|c| c.as_str()).collect();
let descriptions = Capability::ALL let descriptions = Capability::ALL
.iter() .iter()
@ -256,7 +256,7 @@ pub(super) async fn post_capabilities(
if let Some(reject) = guard_agent_name(&state, &logical).await { if let Some(reject) = guard_agent_name(&state, &logical).await {
return Ok(reject); return Ok(reject);
} }
let known: Vec<&str> = hive_sh4re::permissions::Capability::ALL let known: Vec<&str> = hive_sh4re::Capability::ALL
.iter() .iter()
.map(|c| c.as_str()) .map(|c| c.as_str())
.collect(); .collect();
@ -334,7 +334,7 @@ pub(super) async fn post_permissions(
State(state): State<AppState>, State(state): State<AppState>,
axum::Json(body): axum::Json<BatchPermsBody>, axum::Json(body): axum::Json<BatchPermsBody>,
) -> Result<Response, ProblemDetails> { ) -> Result<Response, ProblemDetails> {
let known_caps: Vec<&str> = hive_sh4re::permissions::Capability::ALL let known_caps: Vec<&str> = hive_sh4re::Capability::ALL
.iter() .iter()
.map(|c| c.as_str()) .map(|c| c.as_str())
.collect(); .collect();

View file

@ -64,27 +64,26 @@ pub(super) async fn post_answer_question(
.with_detail("answer: required"), .with_detail("answer: required"),
); );
} }
let resp = let resp = match state
match state
.coord .coord
.questions .questions
.answer(id, answer, hive_sh4re::manager::OPERATOR_RECIPIENT) .answer(id, answer, hive_sh4re::OPERATOR_RECIPIENT)
{ {
Ok((question, asker, target)) => { Ok((question, asker, target)) => {
tracing::info!(%id, %asker, "operator answered question"); tracing::info!(%id, %asker, "operator answered question");
state.coord.notify_agent( state.coord.notify_agent(
&asker, &asker,
&hive_sh4re::manager::HelperEvent::QuestionAnswered { &hive_sh4re::HelperEvent::QuestionAnswered {
id, id,
question, question,
answer: answer.to_owned(), answer: answer.to_owned(),
answerer: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(), answerer: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
}, },
); );
state.coord.emit_question_resolved( state.coord.emit_question_resolved(
id, id,
answer, answer,
hive_sh4re::manager::OPERATOR_RECIPIENT, hive_sh4re::OPERATOR_RECIPIENT,
false, false,
target.as_deref(), target.as_deref(),
); );
@ -121,25 +120,25 @@ pub(super) async fn post_cancel_question(
match state match state
.coord .coord
.questions .questions
.answer(id, SENTINEL, hive_sh4re::manager::OPERATOR_RECIPIENT) .answer(id, SENTINEL, hive_sh4re::OPERATOR_RECIPIENT)
{ {
Ok((question, asker, target)) => { Ok((question, asker, target)) => {
tracing::info!(%id, %asker, "operator cancelled question"); tracing::info!(%id, %asker, "operator cancelled question");
state.coord.emit_question_resolved( state.coord.emit_question_resolved(
id, id,
SENTINEL, SENTINEL,
hive_sh4re::manager::OPERATOR_RECIPIENT, hive_sh4re::OPERATOR_RECIPIENT,
true, true,
target.as_deref(), target.as_deref(),
); );
state.coord.notify_agent_from( state.coord.notify_agent_from(
hive_sh4re::manager::OPERATOR_RECIPIENT, hive_sh4re::OPERATOR_RECIPIENT,
&asker, &asker,
&hive_sh4re::manager::HelperEvent::QuestionAnswered { &hive_sh4re::HelperEvent::QuestionAnswered {
id, id,
question, question,
answer: SENTINEL.to_owned(), answer: SENTINEL.to_owned(),
answerer: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(), answerer: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
}, },
); );
(StatusCode::OK, "ok").into_response() (StatusCode::OK, "ok").into_response()

View file

@ -69,7 +69,7 @@ pub(super) async fn api_schedules(State(state): State<AppState>) -> Response {
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/schedules", path = "/api/schedules",
// `hive_sh4re::manager::SchedulePromptPayload` (the actual body) has no `ToSchema` — // `hive_sh4re::SchedulePromptPayload` (the actual body) has no `ToSchema` —
// same reasoning as the `Vec<serde_json::Value>` placeholder on // same reasoning as the `Vec<serde_json::Value>` placeholder on
// `api_schedules` above. // `api_schedules` above.
request_body(content = serde_json::Value, description = "SchedulePromptPayload wire shape"), request_body(content = serde_json::Value, description = "SchedulePromptPayload wire shape"),
@ -82,7 +82,7 @@ pub(super) async fn api_schedules(State(state): State<AppState>) -> Response {
)] )]
pub(super) async fn post_schedule_new( pub(super) async fn post_schedule_new(
State(state): State<AppState>, State(state): State<AppState>,
axum::Json(payload): axum::Json<hive_sh4re::manager::SchedulePromptPayload>, axum::Json(payload): axum::Json<hive_sh4re::SchedulePromptPayload>,
) -> Result<Response, ProblemDetails> { ) -> Result<Response, ProblemDetails> {
if payload.targets.is_empty() { if payload.targets.is_empty() {
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST) return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
@ -97,7 +97,7 @@ pub(super) async fn post_schedule_new(
.with_detail("interval_seconds must be > 0 (use None for one-shot)")); .with_detail("interval_seconds must be > 0 (use None for one-shot)"));
} }
let new = crate::scheduled_prompts::NewSchedule { let new = crate::scheduled_prompts::NewSchedule {
owner: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(), owner: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
targets: payload.targets, targets: payload.targets,
body: payload.body, body: payload.body,
first_fire_at_unix: payload.first_fire_at_unix, first_fire_at_unix: payload.first_fire_at_unix,

Some files were not shown because too many files have changed in this diff Show more