hive-agent-mcp, hive-c0re, hive-sh4re: drop agent param from get_loose_ends
get_loose_ends now always returns the caller's own loose ends, for every caller including the manager (ruth) — there is no separate manager surface, ruth is a normal agent with different default capabilities. - AgentGetLooseEndsArgs removed; get_loose_ends takes no args. - Wire Request::GetLooseEnds collapses from an Option<String> target to a unit variant. - hive-c0re's handle_get_loose_ends drops the "*" hive-wide branch and the subtree/capability resolver (resolve_agent_state_target); both are gone since there is no longer a target to resolve. - loose_ends::hive_wide and Capability::QueryAgentState removed as dead code — their only callers were the two functions above. - is_descendant_of is untouched (still used by lifecycle_handlers.rs and schedules.rs independently of this change). - Docs updated: docs/turn-loop/mcp.md, docs/web-ui/dashboard.md, docs/process/conventions.md (Loose-ends wire shape + capabilities table), plus the doc comments in hive-core-agent-sock, mcp_config.rs and capabilities.rs that described the old shape. Refs #4480
This commit is contained in:
parent
67ffb124cb
commit
729c5b4f42
12 changed files with 41 additions and 178 deletions
|
|
@ -7,8 +7,7 @@
|
|||
//!
|
||||
//! ```json
|
||||
//! {
|
||||
//! "atlas": ["read_host_journal"],
|
||||
//! "ruth": ["query_agent_state"]
|
||||
//! "atlas": ["read_host_journal"]
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! Loose-ends aggregator. Walks the `approvals` table once per call and
|
||||
//! assembles a `Vec<LooseEnd>` for either a single agent (`for_agent`) or
|
||||
//! the whole hive (`hive_wide`). `Request::GetLooseEnds` from either the
|
||||
//! agent or manager socket lands here so the routing logic + age-seconds
|
||||
//! derivation stay in one place. Reminders are agent-local (in-container
|
||||
//! assembles a `Vec<LooseEnd>` for a single agent (`for_agent`) — always
|
||||
//! the caller's own. `Request::GetLooseEnds` from either the agent or
|
||||
//! manager socket lands here so the age-seconds derivation stays in one
|
||||
//! place. Reminders are agent-local (in-container
|
||||
//! `hive-agent::reminders` store) and never sourced from here. The
|
||||
//! `ask`/`answer` MCP tools, their wire protocol (`hive-c0re::questions`,
|
||||
//! `stores::operator_questions`), and the operator dashboard's questions
|
||||
|
|
@ -75,21 +75,3 @@ pub fn for_agent(coord: &Coordinator, agent: &str) -> Result<Vec<LooseEnd>> {
|
|||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Hive-wide loose-ends view: EVERY pending approval. Manager surface
|
||||
/// only; sub-agents can't see each other's threads via the agent surface
|
||||
/// (`for_agent` filters by name).
|
||||
pub fn hive_wide(coord: &Coordinator) -> Result<Vec<LooseEnd>> {
|
||||
let now = Utc::now().timestamp();
|
||||
let mut out = Vec::new();
|
||||
for a in coord.approvals.pending()? {
|
||||
out.push(LooseEnd::Approval {
|
||||
id: a.id,
|
||||
agent: a.agent.to_string(),
|
||||
commit_ref: a.commit_ref,
|
||||
description: a.description,
|
||||
age_seconds: saturating_age(now, a.requested_at.timestamp()),
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -541,7 +541,6 @@ fn handle_requeue_inflight(
|
|||
/// Unified dispatch for every socket connection — per-agent sockets and the
|
||||
/// (now pure-transport) manager socket alike. There is no privilege bit;
|
||||
/// authority derives uniformly from the caller's identity: hive-wide
|
||||
/// agent-state queries require the `QueryAgentState` capability; hive-wide
|
||||
/// orchestration verbs (schedules / meta-inputs) require the matching
|
||||
/// tool-group (the grantable capability).
|
||||
async fn dispatch(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Response {
|
||||
|
|
@ -550,11 +549,7 @@ async fn dispatch(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Respo
|
|||
}
|
||||
match req {
|
||||
Request::ListDescendants => handle_list_descendants(coord, agent).await,
|
||||
// Agent-state queries: own subtree is free; other agents + the
|
||||
// hive-wide `"*"` sweep require `QueryAgentState`.
|
||||
Request::GetLooseEnds { agent: target } => {
|
||||
handle_get_loose_ends(coord, agent, target.as_deref())
|
||||
}
|
||||
Request::GetLooseEnds => handle_get_loose_ends(coord, agent),
|
||||
// Orchestration verbs — gated per-verb on tool-group membership
|
||||
// (see `dispatch_orchestration`).
|
||||
_ => dispatch_orchestration(req, agent, coord).await,
|
||||
|
|
@ -652,29 +647,9 @@ fn require_group(agent: &str, group: &str, action: &str) -> Option<Response> {
|
|||
}
|
||||
}
|
||||
|
||||
/// `GetLooseEnds` — read the target's loose ends. `None` / own / a subtree
|
||||
/// descendant resolve freely (a parent sees its subtree, the root sees all);
|
||||
/// any other named agent needs `QueryAgentState`; `"*"` is a hive-wide sweep
|
||||
/// gated on `QueryAgentState`.
|
||||
fn handle_get_loose_ends(coord: &Arc<Coordinator>, agent: &str, target: Option<&str>) -> Response {
|
||||
let result = if target == Some("*") {
|
||||
if !crate::capabilities::has_cap(
|
||||
agent,
|
||||
hive_sh4re::permissions::Capability::QueryAgentState,
|
||||
) {
|
||||
return Response::Err {
|
||||
message: "query_agent_state capability required for hive-wide loose ends"
|
||||
.to_owned(),
|
||||
};
|
||||
}
|
||||
crate::loose_ends::hive_wide(coord)
|
||||
} else {
|
||||
match resolve_agent_state_target(agent, target) {
|
||||
Ok(name) => crate::loose_ends::for_agent(coord, name),
|
||||
Err(message) => return Response::Err { message },
|
||||
}
|
||||
};
|
||||
match result {
|
||||
/// `GetLooseEnds` — always the caller's own loose ends.
|
||||
fn handle_get_loose_ends(coord: &Arc<Coordinator>, agent: &str) -> Response {
|
||||
match crate::loose_ends::for_agent(coord, agent) {
|
||||
Ok(loose_ends) => Response::LooseEnds { loose_ends },
|
||||
Err(e) => Response::Err {
|
||||
message: format!("{e:#}"),
|
||||
|
|
@ -773,44 +748,6 @@ fn check_can_cancel_approval(canceller: &str) -> Result<(), String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Resolve the target agent name for a *named* `GetLooseEnds` query. Rules:
|
||||
///
|
||||
/// - `None` (or `Some(caller)`) → the caller's own threads (always allowed).
|
||||
/// - `Some(descendant)` in the caller's subtree (the root's subtree is the whole hive) → allowed, no extra capability.
|
||||
/// - `Some(other)` outside the subtree → requires the `query_agent_state` capability; error otherwise.
|
||||
/// - `Some("*")` → rejected here; the hive-wide sweep is handled by `handle_get_loose_ends` under the same `query_agent_state` gate.
|
||||
fn resolve_agent_state_target<'a>(
|
||||
caller: &'a str,
|
||||
target: Option<&'a str>,
|
||||
) -> Result<&'a str, String> {
|
||||
match target {
|
||||
None => Ok(caller),
|
||||
Some("*") => Err(
|
||||
"hive-wide query (agent=\"*\") is only valid for loose-ends; \
|
||||
not available for this query"
|
||||
.to_owned(),
|
||||
),
|
||||
Some(name) => {
|
||||
// Own subtree (the root covers all) is visible without extra
|
||||
// capability; `is_descendant_of` returns true for `name == caller`.
|
||||
if crate::topology::is_descendant_of(name, caller) {
|
||||
return Ok(name);
|
||||
}
|
||||
if crate::capabilities::has_cap(
|
||||
caller,
|
||||
hive_sh4re::permissions::Capability::QueryAgentState,
|
||||
) {
|
||||
Ok(name)
|
||||
} else {
|
||||
Err(format!(
|
||||
"agent `{caller}` cannot query `{name}`: not in its subtree and \
|
||||
`query_agent_state` capability is not granted"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Field-named journal-query knobs for [`dispatch_host_journal`].
|
||||
/// Borrows straight from the matched `GetHostJournal` request variant.
|
||||
pub struct HostJournalArgs<'a> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue