diff --git a/hive-agent-mcp/src/mcp/mod.rs b/hive-agent-mcp/src/mcp/mod.rs index 3a965227..e9ec17cb 100644 --- a/hive-agent-mcp/src/mcp/mod.rs +++ b/hive-agent-mcp/src/mcp/mod.rs @@ -392,8 +392,12 @@ impl AgentServer { async fn get_agent_meta(&self, Parameters(args): Parameters) -> String { let log = args.name.clone().unwrap_or_else(|| "".to_owned()); run_tool_envelope("get_agent_meta", log, async move { + let name = match args.name.map(|n| hive_types::Ident::parse(&n)).transpose() { + Ok(name) => name, + Err(reason) => return format!("invalid agent name: {reason}"), + }; let (resp, retries) = self - .dispatch(hive_core_agent_sock::Request::GetAgentMeta { name: args.name }) + .dispatch(hive_core_agent_sock::Request::GetAgentMeta { name }) .await; annotate_retries(format_agent_meta(resp), retries) }) diff --git a/hive-c0re/src/socket_server/mod.rs b/hive-c0re/src/socket_server/mod.rs index d4e522da..27e14ae3 100644 --- a/hive-c0re/src/socket_server/mod.rs +++ b/hive-c0re/src/socket_server/mod.rs @@ -241,7 +241,7 @@ pub(crate) async fn dispatch_shared( } => handle_remind(coord, agent, message, timing, file_path.as_deref()), hive_core_agent_sock::Request::SetStatus { text } => handle_set_status(coord, text), hive_core_agent_sock::Request::GetAgentMeta { name } => { - handle_get_agent_meta(coord, agent, name.as_deref()).await + handle_get_agent_meta(coord, agent, name.as_ref()).await } hive_core_agent_sock::Request::CancelLooseEnd { kind, id } => { crate::questions::handle_cancel_loose_end(coord, agent, *kind, *id).map_or_else( @@ -418,42 +418,43 @@ async fn handle_create_repo(agent: &str, repo: &str) -> hive_core_agent_sock::Re async fn handle_get_agent_meta( coord: &Arc, agent: &str, - name: Option<&str>, + name: Option<&hive_types::Ident>, ) -> hive_core_agent_sock::Response { - let target = name.unwrap_or(agent); - // `name` is agent-supplied and flows into filesystem reads below - // (`read_agent_status_live`, `read_agent_matrix_identities` → - // `agent_notes_dir(target)`), where a `../` component would traverse at - // the OS level. Validate it before any path is built. The `None` default - // (`target == agent`) is the caller's own authenticated name, already - // valid — but validating unconditionally is simplest and harmless. - let target_id = match hive_types::Ident::parse(target) { - Ok(id) => id, - Err(reason) => { - return hive_core_agent_sock::Response::Err { - message: format!("get_agent_meta: invalid agent name {target:?}: {reason}"), - }; - } + // `name` arrives pre-validated by serde (wire field is `Ident`). The + // `None` default (target == caller) still needs a parse since `agent` + // is a plain `&str` here — but it's the caller's own authenticated + // name, already valid in practice. + let target_id = match name { + Some(id) => id.clone(), + None => match hive_types::Ident::parse(agent) { + Ok(id) => id, + Err(reason) => { + return hive_core_agent_sock::Response::Err { + message: format!("get_agent_meta: invalid agent name {agent:?}: {reason}"), + }; + } + }, }; let (status_text, status_set_at, running) = crate::container_view::read_agent_status_live(&target_id).await; let (hive_name, swarm_name) = crate::container_view::hive_swarm_names(); + // Matrix identities are public handles (`name` / `user_id` + // `@user:server` / `homeserver`) — the access token lives separately + // in the agent's `matrix-token` and is never part of this response. + // Peer visibility is intentional: it lets an agent verify/contact + // another on a public matrix instance. The `Ident::parse` gate + // above is what closes the real vector here (path traversal via `../` + // in an agent-supplied name). + let matrix_accounts = read_agent_matrix_identities(&target_id); hive_core_agent_sock::Response::AgentMeta { - name: target.to_owned(), + name: target_id.into_string(), running, hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake), status_text, status_set_at, hive_name, swarm_name, - // Matrix identities are public handles (`name` / `user_id` - // `@user:server` / `homeserver`) — the access token lives separately - // in the agent's `matrix-token` and is never part of this response. - // Peer visibility is intentional: it lets an agent verify/contact - // another on a public matrix instance. The `Ident::parse` gate - // above is what closes the real vector here (path traversal via `../` - // in an agent-supplied name). - matrix_accounts: read_agent_matrix_identities(&target_id), + matrix_accounts, } } diff --git a/hive-core-agent-sock/src/lib.rs b/hive-core-agent-sock/src/lib.rs index 6946b2a6..90e18c4d 100644 --- a/hive-core-agent-sock/src/lib.rs +++ b/hive-core-agent-sock/src/lib.rs @@ -140,7 +140,7 @@ pub enum Request { /// `docs/conventions.md::Agent metadata`. GetAgentMeta { #[serde(default, skip_serializing_if = "Option::is_none")] - name: Option, + name: Option, }, /// Cancel an open thread the agent owns. Authorisation + /// per-kind semantics in