Compare commits
43 changed files with 677 additions and 732 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::manager::HelperEvent`):
|
root agent. Variants (`hive_sh4re::HelperEvent`):
|
||||||
|
|
||||||
- `ApprovalResolved { id, agent, commit_ref, status, note }` —
|
- `ApprovalResolved { id, agent, commit_ref, status, note }` —
|
||||||
fired by `actions::approve` + `actions::deny` whenever an
|
fired by `actions::approve` + `actions::deny` whenever an
|
||||||
|
|
|
||||||
|
|
@ -318,7 +318,7 @@ straight to `new Date(s)` for display.
|
||||||
## Tool groups
|
## Tool groups
|
||||||
|
|
||||||
The MCP tool surface an agent receives is derived from a set of named
|
The MCP tool surface an agent receives is derived from a set of named
|
||||||
`ToolGroup` values (`hive_sh4re::permissions::ToolGroup`), not from a hardcoded
|
`ToolGroup` values (`hive_sh4re::ToolGroup`), not from a hardcoded
|
||||||
binary flavor.
|
binary flavor.
|
||||||
|
|
||||||
| Group | Tools |
|
| Group | Tools |
|
||||||
|
|
|
||||||
|
|
@ -975,7 +975,7 @@ impl AgentServer {
|
||||||
let target_count = args.targets.len();
|
let target_count = args.targets.len();
|
||||||
let (resp, retries) = self
|
let (resp, retries) = self
|
||||||
.dispatch(hive_core_agent_sock::Request::RequestSchedulePrompt(
|
.dispatch(hive_core_agent_sock::Request::RequestSchedulePrompt(
|
||||||
hive_sh4re::manager::SchedulePromptPayload {
|
hive_sh4re::SchedulePromptPayload {
|
||||||
targets: args.targets,
|
targets: args.targets,
|
||||||
body: args.body,
|
body: args.body,
|
||||||
first_fire_at_unix: args.first_fire_at_unix,
|
first_fire_at_unix: args.first_fire_at_unix,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const SEND_ALLOW_PATH: &str = "/etc/hyperhive/send-allow.json";
|
||||||
/// the refusal as the tool result so claude knows the message didn't
|
/// the refusal as the tool result so claude knows the message didn't
|
||||||
/// land and can react (e.g. route via `<parent>` instead).
|
/// land and can react (e.g. route via `<parent>` instead).
|
||||||
pub fn check_send_allowed(to: &str) -> Result<(), String> {
|
pub fn check_send_allowed(to: &str) -> Result<(), String> {
|
||||||
if to == hive_sh4re::manager::PARENT_RECIPIENT {
|
if to == hive_sh4re::PARENT_RECIPIENT {
|
||||||
// Always allow `<parent>` — the allow-list constrains peer
|
// Always allow `<parent>` — the allow-list constrains peer
|
||||||
// chatter, not the structural reporting line; the operator
|
// chatter, not the structural reporting line; the operator
|
||||||
// can rewire who the parent IS via `set_parent` without
|
// can rewire who the parent IS via `set_parent` without
|
||||||
|
|
@ -53,6 +53,6 @@ pub fn check_send_allowed(to: &str) -> Result<(), String> {
|
||||||
(configured in agent.nix). Allowed: {allow:?}. Your structural \
|
(configured in agent.nix). Allowed: {allow:?}. Your structural \
|
||||||
parent is always reachable — route through `send(to: \"{}\", …)` \
|
parent is always reachable — route through `send(to: \"{}\", …)` \
|
||||||
if you need to reach someone outside the allow-list.",
|
if you need to reach someone outside the allow-list.",
|
||||||
hive_sh4re::manager::PARENT_RECIPIENT
|
hive_sh4re::PARENT_RECIPIENT
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,9 +147,7 @@ pub enum Response {
|
||||||
/// `CountPendingReminders` result.
|
/// `CountPendingReminders` result.
|
||||||
PendingRemindersCount { count: u64 },
|
PendingRemindersCount { count: u64 },
|
||||||
/// `ReminderRollup` result.
|
/// `ReminderRollup` result.
|
||||||
ReminderRollup {
|
ReminderRollup { stats: hive_sh4re::ReminderStats },
|
||||||
stats: hive_sh4re::approvals::ReminderStats,
|
|
||||||
},
|
|
||||||
/// Op failed; `message` is operator-facing.
|
/// Op failed; `message` is operator-facing.
|
||||||
Err { message: String },
|
Err { message: String },
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ use crate::turn_stats::TurnStats;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use hive_core_agent_sock::{Request, Response};
|
use hive_core_agent_sock::{Request, Response};
|
||||||
use hive_sh4re::manager::{HelperEvent, SYSTEM_SENDER};
|
use hive_sh4re::{HelperEvent, SYSTEM_SENDER};
|
||||||
use hive_sock_client::Retry;
|
use hive_sock_client::Retry;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
|
@ -406,7 +406,7 @@ impl Surface for AgentSurface {
|
||||||
let res = hive_sock_client::request::<_, Response>(
|
let res = hive_sock_client::request::<_, Response>(
|
||||||
socket,
|
socket,
|
||||||
&Request::Send {
|
&Request::Send {
|
||||||
to: hive_sh4re::manager::PARENT_RECIPIENT.into(),
|
to: hive_sh4re::PARENT_RECIPIENT.into(),
|
||||||
body,
|
body,
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -35,14 +35,14 @@ pub const DEFAULT_MCP_HTTP_PORT: u16 = 8790;
|
||||||
pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &["Edit", "Glob", "Grep", "Read", "Skill", "Write"];
|
pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &["Edit", "Glob", "Grep", "Read", "Skill", "Write"];
|
||||||
|
|
||||||
/// Env var written by the meta renderer with a comma-separated list of
|
/// Env var written by the meta renderer with a comma-separated list of
|
||||||
/// `hive_sh4re::permissions::ToolGroup` `snake_case` names (e.g. `"messaging,inbox,meta"`).
|
/// `hive_sh4re::ToolGroup` `snake_case` names (e.g. `"messaging,inbox,meta"`).
|
||||||
/// When present, the harness expands the groups into per-tool allow entries
|
/// When present, the harness expands the groups into per-tool allow entries
|
||||||
/// instead of using the hardcoded flavor default. See `docs/conventions.md::Tool groups`.
|
/// instead of using the hardcoded flavor default. See `docs/conventions.md::Tool groups`.
|
||||||
const TOOL_GROUPS_ENV: &str = "HIVE_TOOL_GROUPS";
|
const TOOL_GROUPS_ENV: &str = "HIVE_TOOL_GROUPS";
|
||||||
|
|
||||||
/// `HIVE_CAPABILITIES` env var injected by `meta::render_flake` when the
|
/// `HIVE_CAPABILITIES` env var injected by `meta::render_flake` when the
|
||||||
/// operator grants capabilities to this agent. Comma-separated
|
/// operator grants capabilities to this agent. Comma-separated
|
||||||
/// `hive_sh4re::permissions::Capability` `snake_case` names. Absent = no extra capabilities.
|
/// `hive_sh4re::Capability` `snake_case` names. Absent = no extra capabilities.
|
||||||
const CAPABILITIES_ENV: &str = "HIVE_CAPABILITIES";
|
const CAPABILITIES_ENV: &str = "HIVE_CAPABILITIES";
|
||||||
|
|
||||||
/// Returns the MCP tool names (without `mcp__hyperhive__` prefix) that are
|
/// Returns the MCP tool names (without `mcp__hyperhive__` prefix) that are
|
||||||
|
|
@ -87,18 +87,18 @@ fn allowed_capability_tools() -> Vec<String> {
|
||||||
/// (`messaging`, `meta`, `inbox`, `lifecycle`, `approvals`, `scheduling`,
|
/// (`messaging`, `meta`, `inbox`, `lifecycle`, `approvals`, `scheduling`,
|
||||||
/// `diagnostics`, `execution`). Unrecognised tokens are logged and skipped.
|
/// `diagnostics`, `execution`). Unrecognised tokens are logged and skipped.
|
||||||
/// Falls back to `AGENT_DEFAULT` when the env var is absent or empty.
|
/// Falls back to `AGENT_DEFAULT` when the env var is absent or empty.
|
||||||
fn effective_tool_groups() -> Vec<hive_sh4re::permissions::ToolGroup> {
|
fn effective_tool_groups() -> Vec<hive_sh4re::ToolGroup> {
|
||||||
let raw = match std::env::var(TOOL_GROUPS_ENV) {
|
let raw = match std::env::var(TOOL_GROUPS_ENV) {
|
||||||
Ok(v) if !v.trim().is_empty() => v,
|
Ok(v) if !v.trim().is_empty() => v,
|
||||||
_ => return hive_sh4re::permissions::ToolGroup::AGENT_DEFAULT.to_vec(),
|
_ => return hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec(),
|
||||||
};
|
};
|
||||||
let mut groups = Vec::new();
|
let mut groups = Vec::new();
|
||||||
for token in raw.split(',') {
|
for token in raw.split(',') {
|
||||||
let t = token.trim().to_ascii_lowercase();
|
let t = token.trim().to_ascii_lowercase();
|
||||||
// Parse via serde_json (the canonical deserialization path).
|
// Parse via serde_json (the canonical deserialization path).
|
||||||
if let Ok(g) = serde_json::from_value::<hive_sh4re::permissions::ToolGroup>(
|
if let Ok(g) =
|
||||||
serde_json::Value::String(t.clone()),
|
serde_json::from_value::<hive_sh4re::ToolGroup>(serde_json::Value::String(t.clone()))
|
||||||
) {
|
{
|
||||||
groups.push(g);
|
groups.push(g);
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!(token = %t, "{TOOL_GROUPS_ENV}: unknown tool group, skipping");
|
tracing::warn!(token = %t, "{TOOL_GROUPS_ENV}: unknown tool group, skipping");
|
||||||
|
|
@ -109,7 +109,7 @@ fn effective_tool_groups() -> Vec<hive_sh4re::permissions::ToolGroup> {
|
||||||
"{TOOL_GROUPS_ENV} set but contained no recognised groups; \
|
"{TOOL_GROUPS_ENV} set but contained no recognised groups; \
|
||||||
falling back to AGENT_DEFAULT"
|
falling back to AGENT_DEFAULT"
|
||||||
);
|
);
|
||||||
return hive_sh4re::permissions::ToolGroup::AGENT_DEFAULT.to_vec();
|
return hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec();
|
||||||
}
|
}
|
||||||
groups
|
groups
|
||||||
}
|
}
|
||||||
|
|
@ -124,9 +124,9 @@ fn effective_tool_groups() -> Vec<hive_sh4re::permissions::ToolGroup> {
|
||||||
/// an out-of-process server has **no** later enforcement point — once it is
|
/// an out-of-process server has **no** later enforcement point — once it is
|
||||||
/// in the claude MCP config the agent can call it. So this gate, applied at
|
/// in the claude MCP config the agent can call it. So this gate, applied at
|
||||||
/// config-render time, is the security boundary for those servers.
|
/// config-render time, is the security boundary for those servers.
|
||||||
fn extra_server_required_group(server: &str) -> Option<hive_sh4re::permissions::ToolGroup> {
|
fn extra_server_required_group(server: &str) -> Option<hive_sh4re::ToolGroup> {
|
||||||
match server {
|
match server {
|
||||||
"bash" => Some(hive_sh4re::permissions::ToolGroup::Execution),
|
"bash" => Some(hive_sh4re::ToolGroup::Execution),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -134,14 +134,14 @@ fn extra_server_required_group(server: &str) -> Option<hive_sh4re::permissions::
|
||||||
/// Whether an extra MCP server should be exposed to claude given the active
|
/// Whether an extra MCP server should be exposed to claude given the active
|
||||||
/// tool `groups`. A gated server (see [`extra_server_required_group`]) is
|
/// tool `groups`. A gated server (see [`extra_server_required_group`]) is
|
||||||
/// suppressed when the agent lacks its required group.
|
/// suppressed when the agent lacks its required group.
|
||||||
fn extra_server_enabled(server: &str, groups: &[hive_sh4re::permissions::ToolGroup]) -> bool {
|
fn extra_server_enabled(server: &str, groups: &[hive_sh4re::ToolGroup]) -> bool {
|
||||||
extra_server_required_group(server).is_none_or(|required| groups.contains(&required))
|
extra_server_required_group(server).is_none_or(|required| groups.contains(&required))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod extra_server_gate_tests {
|
mod extra_server_gate_tests {
|
||||||
use super::{extra_server_enabled, extra_server_required_group};
|
use super::{extra_server_enabled, extra_server_required_group};
|
||||||
use hive_sh4re::permissions::ToolGroup;
|
use hive_sh4re::ToolGroup;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bash_is_gated_behind_execution() {
|
fn bash_is_gated_behind_execution() {
|
||||||
|
|
@ -173,13 +173,13 @@ mod extra_server_gate_tests {
|
||||||
/// requires updating the matching `ToolGroup::tools()` slice in hive-sh4re
|
/// requires updating the matching `ToolGroup::tools()` slice in hive-sh4re
|
||||||
/// (single source of truth). See `docs/conventions.md::Tool groups`.
|
/// (single source of truth). See `docs/conventions.md::Tool groups`.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn allowed_mcp_tools(groups: &[hive_sh4re::permissions::ToolGroup]) -> Vec<String> {
|
pub fn allowed_mcp_tools(groups: &[hive_sh4re::ToolGroup]) -> Vec<String> {
|
||||||
// Collect all tool names, deduplicating while preserving order.
|
// Collect all tool names, deduplicating while preserving order.
|
||||||
// Always-on tools (e.g. `set_status`) come first so they're present
|
// Always-on tools (e.g. `set_status`) come first so they're present
|
||||||
// regardless of which groups the agent is granted — a misconfigured
|
// regardless of which groups the agent is granted — a misconfigured
|
||||||
// agent still has to be able to report its dashboard status.
|
// agent still has to be able to report its dashboard status.
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
let mut out: Vec<String> = hive_sh4re::permissions::ToolGroup::ALWAYS_ON_TOOLS
|
let mut out: Vec<String> = hive_sh4re::ToolGroup::ALWAYS_ON_TOOLS
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.chain(groups.iter().flat_map(|g| g.tools().iter().copied()))
|
.chain(groups.iter().flat_map(|g| g.tools().iter().copied()))
|
||||||
|
|
@ -406,7 +406,7 @@ fn build_mcp_servers() -> serde_json::Map<String, serde_json::Value> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{SERVER_NAME, allowed_mcp_tools};
|
use super::{SERVER_NAME, allowed_mcp_tools};
|
||||||
use hive_sh4re::permissions::ToolGroup;
|
use hive_sh4re::ToolGroup;
|
||||||
|
|
||||||
fn qualified(tool: &str) -> String {
|
fn qualified(tool: &str) -> String {
|
||||||
format!("mcp__{SERVER_NAME}__{tool}")
|
format!("mcp__{SERVER_NAME}__{tool}")
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ use std::sync::Mutex;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use hive_sh4re::approvals::ReminderStats;
|
use hive_sh4re::ReminderStats;
|
||||||
use hive_sh4re::wire_time;
|
use hive_sh4re::wire_time;
|
||||||
use rusqlite::{Connection, params};
|
use rusqlite::{Connection, params};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use chrono::Utc;
|
||||||
use rusqlite::{Connection, OpenFlags};
|
use rusqlite::{Connection, OpenFlags};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use hive_sh4re::approvals::ReminderStats;
|
use hive_sh4re::ReminderStats;
|
||||||
|
|
||||||
/// Window param accepted by `/api/stats?window=`. Each maps to a
|
/// Window param accepted by `/api/stats?window=`. Each maps to a
|
||||||
/// total span + the bucket width used to roll up trend series.
|
/// total span + the bucket width used to roll up trend series.
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ pub(super) async fn api_stats(
|
||||||
/// `HIVE_AGENT_SOCKET` — was a broker RPC before reminders moved
|
/// `HIVE_AGENT_SOCKET` — was a broker RPC before reminders moved
|
||||||
/// in-container. Returns `None` on any transport / decode failure or
|
/// in-container. Returns `None` on any transport / decode failure or
|
||||||
/// when the socket is unset — the stats are decorative, not authoritative.
|
/// when the socket is unset — the stats are decorative, not authoritative.
|
||||||
async fn fetch_reminder_stats(window_secs: u64) -> Option<hive_sh4re::approvals::ReminderStats> {
|
async fn fetch_reminder_stats(window_secs: u64) -> Option<hive_sh4re::ReminderStats> {
|
||||||
match crate::todo_server::dial(&hive_agent_sock::Request::ReminderRollup {
|
match crate::todo_server::dial(&hive_agent_sock::Request::ReminderRollup {
|
||||||
since_secs: window_secs,
|
since_secs: window_secs,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, bail};
|
use anyhow::{Context as _, Result, bail};
|
||||||
use hive_sh4re::approvals::{ApprovalKind, ApprovalStatus};
|
use hive_sh4re::{ApprovalKind, ApprovalStatus, HelperEvent};
|
||||||
use hive_sh4re::manager::HelperEvent;
|
|
||||||
|
|
||||||
use crate::coordinator::Coordinator;
|
use crate::coordinator::Coordinator;
|
||||||
use crate::lifecycle;
|
use crate::lifecycle;
|
||||||
|
|
@ -143,7 +142,7 @@ fn rollback_ref(approval_id: i64) -> String {
|
||||||
/// `pr` and `reviewed` are fields of the approval row the operator signed off
|
/// `pr` and `reviewed` are fields of the approval row the operator signed off
|
||||||
/// on — so re-reading is both cheap and the authoritative source of truth.
|
/// on — so re-reading is both cheap and the authoritative source of truth.
|
||||||
struct DeployCtx {
|
struct DeployCtx {
|
||||||
approval: hive_sh4re::approvals::Approval,
|
approval: hive_sh4re::Approval,
|
||||||
/// PR number, parsed from `approval.commit_ref`.
|
/// PR number, parsed from `approval.commit_ref`.
|
||||||
pr: u64,
|
pr: u64,
|
||||||
/// The PR head sha the operator reviewed (`approval.fetched_sha`).
|
/// The PR head sha the operator reviewed (`approval.fetched_sha`).
|
||||||
|
|
@ -411,7 +410,7 @@ const PR_FAIL_LOG_TAIL_BYTES: usize = 4000;
|
||||||
/// only the error text.
|
/// only the error text.
|
||||||
async fn post_merge_failure_to_pr(
|
async fn post_merge_failure_to_pr(
|
||||||
coord: &Arc<Coordinator>,
|
coord: &Arc<Coordinator>,
|
||||||
approval: &hive_sh4re::approvals::Approval,
|
approval: &hive_sh4re::Approval,
|
||||||
err: &anyhow::Error,
|
err: &anyhow::Error,
|
||||||
) {
|
) {
|
||||||
let Ok(pr) = approval.commit_ref.parse::<u64>() else {
|
let Ok(pr) = approval.commit_ref.parse::<u64>() else {
|
||||||
|
|
@ -473,12 +472,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::approvals::Approval,
|
approval: hive_sh4re::Approval,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let result: Result<()> = async {
|
let result: Result<()> = async {
|
||||||
let payload: hive_sh4re::manager::SchedulePromptPayload =
|
let payload: hive_sh4re::SchedulePromptPayload = serde_json::from_str(&approval.commit_ref)
|
||||||
serde_json::from_str(&approval.commit_ref)
|
.context("decode SchedulePromptPayload from approval.commit_ref")?;
|
||||||
.context("decode SchedulePromptPayload from approval.commit_ref")?;
|
|
||||||
coord
|
coord
|
||||||
.scheduled_prompts
|
.scheduled_prompts
|
||||||
.submit(&crate::scheduled_prompts::NewSchedule {
|
.submit(&crate::scheduled_prompts::NewSchedule {
|
||||||
|
|
@ -600,7 +598,7 @@ fn fetch_approval_for_worker(
|
||||||
coord: &Coordinator,
|
coord: &Coordinator,
|
||||||
approval_id: i64,
|
approval_id: i64,
|
||||||
expected_kind: ApprovalKind,
|
expected_kind: ApprovalKind,
|
||||||
) -> Result<hive_sh4re::approvals::Approval> {
|
) -> Result<hive_sh4re::Approval> {
|
||||||
let approval = coord
|
let approval = coord
|
||||||
.approvals
|
.approvals
|
||||||
.get(approval_id)
|
.get(approval_id)
|
||||||
|
|
@ -643,7 +641,7 @@ async fn forge_after_first_spawn(coord: &Arc<Coordinator>, agent: &str) {
|
||||||
/// work that doesn't justify a queue card.
|
/// work that doesn't justify a queue card.
|
||||||
async fn run_approval_init_config(
|
async fn run_approval_init_config(
|
||||||
coord: &Coordinator,
|
coord: &Coordinator,
|
||||||
approval: hive_sh4re::approvals::Approval,
|
approval: hive_sh4re::Approval,
|
||||||
proposed_dir: std::path::PathBuf,
|
proposed_dir: std::path::PathBuf,
|
||||||
claude_dir: std::path::PathBuf,
|
claude_dir: std::path::PathBuf,
|
||||||
notes_dir: std::path::PathBuf,
|
notes_dir: std::path::PathBuf,
|
||||||
|
|
@ -683,7 +681,7 @@ async fn run_approval_init_config(
|
||||||
|
|
||||||
async fn finish_approval(
|
async fn finish_approval(
|
||||||
coord: &Coordinator,
|
coord: &Coordinator,
|
||||||
approval: &hive_sh4re::approvals::Approval,
|
approval: &hive_sh4re::Approval,
|
||||||
result: Result<()>,
|
result: Result<()>,
|
||||||
terminal_tag: Option<String>,
|
terminal_tag: Option<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
|
@ -962,7 +960,7 @@ pub async fn destroy(coord: &Arc<Coordinator>, name: &str, purge: bool) -> Resul
|
||||||
drop(guard);
|
drop(guard);
|
||||||
let _ = coord
|
let _ = coord
|
||||||
.push_todo(
|
.push_todo(
|
||||||
hive_sh4re::manager::MANAGER_AGENT,
|
hive_sh4re::MANAGER_AGENT,
|
||||||
"core",
|
"core",
|
||||||
Some(format!("destroyed:{name}")),
|
Some(format!("destroyed:{name}")),
|
||||||
format!("agent '{name}' destroyed"),
|
format!("agent '{name}' destroyed"),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
//! and `tool-groups.json`.
|
//! and `tool-groups.json`.
|
||||||
//!
|
//!
|
||||||
//! Format: a JSON object mapping agent name to an array of
|
//! Format: a JSON object mapping agent name to an array of
|
||||||
//! `hive_sh4re::permissions::Capability` `snake_case` strings:
|
//! `hive_sh4re::Capability` `snake_case` strings:
|
||||||
//!
|
//!
|
||||||
//! ```json
|
//! ```json
|
||||||
//! {
|
//! {
|
||||||
|
|
@ -52,7 +52,7 @@ pub fn caps_for(name: &str) -> Vec<String> {
|
||||||
|
|
||||||
/// Check whether an agent holds a specific capability.
|
/// Check whether an agent holds a specific capability.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn has_cap(name: &str, cap: hive_sh4re::permissions::Capability) -> bool {
|
pub fn has_cap(name: &str, cap: hive_sh4re::Capability) -> bool {
|
||||||
caps_for(name)
|
caps_for(name)
|
||||||
.iter()
|
.iter()
|
||||||
.any(|s| s.eq_ignore_ascii_case(cap.as_str()))
|
.any(|s| s.eq_ignore_ascii_case(cap.as_str()))
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
//! and the meta `flake.nix`.
|
//! and the meta `flake.nix`.
|
||||||
//!
|
//!
|
||||||
//! Format: a JSON object mapping agent name to an array of
|
//! Format: a JSON object mapping agent name to an array of
|
||||||
//! `hive_sh4re::permissions::ToolGroup` `snake_case` strings:
|
//! `hive_sh4re::ToolGroup` `snake_case` strings:
|
||||||
//!
|
//!
|
||||||
//! ```json
|
//! ```json
|
||||||
//! {
|
//! {
|
||||||
|
|
@ -70,7 +70,7 @@ fn write(map: &BTreeMap<String, Vec<String>>) -> std::io::Result<()> {
|
||||||
/// Returns `Ok(())` when all names are known, or `Err` listing the
|
/// Returns `Ok(())` when all names are known, or `Err` listing the
|
||||||
/// unrecognised names so callers can surface a useful error message.
|
/// unrecognised names so callers can surface a useful error message.
|
||||||
pub fn validate_groups(groups: &[String]) -> anyhow::Result<()> {
|
pub fn validate_groups(groups: &[String]) -> anyhow::Result<()> {
|
||||||
let valid: std::collections::BTreeSet<&str> = hive_sh4re::permissions::ToolGroup::ALL
|
let valid: std::collections::BTreeSet<&str> = hive_sh4re::ToolGroup::ALL
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| g.as_str())
|
.map(|g| g.as_str())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -85,7 +85,7 @@ pub fn validate_groups(groups: &[String]) -> anyhow::Result<()> {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"unknown tool group(s): {}; valid names are: {}",
|
"unknown tool group(s): {}; valid names are: {}",
|
||||||
unknown.join(", "),
|
unknown.join(", "),
|
||||||
hive_sh4re::permissions::ToolGroup::ALL
|
hive_sh4re::ToolGroup::ALL
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| g.as_str())
|
.map(|g| g.as_str())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ pub fn top_level_agents_in(topo: &BTreeMap<String, Option<String>>) -> Vec<Strin
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a magic recipient sentinel (currently just
|
/// Resolve a magic recipient sentinel (currently just
|
||||||
/// [`hive_sh4re::manager::PARENT_RECIPIENT`]) to a real broker recipient at
|
/// [`hive_sh4re::PARENT_RECIPIENT`]) to a real broker recipient at
|
||||||
/// send time. Returns an owned `String` so callers can plug it
|
/// send time. Returns an owned `String` so callers can plug it
|
||||||
/// straight into [`crate::broker::Broker::send`] without
|
/// straight into [`crate::broker::Broker::send`] without
|
||||||
/// borrow-juggling around the temporary lookup.
|
/// borrow-juggling around the temporary lookup.
|
||||||
|
|
@ -116,7 +116,7 @@ pub fn top_level_agents_in(topo: &BTreeMap<String, Option<String>>) -> Vec<Strin
|
||||||
pub fn resolve_recipient(sender: &str, to: &str) -> String {
|
pub fn resolve_recipient(sender: &str, to: &str) -> String {
|
||||||
// Early exit: only sentinel recipients need topology lookup. This
|
// Early exit: only sentinel recipients need topology lookup. This
|
||||||
// keeps the cost of a normal `send` at one string comparison.
|
// keeps the cost of a normal `send` at one string comparison.
|
||||||
if to != hive_sh4re::manager::PARENT_RECIPIENT {
|
if to != hive_sh4re::PARENT_RECIPIENT {
|
||||||
return to.to_owned();
|
return to.to_owned();
|
||||||
}
|
}
|
||||||
resolve_recipient_in(&read(), sender, to)
|
resolve_recipient_in(&read(), sender, to)
|
||||||
|
|
@ -131,11 +131,11 @@ pub fn resolve_recipient_in(
|
||||||
sender: &str,
|
sender: &str,
|
||||||
to: &str,
|
to: &str,
|
||||||
) -> String {
|
) -> String {
|
||||||
if to == hive_sh4re::manager::PARENT_RECIPIENT {
|
if to == hive_sh4re::PARENT_RECIPIENT {
|
||||||
topo.get(sender)
|
topo.get(sender)
|
||||||
.cloned()
|
.cloned()
|
||||||
.flatten()
|
.flatten()
|
||||||
.unwrap_or_else(|| hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned())
|
.unwrap_or_else(|| hive_sh4re::OPERATOR_RECIPIENT.to_owned())
|
||||||
} else {
|
} else {
|
||||||
to.to_owned()
|
to.to_owned()
|
||||||
}
|
}
|
||||||
|
|
@ -722,8 +722,8 @@ mod tests {
|
||||||
assert_eq!(resolve_recipient_in(&topo, "bob", "alice"), "alice");
|
assert_eq!(resolve_recipient_in(&topo, "bob", "alice"), "alice");
|
||||||
assert_eq!(resolve_recipient_in(&topo, "bob", "*"), "*");
|
assert_eq!(resolve_recipient_in(&topo, "bob", "*"), "*");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_recipient_in(&topo, "bob", hive_sh4re::manager::OPERATOR_RECIPIENT),
|
resolve_recipient_in(&topo, "bob", hive_sh4re::OPERATOR_RECIPIENT),
|
||||||
hive_sh4re::manager::OPERATOR_RECIPIENT
|
hive_sh4re::OPERATOR_RECIPIENT
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -732,12 +732,12 @@ mod tests {
|
||||||
let topo = topo_three_level();
|
let topo = topo_three_level();
|
||||||
// bob's parent is alice → `<parent>` from bob goes to alice.
|
// bob's parent is alice → `<parent>` from bob goes to alice.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_recipient_in(&topo, "bob", hive_sh4re::manager::PARENT_RECIPIENT),
|
resolve_recipient_in(&topo, "bob", hive_sh4re::PARENT_RECIPIENT),
|
||||||
"alice"
|
"alice"
|
||||||
);
|
);
|
||||||
// alice's parent is the manager — same one-hop rewrite.
|
// alice's parent is the manager — same one-hop rewrite.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_recipient_in(&topo, "alice", hive_sh4re::manager::PARENT_RECIPIENT),
|
resolve_recipient_in(&topo, "alice", hive_sh4re::PARENT_RECIPIENT),
|
||||||
crate::lifecycle::MANAGER_NAME
|
crate::lifecycle::MANAGER_NAME
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -752,9 +752,9 @@ mod tests {
|
||||||
resolve_recipient_in(
|
resolve_recipient_in(
|
||||||
&topo,
|
&topo,
|
||||||
crate::lifecycle::MANAGER_NAME,
|
crate::lifecycle::MANAGER_NAME,
|
||||||
hive_sh4re::manager::PARENT_RECIPIENT
|
hive_sh4re::PARENT_RECIPIENT
|
||||||
),
|
),
|
||||||
hive_sh4re::manager::OPERATOR_RECIPIENT
|
hive_sh4re::OPERATOR_RECIPIENT
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -766,8 +766,8 @@ mod tests {
|
||||||
// its row.
|
// its row.
|
||||||
let topo = topo_three_level();
|
let topo = topo_three_level();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_recipient_in(&topo, "nobody", hive_sh4re::manager::PARENT_RECIPIENT),
|
resolve_recipient_in(&topo, "nobody", hive_sh4re::PARENT_RECIPIENT),
|
||||||
hive_sh4re::manager::OPERATOR_RECIPIENT
|
hive_sh4re::OPERATOR_RECIPIENT
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -696,7 +696,7 @@ impl Coordinator {
|
||||||
/// rebuild-queue worker after a `PermChange` / Capabilities entry
|
/// rebuild-queue worker after a `PermChange` / Capabilities entry
|
||||||
/// commits the JSON file, so the P3RM1SS10NS tab updates live.
|
/// commits the JSON file, so the P3RM1SS10NS tab updates live.
|
||||||
pub fn emit_capabilities_snapshot(self: &Arc<Self>) {
|
pub fn emit_capabilities_snapshot(self: &Arc<Self>) {
|
||||||
use hive_sh4re::permissions::Capability;
|
use hive_sh4re::Capability;
|
||||||
let caps = Capability::ALL.iter().map(|c| c.as_str()).collect();
|
let caps = Capability::ALL.iter().map(|c| c.as_str()).collect();
|
||||||
let descriptions = Capability::ALL
|
let descriptions = Capability::ALL
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -723,7 +723,7 @@ impl Coordinator {
|
||||||
/// rebuild-queue worker after a `PermChange` / `ToolGroups` entry
|
/// rebuild-queue worker after a `PermChange` / `ToolGroups` entry
|
||||||
/// commits the JSON file, so the P3RM1SS10NS tab updates live.
|
/// commits the JSON file, so the P3RM1SS10NS tab updates live.
|
||||||
pub fn emit_tool_groups_snapshot(self: &Arc<Self>) {
|
pub fn emit_tool_groups_snapshot(self: &Arc<Self>) {
|
||||||
use hive_sh4re::permissions::ToolGroup;
|
use hive_sh4re::ToolGroup;
|
||||||
let groups = ToolGroup::ALL.iter().map(|g| g.as_str()).collect();
|
let groups = ToolGroup::ALL.iter().map(|g| g.as_str()).collect();
|
||||||
let descriptions = ToolGroup::ALL
|
let descriptions = ToolGroup::ALL
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -1025,7 +1025,7 @@ impl Coordinator {
|
||||||
/// `<parent>` sentinel's "root → operator" routing (see
|
/// `<parent>` sentinel's "root → operator" routing (see
|
||||||
/// `docs/conventions.md::Recipient sentinels`). The
|
/// `docs/conventions.md::Recipient sentinels`). The
|
||||||
/// notifications fire as ordinary broker messages with
|
/// notifications fire as ordinary broker messages with
|
||||||
/// `from = hive_sh4re::manager::SYSTEM_SENDER` so the dashboard renders
|
/// `from = hive_sh4re::SYSTEM_SENDER` so the dashboard renders
|
||||||
/// them under the existing system-source styling.
|
/// them under the existing system-source styling.
|
||||||
///
|
///
|
||||||
/// First validation failure aborts the whole batch with no disk writes.
|
/// First validation failure aborts the whole batch with no disk writes.
|
||||||
|
|
@ -1056,7 +1056,7 @@ impl Coordinator {
|
||||||
let new_label = new_parent.unwrap_or("<root>");
|
let new_label = new_parent.unwrap_or("<root>");
|
||||||
if let Some(op) = old_parent.as_deref() {
|
if let Some(op) = old_parent.as_deref() {
|
||||||
let _ = self.broker.send(&hive_sh4re::Message {
|
let _ = self.broker.send(&hive_sh4re::Message {
|
||||||
from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::SYSTEM_SENDER),
|
from: hive_sh4re::trusted_sender(hive_sh4re::SYSTEM_SENDER),
|
||||||
to: op.to_owned(),
|
to: op.to_owned(),
|
||||||
body: format!("{child} moved out of your subtree to {new_label}"),
|
body: format!("{child} moved out of your subtree to {new_label}"),
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
|
|
@ -1064,7 +1064,7 @@ impl Coordinator {
|
||||||
}
|
}
|
||||||
if let Some(np) = new_parent {
|
if let Some(np) = new_parent {
|
||||||
let _ = self.broker.send(&hive_sh4re::Message {
|
let _ = self.broker.send(&hive_sh4re::Message {
|
||||||
from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::SYSTEM_SENDER),
|
from: hive_sh4re::trusted_sender(hive_sh4re::SYSTEM_SENDER),
|
||||||
to: np.to_owned(),
|
to: np.to_owned(),
|
||||||
body: format!(
|
body: format!(
|
||||||
"{child} just moved into your subtree (was previously under {old_label})"
|
"{child} just moved into your subtree (was previously under {old_label})"
|
||||||
|
|
@ -1345,7 +1345,7 @@ impl Coordinator {
|
||||||
still in your window."
|
still in your window."
|
||||||
);
|
);
|
||||||
if let Err(e) = self.broker.send(&hive_sh4re::Message {
|
if let Err(e) = self.broker.send(&hive_sh4re::Message {
|
||||||
from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::SYSTEM_SENDER),
|
from: hive_sh4re::trusted_sender(hive_sh4re::SYSTEM_SENDER),
|
||||||
to: name.to_owned(),
|
to: name.to_owned(),
|
||||||
body,
|
body,
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
|
|
@ -1358,8 +1358,8 @@ impl Coordinator {
|
||||||
/// `Message::body`; sender = `SYSTEM_SENDER`. The manager harness
|
/// `Message::body`; sender = `SYSTEM_SENDER`. The manager harness
|
||||||
/// recognises the sender and parses the body. Best-effort: a serde or
|
/// recognises the sender and parses the body. Best-effort: a serde or
|
||||||
/// broker error is logged but does not propagate.
|
/// broker error is logged but does not propagate.
|
||||||
pub fn notify_manager(&self, event: &hive_sh4re::manager::HelperEvent) {
|
pub fn notify_manager(&self, event: &hive_sh4re::HelperEvent) {
|
||||||
self.notify_agent(hive_sh4re::manager::MANAGER_AGENT, event);
|
self.notify_agent(hive_sh4re::MANAGER_AGENT, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Route an approval-scoped helper event to the agent that submitted
|
/// Route an approval-scoped helper event to the agent that submitted
|
||||||
|
|
@ -1367,7 +1367,7 @@ impl Coordinator {
|
||||||
/// time). Legacy rows with no recorded submitter — and any lookup
|
/// time). Legacy rows with no recorded submitter — and any lookup
|
||||||
/// failure — fall back to the root agent, preserving the prior
|
/// failure — fall back to the root agent, preserving the prior
|
||||||
/// always-root behaviour.
|
/// always-root behaviour.
|
||||||
pub fn notify_submitter(&self, approval_id: i64, event: &hive_sh4re::manager::HelperEvent) {
|
pub fn notify_submitter(&self, approval_id: i64, event: &hive_sh4re::HelperEvent) {
|
||||||
let target = self.submitter_or_manager(approval_id);
|
let target = self.submitter_or_manager(approval_id);
|
||||||
self.notify_agent(&target, event);
|
self.notify_agent(&target, event);
|
||||||
}
|
}
|
||||||
|
|
@ -1381,7 +1381,7 @@ impl Coordinator {
|
||||||
self.approvals
|
self.approvals
|
||||||
.submitter_of(approval_id)
|
.submitter_of(approval_id)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.unwrap_or_else(|| hive_sh4re::manager::MANAGER_AGENT.to_owned())
|
.unwrap_or_else(|| hive_sh4re::MANAGER_AGENT.to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push a todo directly into `agent`'s in-container todo store — a
|
/// Push a todo directly into `agent`'s in-container todo store — a
|
||||||
|
|
@ -1471,20 +1471,15 @@ impl Coordinator {
|
||||||
/// body = JSON-encoded event). Used to route `QuestionAnswered`
|
/// body = JSON-encoded event). Used to route `QuestionAnswered`
|
||||||
/// events back to the agent that called `ask`, `QuestionAsked`
|
/// events back to the agent that called `ask`, `QuestionAsked`
|
||||||
/// events to the target of a peer question, etc.
|
/// events to the target of a peer question, etc.
|
||||||
pub fn notify_agent(&self, agent: &str, event: &hive_sh4re::manager::HelperEvent) {
|
pub fn notify_agent(&self, agent: &str, event: &hive_sh4re::HelperEvent) {
|
||||||
self.notify_agent_from(hive_sh4re::manager::SYSTEM_SENDER, agent, event);
|
self.notify_agent_from(hive_sh4re::SYSTEM_SENDER, agent, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as `notify_agent` but with an explicit sender. Use this
|
/// Same as `notify_agent` but with an explicit sender. Use this
|
||||||
/// when the event originates from a known agent or the operator
|
/// when the event originates from a known agent or the operator
|
||||||
/// (e.g. `QuestionAnswered` — the answerer should be the `from`,
|
/// (e.g. `QuestionAnswered` — the answerer should be the `from`,
|
||||||
/// not `system`) so the recipient's terminal shows the right name.
|
/// not `system`) so the recipient's terminal shows the right name.
|
||||||
pub fn notify_agent_from(
|
pub fn notify_agent_from(&self, from: &str, agent: &str, event: &hive_sh4re::HelperEvent) {
|
||||||
&self,
|
|
||||||
from: &str,
|
|
||||||
agent: &str,
|
|
||||||
event: &hive_sh4re::manager::HelperEvent,
|
|
||||||
) {
|
|
||||||
let body = match serde_json::to_string(event) {
|
let body = match serde_json::to_string(event) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -1493,7 +1488,7 @@ impl Coordinator {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Err(e) = self.broker.send(&hive_sh4re::Message {
|
if let Err(e) = self.broker.send(&hive_sh4re::Message {
|
||||||
from: hive_sh4re::manager::trusted_sender(from),
|
from: hive_sh4re::trusted_sender(from),
|
||||||
to: agent.to_owned(),
|
to: agent.to_owned(),
|
||||||
body,
|
body,
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
|
|
@ -1515,7 +1510,7 @@ impl Coordinator {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Err(e) = self.broker.send(&hive_sh4re::Message {
|
if let Err(e) = self.broker.send(&hive_sh4re::Message {
|
||||||
from: hive_sh4re::manager::trusted_sender(from),
|
from: hive_sh4re::trusted_sender(from),
|
||||||
to: agent_name.clone(),
|
to: agent_name.clone(),
|
||||||
body: broadcast_body.clone(),
|
body: broadcast_body.clone(),
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use axum::{
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use hive_sh4re::approvals::Approval;
|
use hive_sh4re::Approval;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
|
@ -87,8 +87,7 @@ pub(super) fn gc_orphans(coord: &Coordinator, approvals: Vec<Approval>) -> Vec<A
|
||||||
// the proposed dir is supposed to be missing.
|
// the proposed dir is supposed to be missing.
|
||||||
if matches!(
|
if matches!(
|
||||||
a.kind,
|
a.kind,
|
||||||
hive_sh4re::approvals::ApprovalKind::Spawn
|
hive_sh4re::ApprovalKind::Spawn | hive_sh4re::ApprovalKind::InitConfig
|
||||||
| hive_sh4re::approvals::ApprovalKind::InitConfig
|
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ pub(super) async fn post_op_send(
|
||||||
if to == "*" {
|
if to == "*" {
|
||||||
let errors = state
|
let errors = state
|
||||||
.coord
|
.coord
|
||||||
.broadcast_send(hive_sh4re::manager::OPERATOR_RECIPIENT, &body);
|
.broadcast_send(hive_sh4re::OPERATOR_RECIPIENT, &body);
|
||||||
if !errors.is_empty() {
|
if !errors.is_empty() {
|
||||||
return error_response(&format!(
|
return error_response(&format!(
|
||||||
"op-send broadcast partial fail: {}",
|
"op-send broadcast partial fail: {}",
|
||||||
|
|
@ -228,7 +228,7 @@ pub(super) async fn post_op_send(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} else if let Err(e) = state.coord.broker.send(&hive_sh4re::Message {
|
} else if let Err(e) = state.coord.broker.send(&hive_sh4re::Message {
|
||||||
from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::OPERATOR_RECIPIENT),
|
from: hive_sh4re::trusted_sender(hive_sh4re::OPERATOR_RECIPIENT),
|
||||||
to: to.clone(),
|
to: to.clone(),
|
||||||
body,
|
body,
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
|
|
@ -270,7 +270,7 @@ pub(super) async fn post_request_spawn(
|
||||||
}
|
}
|
||||||
match state.coord.approvals.submit_kind(
|
match state.coord.approvals.submit_kind(
|
||||||
&name,
|
&name,
|
||||||
hive_sh4re::approvals::ApprovalKind::Spawn,
|
hive_sh4re::ApprovalKind::Spawn,
|
||||||
"",
|
"",
|
||||||
None,
|
None,
|
||||||
"operator",
|
"operator",
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,11 @@ pub(super) struct ToolGroupsSnapshot {
|
||||||
pub(super) async fn get_tool_groups(
|
pub(super) async fn get_tool_groups(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> axum::Json<ToolGroupsSnapshot> {
|
) -> axum::Json<ToolGroupsSnapshot> {
|
||||||
let groups = hive_sh4re::permissions::ToolGroup::ALL
|
let groups = hive_sh4re::ToolGroup::ALL
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| g.as_str())
|
.map(|g| g.as_str())
|
||||||
.collect();
|
.collect();
|
||||||
let descriptions = hive_sh4re::permissions::ToolGroup::ALL
|
let descriptions = hive_sh4re::ToolGroup::ALL
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| (g.as_str(), g.description()))
|
.map(|g| (g.as_str(), g.description()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -80,7 +80,7 @@ pub(super) async fn get_tool_groups(
|
||||||
/// the container actually runs with.
|
/// the container actually runs with.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub(crate) fn tool_group_default_names() -> Vec<&'static str> {
|
pub(crate) fn tool_group_default_names() -> Vec<&'static str> {
|
||||||
hive_sh4re::permissions::ToolGroup::AGENT_DEFAULT
|
hive_sh4re::ToolGroup::AGENT_DEFAULT
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| g.as_str())
|
.map(|g| g.as_str())
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -204,7 +204,7 @@ pub(super) struct CapabilitiesSnapshot {
|
||||||
pub(super) async fn get_capabilities(
|
pub(super) async fn get_capabilities(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> axum::Json<CapabilitiesSnapshot> {
|
) -> axum::Json<CapabilitiesSnapshot> {
|
||||||
use hive_sh4re::permissions::Capability;
|
use hive_sh4re::Capability;
|
||||||
let caps = Capability::ALL.iter().map(|c| c.as_str()).collect();
|
let caps = Capability::ALL.iter().map(|c| c.as_str()).collect();
|
||||||
let descriptions = Capability::ALL
|
let descriptions = Capability::ALL
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -256,7 +256,7 @@ pub(super) async fn post_capabilities(
|
||||||
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
||||||
return Ok(reject);
|
return Ok(reject);
|
||||||
}
|
}
|
||||||
let known: Vec<&str> = hive_sh4re::permissions::Capability::ALL
|
let known: Vec<&str> = hive_sh4re::Capability::ALL
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.as_str())
|
.map(|c| c.as_str())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -334,7 +334,7 @@ pub(super) async fn post_permissions(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
axum::Json(body): axum::Json<BatchPermsBody>,
|
axum::Json(body): axum::Json<BatchPermsBody>,
|
||||||
) -> Result<Response, ProblemDetails> {
|
) -> Result<Response, ProblemDetails> {
|
||||||
let known_caps: Vec<&str> = hive_sh4re::permissions::Capability::ALL
|
let known_caps: Vec<&str> = hive_sh4re::Capability::ALL
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.as_str())
|
.map(|c| c.as_str())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
|
||||||
|
|
@ -64,34 +64,33 @@ pub(super) async fn post_answer_question(
|
||||||
.with_detail("answer: required"),
|
.with_detail("answer: required"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let resp =
|
let resp = match state
|
||||||
match state
|
.coord
|
||||||
.coord
|
.questions
|
||||||
.questions
|
.answer(id, answer, hive_sh4re::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,
|
|
||||||
question,
|
|
||||||
answer: answer.to_owned(),
|
|
||||||
answerer: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
state.coord.emit_question_resolved(
|
|
||||||
id,
|
id,
|
||||||
answer,
|
question,
|
||||||
hive_sh4re::manager::OPERATOR_RECIPIENT,
|
answer: answer.to_owned(),
|
||||||
false,
|
answerer: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
|
||||||
target.as_deref(),
|
},
|
||||||
);
|
);
|
||||||
(StatusCode::OK, "ok").into_response()
|
state.coord.emit_question_resolved(
|
||||||
}
|
id,
|
||||||
Err(e) => error_response(&format!("answer {id} failed: {e:#}")),
|
answer,
|
||||||
};
|
hive_sh4re::OPERATOR_RECIPIENT,
|
||||||
|
false,
|
||||||
|
target.as_deref(),
|
||||||
|
);
|
||||||
|
(StatusCode::OK, "ok").into_response()
|
||||||
|
}
|
||||||
|
Err(e) => error_response(&format!("answer {id} failed: {e:#}")),
|
||||||
|
};
|
||||||
with_cors(resp)
|
with_cors(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,25 +120,25 @@ pub(super) async fn post_cancel_question(
|
||||||
match state
|
match state
|
||||||
.coord
|
.coord
|
||||||
.questions
|
.questions
|
||||||
.answer(id, SENTINEL, hive_sh4re::manager::OPERATOR_RECIPIENT)
|
.answer(id, SENTINEL, hive_sh4re::OPERATOR_RECIPIENT)
|
||||||
{
|
{
|
||||||
Ok((question, asker, target)) => {
|
Ok((question, asker, target)) => {
|
||||||
tracing::info!(%id, %asker, "operator cancelled question");
|
tracing::info!(%id, %asker, "operator cancelled question");
|
||||||
state.coord.emit_question_resolved(
|
state.coord.emit_question_resolved(
|
||||||
id,
|
id,
|
||||||
SENTINEL,
|
SENTINEL,
|
||||||
hive_sh4re::manager::OPERATOR_RECIPIENT,
|
hive_sh4re::OPERATOR_RECIPIENT,
|
||||||
true,
|
true,
|
||||||
target.as_deref(),
|
target.as_deref(),
|
||||||
);
|
);
|
||||||
state.coord.notify_agent_from(
|
state.coord.notify_agent_from(
|
||||||
hive_sh4re::manager::OPERATOR_RECIPIENT,
|
hive_sh4re::OPERATOR_RECIPIENT,
|
||||||
&asker,
|
&asker,
|
||||||
&hive_sh4re::manager::HelperEvent::QuestionAnswered {
|
&hive_sh4re::HelperEvent::QuestionAnswered {
|
||||||
id,
|
id,
|
||||||
question,
|
question,
|
||||||
answer: SENTINEL.to_owned(),
|
answer: SENTINEL.to_owned(),
|
||||||
answerer: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(),
|
answerer: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
(StatusCode::OK, "ok").into_response()
|
(StatusCode::OK, "ok").into_response()
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ pub(super) async fn api_schedules(State(state): State<AppState>) -> Response {
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/api/schedules",
|
path = "/api/schedules",
|
||||||
// `hive_sh4re::manager::SchedulePromptPayload` (the actual body) has no `ToSchema` —
|
// `hive_sh4re::SchedulePromptPayload` (the actual body) has no `ToSchema` —
|
||||||
// same reasoning as the `Vec<serde_json::Value>` placeholder on
|
// same reasoning as the `Vec<serde_json::Value>` placeholder on
|
||||||
// `api_schedules` above.
|
// `api_schedules` above.
|
||||||
request_body(content = serde_json::Value, description = "SchedulePromptPayload wire shape"),
|
request_body(content = serde_json::Value, description = "SchedulePromptPayload wire shape"),
|
||||||
|
|
@ -82,7 +82,7 @@ pub(super) async fn api_schedules(State(state): State<AppState>) -> Response {
|
||||||
)]
|
)]
|
||||||
pub(super) async fn post_schedule_new(
|
pub(super) async fn post_schedule_new(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
axum::Json(payload): axum::Json<hive_sh4re::manager::SchedulePromptPayload>,
|
axum::Json(payload): axum::Json<hive_sh4re::SchedulePromptPayload>,
|
||||||
) -> Result<Response, ProblemDetails> {
|
) -> Result<Response, ProblemDetails> {
|
||||||
if payload.targets.is_empty() {
|
if payload.targets.is_empty() {
|
||||||
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
|
|
@ -97,7 +97,7 @@ pub(super) async fn post_schedule_new(
|
||||||
.with_detail("interval_seconds must be > 0 (use None for one-shot)"));
|
.with_detail("interval_seconds must be > 0 (use None for one-shot)"));
|
||||||
}
|
}
|
||||||
let new = crate::scheduled_prompts::NewSchedule {
|
let new = crate::scheduled_prompts::NewSchedule {
|
||||||
owner: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(),
|
owner: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
|
||||||
targets: payload.targets,
|
targets: payload.targets,
|
||||||
body: payload.body,
|
body: payload.body,
|
||||||
first_fire_at_unix: payload.first_fire_at_unix,
|
first_fire_at_unix: payload.first_fire_at_unix,
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue