diff --git a/docs/process/conventions.md b/docs/process/conventions.md index cafcc929..e16f99ef 100644 --- a/docs/process/conventions.md +++ b/docs/process/conventions.md @@ -181,14 +181,12 @@ privileged bot, etc.) can land later without breaking existing handlers. Each row carries enough context that the caller renders it directly as a bulleted list, no follow-up fetch needed. -Per-flavour scoping is uniform across the two cancellable variants: - -- **agent-flavour** `GetLooseEnds` only surfaces rows the calling - agent has standing in. `Approval` rows only appear when the - calling agent is the manager (sub-agents don't submit - approvals). `Reminder` rows are scoped to `owner == self`. -- **manager-flavour** `GetLooseEnds` lists every pending row in - the swarm — full audit view. +`GetLooseEnds` takes no target — it always returns the caller's own +rows, on both the agent and manager sockets alike (the manager, `ruth`, +is a normal agent here, just auto-deployed with different default +capabilities). `Approval` rows only appear when the calling agent is +the manager (sub-agents don't submit approvals). `Reminder` rows are +scoped to `owner == self`. Per-variant fields: @@ -364,7 +362,6 @@ that allows the underlying resource access. |---|---| | `manage_root_agent` | may lifecycle-manage the root/manager agent via `kill`/`start`/`restart` | | `read_host_journal` | registers the `get_host_journal` MCP tool + serves `GET /journal-host` requests | -| `query_agent_state` | may call `get_loose_ends` / `CountPendingReminders` targeting non-child agents | **Config storage** — per-agent capabilities live in `/var/lib/hyperhive/meta/capabilities.json` alongside `tool-groups.json`. diff --git a/docs/turn-loop/mcp.md b/docs/turn-loop/mcp.md index 99be1f11..e254d0fd 100644 --- a/docs/turn-loop/mcp.md +++ b/docs/turn-loop/mcp.md @@ -75,20 +75,15 @@ outcomes](README.md#turn-outcomes)) but via a generic "call payload shapes and routing logic in [`docs/agent-lifecycle/approvals.md` § Helper events](../agent-lifecycle/approvals.md#helper-events-to-the-submitting-agent). -**Inbox** (`inbox` group): `get_loose_ends(agent?)`, +**Inbox** (`inbox` group): `get_loose_ends()`, `cancel_loose_end(kind, id)`, `remind(message, delay_seconds? | at_unix_timestamp?)`. -- `get_loose_ends(agent?)` — list scheduled reminders, pending - approvals you submitted, and active local tasks published by - external MCP daemons (for example running bash tasks from - `hive-bash-daemon`). Each row carries an id + kind for - `cancel_loose_end`. Omit `agent` to list your own threads. Pass - `agent: ""` to inspect any agent in your subtree — a child, a - child's child, every agent below them (always accessible per topology - enforcement); a name outside the subtree requires the - `query_agent_state` capability. The `"*"` hive-wide query isn't - available on the agent socket. +- `get_loose_ends()` — list scheduled reminders, pending approvals you + submitted, and active local tasks published by external MCP daemons + (for example running bash tasks from `hive-bash-daemon`). Each row + carries an id + kind for `cancel_loose_end`. Always your own threads — + there is no way to target another agent. - `cancel_loose_end` — hard-delete a `reminder`, cancel a pending `approval` row, or clear a `todo` row (loose-ends-v2). Agents may only cancel rows they own; the `approval` kind is further restricted diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index ca780d0c..47ccbf1e 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -440,7 +440,6 @@ The current capabilities are: |------|--------| | `manage_root_agent` | allows the `set_status` / lifecycle tools on the root agent | | `read_host_journal` | unlocks `get_host_journal` to read journald from inside a container | -| `query_agent_state` | allows `get_loose_ends(agent: "")` calls targeting other agents | Each row is one agent. Columns are the capability names returned by `GET /api/capabilities` as `caps: Vec`. Checking or unchecking diff --git a/hive-agent-mcp/src/mcp/args.rs b/hive-agent-mcp/src/mcp/args.rs index 7843649f..2c3b4ac7 100644 --- a/hive-agent-mcp/src/mcp/args.rs +++ b/hive-agent-mcp/src/mcp/args.rs @@ -131,19 +131,6 @@ pub struct CancelLooseEndArgs { pub id: i64, } -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct AgentGetLooseEndsArgs { - /// Whose loose ends to list. Omit (or `null`) for your own. You may - /// also pass the name of any agent in your subtree — a child, a child's - /// child, and so on down — without any extra capability. - /// Pass any other agent name to inspect their threads — requires the - /// `query_agent_state` capability; without it the request is rejected - /// with an error. The `"*"` hive-wide value is not available on the - /// agent socket. - #[serde(default)] - pub agent: Option, -} - #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct UpdateMetaInputsArgs { /// Flake input names to update (e.g. `["bitburner-agent", "nixpkgs"]`). diff --git a/hive-agent-mcp/src/mcp/mod.rs b/hive-agent-mcp/src/mcp/mod.rs index dbd767e4..7f269a3f 100644 --- a/hive-agent-mcp/src/mcp/mod.rs +++ b/hive-agent-mcp/src/mcp/mod.rs @@ -24,7 +24,7 @@ mod args; mod render; pub use args::{ - AckUntilArgs, AgentGetLooseEndsArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs, + AckUntilArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs, CreateRepoArgs, EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, GetHostJournalArgs, MarkTodosDoneArgs, RecvArgs, RemindArgs, RequestSchedulePromptArgs, SendArgs, SetStatusArgs, UpdateMetaInputsArgs, @@ -248,25 +248,20 @@ impl AgentServer { the same row on its own regardless of whether you marked it done while it was \ still running, so doing that buys nothing and just costs a redundant call later. \ Todos cap at 40 rows; a trailer line says how many more are pending — mark the \ - reviewed ones done with `mark_todos_done`, then call again for the rest.\n\ - Pass `agent: \"\"` to inspect a specific peer agent's threads. Direct \ - child agents are always accessible. For non-children, the `query_agent_state` \ - capability is required — without it the request is rejected with an error." + reviewed ones done with `mark_todos_done`, then call again for the rest." )] - async fn get_loose_ends(&self, Parameters(args): Parameters) -> String { + async fn get_loose_ends(&self) -> String { run_tool_envelope("get_loose_ends", String::new(), async move { - let is_self_query = args.agent.is_none(); let (resp, retries) = self - .dispatch(hive_core_agent_sock::Request::GetLooseEnds { agent: args.agent }) + .dispatch(hive_core_agent_sock::Request::GetLooseEnds) .await; // Extract the vec so we can augment before rendering. let mut loose_ends = match resp { Ok(hive_core_agent_sock::Response::LooseEnds { loose_ends }) => loose_ends, other => return annotate_retries(reply_err(other, "get_loose_ends"), retries), }; - // Prepend matrix unread entry for self-queries only (can't - // reach another agent's matrix daemon from here). - if is_self_query && let Some(unread_rooms) = matrix_unread_summary().await { + // Prepend matrix unread entry. + if let Some(unread_rooms) = matrix_unread_summary().await { let total = u32::try_from(unread_rooms.len()).unwrap_or(u32::MAX); if total > 0 { let summary = format_matrix_summary(&unread_rooms); @@ -279,16 +274,12 @@ impl AgentServer { ); } } - // Merge the harness's local todos (loose-ends v2) for self-queries. - // The harness owns the todo store in-container; another agent's - // todos aren't reachable from here (same as matrix above). - if is_self_query && let Some(todos) = local_todos().await { + // Merge the harness's local todos (loose-ends v2). + if let Some(todos) = local_todos().await { loose_ends.extend(todos); } - // Merge local pending reminders — same self-query-only - // restriction: a manager asking for a child's loose-ends no longer - // sees the child's reminders, matching the todos precedent above). - if is_self_query && let Some(reminders) = local_reminders().await { + // Merge local pending reminders. + if let Some(reminders) = local_reminders().await { loose_ends.extend(reminders); } annotate_retries(render_loose_ends(&loose_ends), retries) diff --git a/hive-agent/src/main.rs b/hive-agent/src/main.rs index d75ba543..a1537602 100644 --- a/hive-agent/src/main.rs +++ b/hive-agent/src/main.rs @@ -358,7 +358,7 @@ impl Surface for AgentSurface { async fn post_turn_counts(socket: &Path) -> (Option, Option) { let threads = match hive_sock_client::request::<_, Response>( socket, - &Request::GetLooseEnds { agent: None }, + &Request::GetLooseEnds, CONTROL_SOCKET_RETRY, ) .await diff --git a/hive-agent/src/mcp_config.rs b/hive-agent/src/mcp_config.rs index deff0150..f24fbb7e 100644 --- a/hive-agent/src/mcp_config.rs +++ b/hive-agent/src/mcp_config.rs @@ -49,12 +49,9 @@ fn allowed_capability_tools() -> Vec { let t = token.trim().to_ascii_lowercase(); match t.as_str() { "read_host_journal" => tools.push("get_host_journal".to_owned()), - // manage_root_agent / query_agent_state don't expose new MCP - // tools: manage_root_agent gates existing lifecycle tools via - // topology enforcement; query_agent_state unlocks the `agent` - // field in get_loose_ends / count_pending_reminders / - // reminder_rollup (c0re enforces the cap server-side). - "manage_root_agent" | "query_agent_state" => {} + // manage_root_agent doesn't expose a new MCP tool: it gates + // existing lifecycle tools via topology enforcement instead. + "manage_root_agent" => {} unknown => { tracing::warn!(capability = %unknown, "unrecognised capability in HIVE_CAPABILITIES — skipped"); } diff --git a/hive-c0re/src/agent_config/capabilities.rs b/hive-c0re/src/agent_config/capabilities.rs index 998c408c..8855e8fb 100644 --- a/hive-c0re/src/agent_config/capabilities.rs +++ b/hive-c0re/src/agent_config/capabilities.rs @@ -7,8 +7,7 @@ //! //! ```json //! { -//! "atlas": ["read_host_journal"], -//! "ruth": ["query_agent_state"] +//! "atlas": ["read_host_journal"] //! } //! ``` //! diff --git a/hive-c0re/src/loose_ends.rs b/hive-c0re/src/loose_ends.rs index 408ee926..6cc8ad03 100644 --- a/hive-c0re/src/loose_ends.rs +++ b/hive-c0re/src/loose_ends.rs @@ -1,8 +1,8 @@ //! Loose-ends aggregator. Walks the `approvals` table once per call and -//! assembles a `Vec` 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` 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> { } 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> { - 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) -} diff --git a/hive-c0re/src/socket_server/mod.rs b/hive-c0re/src/socket_server/mod.rs index 141c0655..c939590d 100644 --- a/hive-c0re/src/socket_server/mod.rs +++ b/hive-c0re/src/socket_server/mod.rs @@ -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) -> Response { @@ -550,11 +549,7 @@ async fn dispatch(req: &Request, agent: &str, coord: &Arc) -> 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 { } } -/// `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, 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, 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> { diff --git a/hive-core-agent-sock/src/lib.rs b/hive-core-agent-sock/src/lib.rs index 9731b6d6..83634056 100644 --- a/hive-core-agent-sock/src/lib.rs +++ b/hive-core-agent-sock/src/lib.rs @@ -63,16 +63,11 @@ pub enum Request { /// Non-mutating — pulls from the broker without delivering. The /// per-agent web UI uses this to render its own inbox section. Recent { limit: u64 }, - /// Loose-ends view. On the agent socket: `None` = self; direct - /// children are always accessible; non-children require the - /// `query_agent_state` capability — rejected with an error otherwise; - /// `"*"` is always rejected (use the manager socket). On the manager - /// socket: `None` = manager self, `"*"` = hive-wide, any name = - /// that agent. See `docs/process/conventions.md::Loose-ends wire shape`. - GetLooseEnds { - #[serde(default, skip_serializing_if = "Option::is_none")] - agent: Option, - }, + /// Loose-ends view — always the caller's own. Same shape on the agent + /// and manager sockets; the manager (`ruth`) is a normal agent here, it + /// just sees its own threads like anyone else. See + /// `docs/process/conventions.md::Loose-ends wire shape`. + GetLooseEnds, /// Set a free-text status string visible on the dashboard. The harness /// writes `{state_dir}/hyperhive-status` locally before sending this /// request; hive-c0re just triggers a dashboard rescan on receipt. diff --git a/hive-sh4re/src/permissions.rs b/hive-sh4re/src/permissions.rs index 37de1f75..8c8b4276 100644 --- a/hive-sh4re/src/permissions.rs +++ b/hive-sh4re/src/permissions.rs @@ -367,25 +367,12 @@ pub enum Capability { /// MCP tool `get_host_journal` is only registered in the harness /// when this capability is present. ReadHostJournal, - /// Agent can query agents outside its own subtree via `GetLooseEnds`, - /// `CountPendingReminders`, and `ReminderRollup` on the agent - /// socket. Without this capability, targeting an agent outside the - /// caller's subtree is rejected with an error (the caller itself and - /// every descendant 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, } 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, - ]; + pub const ALL: &'static [Self] = &[Self::ManageRootAgent, Self::ReadHostJournal]; /// Short human-readable description suitable for a tooltip or help text. #[must_use] @@ -395,9 +382,6 @@ impl Capability { "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" - } } } }