From 8b238bbfaf2ca810dae1fef5eb8e1d7e7ff284cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Sun, 31 May 2026 21:38:28 +0200 Subject: [PATCH] agent UDS: chmod per-agent socket dir 0777 + log web_ui::serve errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The harness runs as the non-root agent user; the per-agent /run/hive-agent// dir lands at 0755 root:root after create_dir_all, so bind(2) of web.sock failed with EACCES. The error was invisible because the web_ui::serve future was tokio::spawn'd with its JoinHandle dropped — no log, no socket, agent looks unreachable through the gateway. --- hive-ag3nt/src/bin/hive.rs | 20 +++++++++++++++++--- hive-c0re/src/lifecycle.rs | 12 ++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) 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}",