Skip to main content

agentmux_srv/server/
name_resolution.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! `POST /agentmux/agents/resolve` — the HTTP face of the one place a typed
5//! agent name is interpreted (identity M3, spec §5). Called by `agentmux-mcp`
6//! before `WorkEnqueue` and `CronCreate` so those tools can store a UID at
7//! authoring time (§5.4) and hand an ambiguous name back to the model with
8//! the candidates (§5.2) instead of enqueueing something that can never be
9//! delivered unambiguously.
10//!
11//! Auth-gated like every other loopback route. The body names a *target*; it
12//! never asserts the caller's own identity.
13
14use axum::extract::State;
15use axum::http::StatusCode;
16use axum::response::Json;
17use serde::Deserialize;
18use serde_json::{json, Value};
19
20use super::AppState;
21
22#[derive(Debug, Deserialize)]
23pub(super) struct ResolveRequest {
24    pub name: String,
25}
26
27pub(super) async fn handle_resolve_agent_name(
28    State(state): State<AppState>,
29    Json(req): Json<ResolveRequest>,
30) -> (StatusCode, Json<Value>) {
31    if req.name.trim().is_empty() {
32        return (
33            StatusCode::BAD_REQUEST,
34            Json(json!({ "error": "name is required" })),
35        );
36    }
37    // A synchronous store read, so off the async worker (incident #1782).
38    let mstore = state.mstore.clone();
39    let registry = state.reactive_handler;
40    let name = req.name.clone();
41    match tokio::task::spawn_blocking(move || {
42        crate::backend::name_resolution::resolve_name_to_uid(&mstore, registry, &name)
43    })
44    .await
45    {
46        // The store could not be read: refuse (503), so the MCP fails the
47        // tool call instead of sending a bare name that a same-named live
48        // agent could take (Codex P1 on #3563).
49        Ok(Err(e)) => (StatusCode::SERVICE_UNAVAILABLE, Json(json!({ "error": e }))),
50        Ok(Ok(resolution)) => (
51            StatusCode::OK,
52            Json(
53                serde_json::to_value(&resolution)
54                    .unwrap_or_else(|_| json!({ "resolution": "none" })),
55            ),
56        ),
57        Err(e) => (
58            StatusCode::INTERNAL_SERVER_ERROR,
59            Json(json!({ "error": format!("resolve task failed: {e}") })),
60        ),
61    }
62}