From 9cd408de8b07637da7cefbd2592a45be2953611c Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 8 Jul 2026 22:49:34 +0200 Subject: [PATCH 1/8] fix(#2290): reset-failed before nixos-container start in hive-priv systemd will refuse to start a unit that has hit start-limit. nixos- container start does not clear the counter first. Add a best-effort systemctl reset-failed container@h-.service before each StartContainer so an earlier lockout cannot block a now- correct start. Ignoring the reset exit code is intentional: the unit may not exist yet on first-time create, and reset-failed on a clean unit is a harmless no-op. --- hive-priv/src/main.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index cdb3f9df..789aa045 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -162,7 +162,16 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String, match req { PrivRequest::StartContainer { ref name } => { validate_container_name(name)?; - container_run(&["start", &container_system_name(name)]).await + let machine = container_system_name(name); + // Clear any start-limit lockout left by earlier failures so a + // now-correct start isn't blocked. nixos-container start does not + // do this itself. Best-effort: if the unit doesn't exist yet + // (first-time create) reset-failed is a no-op and we proceed. + let _ = Command::new("systemctl") + .args(["reset-failed", &format!("container@{machine}.service")]) + .status() + .await; + container_run(&["start", &machine]).await } PrivRequest::StopContainer { ref name } => { From 9d1f5ebe76499e5b06e1bf3bf0c4cbaf136654da Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 8 Jul 2026 22:59:04 +0200 Subject: [PATCH 2/8] feat(#2290): maintain /etc/tmpfiles.d/hyperhive-agents.conf for boot safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the boot outage: container@h-* units try to start before hive-c0re reaches ensure_runtime, so bind-mount source dirs are missing. Fix: hive-c0re (via hive-priv, which runs as root) writes /etc/tmpfiles.d/hyperhive-agents.conf whenever the agent set changes. systemd-tmpfiles-setup.service (sysinit.target) reads it at every boot BEFORE any container units start, pre-creating: /run/hyperhive/agents/ — MCP socket dir (bind -> /run/hive) /run/hive-agent/ — web socket dir (bind -> /run/hive-agent) This alone removes the outage class: even if hive-c0re is slow to start, the bind-mount sources exist and container units can activate. Added: - PrivRequest::SyncAgentTmpfiles { agents } in hive-sh4re - sync_agent_tmpfiles() in hive-priv: generates content, writes atomically, calls systemd-tmpfiles --create to apply immediately - priv_client::sync_agent_tmpfiles() wrapper - lifecycle::sync_tmpfiles() best-effort helper (list + priv call) - Call sites: hive-c0re startup, handle_spawn success, destroy success --- hive-c0re/src/actions.rs | 3 ++ hive-c0re/src/lifecycle/mod.rs | 27 ++++++++++++++ hive-c0re/src/main.rs | 6 ++++ hive-c0re/src/priv_client.rs | 10 ++++++ hive-c0re/src/server.rs | 2 ++ hive-priv/src/main.rs | 65 ++++++++++++++++++++++++++++++++++ hive-sh4re/src/priv_proto.rs | 15 ++++++++ 7 files changed, 128 insertions(+) diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 27238df8..2339cdda 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -967,6 +967,9 @@ pub async fn destroy(coord: &Arc, name: &str, purge: bool) -> Resul // roster, so any schedule that still targets the just-destroyed agent // now drops that ghost column live (no page reload needed). coord.emit_schedules_snapshot(); + // Update tmpfiles.d to remove the destroyed agent's dirs from the + // boot-time pre-creation list. Best-effort: failure is logged only. + tokio::spawn(async { lifecycle::sync_tmpfiles().await }); Ok(()) } diff --git a/hive-c0re/src/lifecycle/mod.rs b/hive-c0re/src/lifecycle/mod.rs index b2c90733..cf91a945 100644 --- a/hive-c0re/src/lifecycle/mod.rs +++ b/hive-c0re/src/lifecycle/mod.rs @@ -722,6 +722,33 @@ pub async fn list() -> Result> { .collect()) } +/// Sync `/etc/tmpfiles.d/hyperhive-agents.conf` with the currently-known +/// agent set (from `nixos-container list`). Strips the `h-` prefix to get +/// logical names. Best-effort: errors are logged but never propagated — a +/// failed tmpfiles write shouldn't block a spawn or destroy. +/// +/// Called at hive-c0re startup and after each spawn / destroy so the file +/// always reflects the live agent set. `systemd-tmpfiles-setup.service` +/// reads the file at boot (before any container units start), pre-creating +/// bind-mount source dirs so container@h-* units don't race hive-c0re. +pub async fn sync_tmpfiles() { + let agents = match list().await { + Ok(containers) => containers + .into_iter() + .filter_map(|c| c.strip_prefix(AGENT_PREFIX).map(str::to_owned)) + .collect::>(), + Err(e) => { + tracing::warn!(error = ?e, "sync_tmpfiles: list failed; skipping"); + return; + } + }; + if let Err(e) = crate::priv_client::sync_agent_tmpfiles(&agents).await { + tracing::warn!(error = ?e, "sync_tmpfiles: priv call failed"); + } else { + tracing::debug!(count = agents.len(), "sync_tmpfiles: ok"); + } +} + /// Build the per-line callback for `create_container_streaming` / /// `update_container_streaming`. Both ops share identical dispatch logic /// (stdout → info + `append_stdout`, stderr → warn + `append_stderr`); this diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 2b61082f..0168b01f 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -283,6 +283,12 @@ async fn cmd_serve( if let Err(e) = auto_update::ensure_root_agent(&coord).await { tracing::warn!(error = ?e, "auto-spawn root agent failed"); } + // Sync /etc/tmpfiles.d/hyperhive-agents.conf so agent runtime dirs are + // pre-declared for the next boot. Best-effort background task — a failure + // here must not block hive-c0re startup. See lifecycle::sync_tmpfiles. + tokio::spawn(async { + hive_c0re::lifecycle::sync_tmpfiles().await; + }); // Auto-update in the background — don't block service start. // Sub-agent rebuilds can take tens of seconds; we want the admin // socket up immediately. diff --git a/hive-c0re/src/priv_client.rs b/hive-c0re/src/priv_client.rs index 9e6c2014..c1ed5a10 100644 --- a/hive-c0re/src/priv_client.rs +++ b/hive-c0re/src/priv_client.rs @@ -389,6 +389,16 @@ pub async fn upgrade_agent_subvolume(agent_name: &str) -> Result<()> { .await?) } +/// Write `/etc/tmpfiles.d/hyperhive-agents.conf` for `agents` (logical names, +/// e.g. `"atlas"`) and immediately apply it with `systemd-tmpfiles --create`. +/// See [`PrivRequest::SyncAgentTmpfiles`] for the full semantics. +pub async fn sync_agent_tmpfiles(agents: &[String]) -> Result<()> { + ok(call(&PrivRequest::SyncAgentTmpfiles { + agents: agents.to_vec(), + }) + .await?) +} + /// Parse `(referenced, exclusive)` bytes from `btrfs qgroup show -f --raw` /// output (a qgroup row is ` …`). /// diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 63c5f885..8b21cd74 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -216,6 +216,8 @@ async fn handle_spawn(coord: &Arc, name: &str) -> Result { // Roll back socket registration if container creation failed. diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 789aa045..3ad94073 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -326,6 +326,10 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String, validate_agent_name(agent_name)?; upgrade_agent_subvolume(agent_name).await } + + PrivRequest::SyncAgentTmpfiles { ref agents } => { + sync_agent_tmpfiles(agents).await + } } } @@ -1493,3 +1497,64 @@ fn write_bridge_dns_marker(container: &str, isolation: Option<&NetworkIsolation> } Ok(()) } + +/// `SyncAgentTmpfiles` — write `/etc/tmpfiles.d/hyperhive-agents.conf` for +/// the given agent set and immediately apply it with `systemd-tmpfiles --create`. +/// +/// Each call atomically replaces the file with entries for all current agents, +/// then creates any missing dirs on the running host. The file survives reboots +/// and is read by `systemd-tmpfiles-setup.service` (runs in `sysinit.target`, +/// before any container units can start), so bind-mount source dirs are always +/// pre-created regardless of whether hive-c0re has reached `ensure_runtime`. +/// +/// Directories written per agent: +/// - `/run/hyperhive/agents/` (MCP socket dir, bind-mounted into container +/// as `/run/hive`) +/// - `/run/hive-agent/` (web socket dir, bind-mounted into container) +const TMPFILES_PATH: &str = "/etc/tmpfiles.d/hyperhive-agents.conf"; +const AGENT_RUNTIME_ROOT: &str = "/run/hyperhive/agents"; + +async fn sync_agent_tmpfiles(agents: &[String]) -> Result<(String, String)> { + for name in agents { + validate_agent_name(name)?; + } + + // Build tmpfiles.d content. Root dirs first, then per-agent. + let mut content = String::from( + "# managed by hive-c0re — do not edit (regenerated on spawn/destroy)\n", + ); + // Parent dirs — created with permissive mode so hive-c0re can make subdirs. + // /run/hyperhive itself is also a RuntimeDirectory of hive-c0re.service; the + // tmpfiles.d entry here ensures it exists before hive-c0re starts (boot race). + content.push_str("d /run/hyperhive 0750 hive-core hive-core -\n"); + content.push_str(&format!("d {AGENT_RUNTIME_ROOT} 0755 hive-core hive-core -\n")); + content.push_str(&format!("d {SOCKET_DIR_ROOT} 0755 root root -\n")); + // Per-agent dirs. + for name in agents { + content.push_str(&format!("d {AGENT_RUNTIME_ROOT}/{name} 0755 hive-core hive-core -\n")); + content.push_str(&format!("d {SOCKET_DIR_ROOT}/{name} 0755 root root -\n")); + } + + // Atomic write: write to a tmp file then rename so a concurrent reader + // always sees a complete file. + let tmp = format!("{TMPFILES_PATH}.tmp"); + std::fs::write(&tmp, &content).with_context(|| format!("write {tmp}"))?; + std::fs::rename(&tmp, TMPFILES_PATH) + .with_context(|| format!("rename {TMPFILES_PATH}.tmp -> {TMPFILES_PATH}"))?; + tracing::info!(agents = agents.len(), "tmpfiles.d: wrote {TMPFILES_PATH}"); + + // Apply immediately so dirs exist on the running host, not just after next boot. + let out = Command::new("systemd-tmpfiles") + .args(["--create", TMPFILES_PATH]) + .output() + .await + .context("systemd-tmpfiles --create")?; + if !out.status.success() { + let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned(); + anyhow::bail!( + "systemd-tmpfiles --create failed ({}): {stderr}", + out.status + ); + } + Ok((String::new(), String::new())) +} diff --git a/hive-sh4re/src/priv_proto.rs b/hive-sh4re/src/priv_proto.rs index 618f312c..5ca0268e 100644 --- a/hive-sh4re/src/priv_proto.rs +++ b/hive-sh4re/src/priv_proto.rs @@ -512,6 +512,21 @@ pub enum PrivRequest { /// Logical agent name (validated by `validate_agent_name`). agent_name: String, }, + + /// Write `/etc/tmpfiles.d/hyperhive-agents.conf` for the given agent set + /// and immediately apply it with `systemd-tmpfiles --create`. Each entry + /// declares the per-agent runtime dirs (`/run/hyperhive/agents/` and + /// `/run/hive-agent/`) so systemd recreates them at every boot before + /// any container units start — preventing bind-mount source missing errors + /// when container@h-* units race hive-c0re after a reboot. + /// + /// Called at hive-c0re startup and after every agent spawn / destroy. + /// Agents are logical names (validated by `validate_agent_name`). + SyncAgentTmpfiles { + /// Logical agent names (e.g. `"atlas"`, `"ruth"`). hive-priv validates + /// each name before writing any path component derived from it. + agents: Vec, + }, } /// Response from the privileged helper. From f32fba02383f7b1669a13a23d8551e8b33030bb1 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 8 Jul 2026 23:08:17 +0200 Subject: [PATCH 3/8] fix(#2290): use 0777 for per-agent socket dirs in tmpfiles.d MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit systemd-tmpfiles d entries adjust mode+owner on existing dirs. Using 0755 root root would stomp live agents' socket dirs (owned by agent uid:gid) on every sync_tmpfiles call, breaking the harness's ability to bind new sockets until host_config rechowns them. Fix: 0777 root root — matches the chmod_socket_dir(0o777) fallback already used by host_config when the agent uid is unavailable. World- writable dirs let the non-root harness bind sockets regardless of who owns the dir. host_config's chown_socket_dir tightens ownership when the agent uid is resolved. Also add missing # Errors doc to priv_client::sync_agent_tmpfiles. --- hive-c0re/src/priv_client.rs | 5 +++++ hive-priv/src/main.rs | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/hive-c0re/src/priv_client.rs b/hive-c0re/src/priv_client.rs index c1ed5a10..c8337f21 100644 --- a/hive-c0re/src/priv_client.rs +++ b/hive-c0re/src/priv_client.rs @@ -392,6 +392,11 @@ pub async fn upgrade_agent_subvolume(agent_name: &str) -> Result<()> { /// Write `/etc/tmpfiles.d/hyperhive-agents.conf` for `agents` (logical names, /// e.g. `"atlas"`) and immediately apply it with `systemd-tmpfiles --create`. /// See [`PrivRequest::SyncAgentTmpfiles`] for the full semantics. +/// +/// # Errors +/// +/// Returns an error if the priv socket call fails, if any agent name is +/// invalid, or if `systemd-tmpfiles --create` exits non-zero. pub async fn sync_agent_tmpfiles(agents: &[String]) -> Result<()> { ok(call(&PrivRequest::SyncAgentTmpfiles { agents: agents.to_vec(), diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 3ad94073..3aee0b2c 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -1532,7 +1532,12 @@ async fn sync_agent_tmpfiles(agents: &[String]) -> Result<(String, String)> { // Per-agent dirs. for name in agents { content.push_str(&format!("d {AGENT_RUNTIME_ROOT}/{name} 0755 hive-core hive-core -\n")); - content.push_str(&format!("d {SOCKET_DIR_ROOT}/{name} 0755 root root -\n")); + // 0777: agent harness (non-root uid) must bind sockets here. + // `d` adjusts mode/owner on existing dirs; world-writable matches + // the chmod_socket_dir(0o777) fallback so a runtime re-sync doesn't + // break a live agent's socket dir. host_config's chown_socket_dir + // tightens ownership afterwards when the agent uid is available. + content.push_str(&format!("d {SOCKET_DIR_ROOT}/{name} 0777 root root -\n")); } // Atomic write: write to a tmp file then rename so a concurrent reader From 14c2c6d4a507017e69e7c3a79c9b362e2c11a0dd Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 8 Jul 2026 23:13:03 +0200 Subject: [PATCH 4/8] feat(#2290): ConditionPathIsDirectory= in container service drop-in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a [Unit] section to hyperhive-limits.conf (the drop-in written by write_resource_limits) with: ConditionPathIsDirectory=/run/hyperhive/agents/ When this condition is not met, systemd skips the unit with result "condition" — NOT a failure, so the start-limit counter is not incremented. Belt-and-braces on top of the tmpfiles.d fix (subtask 2): if a dir is somehow absent at start time, the container idles instead of restart-looping into start-limit-hit. Also promote AGENT_RUNTIME_ROOT to a module-level const (was duplicated inside two functions) and remove the duplicates. --- hive-priv/src/main.rs | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 3aee0b2c..bc447a7f 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -33,6 +33,10 @@ use tokio::process::Command; /// Root of the per-agent unix-socket dirs on the host. const SOCKET_DIR_ROOT: &str = "/run/hive-agent"; +/// Root of the per-agent MCP socket dirs on the host. +/// Matches `coordinator::AGENT_RUNTIME_ROOT` in hive-c0re. +const AGENT_RUNTIME_ROOT: &str = "/run/hyperhive/agents"; + #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt() @@ -422,17 +426,40 @@ fn chmod_socket_dir(agent_name: &str, mode: u32) -> Result<(String, String)> { } /// `WriteResourceLimits` — drop a systemd `MemoryMax`/`CPUQuota` -/// override into the container service's drop-in dir. +/// override into the container service's drop-in dir, together with a +/// `ConditionPathIsDirectory=` guard on the agent's MCP runtime dir. +/// +/// The condition causes systemd to *skip* (not *fail*) the unit when the +/// bind-mount source dir is absent — result is `condition`, which does not +/// increment the start-limit counter. This is belt-and-braces on top of +/// the tmpfiles.d entries written by `SyncAgentTmpfiles`: in the unlikely +/// event the dir is missing at start time, the unit idles rather than +/// restart-looping into `start-limit-hit`. fn write_resource_limits( container: &str, memory_max: &str, cpu_quota: &str, ) -> Result<(String, String)> { validate_container_system_name(container)?; + // Derive the logical agent name (strip h- prefix) to form the runtime + // dir path. Falls back to the full container name for infra containers + // that don't use the h- prefix. + let logical = container.strip_prefix(AGENT_PREFIX).unwrap_or(container); + let runtime_dir = format!("{AGENT_RUNTIME_ROOT}/{logical}"); let dir = format!("/run/systemd/system/container@{container}.service.d"); std::fs::create_dir_all(&dir).with_context(|| format!("create {dir}"))?; let path = format!("{dir}/hyperhive-limits.conf"); - let content = format!("[Service]\nMemoryMax={memory_max}\nCPUQuota={cpu_quota}\n"); + // [Unit] section: condition checked at start time — skips (not fails) + // the unit when the MCP socket dir is absent, avoiding restart loops. + // [Service] section: resource caps. + let content = format!( + "[Unit]\n\ + ConditionPathIsDirectory={runtime_dir}\n\ + \n\ + [Service]\n\ + MemoryMax={memory_max}\n\ + CPUQuota={cpu_quota}\n" + ); std::fs::write(&path, content).with_context(|| format!("write {path}"))?; Ok((String::new(), String::new())) } @@ -1512,7 +1539,6 @@ fn write_bridge_dns_marker(container: &str, isolation: Option<&NetworkIsolation> /// as `/run/hive`) /// - `/run/hive-agent/` (web socket dir, bind-mounted into container) const TMPFILES_PATH: &str = "/etc/tmpfiles.d/hyperhive-agents.conf"; -const AGENT_RUNTIME_ROOT: &str = "/run/hyperhive/agents"; async fn sync_agent_tmpfiles(agents: &[String]) -> Result<(String, String)> { for name in agents { From dce97f482687f9e1e8fb01012764099182705b68 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 8 Jul 2026 23:18:19 +0200 Subject: [PATCH 5/8] style: rustfmt --- hive-priv/src/main.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index bc447a7f..4f840d5c 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -331,9 +331,7 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String, upgrade_agent_subvolume(agent_name).await } - PrivRequest::SyncAgentTmpfiles { ref agents } => { - sync_agent_tmpfiles(agents).await - } + PrivRequest::SyncAgentTmpfiles { ref agents } => sync_agent_tmpfiles(agents).await, } } @@ -1546,18 +1544,21 @@ async fn sync_agent_tmpfiles(agents: &[String]) -> Result<(String, String)> { } // Build tmpfiles.d content. Root dirs first, then per-agent. - let mut content = String::from( - "# managed by hive-c0re — do not edit (regenerated on spawn/destroy)\n", - ); + let mut content = + String::from("# managed by hive-c0re — do not edit (regenerated on spawn/destroy)\n"); // Parent dirs — created with permissive mode so hive-c0re can make subdirs. // /run/hyperhive itself is also a RuntimeDirectory of hive-c0re.service; the // tmpfiles.d entry here ensures it exists before hive-c0re starts (boot race). content.push_str("d /run/hyperhive 0750 hive-core hive-core -\n"); - content.push_str(&format!("d {AGENT_RUNTIME_ROOT} 0755 hive-core hive-core -\n")); + content.push_str(&format!( + "d {AGENT_RUNTIME_ROOT} 0755 hive-core hive-core -\n" + )); content.push_str(&format!("d {SOCKET_DIR_ROOT} 0755 root root -\n")); // Per-agent dirs. for name in agents { - content.push_str(&format!("d {AGENT_RUNTIME_ROOT}/{name} 0755 hive-core hive-core -\n")); + content.push_str(&format!( + "d {AGENT_RUNTIME_ROOT}/{name} 0755 hive-core hive-core -\n" + )); // 0777: agent harness (non-root uid) must bind sockets here. // `d` adjusts mode/owner on existing dirs; world-writable matches // the chmod_socket_dir(0o777) fallback so a runtime re-sync doesn't From 949bab62d3327a207b32132397d81ab09f8c5c54 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 8 Jul 2026 23:41:21 +0200 Subject: [PATCH 6/8] fix(#2290): use write! instead of push_str(format!()) to satisfy clippy --- hive-priv/src/main.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 4f840d5c..5ec1bb6c 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -1544,27 +1544,32 @@ async fn sync_agent_tmpfiles(agents: &[String]) -> Result<(String, String)> { } // Build tmpfiles.d content. Root dirs first, then per-agent. + use std::fmt::Write as _; let mut content = String::from("# managed by hive-c0re — do not edit (regenerated on spawn/destroy)\n"); // Parent dirs — created with permissive mode so hive-c0re can make subdirs. // /run/hyperhive itself is also a RuntimeDirectory of hive-c0re.service; the // tmpfiles.d entry here ensures it exists before hive-c0re starts (boot race). content.push_str("d /run/hyperhive 0750 hive-core hive-core -\n"); - content.push_str(&format!( + write!( + content, "d {AGENT_RUNTIME_ROOT} 0755 hive-core hive-core -\n" - )); - content.push_str(&format!("d {SOCKET_DIR_ROOT} 0755 root root -\n")); + ) + .ok(); + write!(content, "d {SOCKET_DIR_ROOT} 0755 root root -\n").ok(); // Per-agent dirs. for name in agents { - content.push_str(&format!( + write!( + content, "d {AGENT_RUNTIME_ROOT}/{name} 0755 hive-core hive-core -\n" - )); + ) + .ok(); // 0777: agent harness (non-root uid) must bind sockets here. // `d` adjusts mode/owner on existing dirs; world-writable matches // the chmod_socket_dir(0o777) fallback so a runtime re-sync doesn't // break a live agent's socket dir. host_config's chown_socket_dir // tightens ownership afterwards when the agent uid is available. - content.push_str(&format!("d {SOCKET_DIR_ROOT}/{name} 0777 root root -\n")); + write!(content, "d {SOCKET_DIR_ROOT}/{name} 0777 root root -\n").ok(); } // Atomic write: write to a tmp file then rename so a concurrent reader From ae3ecc1de2aa14dc86a4683fc35dd079b5e4ae41 Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 9 Jul 2026 00:02:57 +0200 Subject: [PATCH 7/8] fix(#2290): drop redundant async blocks around sync_tmpfiles spawn --- hive-c0re/src/actions.rs | 2 +- hive-c0re/src/main.rs | 4 +--- hive-c0re/src/server.rs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 2339cdda..d89defea 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -969,7 +969,7 @@ pub async fn destroy(coord: &Arc, name: &str, purge: bool) -> Resul coord.emit_schedules_snapshot(); // Update tmpfiles.d to remove the destroyed agent's dirs from the // boot-time pre-creation list. Best-effort: failure is logged only. - tokio::spawn(async { lifecycle::sync_tmpfiles().await }); + tokio::spawn(lifecycle::sync_tmpfiles()); Ok(()) } diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 0168b01f..b9752362 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -286,9 +286,7 @@ async fn cmd_serve( // Sync /etc/tmpfiles.d/hyperhive-agents.conf so agent runtime dirs are // pre-declared for the next boot. Best-effort background task — a failure // here must not block hive-c0re startup. See lifecycle::sync_tmpfiles. - tokio::spawn(async { - hive_c0re::lifecycle::sync_tmpfiles().await; - }); + tokio::spawn(hive_c0re::lifecycle::sync_tmpfiles()); // Auto-update in the background — don't block service start. // Sub-agent rebuilds can take tens of seconds; we want the admin // socket up immediately. diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 8b21cd74..f0913c85 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -217,7 +217,7 @@ async fn handle_spawn(coord: &Arc, name: &str) -> Result { // Roll back socket registration if container creation failed. From b1243f149f729367b35efb7ba62c8c453de9cbd5 Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 9 Jul 2026 00:19:15 +0200 Subject: [PATCH 8/8] fix(#2290): move use Write before statements, use writeln! in sync_agent_tmpfiles --- hive-priv/src/main.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 5ec1bb6c..5a39d5ef 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -1539,29 +1539,25 @@ fn write_bridge_dns_marker(container: &str, isolation: Option<&NetworkIsolation> const TMPFILES_PATH: &str = "/etc/tmpfiles.d/hyperhive-agents.conf"; async fn sync_agent_tmpfiles(agents: &[String]) -> Result<(String, String)> { + use std::fmt::Write as _; for name in agents { validate_agent_name(name)?; } // Build tmpfiles.d content. Root dirs first, then per-agent. - use std::fmt::Write as _; let mut content = String::from("# managed by hive-c0re — do not edit (regenerated on spawn/destroy)\n"); // Parent dirs — created with permissive mode so hive-c0re can make subdirs. // /run/hyperhive itself is also a RuntimeDirectory of hive-c0re.service; the // tmpfiles.d entry here ensures it exists before hive-c0re starts (boot race). content.push_str("d /run/hyperhive 0750 hive-core hive-core -\n"); - write!( - content, - "d {AGENT_RUNTIME_ROOT} 0755 hive-core hive-core -\n" - ) - .ok(); - write!(content, "d {SOCKET_DIR_ROOT} 0755 root root -\n").ok(); + writeln!(content, "d {AGENT_RUNTIME_ROOT} 0755 hive-core hive-core -").ok(); + writeln!(content, "d {SOCKET_DIR_ROOT} 0755 root root -").ok(); // Per-agent dirs. for name in agents { - write!( + writeln!( content, - "d {AGENT_RUNTIME_ROOT}/{name} 0755 hive-core hive-core -\n" + "d {AGENT_RUNTIME_ROOT}/{name} 0755 hive-core hive-core -" ) .ok(); // 0777: agent harness (non-root uid) must bind sockets here. @@ -1569,7 +1565,7 @@ async fn sync_agent_tmpfiles(agents: &[String]) -> Result<(String, String)> { // the chmod_socket_dir(0o777) fallback so a runtime re-sync doesn't // break a live agent's socket dir. host_config's chown_socket_dir // tightens ownership afterwards when the agent uid is available. - write!(content, "d {SOCKET_DIR_ROOT}/{name} 0777 root root -\n").ok(); + writeln!(content, "d {SOCKET_DIR_ROOT}/{name} 0777 root root -").ok(); } // Atomic write: write to a tmp file then rename so a concurrent reader