feat(#2290): ConditionPathIsDirectory= in container service drop-in

Add a [Unit] section to hyperhive-limits.conf (the drop-in written by
write_resource_limits) with:

  ConditionPathIsDirectory=/run/hyperhive/agents/<name>

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.
This commit is contained in:
atlas 2026-07-08 23:13:03 +02:00 committed by mara
commit 14c2c6d4a5

View file

@ -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/<name>` (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 {