The two report_compact tests build a Bus, which resolves the events db under HYPERHIVE_HARNESS_DIR and panics when it is unset, as in the nix test sandbox. Add a test-only paths::with_harness_dir that points the variable at a temp dir under a lock, and use it in both the new tests and the existing swarm_inference Bus test, which set the variable unguarded.
168 lines
7.3 KiB
Rust
168 lines
7.3 KiB
Rust
//! Per-agent path resolution for state, harness, and credential directories.
|
|
//!
|
|
//! All agents (including the manager `root`) use `/agents/{label}/state`
|
|
//! for agent-owned durable notes, and `/agents/{label}/harness` for
|
|
//! harness-internal files (`hyperhive-events.sqlite`, `hyperhive-model`, etc.)
|
|
//! that should not clutter what claude sees as "my notes dir".
|
|
//! Claude credentials live at `$HOME/.claude` (resolves to
|
|
//! `/home/<agent-name>/.claude` because the harness service runs as a
|
|
//! non-root unix user matching the agent label — see
|
|
//! `docs/agent-lifecycle/persistence.md::First-boot agent-user migration`).
|
|
//!
|
|
//! All three paths can be overridden via env vars (`HYPERHIVE_STATE_DIR`,
|
|
//! `HYPERHIVE_HARNESS_DIR`, `HYPERHIVE_CLAUDE_DIR`) for dev / test scenarios.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
/// Durable state directory for the current agent. Reads `HYPERHIVE_STATE_DIR`
|
|
/// first (always set by the meta flake to `/agents/{label}/state`); falls back
|
|
/// to the same pattern derived from `HIVE_LABEL` for dev/test environments
|
|
/// where the env var may not be set.
|
|
#[must_use]
|
|
pub fn state_dir() -> PathBuf {
|
|
if let Some(p) = std::env::var_os("HYPERHIVE_STATE_DIR") {
|
|
return PathBuf::from(p);
|
|
}
|
|
let label = std::env::var("HIVE_LABEL").unwrap_or_default();
|
|
PathBuf::from(format!("/agents/{label}/state"))
|
|
}
|
|
|
|
/// Harness-internal state directory. Holds files the harness owns
|
|
/// (`hyperhive-events.sqlite`, `hyperhive-turn-stats.sqlite`,
|
|
/// `hyperhive-model`) so they do not appear inside the agent-visible
|
|
/// `/agents/{label}/state` tree. Delegates to the shared canonical
|
|
/// resolver in `hive_agent_sock::paths` so the harness + every
|
|
/// out-of-process MCP daemon resolve this identically (reads
|
|
/// `HYPERHIVE_HARNESS_DIR`, always injected by the meta flake — panics
|
|
/// if it's unset rather than silently deriving a fallback path).
|
|
#[must_use]
|
|
pub fn harness_dir() -> PathBuf {
|
|
hive_agent_sock::paths::harness_dir()
|
|
}
|
|
|
|
/// Consolidated harness-local state db — todos + reminders, one table
|
|
/// each — mutable per-agent state the harness owns, kept out of the
|
|
/// append-only `hyperhive-events.sqlite` sink. Each store's `open()` only
|
|
/// applies its own `CREATE TABLE IF NOT EXISTS` against this same path
|
|
/// (see their call sites), so distinct table names keep them from
|
|
/// colliding without needing a per-store path wrapper here.
|
|
///
|
|
/// ⚠️ A pre-existing harness may still carry an inert `questions` table
|
|
/// here from the now-removed ask/answer mechanism — nothing opens or
|
|
/// writes it, and any rows in it are harmless leftovers, not a store to
|
|
/// migrate or clean up.
|
|
///
|
|
/// A one-time boot migration (`db_migrate::run`) folds the older
|
|
/// `hyperhive-todos.sqlite` / `hyperhive-reminders.sqlite` files into this
|
|
/// path on first boot after an upgrade.
|
|
#[must_use]
|
|
pub fn state_db() -> PathBuf {
|
|
harness_dir().join("hyperhive-state.sqlite")
|
|
}
|
|
|
|
/// Legacy pre-consolidation todos db path, consulted only by
|
|
/// [`crate::db_migrate`] on the first boot after the upgrade.
|
|
#[must_use]
|
|
pub fn legacy_todos_db() -> PathBuf {
|
|
harness_dir().join("hyperhive-todos.sqlite")
|
|
}
|
|
|
|
/// Legacy pre-consolidation reminders db path, consulted only by
|
|
/// [`crate::db_migrate`] on the first boot after the upgrade.
|
|
#[must_use]
|
|
pub fn legacy_reminders_db() -> PathBuf {
|
|
harness_dir().join("hyperhive-reminders.sqlite")
|
|
}
|
|
|
|
/// Per-turn config dir for the regenerated claude-{mcp-config,settings,
|
|
/// system-prompt} files the harness drops before each turn. Set by
|
|
/// systemd via `RuntimeDirectory = "hive-config"`: a per-service runtime
|
|
/// dir owned by the agent unix user, auto-cleared on stop. Kept separate
|
|
/// from `/run/hive` (the host-owned mcp.sock bind) so the harness owns
|
|
/// its own write surface and we don't have to chown a bind-mounted dir.
|
|
/// Overridable via `HYPERHIVE_CONFIG_DIR` for dev / test scenarios.
|
|
#[must_use]
|
|
pub fn config_dir() -> PathBuf {
|
|
if let Some(p) = std::env::var_os("HYPERHIVE_CONFIG_DIR") {
|
|
return PathBuf::from(p);
|
|
}
|
|
PathBuf::from("/run/hive-config")
|
|
}
|
|
|
|
/// Marker file whose presence means "this agent is paused": the harness
|
|
/// keeps serving its web UI and MCP daemons but drives no turns, so
|
|
/// inbox messages queue up unacked until it's removed.
|
|
///
|
|
/// It lives in the harness dir rather than `state/` because `state/` is
|
|
/// the agent's own scratch space — this is harness control state. The
|
|
/// harness dir is bind-mounted from the host, so the marker is the
|
|
/// single source of truth for both sides: the harness stats it to gate
|
|
/// the turn loop, and hive-c0re stats it to render the paused
|
|
/// indicator and creates/removes it for `hivectl pause|resume`. Being a
|
|
/// plain file, it survives container restarts — pause is sticky by
|
|
/// construction, and works even when the harness isn't running.
|
|
/// The filename constant is shared via `hive_sh4re::paths::PAUSED_MARKER_FILE`
|
|
/// (itself re-exported from `hive-priv-sock`, the actual writer on the host
|
|
/// side) so this resolver and hive-c0re's host-side one
|
|
/// (`Coordinator::agent_paused_marker`) cannot drift apart — only the
|
|
/// call-site-specific composition lived in the shared crate for no reason,
|
|
/// since hive-agent is the only in-container caller.
|
|
#[must_use]
|
|
pub fn paused_marker() -> PathBuf {
|
|
harness_dir().join(hive_sh4re::paths::PAUSED_MARKER_FILE)
|
|
}
|
|
|
|
/// Claude credentials directory for the current agent. `$HOME/.claude`
|
|
/// matches what the `claude` CLI reads at runtime — the harness sees
|
|
/// the same `$HOME` set by the per-service systemd `environment`
|
|
/// declaration (`/home/<agent>`). Falls back to `/root/.claude` for
|
|
/// dev / test environments where `HOME` isn't set so the previous
|
|
/// root-by-default shape keeps working without env wiring.
|
|
/// Overridable via `HYPERHIVE_CLAUDE_DIR` for dev / test scenarios.
|
|
#[must_use]
|
|
pub fn claude_dir() -> PathBuf {
|
|
if let Some(p) = std::env::var_os("HYPERHIVE_CLAUDE_DIR") {
|
|
return PathBuf::from(p);
|
|
}
|
|
if let Some(home) = std::env::var_os("HOME") {
|
|
let mut path = PathBuf::from(home);
|
|
path.push(".claude");
|
|
return path;
|
|
}
|
|
PathBuf::from("/root/.claude")
|
|
}
|
|
|
|
/// Run `f` with `HYPERHIVE_HARNESS_DIR` pointing at a fresh temp dir, then
|
|
/// restore the previous value. Tests that build a `Bus` or read the harness
|
|
/// state files go through this: the variable is process-wide, so the calls
|
|
/// are serialised on a lock.
|
|
#[cfg(test)]
|
|
pub fn with_harness_dir<T>(f: impl FnOnce() -> T) -> T {
|
|
use std::ffi::OsString;
|
|
use std::sync::Mutex;
|
|
|
|
static LOCK: Mutex<()> = Mutex::new(());
|
|
|
|
struct Restore(Option<OsString>);
|
|
impl Drop for Restore {
|
|
fn drop(&mut self) {
|
|
// SAFETY: still holding `LOCK`, the only writer of this variable.
|
|
unsafe {
|
|
match self.0.take() {
|
|
Some(v) => std::env::set_var("HYPERHIVE_HARNESS_DIR", v),
|
|
None => std::env::remove_var("HYPERHIVE_HARNESS_DIR"),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let _lock = LOCK
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let dir = tempfile::tempdir().unwrap();
|
|
// Declared after `_lock` so the variable is restored before the lock drops.
|
|
let _restore = Restore(std::env::var_os("HYPERHIVE_HARNESS_DIR"));
|
|
// SAFETY: serialised by `LOCK`.
|
|
unsafe { std::env::set_var("HYPERHIVE_HARNESS_DIR", dir.path()) };
|
|
f()
|
|
}
|