diff --git a/hive-ag3nt/src/bin/hive.rs b/hive-ag3nt/src/bin/hive.rs index fb3f0d1c..964884f1 100644 --- a/hive-ag3nt/src/bin/hive.rs +++ b/hive-ag3nt/src/bin/hive.rs @@ -564,15 +564,29 @@ async fn serve_main(socket: &Path, poll_ms: u64) -> Result<()> { socket.to_path_buf(), S::FORGE_IS_MANAGER, )); - tokio::spawn(web_ui::serve( - label, + // Log web_ui::serve's error instead of dropping it. A bare + // `tokio::spawn(web_ui::serve(...))` discards the JoinHandle, so + // any Err (e.g. EACCES from `bind_unix` when HIVE_WEB_SOCKET points + // at a dir the agent user can't write) vanishes — leaving an + // operator with no log line and no socket, debuggable only by + // staring at lifecycle.rs. + let web_ui_args = ( + label.clone(), port, login_state.clone(), bus.clone(), socket.to_path_buf(), files.clone(), turn_lock.clone(), - )); + ); + tokio::spawn(async move { + let (label, port, login_state, bus, socket, files, turn_lock) = web_ui_args; + if let Err(e) = + web_ui::serve(label, port, login_state, bus, socket, files, turn_lock).await + { + tracing::error!(error = %e, "web_ui::serve exited with error"); + } + }); if matches!(initial, LoginState::NeedsLogin) { turn::wait_for_login(&claude_dir, login_state.clone(), &bus, poll_ms).await; } else { diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 8b5b056b..969ed166 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -1219,6 +1219,18 @@ fn set_nspawn_flags( let socket_dir = crate::agent_sockets::agent_dir_for(agent_name); std::fs::create_dir_all(&socket_dir) .with_context(|| format!("create {}", socket_dir.display()))?; + // 0777 so the in-container agent user (`sock`, `iris`, …) can + // `bind(2)` the `web.sock` file here. `create_dir_all` lands + // the dir at 0755 root:root; the harness runs as the non-root + // agent user inside the container and otherwise fails the + // bind with EACCES — silently, since web_ui::serve's spawn + // drops its JoinHandle (the gateway then sees an empty + // agent-sockets.json and the agent appears unreachable). + // Per-agent dir + per-agent bind-mount means no other + // container ever sees this path, so the wide mode is contained. + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&socket_dir, std::fs::Permissions::from_mode(0o777)) + .with_context(|| format!("chmod 0777 {}", socket_dir.display()))?; let _ = write!( binds, " --bind={socket_dir}:{socket_dir}",