agentmux_srv/server/app_api/
dev_server.rs1use axum::extract::State;
16use axum::http::StatusCode;
17use axum::response::IntoResponse;
18use axum::Json;
19use serde_json::json;
20
21use agentmux_common::api_types::{RegisterDevServerRequest, RegisterDevServerResponse};
22
23use crate::backend::container::container_name_for_slug;
24use crate::backend::dev_proxy::DEV_PROXY_PORT;
25
26use super::AppState;
27
28pub(crate) async fn handle_register_dev_server(
29 State(state): State<AppState>,
30 caller: Option<axum::Extension<crate::server::caller::Caller>>,
31 Json(req): Json<RegisterDevServerRequest>,
32) -> impl IntoResponse {
33 if let Err(e) =
38 crate::server::ui_handlers::verified_block_id(&state, caller.as_deref(), &req.auth)
39 {
40 return (StatusCode::UNAUTHORIZED, Json(json!({ "error": e }))).into_response();
41 }
42 let agent_id = req.auth.agent_id.clone();
43
44 let project = req.project.trim().to_lowercase();
45 if project.is_empty() {
46 return (
47 StatusCode::BAD_REQUEST,
48 Json(json!({ "error": "project must not be empty" })),
49 )
50 .into_response();
51 }
52 let is_hostname_label = !project.is_empty()
59 && project.len() <= 63
60 && !project.starts_with('-')
61 && !project.ends_with('-')
62 && project
63 .bytes()
64 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-');
65 if !is_hostname_label {
66 return (
67 StatusCode::BAD_REQUEST,
68 Json(json!({
69 "error": "project must be a valid hostname label: lowercase \
70 letters, digits, and hyphens only, not starting or \
71 ending with a hyphen, 63 characters or fewer"
72 })),
73 )
74 .into_response();
75 }
76 if req.port == 0 {
77 return (
78 StatusCode::BAD_REQUEST,
79 Json(json!({ "error": "port must be nonzero" })),
80 )
81 .into_response();
82 }
83
84 let Some(cm) = state.container_manager.get().await else {
85 return (
86 StatusCode::SERVICE_UNAVAILABLE,
87 Json(json!({
88 "error": "Docker is not available — RegisterDevServer only works from a \
89 container-type agent's own container"
90 })),
91 )
92 .into_response();
93 };
94
95 let container_name = container_name_for_slug(&agent_id);
96 let Some(ip) = cm.agent_network_ip(&container_name).await else {
97 return (
98 StatusCode::NOT_FOUND,
99 Json(json!({
100 "error": format!(
101 "no running container found for agent {agent_id:?} on the dev-proxy \
102 network — RegisterDevServer only works from inside a container-type \
103 agent's own container"
104 )
105 })),
106 )
107 .into_response();
108 };
109
110 let addr: std::net::SocketAddr = match format!("{ip}:{}", req.port).parse() {
111 Ok(a) => a,
112 Err(e) => {
113 return (
114 StatusCode::BAD_REQUEST,
115 Json(json!({ "error": format!("invalid address {ip}:{}: {e}", req.port) })),
116 )
117 .into_response();
118 }
119 };
120
121 state.dev_proxy.register(&project, &agent_id, addr).await;
122
123 let url = format!(
127 "http://{project}-{}.localhost:{DEV_PROXY_PORT}",
128 agent_id.to_lowercase()
129 );
130 (StatusCode::OK, Json(RegisterDevServerResponse { url })).into_response()
131}