fix(#1021): error on unauthorized target (not silent ignore); allow child targeting without cap
This commit is contained in:
parent
4834ca413c
commit
d35b7ab9b4
3 changed files with 44 additions and 38 deletions
|
|
@ -662,9 +662,9 @@ impl AgentServer {
|
||||||
inbox history. Output is a short bulleted list with ids, ages in seconds, and \
|
inbox history. Output is a short bulleted list with ids, ages in seconds, and \
|
||||||
the relevant context. Each `question` or `reminder` row can be cancelled by \
|
the relevant context. Each `question` or `reminder` row can be cancelled by \
|
||||||
passing its id + kind to `cancel_loose_end`. Empty result is reported clearly.\n\
|
passing its id + kind to `cancel_loose_end`. Empty result is reported clearly.\n\
|
||||||
Pass `agent: \"<name>\"` to inspect a specific peer agent's threads — requires \
|
Pass `agent: \"<name>\"` to inspect a specific peer agent's threads. Direct \
|
||||||
the `query_agent_state` capability; without it the field is ignored and results \
|
child agents are always accessible. For non-children, the `query_agent_state` \
|
||||||
are scoped to you."
|
capability is required — without it the request is rejected with an error."
|
||||||
)]
|
)]
|
||||||
async fn get_loose_ends(&self, Parameters(args): Parameters<AgentGetLooseEndsArgs>) -> String {
|
async fn get_loose_ends(&self, Parameters(args): Parameters<AgentGetLooseEndsArgs>) -> String {
|
||||||
run_tool_envelope("get_loose_ends", String::new(), async move {
|
run_tool_envelope("get_loose_ends", String::new(), async move {
|
||||||
|
|
@ -1061,12 +1061,12 @@ pub struct GetLooseEndsArgs {
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||||
pub struct AgentGetLooseEndsArgs {
|
pub struct AgentGetLooseEndsArgs {
|
||||||
/// Whose loose ends to list. Omit (or `null`) for your own. Pass a
|
/// Whose loose ends to list. Omit (or `null`) for your own. You may
|
||||||
/// specific agent name to inspect that agent's threads — requires
|
/// also pass a direct child agent's name without any extra capability.
|
||||||
/// the `query_agent_state` capability; without it the field is
|
/// Pass any other agent name to inspect their threads — requires the
|
||||||
/// ignored and results are scoped to you. The `"*"` hive-wide value
|
/// `query_agent_state` capability; without it the request is rejected
|
||||||
/// is not available on the agent socket; use the manager socket for
|
/// with an error. The `"*"` hive-wide value is not available on the
|
||||||
/// swarm-wide scans.
|
/// agent socket; use the manager socket for swarm-wide scans.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub agent: Option<String>,
|
pub agent: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -625,10 +625,15 @@ fn auto_reminder_path(agent: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the target agent name for `GetLooseEnds`, `CountPendingReminders`,
|
/// Resolve the target agent name for `GetLooseEnds`, `CountPendingReminders`,
|
||||||
/// and `ReminderRollup` on the agent socket. Returns the caller's own name
|
/// and `ReminderRollup` on the agent socket. Rules:
|
||||||
/// when no target is specified or when the caller lacks `query_agent_state`.
|
///
|
||||||
/// Returns an error string when the caller requests `"*"` (hive-wide scans
|
/// - `None` → caller's own threads (always allowed).
|
||||||
/// are manager-only) or requests another agent without the capability.
|
/// - `Some(caller)` → same as `None`.
|
||||||
|
/// - `Some("<child>")` where child is a direct descendant of caller per
|
||||||
|
/// `topology.json` → allowed without any extra capability.
|
||||||
|
/// - `Some("<other>")` where other is not a child → requires the
|
||||||
|
/// `query_agent_state` capability; returns an error otherwise.
|
||||||
|
/// - `Some("*")` → always rejected (hive-wide scans are manager-only).
|
||||||
fn resolve_agent_state_target<'a>(caller: &'a str, target: Option<&'a str>) -> Result<&'a str, String> {
|
fn resolve_agent_state_target<'a>(caller: &'a str, target: Option<&'a str>) -> Result<&'a str, String> {
|
||||||
match target {
|
match target {
|
||||||
None => Ok(caller),
|
None => Ok(caller),
|
||||||
|
|
@ -640,12 +645,16 @@ fn resolve_agent_state_target<'a>(caller: &'a str, target: Option<&'a str>) -> R
|
||||||
if name == caller {
|
if name == caller {
|
||||||
return Ok(caller);
|
return Ok(caller);
|
||||||
}
|
}
|
||||||
|
// Direct children are visible to their parent without extra capability.
|
||||||
|
if crate::topology::children_of(caller).iter().any(|c| c == name) {
|
||||||
|
return Ok(name);
|
||||||
|
}
|
||||||
if crate::capabilities::has_cap(caller, hive_sh4re::Capability::QueryAgentState) {
|
if crate::capabilities::has_cap(caller, hive_sh4re::Capability::QueryAgentState) {
|
||||||
Ok(name)
|
Ok(name)
|
||||||
} else {
|
} else {
|
||||||
Err(format!(
|
Err(format!(
|
||||||
"agent `{caller}` does not have the query_agent_state capability; \
|
"agent `{caller}` cannot query `{name}`: not a direct child and \
|
||||||
agent field ignored — grant the capability to query other agents"
|
`query_agent_state` capability is not granted"
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -372,22 +372,20 @@ pub enum Request {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
file_path: Option<String>,
|
file_path: Option<String>,
|
||||||
},
|
},
|
||||||
/// Loose-ends view. On the agent socket, scoped to the calling agent
|
/// Loose-ends view. On the agent socket: `None` = self; direct
|
||||||
/// unless the agent holds the `query_agent_state` capability, in
|
/// children are always accessible; non-children require the
|
||||||
/// which case `Some("<name>")` targets a specific agent's threads
|
/// `query_agent_state` capability — rejected with an error otherwise;
|
||||||
/// (the `"*"` hive-wide value is manager-only). On the manager
|
/// `"*"` is always rejected (use the manager socket). On the manager
|
||||||
/// socket, `agent = None` scopes to the manager itself,
|
/// socket: `None` = manager self, `"*"` = hive-wide, any name =
|
||||||
/// `Some("*")` is hive-wide, `Some("<name>")` is that agent's
|
/// that agent. See `docs/conventions.md::Loose-ends wire shape`.
|
||||||
/// loose ends. See `docs/conventions.md::Loose-ends wire shape`.
|
|
||||||
GetLooseEnds {
|
GetLooseEnds {
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
agent: Option<String>,
|
agent: Option<String>,
|
||||||
},
|
},
|
||||||
/// Count of pending (un-delivered) reminders. On the agent socket,
|
/// Count of pending (un-delivered) reminders. On the agent socket:
|
||||||
/// scoped to the calling agent unless the agent holds
|
/// same target rules as `GetLooseEnds` (self/children free;
|
||||||
/// `query_agent_state`, in which case `Some("<name>")` targets
|
/// non-children require `query_agent_state`; `"*"` rejected).
|
||||||
/// that agent. On the manager socket, `agent = None` means self,
|
/// On the manager socket: `None` = self, any name = that agent.
|
||||||
/// `Some("<name>")` means that agent.
|
|
||||||
/// Used by the harness's per-turn stats sink.
|
/// Used by the harness's per-turn stats sink.
|
||||||
CountPendingReminders {
|
CountPendingReminders {
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
|
@ -395,11 +393,10 @@ pub enum Request {
|
||||||
},
|
},
|
||||||
/// Reminder statistics: counts of scheduled, delivered, and pending
|
/// Reminder statistics: counts of scheduled, delivered, and pending
|
||||||
/// reminders over a time window. `since_secs` filters to reminders
|
/// reminders over a time window. `since_secs` filters to reminders
|
||||||
/// created in the last N seconds (0 = all). On the agent socket,
|
/// created in the last N seconds (0 = all). On the agent socket:
|
||||||
/// scoped to the calling agent unless the agent holds
|
/// same target rules as `GetLooseEnds` (self/children free;
|
||||||
/// `query_agent_state`, in which case `Some("<name>")` targets
|
/// non-children require `query_agent_state`; `"*"` rejected).
|
||||||
/// that agent. On the manager socket, `agent = None` means self,
|
/// On the manager socket: `None` = self, any name = that agent.
|
||||||
/// `Some("<name>")` means that agent.
|
|
||||||
ReminderRollup {
|
ReminderRollup {
|
||||||
/// Only count reminders created in the last N seconds from now.
|
/// Only count reminders created in the last N seconds from now.
|
||||||
/// Pass 0 to include all reminders.
|
/// Pass 0 to include all reminders.
|
||||||
|
|
@ -917,13 +914,13 @@ pub enum Capability {
|
||||||
/// MCP tool `get_host_journal` is only registered in the harness
|
/// MCP tool `get_host_journal` is only registered in the harness
|
||||||
/// when this capability is present.
|
/// when this capability is present.
|
||||||
ReadHostJournal,
|
ReadHostJournal,
|
||||||
/// Agent can query another agent's state via `GetLooseEnds`,
|
/// Agent can query non-child agents via `GetLooseEnds`,
|
||||||
/// `CountPendingReminders`, and `ReminderRollup` on the agent
|
/// `CountPendingReminders`, and `ReminderRollup` on the agent
|
||||||
/// socket. Without this capability the `agent` field in those
|
/// socket. Without this capability, targeting a non-child agent is
|
||||||
/// requests is ignored and results are scoped to the caller.
|
/// rejected with an error (direct children are always accessible
|
||||||
/// The `"*"` hive-wide value is not available on the agent socket
|
/// without any capability). The `"*"` hive-wide value is not
|
||||||
/// even with this capability — use the manager socket for swarm-wide
|
/// available on the agent socket even with this capability — use the
|
||||||
/// scans.
|
/// manager socket for swarm-wide scans.
|
||||||
QueryAgentState,
|
QueryAgentState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue