diff --git a/server/src/chat/transport/matrix/new_project.rs b/server/src/chat/transport/matrix/new_project.rs index 0ac1810c..89851012 100644 --- a/server/src/chat/transport/matrix/new_project.rs +++ b/server/src/chat/transport/matrix/new_project.rs @@ -913,6 +913,11 @@ pub async fn handle_new_project( /// to all interfaces, making Docker port forwarding reachable from the host. /// Without this the server defaults to `127.0.0.1` inside the container — /// reachable only from within the container itself, not via `docker -p`. +/// +/// Host-side port mappings use `0.0.0.0` so the MCP and SSH endpoints are +/// reachable from outside the docker host (not just from the host itself). +/// `127.0.0.1` had been the conservative default but blocked remote MCP +/// clients and out-of-host SSH onto the project container. fn project_docker_run_args( container_name: &str, port: u16, @@ -927,9 +932,9 @@ fn project_docker_run_args( "--name".into(), container_name.to_string(), "-p".into(), - format!("127.0.0.1:{port}:3001"), + format!("0.0.0.0:{port}:3001"), "-p".into(), - format!("127.0.0.1:{ssh_port}:22"), + format!("0.0.0.0:{ssh_port}:22"), "-e".into(), "HUSKIES_HOST=0.0.0.0".into(), "-e".into(), @@ -1348,6 +1353,39 @@ mod tests { ); } + #[test] + fn project_docker_args_bind_host_ports_to_all_interfaces() { + // Project containers must be reachable from outside the docker host + // (remote MCP clients, ssh into the project container) — host-side + // mapping must use 0.0.0.0, not 127.0.0.1. + let args = project_docker_run_args( + "huskies-myapp", + 3100, + 2200, + "ssh-ed25519 AAAA...", + "Test User", + "test@example.com", + ); + let pairs: Vec<_> = args.windows(2).collect(); + assert!( + pairs + .iter() + .any(|w| w[0] == "-p" && w[1] == "0.0.0.0:3100:3001"), + "expected -p 0.0.0.0:3100:3001 in docker args, got: {args:?}" + ); + assert!( + pairs + .iter() + .any(|w| w[0] == "-p" && w[1] == "0.0.0.0:2200:22"), + "expected -p 0.0.0.0:2200:22 in docker args, got: {args:?}" + ); + assert!( + !pairs.iter().any(|w| w[0] == "-p" + && (w[1].starts_with("127.0.0.1:") || w[1] == "127.0.0.1")), + "host-side mapping must not be 127.0.0.1-restricted: {args:?}" + ); + } + #[test] fn interpret_docker_run_error_missing_image_points_at_script() { let stderr = "Unable to find image 'huskies-project-rust:latest' locally\n\