feat(#1021): query_agent_state capability for agent socket GetLooseEnds/CountPendingReminders/ReminderRollup

This commit is contained in:
damocles 2026-06-01 21:21:25 +02:00
commit 4834ca413c
3 changed files with 109 additions and 30 deletions

View file

@ -307,26 +307,40 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
return resp;
}
match req {
AgentRequest::GetLooseEnds { .. } => match crate::loose_ends::for_agent(coord, agent) {
Ok(loose_ends) => AgentResponse::LooseEnds { loose_ends },
Err(e) => AgentResponse::Err {
message: format!("{e:#}"),
},
},
AgentRequest::CountPendingReminders { .. } => {
match coord.broker.count_pending_reminders_for(agent) {
Ok(count) => AgentResponse::PendingRemindersCount { count },
Err(e) => AgentResponse::Err {
message: format!("{e:#}"),
AgentRequest::GetLooseEnds { agent: target } => {
let name = resolve_agent_state_target(agent, target.as_deref());
match name {
Ok(name) => match crate::loose_ends::for_agent(coord, name) {
Ok(loose_ends) => AgentResponse::LooseEnds { loose_ends },
Err(e) => AgentResponse::Err {
message: format!("{e:#}"),
},
},
Err(message) => AgentResponse::Err { message },
}
}
AgentRequest::ReminderRollup { since_secs, .. } => {
match coord.broker.reminder_rollup_for(agent, *since_secs) {
Ok(stats) => AgentResponse::ReminderRollup(stats),
Err(e) => AgentResponse::Err {
message: format!("{e:#}"),
AgentRequest::CountPendingReminders { agent: target } => {
let name = resolve_agent_state_target(agent, target.as_deref());
match name {
Ok(name) => match coord.broker.count_pending_reminders_for(name) {
Ok(count) => AgentResponse::PendingRemindersCount { count },
Err(e) => AgentResponse::Err {
message: format!("{e:#}"),
},
},
Err(message) => AgentResponse::Err { message },
}
}
AgentRequest::ReminderRollup { since_secs, agent: target } => {
let name = resolve_agent_state_target(agent, target.as_deref());
match name {
Ok(name) => match coord.broker.reminder_rollup_for(name, *since_secs) {
Ok(stats) => AgentResponse::ReminderRollup(stats),
Err(e) => AgentResponse::Err {
message: format!("{e:#}"),
},
},
Err(message) => AgentResponse::Err { message },
}
}
// Manager-only variants are not valid on the agent socket.
@ -610,6 +624,34 @@ fn auto_reminder_path(agent: &str) -> String {
format!("/agents/{agent}/state/reminders/auto-{ts_ns}.md")
}
/// Resolve the target agent name for `GetLooseEnds`, `CountPendingReminders`,
/// and `ReminderRollup` on the agent socket. Returns the caller's own name
/// when no target is specified or when the caller lacks `query_agent_state`.
/// Returns an error string when the caller requests `"*"` (hive-wide scans
/// are manager-only) or requests another agent without the capability.
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 not available on the agent socket; \
use the manager socket for swarm-wide scans".to_owned()
),
Some(name) => {
if name == caller {
return Ok(caller);
}
if crate::capabilities::has_cap(caller, hive_sh4re::Capability::QueryAgentState) {
Ok(name)
} else {
Err(format!(
"agent `{caller}` does not have the query_agent_state capability; \
agent field ignored grant the capability to query other agents"
))
}
}
}
}
/// Resolve the `due_at` unix timestamp for a Remind request. Returns
/// distinct error messages for each failure mode (overflow on
/// `InSeconds`, pre-epoch clock, `i64` cast wrap) so the caller can tell