refactor(#838): consolidate harness state files into hyperhive-harness.json

This commit is contained in:
damocles 2026-05-31 20:19:56 +02:00
commit fce1f49f6a
7 changed files with 166 additions and 78 deletions

View file

@ -4,8 +4,8 @@
//! excluded from the map (manager UI routes via the dashboard
//! upstream, not per-agent `/agent/<name>/`).
//!
//! Full mechanism — per-agent subdir bind-mount, `.bound` marker
//! gate, gateway UDS upstream, transition vs `agent-ports.json`,
//! Full mechanism — per-agent subdir bind-mount, `hyperhive-socket-bound`
//! marker gate, gateway UDS upstream, transition vs `agent-ports.json`,
//! 10s poll loop: `docs/gateway.md::Per-agent unix-socket upstream`.
use std::collections::BTreeMap;
@ -36,7 +36,13 @@ pub const SOCKET_FILENAME: &str = "web.sock";
/// publish the unix upstream for this agent yet". Without this gate
/// the gateway would `proxy_pass` to a non-existent socket for every
/// sub-agent that hasn't flipped the option yet.
pub const READY_MARKER: &str = ".bound";
///
/// Renamed from `.bound` (legacy) to match the `hyperhive-` prefix
/// convention for all harness-written state files (#838). `build_map`
/// checks both names during the transition window so existing containers
/// don't lose gateway routing before their next rebuild.
pub const READY_MARKER: &str = "hyperhive-socket-bound";
const READY_MARKER_LEGACY: &str = ".bound";
#[must_use]
pub fn host_sockets_path() -> PathBuf {
@ -72,18 +78,27 @@ pub fn socket_path_for(name: &str) -> PathBuf {
/// `proxy_pass` to a non-existent socket for every sub-agent that
/// hasn't yet flipped `hyperhive.web.useUnixSocket = true`.
///
/// Accepts either the new `hyperhive-socket-bound` marker or the legacy
/// `.bound` marker so existing containers keep their gateway routing
/// through the transition window (before their next rebuild writes the
/// new marker name).
///
/// `BTreeMap` keeps the JSON output sorted by key so a re-emit
/// without churn produces byte-identical output — same idempotency
/// shape `agent_ports::write` relies on.
#[must_use]
pub fn build_map(names: &[String]) -> BTreeMap<String, PathBuf> {
build_map_with(names, |name| ready_marker_for(name).exists())
build_map_with(names, |name| {
ready_marker_for(name).exists()
|| agent_dir_for(name).join(READY_MARKER_LEGACY).exists()
})
}
/// Body of `build_map` with the ready-check parameterised. Tests
/// pass a predicate they control (no real filesystem access).
/// Production callers go through `build_map` which wires the
/// predicate to the on-disk `.bound` marker check.
/// predicate to the on-disk `hyperhive-socket-bound` (or legacy
/// `.bound`) marker check.
fn build_map_with<F>(names: &[String], is_ready: F) -> BTreeMap<String, PathBuf>
where
F: Fn(&str) -> bool,
@ -96,7 +111,7 @@ where
.collect()
}
/// Path to the per-agent `.bound` marker file the harness writes
/// Path to the `hyperhive-socket-bound` marker file the harness writes
/// after a successful `bind_unix`. Lives next to `web.sock` in the
/// per-agent subdir so it's covered by the same bind-mount and same
/// per-agent isolation as the socket itself.
@ -271,10 +286,9 @@ mod tests {
#[test]
fn build_map_filters_by_ready_predicate() {
// The new gate: only ready agents (with `.bound` marker) get
// published. Pin the behaviour so a future refactor that
// drops the filter surfaces here, not as a 502-spew in the
// gateway.
// Only ready agents (with `hyperhive-socket-bound` marker) get
// published. Pin the behaviour so a future refactor that drops
// the filter surfaces here, not as a 502-spew in the gateway.
let names: Vec<String> = ["iris", "argus", "atlas"]
.iter()
.map(|s| (*s).to_owned())
@ -296,7 +310,7 @@ mod tests {
let marker = ready_marker_for("iris");
let socket = socket_path_for("iris");
assert_eq!(marker.parent(), socket.parent());
assert_eq!(marker, Path::new("/run/hive-agent/iris/.bound"));
assert_eq!(marker, Path::new("/run/hive-agent/iris/hyperhive-socket-bound"));
}
#[test]