Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3ac4de8fb | ||
|
|
138f6b6c10 | ||
|
|
02bbff1e34 |
43 changed files with 732 additions and 677 deletions
|
|
@ -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::HelperEvent`):
|
root agent. Variants (`hive_sh4re::manager::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
|
||||||
|
|
|
||||||
|
|
@ -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::ToolGroup`), not from a hardcoded
|
`ToolGroup` values (`hive_sh4re::permissions::ToolGroup`), not from a hardcoded
|
||||||
binary flavor.
|
binary flavor.
|
||||||
|
|
||||||
| Group | Tools |
|
| Group | Tools |
|
||||||
|
|
|
||||||
|
|
@ -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::SchedulePromptPayload {
|
hive_sh4re::manager::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,
|
||||||
|
|
|
||||||
|
|
@ -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::PARENT_RECIPIENT {
|
if to == hive_sh4re::manager::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::PARENT_RECIPIENT
|
hive_sh4re::manager::PARENT_RECIPIENT
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,9 @@ pub enum Response {
|
||||||
/// `CountPendingReminders` result.
|
/// `CountPendingReminders` result.
|
||||||
PendingRemindersCount { count: u64 },
|
PendingRemindersCount { count: u64 },
|
||||||
/// `ReminderRollup` result.
|
/// `ReminderRollup` result.
|
||||||
ReminderRollup { stats: hive_sh4re::ReminderStats },
|
ReminderRollup {
|
||||||
|
stats: hive_sh4re::approvals::ReminderStats,
|
||||||
|
},
|
||||||
/// Op failed; `message` is operator-facing.
|
/// Op failed; `message` is operator-facing.
|
||||||
Err { message: String },
|
Err { message: String },
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::{HelperEvent, SYSTEM_SENDER};
|
use hive_sh4re::manager::{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::PARENT_RECIPIENT.into(),
|
to: hive_sh4re::manager::PARENT_RECIPIENT.into(),
|
||||||
body,
|
body,
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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::ToolGroup` `snake_case` names (e.g. `"messaging,inbox,meta"`).
|
/// `hive_sh4re::permissions::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::Capability` `snake_case` names. Absent = no extra capabilities.
|
/// `hive_sh4re::permissions::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::ToolGroup> {
|
fn effective_tool_groups() -> Vec<hive_sh4re::permissions::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::ToolGroup::AGENT_DEFAULT.to_vec(),
|
_ => return hive_sh4re::permissions::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) =
|
if let Ok(g) = serde_json::from_value::<hive_sh4re::permissions::ToolGroup>(
|
||||||
serde_json::from_value::<hive_sh4re::ToolGroup>(serde_json::Value::String(t.clone()))
|
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::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::ToolGroup::AGENT_DEFAULT.to_vec();
|
return hive_sh4re::permissions::ToolGroup::AGENT_DEFAULT.to_vec();
|
||||||
}
|
}
|
||||||
groups
|
groups
|
||||||
}
|
}
|
||||||
|
|
@ -124,9 +124,9 @@ fn effective_tool_groups() -> Vec<hive_sh4re::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::ToolGroup> {
|
fn extra_server_required_group(server: &str) -> Option<hive_sh4re::permissions::ToolGroup> {
|
||||||
match server {
|
match server {
|
||||||
"bash" => Some(hive_sh4re::ToolGroup::Execution),
|
"bash" => Some(hive_sh4re::permissions::ToolGroup::Execution),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -134,14 +134,14 @@ fn extra_server_required_group(server: &str) -> Option<hive_sh4re::ToolGroup> {
|
||||||
/// 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::ToolGroup]) -> bool {
|
fn extra_server_enabled(server: &str, groups: &[hive_sh4re::permissions::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::ToolGroup;
|
use hive_sh4re::permissions::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::ToolGroup]) -> Vec<String> {
|
pub fn allowed_mcp_tools(groups: &[hive_sh4re::permissions::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::ToolGroup::ALWAYS_ON_TOOLS
|
let mut out: Vec<String> = hive_sh4re::permissions::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::ToolGroup;
|
use hive_sh4re::permissions::ToolGroup;
|
||||||
|
|
||||||
fn qualified(tool: &str) -> String {
|
fn qualified(tool: &str) -> String {
|
||||||
format!("mcp__{SERVER_NAME}__{tool}")
|
format!("mcp__{SERVER_NAME}__{tool}")
|
||||||
|
|
|
||||||
|
|
@ -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::ReminderStats;
|
use hive_sh4re::approvals::ReminderStats;
|
||||||
use hive_sh4re::wire_time;
|
use hive_sh4re::wire_time;
|
||||||
use rusqlite::{Connection, params};
|
use rusqlite::{Connection, params};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use chrono::Utc;
|
||||||
use rusqlite::{Connection, OpenFlags};
|
use rusqlite::{Connection, OpenFlags};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use hive_sh4re::ReminderStats;
|
use hive_sh4re::approvals::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.
|
||||||
|
|
|
||||||
|
|
@ -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::ReminderStats> {
|
async fn fetch_reminder_stats(window_secs: u64) -> Option<hive_sh4re::approvals::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,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, bail};
|
use anyhow::{Context as _, Result, bail};
|
||||||
use hive_sh4re::{ApprovalKind, ApprovalStatus, HelperEvent};
|
use hive_sh4re::approvals::{ApprovalKind, ApprovalStatus};
|
||||||
|
use hive_sh4re::manager::HelperEvent;
|
||||||
|
|
||||||
use crate::coordinator::Coordinator;
|
use crate::coordinator::Coordinator;
|
||||||
use crate::lifecycle;
|
use crate::lifecycle;
|
||||||
|
|
@ -142,7 +143,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::Approval,
|
approval: hive_sh4re::approvals::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`).
|
||||||
|
|
@ -410,7 +411,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::Approval,
|
approval: &hive_sh4re::approvals::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 {
|
||||||
|
|
@ -472,10 +473,11 @@ 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::Approval,
|
approval: hive_sh4re::approvals::Approval,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let result: Result<()> = async {
|
let result: Result<()> = async {
|
||||||
let payload: hive_sh4re::SchedulePromptPayload = serde_json::from_str(&approval.commit_ref)
|
let payload: hive_sh4re::manager::SchedulePromptPayload =
|
||||||
|
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
|
||||||
|
|
@ -598,7 +600,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::Approval> {
|
) -> Result<hive_sh4re::approvals::Approval> {
|
||||||
let approval = coord
|
let approval = coord
|
||||||
.approvals
|
.approvals
|
||||||
.get(approval_id)
|
.get(approval_id)
|
||||||
|
|
@ -641,7 +643,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::Approval,
|
approval: hive_sh4re::approvals::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,
|
||||||
|
|
@ -681,7 +683,7 @@ async fn run_approval_init_config(
|
||||||
|
|
||||||
async fn finish_approval(
|
async fn finish_approval(
|
||||||
coord: &Coordinator,
|
coord: &Coordinator,
|
||||||
approval: &hive_sh4re::Approval,
|
approval: &hive_sh4re::approvals::Approval,
|
||||||
result: Result<()>,
|
result: Result<()>,
|
||||||
terminal_tag: Option<String>,
|
terminal_tag: Option<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
|
@ -960,7 +962,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_AGENT,
|
hive_sh4re::manager::MANAGER_AGENT,
|
||||||
"core",
|
"core",
|
||||||
Some(format!("destroyed:{name}")),
|
Some(format!("destroyed:{name}")),
|
||||||
format!("agent '{name}' destroyed"),
|
format!("agent '{name}' destroyed"),
|
||||||
|
|
|
||||||
|
|
@ -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::Capability` `snake_case` strings:
|
//! `hive_sh4re::permissions::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::Capability) -> bool {
|
pub fn has_cap(name: &str, cap: hive_sh4re::permissions::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()))
|
||||||
|
|
|
||||||
|
|
@ -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::ToolGroup` `snake_case` strings:
|
//! `hive_sh4re::permissions::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::ToolGroup::ALL
|
let valid: std::collections::BTreeSet<&str> = hive_sh4re::permissions::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::ToolGroup::ALL
|
hive_sh4re::permissions::ToolGroup::ALL
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| g.as_str())
|
.map(|g| g.as_str())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
|
|
||||||
|
|
@ -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::PARENT_RECIPIENT`]) to a real broker recipient at
|
/// [`hive_sh4re::manager::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::PARENT_RECIPIENT {
|
if to != hive_sh4re::manager::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::PARENT_RECIPIENT {
|
if to == hive_sh4re::manager::PARENT_RECIPIENT {
|
||||||
topo.get(sender)
|
topo.get(sender)
|
||||||
.cloned()
|
.cloned()
|
||||||
.flatten()
|
.flatten()
|
||||||
.unwrap_or_else(|| hive_sh4re::OPERATOR_RECIPIENT.to_owned())
|
.unwrap_or_else(|| hive_sh4re::manager::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::OPERATOR_RECIPIENT),
|
resolve_recipient_in(&topo, "bob", hive_sh4re::manager::OPERATOR_RECIPIENT),
|
||||||
hive_sh4re::OPERATOR_RECIPIENT
|
hive_sh4re::manager::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::PARENT_RECIPIENT),
|
resolve_recipient_in(&topo, "bob", hive_sh4re::manager::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::PARENT_RECIPIENT),
|
resolve_recipient_in(&topo, "alice", hive_sh4re::manager::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::PARENT_RECIPIENT
|
hive_sh4re::manager::PARENT_RECIPIENT
|
||||||
),
|
),
|
||||||
hive_sh4re::OPERATOR_RECIPIENT
|
hive_sh4re::manager::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::PARENT_RECIPIENT),
|
resolve_recipient_in(&topo, "nobody", hive_sh4re::manager::PARENT_RECIPIENT),
|
||||||
hive_sh4re::OPERATOR_RECIPIENT
|
hive_sh4re::manager::OPERATOR_RECIPIENT
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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::Capability;
|
use hive_sh4re::permissions::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::ToolGroup;
|
use hive_sh4re::permissions::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::SYSTEM_SENDER` so the dashboard renders
|
/// `from = hive_sh4re::manager::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::trusted_sender(hive_sh4re::SYSTEM_SENDER),
|
from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::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::trusted_sender(hive_sh4re::SYSTEM_SENDER),
|
from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::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::trusted_sender(hive_sh4re::SYSTEM_SENDER),
|
from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::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::HelperEvent) {
|
pub fn notify_manager(&self, event: &hive_sh4re::manager::HelperEvent) {
|
||||||
self.notify_agent(hive_sh4re::MANAGER_AGENT, event);
|
self.notify_agent(hive_sh4re::manager::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::HelperEvent) {
|
pub fn notify_submitter(&self, approval_id: i64, event: &hive_sh4re::manager::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_AGENT.to_owned())
|
.unwrap_or_else(|| hive_sh4re::manager::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,15 +1471,20 @@ 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::HelperEvent) {
|
pub fn notify_agent(&self, agent: &str, event: &hive_sh4re::manager::HelperEvent) {
|
||||||
self.notify_agent_from(hive_sh4re::SYSTEM_SENDER, agent, event);
|
self.notify_agent_from(hive_sh4re::manager::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(&self, from: &str, agent: &str, event: &hive_sh4re::HelperEvent) {
|
pub fn notify_agent_from(
|
||||||
|
&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) => {
|
||||||
|
|
@ -1488,7 +1493,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::trusted_sender(from),
|
from: hive_sh4re::manager::trusted_sender(from),
|
||||||
to: agent.to_owned(),
|
to: agent.to_owned(),
|
||||||
body,
|
body,
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
|
|
@ -1510,7 +1515,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::trusted_sender(from),
|
from: hive_sh4re::manager::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,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use axum::{
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use hive_sh4re::Approval;
|
use hive_sh4re::approvals::Approval;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
|
@ -87,7 +87,8 @@ 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::ApprovalKind::Spawn | hive_sh4re::ApprovalKind::InitConfig
|
hive_sh4re::approvals::ApprovalKind::Spawn
|
||||||
|
| hive_sh4re::approvals::ApprovalKind::InitConfig
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::OPERATOR_RECIPIENT, &body);
|
.broadcast_send(hive_sh4re::manager::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::trusted_sender(hive_sh4re::OPERATOR_RECIPIENT),
|
from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::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::ApprovalKind::Spawn,
|
hive_sh4re::approvals::ApprovalKind::Spawn,
|
||||||
"",
|
"",
|
||||||
None,
|
None,
|
||||||
"operator",
|
"operator",
|
||||||
|
|
|
||||||
|
|
@ -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::ToolGroup::ALL
|
let groups = hive_sh4re::permissions::ToolGroup::ALL
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| g.as_str())
|
.map(|g| g.as_str())
|
||||||
.collect();
|
.collect();
|
||||||
let descriptions = hive_sh4re::ToolGroup::ALL
|
let descriptions = hive_sh4re::permissions::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::ToolGroup::AGENT_DEFAULT
|
hive_sh4re::permissions::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::Capability;
|
use hive_sh4re::permissions::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::Capability::ALL
|
let known: Vec<&str> = hive_sh4re::permissions::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::Capability::ALL
|
let known_caps: Vec<&str> = hive_sh4re::permissions::Capability::ALL
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.as_str())
|
.map(|c| c.as_str())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
|
||||||
|
|
@ -64,26 +64,27 @@ pub(super) async fn post_answer_question(
|
||||||
.with_detail("answer: required"),
|
.with_detail("answer: required"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let resp = match state
|
let resp =
|
||||||
|
match state
|
||||||
.coord
|
.coord
|
||||||
.questions
|
.questions
|
||||||
.answer(id, answer, hive_sh4re::OPERATOR_RECIPIENT)
|
.answer(id, answer, hive_sh4re::manager::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::HelperEvent::QuestionAnswered {
|
&hive_sh4re::manager::HelperEvent::QuestionAnswered {
|
||||||
id,
|
id,
|
||||||
question,
|
question,
|
||||||
answer: answer.to_owned(),
|
answer: answer.to_owned(),
|
||||||
answerer: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
|
answerer: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
state.coord.emit_question_resolved(
|
state.coord.emit_question_resolved(
|
||||||
id,
|
id,
|
||||||
answer,
|
answer,
|
||||||
hive_sh4re::OPERATOR_RECIPIENT,
|
hive_sh4re::manager::OPERATOR_RECIPIENT,
|
||||||
false,
|
false,
|
||||||
target.as_deref(),
|
target.as_deref(),
|
||||||
);
|
);
|
||||||
|
|
@ -120,25 +121,25 @@ pub(super) async fn post_cancel_question(
|
||||||
match state
|
match state
|
||||||
.coord
|
.coord
|
||||||
.questions
|
.questions
|
||||||
.answer(id, SENTINEL, hive_sh4re::OPERATOR_RECIPIENT)
|
.answer(id, SENTINEL, hive_sh4re::manager::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::OPERATOR_RECIPIENT,
|
hive_sh4re::manager::OPERATOR_RECIPIENT,
|
||||||
true,
|
true,
|
||||||
target.as_deref(),
|
target.as_deref(),
|
||||||
);
|
);
|
||||||
state.coord.notify_agent_from(
|
state.coord.notify_agent_from(
|
||||||
hive_sh4re::OPERATOR_RECIPIENT,
|
hive_sh4re::manager::OPERATOR_RECIPIENT,
|
||||||
&asker,
|
&asker,
|
||||||
&hive_sh4re::HelperEvent::QuestionAnswered {
|
&hive_sh4re::manager::HelperEvent::QuestionAnswered {
|
||||||
id,
|
id,
|
||||||
question,
|
question,
|
||||||
answer: SENTINEL.to_owned(),
|
answer: SENTINEL.to_owned(),
|
||||||
answerer: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
|
answerer: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
(StatusCode::OK, "ok").into_response()
|
(StatusCode::OK, "ok").into_response()
|
||||||
|
|
|
||||||
|
|
@ -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::SchedulePromptPayload` (the actual body) has no `ToSchema` —
|
// `hive_sh4re::manager::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::SchedulePromptPayload>,
|
axum::Json(payload): axum::Json<hive_sh4re::manager::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::OPERATOR_RECIPIENT.to_owned(),
|
owner: hive_sh4re::manager::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,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use axum::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use hive_sh4re::Approval;
|
use hive_sh4re::approvals::Approval;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio_stream::wrappers::BroadcastStream;
|
use tokio_stream::wrappers::BroadcastStream;
|
||||||
use tokio_stream::{Stream, StreamExt};
|
use tokio_stream::{Stream, StreamExt};
|
||||||
|
|
@ -561,12 +561,12 @@ fn history_view(a: Approval) -> ApprovalHistoryView {
|
||||||
Some(displayed[..displayed.len().min(12)].to_owned())
|
Some(displayed[..displayed.len().min(12)].to_owned())
|
||||||
};
|
};
|
||||||
let status = match a.status {
|
let status = match a.status {
|
||||||
hive_sh4re::ApprovalStatus::Approved => "approved",
|
hive_sh4re::approvals::ApprovalStatus::Approved => "approved",
|
||||||
hive_sh4re::ApprovalStatus::Denied => "denied",
|
hive_sh4re::approvals::ApprovalStatus::Denied => "denied",
|
||||||
hive_sh4re::ApprovalStatus::Failed => "failed",
|
hive_sh4re::approvals::ApprovalStatus::Failed => "failed",
|
||||||
hive_sh4re::ApprovalStatus::Cancelled => "cancelled",
|
hive_sh4re::approvals::ApprovalStatus::Cancelled => "cancelled",
|
||||||
// Pending shouldn't appear in recent_resolved, but be defensive.
|
// Pending shouldn't appear in recent_resolved, but be defensive.
|
||||||
hive_sh4re::ApprovalStatus::Pending => "pending",
|
hive_sh4re::approvals::ApprovalStatus::Pending => "pending",
|
||||||
};
|
};
|
||||||
let kind = a.kind.as_str();
|
let kind = a.kind.as_str();
|
||||||
ApprovalHistoryView {
|
ApprovalHistoryView {
|
||||||
|
|
@ -584,7 +584,7 @@ fn build_approval_views(approvals: Vec<Approval>) -> Vec<ApprovalView> {
|
||||||
let mut out = Vec::with_capacity(approvals.len());
|
let mut out = Vec::with_capacity(approvals.len());
|
||||||
for a in approvals {
|
for a in approvals {
|
||||||
out.push(match a.kind {
|
out.push(match a.kind {
|
||||||
hive_sh4re::ApprovalKind::Spawn => ApprovalView {
|
hive_sh4re::approvals::ApprovalKind::Spawn => ApprovalView {
|
||||||
id: a.id,
|
id: a.id,
|
||||||
agent: a.agent.to_string(),
|
agent: a.agent.to_string(),
|
||||||
kind: "spawn",
|
kind: "spawn",
|
||||||
|
|
@ -594,7 +594,7 @@ fn build_approval_views(approvals: Vec<Approval>) -> Vec<ApprovalView> {
|
||||||
commit_ref: None,
|
commit_ref: None,
|
||||||
requested_at: a.requested_at,
|
requested_at: a.requested_at,
|
||||||
},
|
},
|
||||||
hive_sh4re::ApprovalKind::InitConfig => ApprovalView {
|
hive_sh4re::approvals::ApprovalKind::InitConfig => ApprovalView {
|
||||||
id: a.id,
|
id: a.id,
|
||||||
agent: a.agent.to_string(),
|
agent: a.agent.to_string(),
|
||||||
kind: "init_config",
|
kind: "init_config",
|
||||||
|
|
@ -604,7 +604,7 @@ fn build_approval_views(approvals: Vec<Approval>) -> Vec<ApprovalView> {
|
||||||
commit_ref: None,
|
commit_ref: None,
|
||||||
requested_at: a.requested_at,
|
requested_at: a.requested_at,
|
||||||
},
|
},
|
||||||
hive_sh4re::ApprovalKind::UpdateMetaInputs => ApprovalView {
|
hive_sh4re::approvals::ApprovalKind::UpdateMetaInputs => ApprovalView {
|
||||||
id: a.id,
|
id: a.id,
|
||||||
agent: a.agent.to_string(),
|
agent: a.agent.to_string(),
|
||||||
kind: "update_meta_inputs",
|
kind: "update_meta_inputs",
|
||||||
|
|
@ -614,7 +614,7 @@ fn build_approval_views(approvals: Vec<Approval>) -> Vec<ApprovalView> {
|
||||||
commit_ref: Some(a.commit_ref),
|
commit_ref: Some(a.commit_ref),
|
||||||
requested_at: a.requested_at,
|
requested_at: a.requested_at,
|
||||||
},
|
},
|
||||||
hive_sh4re::ApprovalKind::SchedulePrompt => ApprovalView {
|
hive_sh4re::approvals::ApprovalKind::SchedulePrompt => ApprovalView {
|
||||||
id: a.id,
|
id: a.id,
|
||||||
agent: a.agent.to_string(),
|
agent: a.agent.to_string(),
|
||||||
kind: "schedule_prompt",
|
kind: "schedule_prompt",
|
||||||
|
|
@ -624,7 +624,7 @@ fn build_approval_views(approvals: Vec<Approval>) -> Vec<ApprovalView> {
|
||||||
commit_ref: Some(a.commit_ref),
|
commit_ref: Some(a.commit_ref),
|
||||||
requested_at: a.requested_at,
|
requested_at: a.requested_at,
|
||||||
},
|
},
|
||||||
hive_sh4re::ApprovalKind::MergeConfigPr => {
|
hive_sh4re::approvals::ApprovalKind::MergeConfigPr => {
|
||||||
// commit_ref = PR number; fetched_sha = the reviewed PR
|
// commit_ref = PR number; fetched_sha = the reviewed PR
|
||||||
// head. Show the head sha; the config diff surface lives
|
// head. Show the head sha; the config diff surface lives
|
||||||
// on the forge PR itself.
|
// on the forge PR itself.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
//! - **`/webhook/knowledge`** — push events on `internal/knowledge` trigger a
|
//! - **`/webhook/knowledge`** — push events on `internal/knowledge` trigger a
|
||||||
//! `git pull` on the local clone so agents see up-to-date docs.
|
//! `git pull` on the local clone so agents see up-to-date docs.
|
||||||
//! - **`/webhook/config-pr`** — `pull_request` events on any `agent-configs/*`
|
//! - **`/webhook/config-pr`** — `pull_request` events on any `agent-configs/*`
|
||||||
//! repo queue a [`hive_sh4re::ApprovalKind::MergeConfigPr`] approval row
|
//! repo queue a [`hive_sh4re::approvals::ApprovalKind::MergeConfigPr`] approval row
|
||||||
//! so the operator can review + approve the merge from the dashboard.
|
//! so the operator can review + approve the merge from the dashboard.
|
||||||
//!
|
//!
|
||||||
//! Both endpoints are reached via the gateway (HTTPS, public domain URL) so
|
//! Both endpoints are reached via the gateway (HTTPS, public domain URL) so
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ fn reconcile_stale_config_pr_approvals(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
for a in pending {
|
for a in pending {
|
||||||
if a.kind != hive_sh4re::ApprovalKind::MergeConfigPr
|
if a.kind != hive_sh4re::approvals::ApprovalKind::MergeConfigPr
|
||||||
|| !scanned_agents.contains(a.agent.as_str())
|
|| !scanned_agents.contains(a.agent.as_str())
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ async fn run_emit_rebuilt(coord: &Arc<Coordinator>, agent: &str, dag_id: Option<
|
||||||
let summary = crate::coordinator::rebuilt_todo_summary(agent, ok, note.as_deref(), None, None);
|
let summary = crate::coordinator::rebuilt_todo_summary(agent, ok, note.as_deref(), None, None);
|
||||||
let _ = coord
|
let _ = coord
|
||||||
.push_todo(
|
.push_todo(
|
||||||
hive_sh4re::MANAGER_AGENT,
|
hive_sh4re::manager::MANAGER_AGENT,
|
||||||
"core",
|
"core",
|
||||||
Some(format!("rebuilt:{agent}")),
|
Some(format!("rebuilt:{agent}")),
|
||||||
summary,
|
summary,
|
||||||
|
|
@ -429,7 +429,7 @@ async fn run_stop(coord: &Arc<Coordinator>, name: &str) -> Result<()> {
|
||||||
coord.unregister_agent(name);
|
coord.unregister_agent(name);
|
||||||
let _ = coord
|
let _ = coord
|
||||||
.push_todo(
|
.push_todo(
|
||||||
hive_sh4re::MANAGER_AGENT,
|
hive_sh4re::manager::MANAGER_AGENT,
|
||||||
"core",
|
"core",
|
||||||
Some(format!("killed:{name}")),
|
Some(format!("killed:{name}")),
|
||||||
format!("agent '{name}' killed"),
|
format!("agent '{name}' killed"),
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ pub fn handle_ask(
|
||||||
// "is this string the operator?".
|
// "is this string the operator?".
|
||||||
let target = match to {
|
let target = match to {
|
||||||
None => None,
|
None => None,
|
||||||
Some(t) if t == hive_sh4re::OPERATOR_RECIPIENT => None,
|
Some(t) if t == hive_sh4re::manager::OPERATOR_RECIPIENT => None,
|
||||||
Some("") => {
|
Some("") => {
|
||||||
return Err("ask: `to` cannot be empty (omit it for the operator path)".to_owned());
|
return Err("ask: `to` cannot be empty (omit it for the operator path)".to_owned());
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +78,7 @@ pub fn handle_ask(
|
||||||
if let Some(target_agent) = target {
|
if let Some(target_agent) = target {
|
||||||
coord.notify_agent(
|
coord.notify_agent(
|
||||||
target_agent,
|
target_agent,
|
||||||
&hive_sh4re::HelperEvent::QuestionAsked {
|
&hive_sh4re::manager::HelperEvent::QuestionAsked {
|
||||||
id,
|
id,
|
||||||
asker: asker.to_owned(),
|
asker: asker.to_owned(),
|
||||||
question: question.to_owned(),
|
question: question.to_owned(),
|
||||||
|
|
@ -125,7 +125,7 @@ pub fn handle_answer(
|
||||||
coord.notify_agent_from(
|
coord.notify_agent_from(
|
||||||
answerer,
|
answerer,
|
||||||
&asker,
|
&asker,
|
||||||
&hive_sh4re::HelperEvent::QuestionAnswered {
|
&hive_sh4re::manager::HelperEvent::QuestionAnswered {
|
||||||
id,
|
id,
|
||||||
question,
|
question,
|
||||||
answer: answer.to_owned(),
|
answer: answer.to_owned(),
|
||||||
|
|
@ -174,7 +174,7 @@ pub fn handle_cancel_loose_end(
|
||||||
coord.notify_agent_from(
|
coord.notify_agent_from(
|
||||||
canceller,
|
canceller,
|
||||||
&asker,
|
&asker,
|
||||||
&hive_sh4re::HelperEvent::QuestionAnswered {
|
&hive_sh4re::manager::HelperEvent::QuestionAnswered {
|
||||||
id,
|
id,
|
||||||
question,
|
question,
|
||||||
answer: sentinel.clone(),
|
answer: sentinel.clone(),
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
||||||
tracing::info!(%name, "request_spawn");
|
tracing::info!(%name, "request_spawn");
|
||||||
let id = coord.approvals.submit_kind(
|
let id = coord.approvals.submit_kind(
|
||||||
name.as_str(),
|
name.as_str(),
|
||||||
hive_sh4re::ApprovalKind::Spawn,
|
hive_sh4re::approvals::ApprovalKind::Spawn,
|
||||||
"",
|
"",
|
||||||
None,
|
None,
|
||||||
"operator",
|
"operator",
|
||||||
|
|
@ -298,7 +298,7 @@ async fn handle_spawn(coord: &Arc<Coordinator>, name: &str) -> Result<HostRespon
|
||||||
coord.register_agent(name)?;
|
coord.register_agent(name)?;
|
||||||
let _ = coord
|
let _ = coord
|
||||||
.push_todo(
|
.push_todo(
|
||||||
hive_sh4re::MANAGER_AGENT,
|
hive_sh4re::manager::MANAGER_AGENT,
|
||||||
"core",
|
"core",
|
||||||
Some(format!("spawned:{name}")),
|
Some(format!("spawned:{name}")),
|
||||||
format!("agent '{name}' spawned"),
|
format!("agent '{name}' spawned"),
|
||||||
|
|
@ -313,7 +313,7 @@ async fn handle_spawn(coord: &Arc<Coordinator>, name: &str) -> Result<HostRespon
|
||||||
// nothing to unregister. Notify the manager and propagate.
|
// nothing to unregister. Notify the manager and propagate.
|
||||||
let _ = coord
|
let _ = coord
|
||||||
.push_todo(
|
.push_todo(
|
||||||
hive_sh4re::MANAGER_AGENT,
|
hive_sh4re::manager::MANAGER_AGENT,
|
||||||
"core",
|
"core",
|
||||||
Some(format!("spawned:{name}")),
|
Some(format!("spawned:{name}")),
|
||||||
format!("agent '{name}' spawn FAILED: {e:#}"),
|
format!("agent '{name}' spawn FAILED: {e:#}"),
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ pub(super) fn handle_request_update_meta_inputs(
|
||||||
.approvals
|
.approvals
|
||||||
.submit_kind(
|
.submit_kind(
|
||||||
requester,
|
requester,
|
||||||
hive_sh4re::ApprovalKind::UpdateMetaInputs,
|
hive_sh4re::approvals::ApprovalKind::UpdateMetaInputs,
|
||||||
&commit_ref,
|
&commit_ref,
|
||||||
description,
|
description,
|
||||||
requester,
|
requester,
|
||||||
|
|
@ -158,7 +158,7 @@ pub(crate) async fn submit_merge_config_pr(
|
||||||
.approvals
|
.approvals
|
||||||
.submit_kind(
|
.submit_kind(
|
||||||
agent,
|
agent,
|
||||||
hive_sh4re::ApprovalKind::MergeConfigPr,
|
hive_sh4re::approvals::ApprovalKind::MergeConfigPr,
|
||||||
&pr_number.to_string(),
|
&pr_number.to_string(),
|
||||||
description,
|
description,
|
||||||
submitter,
|
submitter,
|
||||||
|
|
@ -210,7 +210,7 @@ pub(crate) fn submit_init_config(
|
||||||
.approvals
|
.approvals
|
||||||
.submit_kind(
|
.submit_kind(
|
||||||
name,
|
name,
|
||||||
hive_sh4re::ApprovalKind::InitConfig,
|
hive_sh4re::approvals::ApprovalKind::InitConfig,
|
||||||
parent.unwrap_or(""),
|
parent.unwrap_or(""),
|
||||||
description.as_deref(),
|
description.as_deref(),
|
||||||
// `parent` is the requesting agent (becomes the new child's
|
// `parent` is the requesting agent (becomes the new child's
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ async fn handle_restart_infra(
|
||||||
coord.emit_audit_entry(entry);
|
coord.emit_audit_entry(entry);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::InfraAdmin) {
|
if !crate::capabilities::has_cap(agent, hive_sh4re::permissions::Capability::InfraAdmin) {
|
||||||
tracing::warn!(%agent, %name, "agent: infra restart denied (no infra_admin capability)");
|
tracing::warn!(%agent, %name, "agent: infra restart denied (no infra_admin capability)");
|
||||||
audit(
|
audit(
|
||||||
crate::audit_log::AuditOutcome::Err,
|
crate::audit_log::AuditOutcome::Err,
|
||||||
|
|
@ -123,7 +123,7 @@ pub(super) async fn handle_kill(coord: &Arc<Coordinator>, agent: &str, name: &st
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let _ = coord
|
let _ = coord
|
||||||
.push_todo(
|
.push_todo(
|
||||||
hive_sh4re::MANAGER_AGENT,
|
hive_sh4re::manager::MANAGER_AGENT,
|
||||||
"core",
|
"core",
|
||||||
Some(format!("killed:{name}")),
|
Some(format!("killed:{name}")),
|
||||||
format!("agent '{name}' killed"),
|
format!("agent '{name}' killed"),
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use hive_core_agent_sock::{Request, Response};
|
use hive_core_agent_sock::{Request, Response};
|
||||||
use hive_sh4re::{MANAGER_AGENT, Message};
|
use hive_sh4re::Message;
|
||||||
|
use hive_sh4re::manager::MANAGER_AGENT;
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
use tokio::net::{UnixListener, UnixStream};
|
use tokio::net::{UnixListener, UnixStream};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
|
|
@ -340,7 +341,7 @@ fn handle_wake(
|
||||||
body: &str,
|
body: &str,
|
||||||
) -> hive_core_agent_sock::Response {
|
) -> hive_core_agent_sock::Response {
|
||||||
match coord.broker.send(&Message {
|
match coord.broker.send(&Message {
|
||||||
from: hive_sh4re::trusted_sender(from),
|
from: hive_sh4re::manager::trusted_sender(from),
|
||||||
to: agent.to_owned(),
|
to: agent.to_owned(),
|
||||||
body: body.to_owned(),
|
body: body.to_owned(),
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
|
|
@ -483,7 +484,7 @@ fn handle_operator_msg(
|
||||||
body: &str,
|
body: &str,
|
||||||
) -> hive_core_agent_sock::Response {
|
) -> hive_core_agent_sock::Response {
|
||||||
match coord.broker.send(&Message {
|
match coord.broker.send(&Message {
|
||||||
from: hive_sh4re::trusted_sender(hive_sh4re::OPERATOR_RECIPIENT),
|
from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::OPERATOR_RECIPIENT),
|
||||||
to: agent.to_owned(),
|
to: agent.to_owned(),
|
||||||
body: body.to_owned(),
|
body: body.to_owned(),
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
|
|
@ -756,7 +757,10 @@ fn require_new_child(agent: &str, target: &str, action: &str) -> Option<Response
|
||||||
/// gated on `QueryAgentState`.
|
/// gated on `QueryAgentState`.
|
||||||
fn handle_get_loose_ends(coord: &Arc<Coordinator>, agent: &str, target: Option<&str>) -> Response {
|
fn handle_get_loose_ends(coord: &Arc<Coordinator>, agent: &str, target: Option<&str>) -> Response {
|
||||||
let result = if target == Some("*") {
|
let result = if target == Some("*") {
|
||||||
if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::QueryAgentState) {
|
if !crate::capabilities::has_cap(
|
||||||
|
agent,
|
||||||
|
hive_sh4re::permissions::Capability::QueryAgentState,
|
||||||
|
) {
|
||||||
return Response::Err {
|
return Response::Err {
|
||||||
message: "query_agent_state capability required for hive-wide loose ends"
|
message: "query_agent_state capability required for hive-wide loose ends"
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
|
|
@ -800,7 +804,10 @@ fn resolve_agent_state_target<'a>(
|
||||||
if crate::topology::is_descendant_of(name, caller) {
|
if crate::topology::is_descendant_of(name, caller) {
|
||||||
return Ok(name);
|
return Ok(name);
|
||||||
}
|
}
|
||||||
if crate::capabilities::has_cap(caller, hive_sh4re::Capability::QueryAgentState) {
|
if crate::capabilities::has_cap(
|
||||||
|
caller,
|
||||||
|
hive_sh4re::permissions::Capability::QueryAgentState,
|
||||||
|
) {
|
||||||
Ok(name)
|
Ok(name)
|
||||||
} else {
|
} else {
|
||||||
Err(format!(
|
Err(format!(
|
||||||
|
|
@ -841,7 +848,7 @@ pub async fn dispatch_host_journal(agent: &str, args: HostJournalArgs<'_>) -> Re
|
||||||
since,
|
since,
|
||||||
until,
|
until,
|
||||||
} = args;
|
} = args;
|
||||||
if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::ReadHostJournal) {
|
if !crate::capabilities::has_cap(agent, hive_sh4re::permissions::Capability::ReadHostJournal) {
|
||||||
return Response::Err {
|
return Response::Err {
|
||||||
message: "agent does not have the read_host_journal capability".to_owned(),
|
message: "agent does not have the read_host_journal capability".to_owned(),
|
||||||
};
|
};
|
||||||
|
|
@ -938,7 +945,7 @@ pub(crate) fn fan_out_send(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Err(e) = coord.broker.send(&Message {
|
if let Err(e) = coord.broker.send(&Message {
|
||||||
from: hive_sh4re::trusted_sender(from),
|
from: hive_sh4re::manager::trusted_sender(from),
|
||||||
to: target.clone(),
|
to: target.clone(),
|
||||||
body: body.to_owned(),
|
body: body.to_owned(),
|
||||||
in_reply_to,
|
in_reply_to,
|
||||||
|
|
@ -977,7 +984,7 @@ pub(crate) fn handle_send(
|
||||||
// topology.json. Bypasses the allow-list check — structural fan-out
|
// topology.json. Bypasses the allow-list check — structural fan-out
|
||||||
// targets are never user-listed peers. No-op (returns Ok) for leaf
|
// targets are never user-listed peers. No-op (returns Ok) for leaf
|
||||||
// agents that have no children.
|
// agents that have no children.
|
||||||
if to == hive_sh4re::CHILDREN_RECIPIENT {
|
if to == hive_sh4re::manager::CHILDREN_RECIPIENT {
|
||||||
let children = crate::topology::children_of(agent);
|
let children = crate::topology::children_of(agent);
|
||||||
let errors = fan_out_send(coord, agent, body, in_reply_to, &children);
|
let errors = fan_out_send(coord, agent, body, in_reply_to, &children);
|
||||||
return if errors.is_empty() {
|
return if errors.is_empty() {
|
||||||
|
|
@ -1007,7 +1014,7 @@ pub(crate) fn handle_send(
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if resolved != hive_sh4re::OPERATOR_RECIPIENT {
|
if resolved != hive_sh4re::manager::OPERATOR_RECIPIENT {
|
||||||
// A name that doesn't parse as an Ident can't be a local agent, so
|
// A name that doesn't parse as an Ident can't be a local agent, so
|
||||||
// it collapses into the same "unknown recipient" error as a valid
|
// it collapses into the same "unknown recipient" error as a valid
|
||||||
// name with no state dir.
|
// name with no state dir.
|
||||||
|
|
@ -1023,7 +1030,7 @@ pub(crate) fn handle_send(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match coord.broker.send(&Message {
|
match coord.broker.send(&Message {
|
||||||
from: hive_sh4re::trusted_sender(agent),
|
from: hive_sh4re::manager::trusted_sender(agent),
|
||||||
to: resolved,
|
to: resolved,
|
||||||
body: body.to_owned(),
|
body: body.to_owned(),
|
||||||
in_reply_to,
|
in_reply_to,
|
||||||
|
|
@ -1086,12 +1093,12 @@ pub fn spawn_question_watchdog(coord: &Arc<Coordinator>, id: i64, ttl_secs: u64)
|
||||||
if let Ok((question, asker, target)) =
|
if let Ok((question, asker, target)) =
|
||||||
coord
|
coord
|
||||||
.questions
|
.questions
|
||||||
.answer(id, TTL_SENTINEL, hive_sh4re::OPERATOR_RECIPIENT)
|
.answer(id, TTL_SENTINEL, hive_sh4re::manager::OPERATOR_RECIPIENT)
|
||||||
{
|
{
|
||||||
tracing::info!(%id, %asker, "question expired (ttl)");
|
tracing::info!(%id, %asker, "question expired (ttl)");
|
||||||
coord.notify_agent(
|
coord.notify_agent(
|
||||||
&asker,
|
&asker,
|
||||||
&hive_sh4re::HelperEvent::QuestionAnswered {
|
&hive_sh4re::manager::HelperEvent::QuestionAnswered {
|
||||||
id,
|
id,
|
||||||
question,
|
question,
|
||||||
answer: TTL_SENTINEL.to_owned(),
|
answer: TTL_SENTINEL.to_owned(),
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ pub(super) fn handle_list_schedules(coord: &Arc<Coordinator>) -> Response {
|
||||||
pub(super) fn handle_request_schedule_prompt(
|
pub(super) fn handle_request_schedule_prompt(
|
||||||
coord: &Arc<Coordinator>,
|
coord: &Arc<Coordinator>,
|
||||||
requester: &str,
|
requester: &str,
|
||||||
payload: &hive_sh4re::SchedulePromptPayload,
|
payload: &hive_sh4re::manager::SchedulePromptPayload,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if payload.targets.is_empty() {
|
if payload.targets.is_empty() {
|
||||||
return Response::Err {
|
return Response::Err {
|
||||||
|
|
@ -58,7 +58,7 @@ pub(super) fn handle_request_schedule_prompt(
|
||||||
};
|
};
|
||||||
let id = match coord.approvals.submit_kind(
|
let id = match coord.approvals.submit_kind(
|
||||||
requester,
|
requester,
|
||||||
hive_sh4re::ApprovalKind::SchedulePrompt,
|
hive_sh4re::approvals::ApprovalKind::SchedulePrompt,
|
||||||
&commit_ref,
|
&commit_ref,
|
||||||
payload.description.as_deref(),
|
payload.description.as_deref(),
|
||||||
requester,
|
requester,
|
||||||
|
|
@ -275,7 +275,7 @@ fn cancel_authorized(requester: &str, owner: &str) -> bool {
|
||||||
if requester == owner {
|
if requester == owner {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if requester == hive_sh4re::OPERATOR_RECIPIENT {
|
if requester == hive_sh4re::manager::OPERATOR_RECIPIENT {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Manager can cancel anything owned by an agent in its subtree.
|
// Manager can cancel anything owned by an agent in its subtree.
|
||||||
|
|
@ -310,8 +310,9 @@ pub(crate) fn filter_ghost_schedule_targets(
|
||||||
live: &std::collections::HashSet<String>,
|
live: &std::collections::HashSet<String>,
|
||||||
) {
|
) {
|
||||||
for s in schedules.iter_mut() {
|
for s in schedules.iter_mut() {
|
||||||
s.targets
|
s.targets.retain(|t| {
|
||||||
.retain(|t| t.target == hive_sh4re::OPERATOR_RECIPIENT || live.contains(&t.target));
|
t.target == hive_sh4re::manager::OPERATOR_RECIPIENT || live.contains(&t.target)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use std::sync::Mutex;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use hive_sh4re::{Approval, ApprovalKind, ApprovalStatus};
|
use hive_sh4re::approvals::{Approval, ApprovalKind, ApprovalStatus};
|
||||||
use rusqlite::{Connection, OptionalExtension, params};
|
use rusqlite::{Connection, OptionalExtension, params};
|
||||||
|
|
||||||
use crate::db::Migration;
|
use crate::db::Migration;
|
||||||
|
|
@ -445,7 +445,7 @@ fn kind_from_str(s: &str) -> Result<ApprovalKind> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use hive_sh4re::ApprovalKind;
|
use hive_sh4re::approvals::ApprovalKind;
|
||||||
|
|
||||||
fn open_temp() -> (tempfile::TempDir, std::path::PathBuf, Approvals) {
|
fn open_temp() -> (tempfile::TempDir, std::path::PathBuf, Approvals) {
|
||||||
let dir = tempfile::tempdir().expect("tempdir");
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,8 @@ impl Broker {
|
||||||
// Operator messages get elevated priority so they surface before
|
// Operator messages get elevated priority so they surface before
|
||||||
// queued wakes (bash completions, forge events, etc.) when the
|
// queued wakes (bash completions, forge events, etc.) when the
|
||||||
// harness pops the next turn driver. All other senders stay at 0.
|
// harness pops the next turn driver. All other senders stay at 0.
|
||||||
let priority: i64 = i64::from(message.from.as_str() == hive_sh4re::OPERATOR_RECIPIENT);
|
let priority: i64 =
|
||||||
|
i64::from(message.from.as_str() == hive_sh4re::manager::OPERATOR_RECIPIENT);
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO messages (sender, recipient, body, sent_at, in_reply_to, priority) \
|
"INSERT INTO messages (sender, recipient, body, sent_at, in_reply_to, priority) \
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||||
|
|
@ -382,7 +383,11 @@ impl Broker {
|
||||||
AND delivered_at IS NULL
|
AND delivered_at IS NULL
|
||||||
AND acked_at IS NULL
|
AND acked_at IS NULL
|
||||||
LIMIT 1",
|
LIMIT 1",
|
||||||
params![child, hive_sh4re::SYSTEM_SENDER, format!("{PREFIX}%")],
|
params![
|
||||||
|
child,
|
||||||
|
hive_sh4re::manager::SYSTEM_SENDER,
|
||||||
|
format!("{PREFIX}%")
|
||||||
|
],
|
||||||
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
|
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
|
||||||
)
|
)
|
||||||
.optional()?;
|
.optional()?;
|
||||||
|
|
@ -400,7 +405,7 @@ impl Broker {
|
||||||
drop(conn);
|
drop(conn);
|
||||||
let _ = self.events.send(MessageEvent::Sent {
|
let _ = self.events.send(MessageEvent::Sent {
|
||||||
id: row_id,
|
id: row_id,
|
||||||
from: hive_sh4re::SYSTEM_SENDER.to_owned(),
|
from: hive_sh4re::manager::SYSTEM_SENDER.to_owned(),
|
||||||
to: child.to_owned(),
|
to: child.to_owned(),
|
||||||
body: new_body,
|
body: new_body,
|
||||||
at: now,
|
at: now,
|
||||||
|
|
@ -411,13 +416,13 @@ impl Broker {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO messages (sender, recipient, body, sent_at, in_reply_to)
|
"INSERT INTO messages (sender, recipient, body, sent_at, in_reply_to)
|
||||||
VALUES (?1, ?2, ?3, ?4, NULL)",
|
VALUES (?1, ?2, ?3, ?4, NULL)",
|
||||||
params![hive_sh4re::SYSTEM_SENDER, child, body, now],
|
params![hive_sh4re::manager::SYSTEM_SENDER, child, body, now],
|
||||||
)?;
|
)?;
|
||||||
let row_id = conn.last_insert_rowid();
|
let row_id = conn.last_insert_rowid();
|
||||||
drop(conn);
|
drop(conn);
|
||||||
let _ = self.events.send(MessageEvent::Sent {
|
let _ = self.events.send(MessageEvent::Sent {
|
||||||
id: row_id,
|
id: row_id,
|
||||||
from: hive_sh4re::SYSTEM_SENDER.to_owned(),
|
from: hive_sh4re::manager::SYSTEM_SENDER.to_owned(),
|
||||||
to: child.to_owned(),
|
to: child.to_owned(),
|
||||||
body,
|
body,
|
||||||
at: now,
|
at: now,
|
||||||
|
|
@ -583,7 +588,7 @@ impl Broker {
|
||||||
id,
|
id,
|
||||||
redelivered,
|
redelivered,
|
||||||
message: Message {
|
message: Message {
|
||||||
from: hive_sh4re::trusted_sender(&from),
|
from: hive_sh4re::manager::trusted_sender(&from),
|
||||||
to,
|
to,
|
||||||
body,
|
body,
|
||||||
in_reply_to,
|
in_reply_to,
|
||||||
|
|
|
||||||
|
|
@ -165,13 +165,15 @@ impl OperatorQuestions {
|
||||||
// can additionally override agent-to-agent questions to close
|
// can additionally override agent-to-agent questions to close
|
||||||
// stuck threads).
|
// stuck threads).
|
||||||
let authorised = match target.as_deref() {
|
let authorised = match target.as_deref() {
|
||||||
None => answerer == hive_sh4re::OPERATOR_RECIPIENT,
|
None => answerer == hive_sh4re::manager::OPERATOR_RECIPIENT,
|
||||||
Some(t) => answerer == t || answerer == hive_sh4re::OPERATOR_RECIPIENT,
|
Some(t) => answerer == t || answerer == hive_sh4re::manager::OPERATOR_RECIPIENT,
|
||||||
};
|
};
|
||||||
if !authorised {
|
if !authorised {
|
||||||
bail!(
|
bail!(
|
||||||
"question {id} not addressed to '{answerer}' (target = {:?})",
|
"question {id} not addressed to '{answerer}' (target = {:?})",
|
||||||
target.as_deref().unwrap_or(hive_sh4re::OPERATOR_RECIPIENT)
|
target
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(hive_sh4re::manager::OPERATOR_RECIPIENT)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|
@ -216,8 +218,9 @@ impl OperatorQuestions {
|
||||||
if answered_at.is_some() {
|
if answered_at.is_some() {
|
||||||
bail!("question {id} already answered/cancelled");
|
bail!("question {id} already answered/cancelled");
|
||||||
}
|
}
|
||||||
let authorised =
|
let authorised = privileged
|
||||||
privileged || canceller == asker || canceller == hive_sh4re::OPERATOR_RECIPIENT;
|
|| canceller == asker
|
||||||
|
|| canceller == hive_sh4re::manager::OPERATOR_RECIPIENT;
|
||||||
if !authorised {
|
if !authorised {
|
||||||
bail!("question {id}: '{canceller}' not allowed to cancel (asker = '{asker}')");
|
bail!("question {id}: '{canceller}' not allowed to cancel (asker = '{asker}')");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ fn seed_manager_tool_groups() {
|
||||||
tracing::debug!("manager tool groups already set — leaving as-is");
|
tracing::debug!("manager tool groups already set — leaving as-is");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let all_groups: Vec<String> = hive_sh4re::ToolGroup::MANAGER_DEFAULT
|
let all_groups: Vec<String> = hive_sh4re::permissions::ToolGroup::MANAGER_DEFAULT
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| g.as_str().to_owned())
|
.map(|g| g.as_str().to_owned())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ fn emit_crash_transitions(coord: &Coordinator, prev: &HashSet<String>, current:
|
||||||
}
|
}
|
||||||
tracing::warn!(agent = %stopped, "container crash detected");
|
tracing::warn!(agent = %stopped, "container crash detected");
|
||||||
coord.record_crash(stopped);
|
coord.record_crash(stopped);
|
||||||
coord.notify_manager(&hive_sh4re::HelperEvent::ContainerCrash {
|
coord.notify_manager(&hive_sh4re::manager::HelperEvent::ContainerCrash {
|
||||||
agent: stopped.clone(),
|
agent: stopped.clone(),
|
||||||
note: Some("container stopped without an operator action".into()),
|
note: Some("container stopped without an operator action".into()),
|
||||||
});
|
});
|
||||||
|
|
@ -138,7 +138,7 @@ async fn emit_login_transitions(
|
||||||
tracing::info!(%agent, "agent logged in");
|
tracing::info!(%agent, "agent logged in");
|
||||||
let _ = coord
|
let _ = coord
|
||||||
.push_todo(
|
.push_todo(
|
||||||
hive_sh4re::MANAGER_AGENT,
|
hive_sh4re::manager::MANAGER_AGENT,
|
||||||
"core",
|
"core",
|
||||||
Some(format!("logged_in:{agent}")),
|
Some(format!("logged_in:{agent}")),
|
||||||
format!("agent '{agent}' logged in"),
|
format!("agent '{agent}' logged in"),
|
||||||
|
|
@ -169,7 +169,7 @@ async fn emit_login_transitions(
|
||||||
tracing::info!(%agent, "agent needs login");
|
tracing::info!(%agent, "agent needs login");
|
||||||
let _ = coord
|
let _ = coord
|
||||||
.push_todo(
|
.push_todo(
|
||||||
hive_sh4re::MANAGER_AGENT,
|
hive_sh4re::manager::MANAGER_AGENT,
|
||||||
"core",
|
"core",
|
||||||
Some(format!("needs_login:{agent}")),
|
Some(format!("needs_login:{agent}")),
|
||||||
format!("agent '{agent}' needs login"),
|
format!("agent '{agent}' needs login"),
|
||||||
|
|
|
||||||
|
|
@ -344,7 +344,7 @@ async fn broadcast_change(coord: &Coordinator, before: &str, after: &str) {
|
||||||
} else {
|
} else {
|
||||||
format!("[system] /knowledge updated:\n{stat}")
|
format!("[system] /knowledge updated:\n{stat}")
|
||||||
};
|
};
|
||||||
let errors = coord.broadcast_send(hive_sh4re::SYSTEM_SENDER, &body);
|
let errors = coord.broadcast_send(hive_sh4re::manager::SYSTEM_SENDER, &body);
|
||||||
if !errors.is_empty() {
|
if !errors.is_empty() {
|
||||||
tracing::warn!(?errors, "knowledge: broadcast had per-agent failures");
|
tracing::warn!(?errors, "knowledge: broadcast had per-agent failures");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ async fn fire_schedule(coord: &Arc<Coordinator>, schedule: &Schedule, now: i64)
|
||||||
// no in-container todo inbox, so it keeps the regular broker
|
// no in-container todo inbox, so it keeps the regular broker
|
||||||
// `Message` path; the dashboard mirrors `to == operator` into
|
// `Message` path; the dashboard mirrors `to == operator` into
|
||||||
// its own pane.
|
// its own pane.
|
||||||
if target != hive_sh4re::OPERATOR_RECIPIENT && !known.contains(target) {
|
if target != hive_sh4re::manager::OPERATOR_RECIPIENT && !known.contains(target) {
|
||||||
let reason = format!("no such agent: {target}");
|
let reason = format!("no such agent: {target}");
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
coord
|
coord
|
||||||
|
|
@ -169,9 +169,9 @@ async fn deliver_to_target(
|
||||||
target: &str,
|
target: &str,
|
||||||
body: &str,
|
body: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if target == hive_sh4re::OPERATOR_RECIPIENT {
|
if target == hive_sh4re::manager::OPERATOR_RECIPIENT {
|
||||||
let msg = Message {
|
let msg = Message {
|
||||||
from: hive_sh4re::trusted_sender("scheduled"),
|
from: hive_sh4re::manager::trusted_sender("scheduled"),
|
||||||
to: target.to_owned(),
|
to: target.to_owned(),
|
||||||
body: body.to_owned(),
|
body: body.to_owned(),
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
|
|
@ -205,8 +205,8 @@ fn notify_operator_missing_target(coord: &Coordinator, schedule: &Schedule, targ
|
||||||
body = schedule.body
|
body = schedule.body
|
||||||
);
|
);
|
||||||
let msg = Message {
|
let msg = Message {
|
||||||
from: hive_sh4re::trusted_sender("scheduled"),
|
from: hive_sh4re::manager::trusted_sender("scheduled"),
|
||||||
to: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
|
to: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(),
|
||||||
body,
|
body,
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
};
|
};
|
||||||
|
|
@ -291,7 +291,7 @@ pub async fn fire_now(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let target = &target_row.target;
|
let target = &target_row.target;
|
||||||
if target != hive_sh4re::OPERATOR_RECIPIENT && !known.contains(target) {
|
if target != hive_sh4re::manager::OPERATOR_RECIPIENT && !known.contains(target) {
|
||||||
let reason = format!("manual fire: no such agent: {target}");
|
let reason = format!("manual fire: no such agent: {target}");
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
coord
|
coord
|
||||||
|
|
@ -371,7 +371,7 @@ pub async fn fire_now(
|
||||||
async fn known_agents() -> std::collections::HashSet<String> {
|
async fn known_agents() -> std::collections::HashSet<String> {
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
let mut out: HashSet<String> = HashSet::new();
|
let mut out: HashSet<String> = HashSet::new();
|
||||||
out.insert(hive_sh4re::MANAGER_AGENT.to_owned());
|
out.insert(hive_sh4re::manager::MANAGER_AGENT.to_owned());
|
||||||
match crate::lifecycle::list().await {
|
match crate::lifecycle::list().await {
|
||||||
Ok(list) => {
|
Ok(list) => {
|
||||||
for raw in list {
|
for raw in list {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,10 @@
|
||||||
//! shared payload types it references (`Message`, `LooseEnd`, `Approval`, …)
|
//! shared payload types it references (`Message`, `LooseEnd`, `Approval`, …)
|
||||||
//! stay in `hive-sh4re`, which this crate depends on.
|
//! stay in `hive-sh4re`, which this crate depends on.
|
||||||
|
|
||||||
|
use hive_sh4re::manager::SchedulePromptPayload;
|
||||||
use hive_sh4re::{
|
use hive_sh4re::{
|
||||||
CancelLooseEndKind, ContainerInfo, DeliveredMessage, InboxRow, JournalPriority, LooseEnd,
|
CancelLooseEndKind, ContainerInfo, DeliveredMessage, InboxRow, JournalPriority, LooseEnd,
|
||||||
MatrixIdentity, SchedulePromptPayload, WireSchedule,
|
MatrixIdentity, WireSchedule,
|
||||||
};
|
};
|
||||||
use hive_types::Ident;
|
use hive_types::Ident;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use hive_sh4re::{AgentStatusRow, Approval};
|
use hive_sh4re::AgentStatusRow;
|
||||||
|
use hive_sh4re::approvals::Approval;
|
||||||
use hive_types::Ident;
|
use hive_types::Ident;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
|
||||||
111
hive-sh4re/src/approvals.rs
Normal file
111
hive-sh4re/src/approvals.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
//! 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/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,
|
||||||
|
#[serde(default)]
|
||||||
|
pub kind: ApprovalKind,
|
||||||
|
/// Kind-specific payload (git sha / inputs array / schedule
|
||||||
|
/// payload / empty). See the Approval struct doc.
|
||||||
|
pub commit_ref: String,
|
||||||
|
/// The canonical hive-c0re-vouched sha. For `MergeConfigPr`: the
|
||||||
|
/// reviewed PR head pinned at submit; if the PR head drifts off it
|
||||||
|
/// before merge, hive-c0re cancels the stale approval and re-queues a
|
||||||
|
/// fresh one for re-review.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub fetched_sha: Option<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/approvals.md::Approval kinds (wire shapes)`.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ApprovalKind {
|
||||||
|
/// Create + start a new sub-agent container with the given name
|
||||||
|
/// (under the default `agent.nix` template).
|
||||||
|
Spawn,
|
||||||
|
/// Create an agent's config repo and seed it from the default
|
||||||
|
/// template (step 1 of the two-step spawn flow). Creating it is the
|
||||||
|
/// whole of this step — tailoring what the template seeded is not a
|
||||||
|
/// separate mechanism, it's a `MergeConfigPr` like every later
|
||||||
|
/// change.
|
||||||
|
InitConfig,
|
||||||
|
/// Run `nix flake update [inputs...]` on the meta flake and commit
|
||||||
|
/// the resulting lock changes.
|
||||||
|
UpdateMetaInputs,
|
||||||
|
/// Add a scheduled prompt to the broker queue.
|
||||||
|
SchedulePrompt,
|
||||||
|
/// Merge an operator-reviewed config PR: hive-c0re verifies the
|
||||||
|
/// reviewed PR head, fast-forwards the forge config repo's `main`
|
||||||
|
/// to it, marks the PR merged, then runs the deploy tail. This is the
|
||||||
|
/// sole config-change flow — a manager opens a PR on its
|
||||||
|
/// `agent-configs/<agent>` repo and the operator reviews + approves it.
|
||||||
|
/// `commit_ref` = PR number; `fetched_sha` = the reviewed PR head
|
||||||
|
/// pinned at submit. See `docs/approvals.md`.
|
||||||
|
#[default]
|
||||||
|
MergeConfigPr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApprovalKind {
|
||||||
|
/// Wire/UI string — the same value serde's `snake_case` rename
|
||||||
|
/// produces. The single source of truth for every place that needs
|
||||||
|
/// the kind as a `&'static str` (sqlite storage, dashboard events),
|
||||||
|
/// so adding a variant can't silently miss a hand-rolled match.
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ApprovalKind::Spawn => "spawn",
|
||||||
|
ApprovalKind::InitConfig => "init_config",
|
||||||
|
ApprovalKind::UpdateMetaInputs => "update_meta_inputs",
|
||||||
|
ApprovalKind::SchedulePrompt => "schedule_prompt",
|
||||||
|
ApprovalKind::MergeConfigPr => "merge_config_pr",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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/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,
|
||||||
|
}
|
||||||
|
|
@ -4,9 +4,12 @@ use chrono::{DateTime, Utc};
|
||||||
use hive_types::Ident;
|
use hive_types::Ident;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub mod approvals;
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
pub mod bash_task;
|
pub mod bash_task;
|
||||||
|
pub mod manager;
|
||||||
pub mod paths;
|
pub mod paths;
|
||||||
|
pub mod permissions;
|
||||||
pub mod wire_time;
|
pub mod wire_time;
|
||||||
|
|
||||||
/// Server-side hard cap on `Recv.max` (see the `Recv` request). Bounds
|
/// Server-side hard cap on `Recv.max` (see the `Recv` request). Bounds
|
||||||
|
|
@ -56,109 +59,6 @@ pub fn pending_hint(remaining: u64) -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One row in the approval queue. `commit_ref` is overloaded per
|
|
||||||
/// `kind` — see `docs/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,
|
|
||||||
#[serde(default)]
|
|
||||||
pub kind: ApprovalKind,
|
|
||||||
/// Kind-specific payload (git sha / inputs array / schedule
|
|
||||||
/// payload / empty). See the Approval struct doc.
|
|
||||||
pub commit_ref: String,
|
|
||||||
/// The canonical hive-c0re-vouched sha. For `MergeConfigPr`: the
|
|
||||||
/// reviewed PR head pinned at submit; if the PR head drifts off it
|
|
||||||
/// before merge, hive-c0re cancels the stale approval and re-queues a
|
|
||||||
/// fresh one for re-review.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub fetched_sha: Option<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/approvals.md::Approval kinds (wire shapes)`.
|
|
||||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum ApprovalKind {
|
|
||||||
/// Create + start a new sub-agent container with the given name
|
|
||||||
/// (under the default `agent.nix` template).
|
|
||||||
Spawn,
|
|
||||||
/// Create an agent's config repo and seed it from the default
|
|
||||||
/// template (step 1 of the two-step spawn flow). Creating it is the
|
|
||||||
/// whole of this step — tailoring what the template seeded is not a
|
|
||||||
/// separate mechanism, it's a `MergeConfigPr` like every later
|
|
||||||
/// change.
|
|
||||||
InitConfig,
|
|
||||||
/// Run `nix flake update [inputs...]` on the meta flake and commit
|
|
||||||
/// the resulting lock changes.
|
|
||||||
UpdateMetaInputs,
|
|
||||||
/// Add a scheduled prompt to the broker queue.
|
|
||||||
SchedulePrompt,
|
|
||||||
/// Merge an operator-reviewed config PR: hive-c0re verifies the
|
|
||||||
/// reviewed PR head, fast-forwards the forge config repo's `main`
|
|
||||||
/// to it, marks the PR merged, then runs the deploy tail. This is the
|
|
||||||
/// sole config-change flow — a manager opens a PR on its
|
|
||||||
/// `agent-configs/<agent>` repo and the operator reviews + approves it.
|
|
||||||
/// `commit_ref` = PR number; `fetched_sha` = the reviewed PR head
|
|
||||||
/// pinned at submit. See `docs/approvals.md`.
|
|
||||||
#[default]
|
|
||||||
MergeConfigPr,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApprovalKind {
|
|
||||||
/// Wire/UI string — the same value serde's `snake_case` rename
|
|
||||||
/// produces. The single source of truth for every place that needs
|
|
||||||
/// the kind as a `&'static str` (sqlite storage, dashboard events),
|
|
||||||
/// so adding a variant can't silently miss a hand-rolled match.
|
|
||||||
#[must_use]
|
|
||||||
pub fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
ApprovalKind::Spawn => "spawn",
|
|
||||||
ApprovalKind::InitConfig => "init_config",
|
|
||||||
ApprovalKind::UpdateMetaInputs => "update_meta_inputs",
|
|
||||||
ApprovalKind::SchedulePrompt => "schedule_prompt",
|
|
||||||
ApprovalKind::MergeConfigPr => "merge_config_pr",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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/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,
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Per-agent socket — /run/hyperhive/agents/<name>/mcp.sock on the host,
|
// Per-agent socket — /run/hyperhive/agents/<name>/mcp.sock on the host,
|
||||||
// bind-mounted into the container at /run/hive/mcp.sock.
|
// bind-mounted into the container at /run/hive/mcp.sock.
|
||||||
|
|
@ -391,319 +291,6 @@ pub struct MatrixIdentity {
|
||||||
pub homeserver: String,
|
pub homeserver: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// Manager socket — /run/hyperhive/manager/mcp.sock on the host, bind-mounted
|
|
||||||
// into the manager container at /run/hive/mcp.sock.
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Logical name the broker uses for the manager.
|
|
||||||
pub const MANAGER_AGENT: &str = "ruth";
|
|
||||||
|
|
||||||
/// Logical name the broker uses for the human operator. Messages with
|
|
||||||
/// `to = OPERATOR_RECIPIENT` accumulate in sqlite and surface on the
|
|
||||||
/// dashboard's inbox view — they are never `recv`'d by an agent harness.
|
|
||||||
pub const OPERATOR_RECIPIENT: &str = "operator";
|
|
||||||
|
|
||||||
/// Reserved magic recipient — `send(to: "<parent>", ...)` is rewritten
|
|
||||||
/// by hive-c0re at delivery time to whoever `topology::parent_of(sender)`
|
|
||||||
/// returns, or to [`OPERATOR_RECIPIENT`] when the sender is a root agent
|
|
||||||
/// (no parent). Lets agents address their parent without hardcoding the
|
|
||||||
/// label, so runtime reparenting requires no agent-side restart. The
|
|
||||||
/// angle brackets are not valid in agent names (validators reject
|
|
||||||
/// `<`/`>`), so this name can never collide with a real recipient.
|
|
||||||
pub const PARENT_RECIPIENT: &str = "<parent>";
|
|
||||||
|
|
||||||
/// Reserved magic recipient — `send(to: "<children>", ...)` fans out to
|
|
||||||
/// every agent whose direct parent (per `topology.json`) is the sender.
|
|
||||||
/// Lets a sub-manager nudge its subtree without enumerating labels at
|
|
||||||
/// call-time; topology changes propagate for free. The angle brackets
|
|
||||||
/// are structurally safe — agent name validation rejects `<`/`>`.
|
|
||||||
/// Delivers to an empty set (no-op) for leaf agents that have no children.
|
|
||||||
pub const CHILDREN_RECIPIENT: &str = "<children>";
|
|
||||||
|
|
||||||
/// Sender hive-c0re uses for events it pushes into the manager's inbox.
|
|
||||||
/// Manager harness recognises this and parses the body as a `HelperEvent`.
|
|
||||||
pub const SYSTEM_SENDER: &str = "system";
|
|
||||||
|
|
||||||
/// Parse `s` as a [`Ident`] for use as `Message.from`, falling back to
|
|
||||||
/// [`SYSTEM_SENDER`] on the (should-be-unreachable) case that `s` isn't
|
|
||||||
/// ident-shaped. `Message.from` is always either a fixed sentinel literal
|
|
||||||
/// (`SYSTEM_SENDER`, `OPERATOR_RECIPIENT`, `"scheduled"`, …) or an
|
|
||||||
/// already-registered agent's own name reaching this point through
|
|
||||||
/// hive-c0re's internal dispatch — never arbitrary external input — so
|
|
||||||
/// this is a defensive fallback for a programming-bug case, not a
|
|
||||||
/// validation gate.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// Never, unless [`SYSTEM_SENDER`] itself stops being ident-shaped (which
|
|
||||||
/// would also be a programming bug, caught by `hive-types`' own tests).
|
|
||||||
#[must_use]
|
|
||||||
pub fn trusted_sender(s: &str) -> Ident {
|
|
||||||
Ident::parse(s)
|
|
||||||
.unwrap_or_else(|_| Ident::parse(SYSTEM_SENDER).expect("SYSTEM_SENDER is a valid 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/approvals.md::Helper events to the manager`.
|
|
||||||
#[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>,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
sha: Option<String>,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
tag: 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).
|
|
||||||
ContainerCrash {
|
|
||||||
agent: String,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
note: Option<String>,
|
|
||||||
},
|
|
||||||
/// A question queued via `Ask` was answered. `id` matches the
|
|
||||||
/// originating `QuestionQueued.id`; `answerer` is `"operator"` /
|
|
||||||
/// a peer agent name / `"ttl-watchdog"` on expiry.
|
|
||||||
QuestionAnswered {
|
|
||||||
id: i64,
|
|
||||||
question: String,
|
|
||||||
answer: String,
|
|
||||||
answerer: String,
|
|
||||||
},
|
|
||||||
/// A peer (or the manager) asked this agent a question. Recipient
|
|
||||||
/// replies via `Answer { id, answer }`; the answer routes back to
|
|
||||||
/// the asker as `QuestionAnswered`.
|
|
||||||
QuestionAsked {
|
|
||||||
id: i64,
|
|
||||||
asker: String,
|
|
||||||
question: String,
|
|
||||||
#[serde(default)]
|
|
||||||
options: Vec<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
multi: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Submission payload for `RequestSchedulePrompt`. Lives outside the
|
|
||||||
/// enum so it can also serialize into the approval row's `commit_ref`
|
|
||||||
/// (the dispatcher re-parses it on approve and inserts the schedule).
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct SchedulePromptPayload {
|
|
||||||
/// Names of recipient agents. Operator + `root` allowed.
|
|
||||||
pub targets: Vec<String>,
|
|
||||||
/// Message body delivered to each target's inbox at fire time.
|
|
||||||
/// Same size budget as `Send.body` — soft cap at the broker level.
|
|
||||||
pub body: String,
|
|
||||||
/// Absolute unix timestamp (seconds) for the FIRST fire. For
|
|
||||||
/// recurring schedules the worker then re-arms in
|
|
||||||
/// `interval_seconds` steps.
|
|
||||||
pub first_fire_at_unix: i64,
|
|
||||||
/// `None` = one-shot. `Some(n > 0)` = recurring every `n` seconds.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub interval_seconds: Option<u64>,
|
|
||||||
/// Optional description shown on the dashboard approval card AND
|
|
||||||
/// stored on the resulting schedule row for the operator's
|
|
||||||
/// "what is this?" reference later.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Named group of MCP tools an agent may be granted. The harness reads
|
|
||||||
/// `HIVE_TOOL_GROUPS` from the environment (a comma-separated list of
|
|
||||||
/// `snake_case` group names written by the meta renderer from per-agent
|
|
||||||
/// config) and expands it to the matching tool names for `--allowedTools`.
|
|
||||||
/// When the env var is absent the harness falls back to `AGENT_DEFAULT`.
|
|
||||||
/// See `docs/conventions.md::Tool groups`.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum ToolGroup {
|
|
||||||
/// `send`, `recv`, `ask`, `answer`
|
|
||||||
Messaging,
|
|
||||||
/// `get_agent_meta` (`set_status` is always-on — see `ALWAYS_ON_TOOLS`)
|
|
||||||
Meta,
|
|
||||||
/// `get_loose_ends`, `cancel_loose_end`, `remind`
|
|
||||||
Inbox,
|
|
||||||
/// `kill`, `start`, `restart`, `update` - *(privileged)*
|
|
||||||
Lifecycle,
|
|
||||||
/// `request_init_config`, `request_update_meta_inputs` - *(privileged)*
|
|
||||||
Approvals,
|
|
||||||
/// `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`,
|
|
||||||
/// `edit_schedule`, `list_schedules` - *(privileged)*
|
|
||||||
Scheduling,
|
|
||||||
/// `get_logs` - *(privileged)*
|
|
||||||
Diagnostics,
|
|
||||||
/// `create_repo` — create git repos through hive-c0re (the only path
|
|
||||||
/// now that agents can't create them directly). Opt-in per
|
|
||||||
/// agent so the operator controls who can spin up repos.
|
|
||||||
Forge,
|
|
||||||
/// `run`, `status` (via `mcp__bash__*`)
|
|
||||||
Execution,
|
|
||||||
/// Claude built-in web egress tools: `WebFetch` (retrieve a URL) and
|
|
||||||
/// `WebSearch` (search the web). Both are omitted from `--tools` by
|
|
||||||
/// default; adding this group to an agent enables them in the session
|
|
||||||
/// and in `--allowedTools` so they run without a confirmation prompt.
|
|
||||||
/// Does not gate any MCP tools — `tools()` returns `&[]`.
|
|
||||||
WebTools,
|
|
||||||
}
|
|
||||||
|
|
||||||
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()`.
|
|
||||||
#[must_use]
|
|
||||||
pub fn tools(self) -> &'static [&'static str] {
|
|
||||||
match self {
|
|
||||||
Self::Messaging => &["send", "recv", "ack_until", "ask", "answer"],
|
|
||||||
Self::Meta => &["get_agent_meta"],
|
|
||||||
Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"],
|
|
||||||
Self::Lifecycle => &["kill", "start", "restart", "update", "list_containers"],
|
|
||||||
Self::Approvals => &["request_init_config", "request_update_meta_inputs"],
|
|
||||||
Self::Scheduling => &[
|
|
||||||
"request_schedule_prompt",
|
|
||||||
"fire_schedule_now",
|
|
||||||
"cancel_schedule",
|
|
||||||
"edit_schedule",
|
|
||||||
"list_schedules",
|
|
||||||
],
|
|
||||||
Self::Diagnostics => &["get_logs"],
|
|
||||||
Self::Forge => &["create_repo"],
|
|
||||||
Self::Execution => &["run", "status"],
|
|
||||||
Self::WebTools => &[],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MCP tools that are always exposed regardless of which tool groups an
|
|
||||||
/// agent is granted. `set_status` lives here because the operator
|
|
||||||
/// dashboard depends on every agent being able to report its status
|
|
||||||
/// chip — gating it behind a group would let a misconfigured agent go
|
|
||||||
/// dark on the dashboard. The server-side `SetStatus` handler has no
|
|
||||||
/// tool-group check either (only length validation), so listing it here
|
|
||||||
/// keeps the `--allowedTools` list honest with that reality.
|
|
||||||
///
|
|
||||||
/// `compact` lives here too: it's pure self-management (no cross-agent
|
|
||||||
/// effect, no privilege), gated server-side on context usage rather
|
|
||||||
/// than on tool groups, and every agent should be able to reach for it
|
|
||||||
/// regardless of which optional groups it's been granted — same
|
|
||||||
/// reasoning as `set_status`.
|
|
||||||
///
|
|
||||||
/// `mark_todos_done` too (a critical bug every agent hit): it was
|
|
||||||
/// declared as a `#[tool]` fn but never added to *any*
|
|
||||||
/// group's [`tools`](Self::tools), including `Inbox`, so no agent could
|
|
||||||
/// ever get it into `--allowedTools` and every call prompted for
|
|
||||||
/// approval it can't get. Todos are pushed to an agent independent of
|
|
||||||
/// whether it holds `Inbox` (that group only gates
|
|
||||||
/// `get_loose_ends`/`cancel_loose_end`/`remind`), so an agent without
|
|
||||||
/// `Inbox` could accumulate todos it can never clear — same
|
|
||||||
/// "every agent needs this regardless of optional groups" shape as
|
|
||||||
/// `set_status`/`compact`, not a narrower `Inbox`-only fix.
|
|
||||||
pub const ALWAYS_ON_TOOLS: &'static [&'static str] =
|
|
||||||
&["set_status", "compact", "mark_todos_done"];
|
|
||||||
|
|
||||||
/// The Claude built-in tool names enabled by this group. Only
|
|
||||||
/// `WebTools` returns a non-empty slice; all other groups return `&[]`
|
|
||||||
/// (they control MCP tools via `tools()` instead).
|
|
||||||
#[must_use]
|
|
||||||
pub fn builtin_tools(self) -> &'static [&'static str] {
|
|
||||||
match self {
|
|
||||||
Self::WebTools => &["WebFetch", "WebSearch"],
|
|
||||||
_ => &[],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Default tool groups for an agent harness. Used when `HIVE_TOOL_GROUPS` is unset.
|
|
||||||
pub const AGENT_DEFAULT: &'static [Self] =
|
|
||||||
&[Self::Messaging, Self::Meta, Self::Inbox, Self::Execution];
|
|
||||||
|
|
||||||
/// Convenience preset for a fully-privileged agent (all groups).
|
|
||||||
/// Use this as a starting point in `tool-groups.json` for root/manager agents.
|
|
||||||
pub const MANAGER_DEFAULT: &'static [Self] = &[
|
|
||||||
Self::Messaging,
|
|
||||||
Self::Meta,
|
|
||||||
Self::Inbox,
|
|
||||||
Self::Lifecycle,
|
|
||||||
Self::Approvals,
|
|
||||||
Self::Scheduling,
|
|
||||||
Self::Diagnostics,
|
|
||||||
Self::Execution,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Every known tool group in a stable order. Use this to enumerate
|
|
||||||
/// columns in the capabilities UI or any other place that needs the
|
|
||||||
/// full list without hard-coding it at the call site.
|
|
||||||
pub const ALL: &'static [Self] = &[
|
|
||||||
Self::Messaging,
|
|
||||||
Self::Meta,
|
|
||||||
Self::Inbox,
|
|
||||||
Self::Lifecycle,
|
|
||||||
Self::Approvals,
|
|
||||||
Self::Scheduling,
|
|
||||||
Self::Diagnostics,
|
|
||||||
Self::Forge,
|
|
||||||
Self::Execution,
|
|
||||||
Self::WebTools,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// The `snake_case` wire name for this group (matches `serde(rename_all =
|
|
||||||
/// "snake_case")` serialisation).
|
|
||||||
#[must_use]
|
|
||||||
pub fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Messaging => "messaging",
|
|
||||||
Self::Meta => "meta",
|
|
||||||
Self::Inbox => "inbox",
|
|
||||||
Self::Lifecycle => "lifecycle",
|
|
||||||
Self::Approvals => "approvals",
|
|
||||||
Self::Scheduling => "scheduling",
|
|
||||||
Self::Diagnostics => "diagnostics",
|
|
||||||
Self::Forge => "forge",
|
|
||||||
Self::Execution => "execution",
|
|
||||||
Self::WebTools => "web_tools",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Short human-readable description suitable for a tooltip or help text.
|
|
||||||
#[must_use]
|
|
||||||
pub fn description(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Messaging => "send, recv, ask, answer — core agent communication",
|
|
||||||
Self::Meta => {
|
|
||||||
"get_agent_meta — identity introspection (set_status is always available)"
|
|
||||||
}
|
|
||||||
Self::Inbox => "get_loose_ends, cancel_loose_end, remind — self-scheduling",
|
|
||||||
Self::Lifecycle => {
|
|
||||||
"kill, start, restart, update, list_containers — container lifecycle (privileged)"
|
|
||||||
}
|
|
||||||
Self::Approvals => {
|
|
||||||
"request_init_config, request_update_meta_inputs — config change flow (privileged)"
|
|
||||||
}
|
|
||||||
Self::Scheduling => {
|
|
||||||
"request_schedule_prompt and related — operator-visible scheduled prompts (privileged)"
|
|
||||||
}
|
|
||||||
Self::Diagnostics => {
|
|
||||||
"get_logs — read a sub-agent container's systemd journal (privileged)"
|
|
||||||
}
|
|
||||||
Self::Forge => {
|
|
||||||
"create_repo — create git repos through hive-c0re (operator-gated merge)"
|
|
||||||
}
|
|
||||||
Self::Execution => {
|
|
||||||
"run, status — run shell commands via mcp__bash__run / mcp__bash__status"
|
|
||||||
}
|
|
||||||
Self::WebTools => "WebFetch, WebSearch — Claude built-in web egress; not MCP tools",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Per-agent capability grants. Stored in `meta/capabilities.json`
|
/// Per-agent capability grants. Stored in `meta/capabilities.json`
|
||||||
/// (same shape as `tool-groups.json`: `{ "alice": ["read_host_journal"] }`).
|
/// (same shape as `tool-groups.json`: `{ "alice": ["read_host_journal"] }`).
|
||||||
/// Capabilities control system-level access that hive-c0re enforces
|
/// Capabilities control system-level access that hive-c0re enforces
|
||||||
|
|
@ -743,83 +330,6 @@ impl JournalPriority {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// at dispatch time; they are orthogonal to tool groups (which control
|
|
||||||
/// which MCP tools the harness exposes to claude).
|
|
||||||
///
|
|
||||||
/// Injected into containers as `HIVE_CAPABILITIES` (comma-separated
|
|
||||||
/// `snake_case`) via `meta::render_flake`. The harness reads this to
|
|
||||||
/// conditionally register capability-gated MCP tools so claude only
|
|
||||||
/// sees tools it can actually invoke. See `docs/conventions.md::Capabilities`.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum Capability {
|
|
||||||
/// Agent can lifecycle-manage the root agent (kill/start/restart)
|
|
||||||
/// on behalf of the hive when the root has crashed. Named capability
|
|
||||||
/// for the existing manager privilege — future topology enforcement
|
|
||||||
/// will gate this via the capability system instead of the hardcoded
|
|
||||||
/// `container == MANAGER_CONTAINER` check.
|
|
||||||
ManageRootAgent,
|
|
||||||
/// Agent can read the full host journal via `GetHostJournal`.
|
|
||||||
/// hive-c0re checks this capability before running journalctl.
|
|
||||||
/// MCP tool `get_host_journal` is only registered in the harness
|
|
||||||
/// when this capability is present.
|
|
||||||
ReadHostJournal,
|
|
||||||
/// Agent can query non-child agents via `GetLooseEnds`,
|
|
||||||
/// `CountPendingReminders`, and `ReminderRollup` on the agent
|
|
||||||
/// socket. Without this capability, targeting a non-child agent is
|
|
||||||
/// rejected with an error (direct children are always accessible
|
|
||||||
/// without any capability). The `"*"` hive-wide value is not
|
|
||||||
/// available on the agent socket even with this capability — use the
|
|
||||||
/// manager socket for swarm-wide scans.
|
|
||||||
QueryAgentState,
|
|
||||||
/// Agent can restart hive infrastructure containers (hive-ci,
|
|
||||||
/// hive-gateway, hive-forge) via the `restart` MCP tool. hive-c0re
|
|
||||||
/// checks this capability before routing the restart through
|
|
||||||
/// hive-priv; the concrete service allowlist lives root-side in
|
|
||||||
/// hive-priv. Deliberately generic ("infra admin") so future
|
|
||||||
/// privileged infra ops can hang off the same grant.
|
|
||||||
InfraAdmin,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Capability {
|
|
||||||
/// Every known capability in a stable order. Use this to enumerate
|
|
||||||
/// columns in the permissions UI or validate incoming capability strings.
|
|
||||||
pub const ALL: &'static [Self] = &[
|
|
||||||
Self::ManageRootAgent,
|
|
||||||
Self::ReadHostJournal,
|
|
||||||
Self::QueryAgentState,
|
|
||||||
Self::InfraAdmin,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Canonical `snake_case` name for this capability (matches serde).
|
|
||||||
#[must_use]
|
|
||||||
pub fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::ManageRootAgent => "manage_root_agent",
|
|
||||||
Self::ReadHostJournal => "read_host_journal",
|
|
||||||
Self::QueryAgentState => "query_agent_state",
|
|
||||||
Self::InfraAdmin => "infra_admin",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Short human-readable description suitable for a tooltip or help text.
|
|
||||||
#[must_use]
|
|
||||||
pub fn description(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::ManageRootAgent => {
|
|
||||||
"lifecycle-manage the root/manager agent on hive crash recovery"
|
|
||||||
}
|
|
||||||
Self::ReadHostJournal => "read host journald via get_host_journal MCP tool",
|
|
||||||
Self::QueryAgentState => {
|
|
||||||
"query non-child agents' loose ends and reminder state via get_loose_ends"
|
|
||||||
}
|
|
||||||
Self::InfraAdmin => {
|
|
||||||
"restart hive infrastructure containers (hive-ci, hive-gateway, hive-forge) via the restart tool"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Schedule row shape on the wire — mirror of
|
/// Schedule row shape on the wire — mirror of
|
||||||
/// `scheduled_prompts::Schedule` but in the public crate so the
|
/// `scheduled_prompts::Schedule` but in the public crate so the
|
||||||
/// dashboard and agent surfaces can deserialize without depending
|
/// dashboard and agent surfaces can deserialize without depending
|
||||||
|
|
|
||||||
133
hive-sh4re/src/manager.rs
Normal file
133
hive-sh4re/src/manager.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
//! Manager socket — `/run/hyperhive/manager/mcp.sock` on the host,
|
||||||
|
//! bind-mounted into the manager container at `/run/hive/mcp.sock`.
|
||||||
|
//! Reserved recipient names/senders, the out-of-band `HelperEvent`
|
||||||
|
//! payload hive-c0re pushes into the manager's inbox, and the
|
||||||
|
//! schedule-prompt submission payload.
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
/// Logical name the broker uses for the human operator. Messages with
|
||||||
|
/// `to = OPERATOR_RECIPIENT` accumulate in sqlite and surface on the
|
||||||
|
/// dashboard's inbox view — they are never `recv`'d by an agent harness.
|
||||||
|
pub const OPERATOR_RECIPIENT: &str = "operator";
|
||||||
|
|
||||||
|
/// Reserved magic recipient — `send(to: "<parent>", ...)` is rewritten
|
||||||
|
/// by hive-c0re at delivery time to whoever `topology::parent_of(sender)`
|
||||||
|
/// returns, or to [`OPERATOR_RECIPIENT`] when the sender is a root agent
|
||||||
|
/// (no parent). Lets agents address their parent without hardcoding the
|
||||||
|
/// label, so runtime reparenting requires no agent-side restart. The
|
||||||
|
/// angle brackets are not valid in agent names (validators reject
|
||||||
|
/// `<`/`>`), so this name can never collide with a real recipient.
|
||||||
|
pub const PARENT_RECIPIENT: &str = "<parent>";
|
||||||
|
|
||||||
|
/// Reserved magic recipient — `send(to: "<children>", ...)` fans out to
|
||||||
|
/// every agent whose direct parent (per `topology.json`) is the sender.
|
||||||
|
/// Lets a sub-manager nudge its subtree without enumerating labels at
|
||||||
|
/// call-time; topology changes propagate for free. The angle brackets
|
||||||
|
/// are structurally safe — agent name validation rejects `<`/`>`.
|
||||||
|
/// Delivers to an empty set (no-op) for leaf agents that have no children.
|
||||||
|
pub const CHILDREN_RECIPIENT: &str = "<children>";
|
||||||
|
|
||||||
|
/// Sender hive-c0re uses for events it pushes into the manager's inbox.
|
||||||
|
/// Manager harness recognises this and parses the body as a `HelperEvent`.
|
||||||
|
pub const SYSTEM_SENDER: &str = "system";
|
||||||
|
|
||||||
|
/// Parse `s` as a [`Ident`] for use as `Message.from`, falling back to
|
||||||
|
/// [`SYSTEM_SENDER`] on the (should-be-unreachable) case that `s` isn't
|
||||||
|
/// ident-shaped. `Message.from` is always either a fixed sentinel literal
|
||||||
|
/// (`SYSTEM_SENDER`, `OPERATOR_RECIPIENT`, `"scheduled"`, …) or an
|
||||||
|
/// already-registered agent's own name reaching this point through
|
||||||
|
/// hive-c0re's internal dispatch — never arbitrary external input — so
|
||||||
|
/// this is a defensive fallback for a programming-bug case, not a
|
||||||
|
/// validation gate.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Never, unless [`SYSTEM_SENDER`] itself stops being ident-shaped (which
|
||||||
|
/// would also be a programming bug, caught by `hive-types`' own tests).
|
||||||
|
#[must_use]
|
||||||
|
pub fn trusted_sender(s: &str) -> Ident {
|
||||||
|
Ident::parse(s)
|
||||||
|
.unwrap_or_else(|_| Ident::parse(SYSTEM_SENDER).expect("SYSTEM_SENDER is a valid 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/approvals.md::Helper events to the manager`.
|
||||||
|
#[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>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
sha: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
tag: 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).
|
||||||
|
ContainerCrash {
|
||||||
|
agent: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
note: Option<String>,
|
||||||
|
},
|
||||||
|
/// A question queued via `Ask` was answered. `id` matches the
|
||||||
|
/// originating `QuestionQueued.id`; `answerer` is `"operator"` /
|
||||||
|
/// a peer agent name / `"ttl-watchdog"` on expiry.
|
||||||
|
QuestionAnswered {
|
||||||
|
id: i64,
|
||||||
|
question: String,
|
||||||
|
answer: String,
|
||||||
|
answerer: String,
|
||||||
|
},
|
||||||
|
/// A peer (or the manager) asked this agent a question. Recipient
|
||||||
|
/// replies via `Answer { id, answer }`; the answer routes back to
|
||||||
|
/// the asker as `QuestionAnswered`.
|
||||||
|
QuestionAsked {
|
||||||
|
id: i64,
|
||||||
|
asker: String,
|
||||||
|
question: String,
|
||||||
|
#[serde(default)]
|
||||||
|
options: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
multi: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Submission payload for `RequestSchedulePrompt`. Lives outside the
|
||||||
|
/// enum so it can also serialize into the approval row's `commit_ref`
|
||||||
|
/// (the dispatcher re-parses it on approve and inserts the schedule).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct SchedulePromptPayload {
|
||||||
|
/// Names of recipient agents. Operator + `root` allowed.
|
||||||
|
pub targets: Vec<String>,
|
||||||
|
/// Message body delivered to each target's inbox at fire time.
|
||||||
|
/// Same size budget as `Send.body` — soft cap at the broker level.
|
||||||
|
pub body: String,
|
||||||
|
/// Absolute unix timestamp (seconds) for the FIRST fire. For
|
||||||
|
/// recurring schedules the worker then re-arms in
|
||||||
|
/// `interval_seconds` steps.
|
||||||
|
pub first_fire_at_unix: i64,
|
||||||
|
/// `None` = one-shot. `Some(n > 0)` = recurring every `n` seconds.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub interval_seconds: Option<u64>,
|
||||||
|
/// Optional description shown on the dashboard approval card AND
|
||||||
|
/// stored on the resulting schedule row for the operator's
|
||||||
|
/// "what is this?" reference later.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
272
hive-sh4re/src/permissions.rs
Normal file
272
hive-sh4re/src/permissions.rs
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
//! Per-agent authorization: named MCP-tool groups (`ToolGroup`) and
|
||||||
|
//! system-level capability grants (`Capability`). Both are declared in
|
||||||
|
//! per-agent config (`tool-groups.json` / `capabilities.json`), injected
|
||||||
|
//! into the container as env vars, and read by the harness to decide
|
||||||
|
//! which MCP tools claude actually sees.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Named group of MCP tools an agent may be granted. The harness reads
|
||||||
|
/// `HIVE_TOOL_GROUPS` from the environment (a comma-separated list of
|
||||||
|
/// `snake_case` group names written by the meta renderer from per-agent
|
||||||
|
/// config) and expands it to the matching tool names for `--allowedTools`.
|
||||||
|
/// When the env var is absent the harness falls back to `AGENT_DEFAULT`.
|
||||||
|
/// See `docs/conventions.md::Tool groups`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ToolGroup {
|
||||||
|
/// `send`, `recv`, `ask`, `answer`
|
||||||
|
Messaging,
|
||||||
|
/// `get_agent_meta` (`set_status` is always-on — see `ALWAYS_ON_TOOLS`)
|
||||||
|
Meta,
|
||||||
|
/// `get_loose_ends`, `cancel_loose_end`, `remind`
|
||||||
|
Inbox,
|
||||||
|
/// `kill`, `start`, `restart`, `update` - *(privileged)*
|
||||||
|
Lifecycle,
|
||||||
|
/// `request_init_config`, `request_update_meta_inputs` - *(privileged)*
|
||||||
|
Approvals,
|
||||||
|
/// `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`,
|
||||||
|
/// `edit_schedule`, `list_schedules` - *(privileged)*
|
||||||
|
Scheduling,
|
||||||
|
/// `get_logs` - *(privileged)*
|
||||||
|
Diagnostics,
|
||||||
|
/// `create_repo` — create git repos through hive-c0re (the only path
|
||||||
|
/// now that agents can't create them directly). Opt-in per
|
||||||
|
/// agent so the operator controls who can spin up repos.
|
||||||
|
Forge,
|
||||||
|
/// `run`, `status` (via `mcp__bash__*`)
|
||||||
|
Execution,
|
||||||
|
/// Claude built-in web egress tools: `WebFetch` (retrieve a URL) and
|
||||||
|
/// `WebSearch` (search the web). Both are omitted from `--tools` by
|
||||||
|
/// default; adding this group to an agent enables them in the session
|
||||||
|
/// and in `--allowedTools` so they run without a confirmation prompt.
|
||||||
|
/// Does not gate any MCP tools — `tools()` returns `&[]`.
|
||||||
|
WebTools,
|
||||||
|
}
|
||||||
|
|
||||||
|
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()`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn tools(self) -> &'static [&'static str] {
|
||||||
|
match self {
|
||||||
|
Self::Messaging => &["send", "recv", "ack_until", "ask", "answer"],
|
||||||
|
Self::Meta => &["get_agent_meta"],
|
||||||
|
Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"],
|
||||||
|
Self::Lifecycle => &["kill", "start", "restart", "update", "list_containers"],
|
||||||
|
Self::Approvals => &["request_init_config", "request_update_meta_inputs"],
|
||||||
|
Self::Scheduling => &[
|
||||||
|
"request_schedule_prompt",
|
||||||
|
"fire_schedule_now",
|
||||||
|
"cancel_schedule",
|
||||||
|
"edit_schedule",
|
||||||
|
"list_schedules",
|
||||||
|
],
|
||||||
|
Self::Diagnostics => &["get_logs"],
|
||||||
|
Self::Forge => &["create_repo"],
|
||||||
|
Self::Execution => &["run", "status"],
|
||||||
|
Self::WebTools => &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MCP tools that are always exposed regardless of which tool groups an
|
||||||
|
/// agent is granted. `set_status` lives here because the operator
|
||||||
|
/// dashboard depends on every agent being able to report its status
|
||||||
|
/// chip — gating it behind a group would let a misconfigured agent go
|
||||||
|
/// dark on the dashboard. The server-side `SetStatus` handler has no
|
||||||
|
/// tool-group check either (only length validation), so listing it here
|
||||||
|
/// keeps the `--allowedTools` list honest with that reality.
|
||||||
|
///
|
||||||
|
/// `compact` lives here too: it's pure self-management (no cross-agent
|
||||||
|
/// effect, no privilege), gated server-side on context usage rather
|
||||||
|
/// than on tool groups, and every agent should be able to reach for it
|
||||||
|
/// regardless of which optional groups it's been granted — same
|
||||||
|
/// reasoning as `set_status`.
|
||||||
|
///
|
||||||
|
/// `mark_todos_done` too (a critical bug every agent hit): it was
|
||||||
|
/// declared as a `#[tool]` fn but never added to *any*
|
||||||
|
/// group's [`tools`](Self::tools), including `Inbox`, so no agent could
|
||||||
|
/// ever get it into `--allowedTools` and every call prompted for
|
||||||
|
/// approval it can't get. Todos are pushed to an agent independent of
|
||||||
|
/// whether it holds `Inbox` (that group only gates
|
||||||
|
/// `get_loose_ends`/`cancel_loose_end`/`remind`), so an agent without
|
||||||
|
/// `Inbox` could accumulate todos it can never clear — same
|
||||||
|
/// "every agent needs this regardless of optional groups" shape as
|
||||||
|
/// `set_status`/`compact`, not a narrower `Inbox`-only fix.
|
||||||
|
pub const ALWAYS_ON_TOOLS: &'static [&'static str] =
|
||||||
|
&["set_status", "compact", "mark_todos_done"];
|
||||||
|
|
||||||
|
/// The Claude built-in tool names enabled by this group. Only
|
||||||
|
/// `WebTools` returns a non-empty slice; all other groups return `&[]`
|
||||||
|
/// (they control MCP tools via `tools()` instead).
|
||||||
|
#[must_use]
|
||||||
|
pub fn builtin_tools(self) -> &'static [&'static str] {
|
||||||
|
match self {
|
||||||
|
Self::WebTools => &["WebFetch", "WebSearch"],
|
||||||
|
_ => &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default tool groups for an agent harness. Used when `HIVE_TOOL_GROUPS` is unset.
|
||||||
|
pub const AGENT_DEFAULT: &'static [Self] =
|
||||||
|
&[Self::Messaging, Self::Meta, Self::Inbox, Self::Execution];
|
||||||
|
|
||||||
|
/// Convenience preset for a fully-privileged agent (all groups).
|
||||||
|
/// Use this as a starting point in `tool-groups.json` for root/manager agents.
|
||||||
|
pub const MANAGER_DEFAULT: &'static [Self] = &[
|
||||||
|
Self::Messaging,
|
||||||
|
Self::Meta,
|
||||||
|
Self::Inbox,
|
||||||
|
Self::Lifecycle,
|
||||||
|
Self::Approvals,
|
||||||
|
Self::Scheduling,
|
||||||
|
Self::Diagnostics,
|
||||||
|
Self::Execution,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Every known tool group in a stable order. Use this to enumerate
|
||||||
|
/// columns in the capabilities UI or any other place that needs the
|
||||||
|
/// full list without hard-coding it at the call site.
|
||||||
|
pub const ALL: &'static [Self] = &[
|
||||||
|
Self::Messaging,
|
||||||
|
Self::Meta,
|
||||||
|
Self::Inbox,
|
||||||
|
Self::Lifecycle,
|
||||||
|
Self::Approvals,
|
||||||
|
Self::Scheduling,
|
||||||
|
Self::Diagnostics,
|
||||||
|
Self::Forge,
|
||||||
|
Self::Execution,
|
||||||
|
Self::WebTools,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The `snake_case` wire name for this group (matches `serde(rename_all =
|
||||||
|
/// "snake_case")` serialisation).
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Messaging => "messaging",
|
||||||
|
Self::Meta => "meta",
|
||||||
|
Self::Inbox => "inbox",
|
||||||
|
Self::Lifecycle => "lifecycle",
|
||||||
|
Self::Approvals => "approvals",
|
||||||
|
Self::Scheduling => "scheduling",
|
||||||
|
Self::Diagnostics => "diagnostics",
|
||||||
|
Self::Forge => "forge",
|
||||||
|
Self::Execution => "execution",
|
||||||
|
Self::WebTools => "web_tools",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Short human-readable description suitable for a tooltip or help text.
|
||||||
|
#[must_use]
|
||||||
|
pub fn description(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Messaging => "send, recv, ask, answer — core agent communication",
|
||||||
|
Self::Meta => {
|
||||||
|
"get_agent_meta — identity introspection (set_status is always available)"
|
||||||
|
}
|
||||||
|
Self::Inbox => "get_loose_ends, cancel_loose_end, remind — self-scheduling",
|
||||||
|
Self::Lifecycle => {
|
||||||
|
"kill, start, restart, update, list_containers — container lifecycle (privileged)"
|
||||||
|
}
|
||||||
|
Self::Approvals => {
|
||||||
|
"request_init_config, request_update_meta_inputs — config change flow (privileged)"
|
||||||
|
}
|
||||||
|
Self::Scheduling => {
|
||||||
|
"request_schedule_prompt and related — operator-visible scheduled prompts (privileged)"
|
||||||
|
}
|
||||||
|
Self::Diagnostics => {
|
||||||
|
"get_logs — read a sub-agent container's systemd journal (privileged)"
|
||||||
|
}
|
||||||
|
Self::Forge => {
|
||||||
|
"create_repo — create git repos through hive-c0re (operator-gated merge)"
|
||||||
|
}
|
||||||
|
Self::Execution => {
|
||||||
|
"run, status — run shell commands via mcp__bash__run / mcp__bash__status"
|
||||||
|
}
|
||||||
|
Self::WebTools => "WebFetch, WebSearch — Claude built-in web egress; not MCP tools",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-agent capability grants. Stored in `meta/capabilities.json`
|
||||||
|
/// (same shape as `tool-groups.json`: `{ "alice": ["read_host_journal"] }`).
|
||||||
|
/// Capabilities control system-level access hive-c0re enforces at
|
||||||
|
/// dispatch time; they are orthogonal to tool groups (which control
|
||||||
|
/// which MCP tools the harness exposes to claude).
|
||||||
|
///
|
||||||
|
/// Injected into containers as `HIVE_CAPABILITIES` (comma-separated
|
||||||
|
/// `snake_case`) via `meta::render_flake`. The harness reads this to
|
||||||
|
/// conditionally register capability-gated MCP tools so claude only
|
||||||
|
/// sees tools it can actually invoke. See `docs/conventions.md::Capabilities`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Capability {
|
||||||
|
/// Agent can lifecycle-manage the root agent (kill/start/restart)
|
||||||
|
/// on behalf of the hive when the root has crashed. Named capability
|
||||||
|
/// for the existing manager privilege — future topology enforcement
|
||||||
|
/// will gate this via the capability system instead of the hardcoded
|
||||||
|
/// `container == MANAGER_CONTAINER` check.
|
||||||
|
ManageRootAgent,
|
||||||
|
/// Agent can read the full host journal via `GetHostJournal`.
|
||||||
|
/// hive-c0re checks this capability before running journalctl.
|
||||||
|
/// MCP tool `get_host_journal` is only registered in the harness
|
||||||
|
/// when this capability is present.
|
||||||
|
ReadHostJournal,
|
||||||
|
/// Agent can query non-child agents via `GetLooseEnds`,
|
||||||
|
/// `CountPendingReminders`, and `ReminderRollup` on the agent
|
||||||
|
/// socket. Without this capability, targeting a non-child agent is
|
||||||
|
/// rejected with an error (direct children are always accessible
|
||||||
|
/// without any capability). The `"*"` hive-wide value is not
|
||||||
|
/// available on the agent socket even with this capability — use the
|
||||||
|
/// manager socket for swarm-wide scans.
|
||||||
|
QueryAgentState,
|
||||||
|
/// Agent can restart hive infrastructure containers (hive-ci,
|
||||||
|
/// hive-gateway, hive-forge) via the `restart` MCP tool. hive-c0re
|
||||||
|
/// checks this capability before routing the restart through
|
||||||
|
/// hive-priv; the concrete service allowlist lives root-side in
|
||||||
|
/// hive-priv. Deliberately generic ("infra admin") so future
|
||||||
|
/// privileged infra ops can hang off the same grant.
|
||||||
|
InfraAdmin,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Capability {
|
||||||
|
/// Every known capability in a stable order. Use this to enumerate
|
||||||
|
/// columns in the permissions UI or validate incoming capability strings.
|
||||||
|
pub const ALL: &'static [Self] = &[
|
||||||
|
Self::ManageRootAgent,
|
||||||
|
Self::ReadHostJournal,
|
||||||
|
Self::QueryAgentState,
|
||||||
|
Self::InfraAdmin,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Canonical `snake_case` name for this capability (matches serde).
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::ManageRootAgent => "manage_root_agent",
|
||||||
|
Self::ReadHostJournal => "read_host_journal",
|
||||||
|
Self::QueryAgentState => "query_agent_state",
|
||||||
|
Self::InfraAdmin => "infra_admin",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Short human-readable description suitable for a tooltip or help text.
|
||||||
|
#[must_use]
|
||||||
|
pub fn description(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::ManageRootAgent => {
|
||||||
|
"lifecycle-manage the root/manager agent on hive crash recovery"
|
||||||
|
}
|
||||||
|
Self::ReadHostJournal => "read host journald via get_host_journal MCP tool",
|
||||||
|
Self::QueryAgentState => {
|
||||||
|
"query non-child agents' loose ends and reminder state via get_loose_ends"
|
||||||
|
}
|
||||||
|
Self::InfraAdmin => {
|
||||||
|
"restart hive infrastructure containers (hive-ci, hive-gateway, hive-forge) via the restart tool"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue