fold hive-c0re module tree into the daemon binary + drop dead pub items surfaced by bin-only (#2513)
This commit is contained in:
parent
d04c86e9ac
commit
673aea4e50
15 changed files with 95 additions and 250 deletions
|
|
@ -54,14 +54,6 @@ pub fn read() -> BTreeMap<String, Option<String>> {
|
|||
serde_json::from_str(&raw).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Look up one agent's parent. Returns `None` when the agent is root
|
||||
/// or absent from the file. Cheap convenience over `read()` for
|
||||
/// callers that want a single entry.
|
||||
#[must_use]
|
||||
pub fn parent_of(name: &str) -> Option<String> {
|
||||
read().get(name).cloned().flatten()
|
||||
}
|
||||
|
||||
/// Return the direct children of `name` — agents whose `topology.json`
|
||||
/// entry has `name` as their parent. Reads the map once and scans all
|
||||
/// entries; cheap enough for the fan-out path (one disk read per send
|
||||
|
|
@ -489,30 +481,9 @@ pub fn has_role_in(roles: &BTreeMap<String, Vec<String>>, name: &str, role: &str
|
|||
.is_some_and(|rs| rs.iter().any(|r| r == role))
|
||||
}
|
||||
|
||||
/// Grant or revoke a role for `name`. Idempotent — no disk write when the
|
||||
/// state is already correct.
|
||||
///
|
||||
/// Empty role lists are kept in the map (never removed). An absent key means
|
||||
/// "never seen" (seed on next `reconcile_roles`); an empty list means
|
||||
/// "explicitly revoked" (do not re-seed). Callers that want to remove an
|
||||
/// agent from the map entirely should use `reconcile_roles` (agent departure).
|
||||
pub fn set_role(name: &str, role: &str, enabled: bool) -> Result<(), String> {
|
||||
let mut roles = read_roles();
|
||||
let list = roles.entry(name.to_owned()).or_default();
|
||||
let held = list.iter().any(|r| r == role);
|
||||
match (enabled, held) {
|
||||
(true, false) => list.push(role.to_owned()),
|
||||
(false, true) => list.retain(|r| r != role),
|
||||
_ => return Ok(()),
|
||||
}
|
||||
// Intentionally do NOT remove empty entries — an empty list signals an
|
||||
// explicit revoke and prevents reconcile_roles from re-seeding the role.
|
||||
write_roles(&roles).map_err(|e| format!("write roles.json: {e}"))
|
||||
}
|
||||
|
||||
/// Reconcile `roles.json` against the current agent set:
|
||||
/// - Seeds root's default `can_manage_top_level_agents` role on first
|
||||
/// appearance (operator can revoke with `set_role`).
|
||||
/// appearance.
|
||||
/// - Drops entries for agents that no longer exist.
|
||||
///
|
||||
/// Returns true when the file changed.
|
||||
|
|
|
|||
|
|
@ -305,15 +305,15 @@ mod serve_config_tests {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct AgentPaths {
|
||||
/// Runtime socket dir (tmpfs, recreated per boot).
|
||||
pub agent_dir: PathBuf,
|
||||
pub agent: PathBuf,
|
||||
/// Manager-editable proposed config repo.
|
||||
pub proposed_dir: PathBuf,
|
||||
pub proposed: PathBuf,
|
||||
/// Hive-c0re-authoritative applied config repo.
|
||||
pub applied_dir: PathBuf,
|
||||
pub applied: PathBuf,
|
||||
/// Claude OAuth credentials (survives purge boundary).
|
||||
pub claude_dir: PathBuf,
|
||||
pub claude: PathBuf,
|
||||
/// Agent durable notes + forge token (survives purge boundary).
|
||||
pub notes_dir: PathBuf,
|
||||
pub notes: PathBuf,
|
||||
}
|
||||
|
||||
/// Per-agent in-progress state that the dashboard surfaces between approve
|
||||
|
|
@ -377,7 +377,7 @@ pub enum TransientKind {
|
|||
Starting,
|
||||
/// `lifecycle::kill` is running.
|
||||
Stopping,
|
||||
/// `lifecycle::restart` is running.
|
||||
/// A restart (`lifecycle::kill` then `lifecycle::start`) is running.
|
||||
Restarting,
|
||||
/// `lifecycle::rebuild` is running (nixos-container update).
|
||||
Rebuilding,
|
||||
|
|
@ -542,11 +542,11 @@ impl Coordinator {
|
|||
#[must_use]
|
||||
pub fn agent_paths(name: &str, agent_dir: PathBuf) -> AgentPaths {
|
||||
AgentPaths {
|
||||
agent_dir,
|
||||
proposed_dir: Self::agent_proposed_dir(name),
|
||||
applied_dir: crate::paths::applied_dir(name),
|
||||
claude_dir: Self::agent_claude_dir(name),
|
||||
notes_dir: Self::agent_notes_dir(name),
|
||||
agent: agent_dir,
|
||||
proposed: Self::agent_proposed_dir(name),
|
||||
applied: crate::paths::applied_dir(name),
|
||||
claude: Self::agent_claude_dir(name),
|
||||
notes: Self::agent_notes_dir(name),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,13 +99,6 @@ const AGENTS_ORG: &str = "agents";
|
|||
/// references this team by name for the merge/approval whitelist, so the
|
||||
/// rule never hardcodes a specific reviewer agent (which may not exist).
|
||||
const OPERATORS_TEAM: &str = "operators";
|
||||
/// Hive-managed Forgejo namespaces that agent-initiated repo creation must
|
||||
/// never target. `internal` is operator-curated shared content;
|
||||
/// `agent-configs` + `core` are hive-c0re-internal mirror/meta namespaces.
|
||||
/// (`hyperhive` is NOT managed — it's just a repo that happens to be built
|
||||
/// by this hive.) hive-c0re's create path forces [`AGENTS_ORG`], so this is
|
||||
/// a defensive guard against any future caller passing an explicit owner.
|
||||
const HIVE_MANAGED_NAMESPACES: &[&str] = &[SHARED_ORG, CONFIG_ORG, "core"];
|
||||
/// Forgejo orgs hive-c0re ensures on startup. The meta repo lives at
|
||||
/// `core/meta` (the `core` user's own namespace — no org needed).
|
||||
const SEEDED_ORGS: &[&str] = &[CONFIG_ORG, SHARED_ORG, AGENTS_ORG];
|
||||
|
|
@ -152,16 +145,6 @@ pub(crate) fn api(token: &str) -> Result<Forgejo> {
|
|||
Forgejo::new(Auth::Token(token), url).context("build forgejo api client")
|
||||
}
|
||||
|
||||
/// Whether `ns` is a hive-managed Forgejo namespace that agent-initiated
|
||||
/// repo creation must never target — `internal` (operator-curated
|
||||
/// shared content) + `agent-configs` / `core` (hive-c0re-internal). The
|
||||
/// create path forces [`AGENTS_ORG`], so this guards a future surface that
|
||||
/// might accept an explicit owner.
|
||||
#[must_use]
|
||||
pub fn is_hive_managed_namespace(ns: &str) -> bool {
|
||||
HIVE_MANAGED_NAMESPACES.contains(&ns)
|
||||
}
|
||||
|
||||
/// Per-agent forge sync: ensure the agent has a forgejo user + token,
|
||||
/// a mirrored config repo, read access to `core/meta`, and the `meta`
|
||||
/// remote in its proposed repo. All operations are idempotent; failures
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ pub struct Claim {
|
|||
/// it.
|
||||
pub agent: String,
|
||||
pub template: Template,
|
||||
pub source: Source,
|
||||
pub approval_id: Option<i64>,
|
||||
pub inputs: Vec<String>,
|
||||
pub perm_payload: Option<PermPayload>,
|
||||
|
|
@ -330,7 +329,6 @@ impl JobQueue {
|
|||
kind: dag.node(node_id).expect("node").kind.clone(),
|
||||
agent: node_agent,
|
||||
template: dag.template,
|
||||
source: dag.source,
|
||||
approval_id: dag.approval_id,
|
||||
inputs: dag.inputs.clone(),
|
||||
perm_payload: dag.perm_payload.clone(),
|
||||
|
|
|
|||
|
|
@ -112,10 +112,12 @@ pub fn approval_deploy(agent: &str, approval_id: i64, reason: String) -> DagSpec
|
|||
}
|
||||
}
|
||||
|
||||
/// Boot-time reconcile: a single `Reconcile` node that converges observed
|
||||
/// power state to the persisted intent — `wanted` is untouched (no
|
||||
/// `SetWanted`), unlike the operator `start`/`stop` templates. Used only
|
||||
/// by the boot sweep now.
|
||||
/// A single `Reconcile` node that converges observed power state to the
|
||||
/// persisted intent — `wanted` is untouched (no `SetWanted`), unlike the
|
||||
/// operator `start`/`stop` templates. Test-only helper now (used to build
|
||||
/// single-node lifecycle DAGs that exercise per-agent lease serialization
|
||||
/// in the queue tests); production paths no longer emit a bare reconcile.
|
||||
#[cfg(test)]
|
||||
pub fn reconcile_only(
|
||||
template: Template,
|
||||
agent: &str,
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
//! `hive-c0re` library — the coordinator daemon's module surface
|
||||
//! (coordinator, broker, axum dashboard, admin/manager/agent unix
|
||||
//! sockets, background sweepers). Consumed by the `hive-c0re` daemon
|
||||
//! binary (`src/main.rs`).
|
||||
//!
|
||||
//! The operator CLI lives in the **standalone `hivectl` crate**, which
|
||||
//! talks to the daemon over the host admin socket (`hive-host-sock` wire
|
||||
//! types) rather than linking this crate — so `hive-c0re` is daemon-only,
|
||||
//! not a library shared with a CLI.
|
||||
//!
|
||||
//! Every module is re-exported `pub` so anything in the crate is
|
||||
//! addressable from the daemon binary; the lib doesn't have a curated
|
||||
//! surface beyond "this is where the modules live".
|
||||
//!
|
||||
//! Cohesive clusters live in directory submodules (`stores`, `stats`,
|
||||
//! `agent_config`, `workers`); each of their children is re-exported
|
||||
//! at the crate root so pre-existing `crate::broker::…` /
|
||||
//! `hive_c0re::broker::…` paths keep compiling unchanged.
|
||||
|
||||
pub mod actions;
|
||||
pub mod agent_config;
|
||||
pub mod container_view;
|
||||
pub mod coordinator;
|
||||
pub mod dashboard;
|
||||
pub mod dashboard_events;
|
||||
pub mod forge;
|
||||
pub mod gateway_nginx;
|
||||
pub mod job_queue;
|
||||
pub mod lifecycle;
|
||||
pub mod loose_ends;
|
||||
pub mod matrix;
|
||||
pub mod meta;
|
||||
pub mod migrate;
|
||||
pub mod paths;
|
||||
pub mod priv_client;
|
||||
pub mod questions;
|
||||
pub mod server;
|
||||
pub mod socket_server;
|
||||
pub mod stats;
|
||||
pub mod stores;
|
||||
pub mod webhook_secret;
|
||||
pub mod workers;
|
||||
|
||||
// Root re-exports: keep every pre-grouping `crate::<module>` /
|
||||
// `hive_c0re::<module>` path compiling without touching consumers.
|
||||
pub use agent_config::{capabilities, limits, tool_groups, topology};
|
||||
pub use stats::{container_stats, hive_stats, host_stats, otel_metrics, sweep_health, warnings};
|
||||
pub use stores::{
|
||||
approvals, audit_log, broker, build_logs, db, operator_questions, power, scheduled_prompts,
|
||||
};
|
||||
pub use workers::{
|
||||
agent_sockets, auto_update, crash_watch, knowledge, mcp_sockets, reminder_scheduler,
|
||||
scheduled_prompts_worker,
|
||||
};
|
||||
|
|
@ -22,13 +22,7 @@ use super::{
|
|||
pub async fn write_dropins(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> {
|
||||
validate(name)?;
|
||||
let container = container_name(name);
|
||||
set_nspawn_flags(
|
||||
&container,
|
||||
&paths.agent_dir,
|
||||
&paths.claude_dir,
|
||||
&paths.notes_dir,
|
||||
)
|
||||
.await?;
|
||||
set_nspawn_flags(&container, &paths.agent, &paths.claude, &paths.notes).await?;
|
||||
set_resource_limits(&container, &hive.agent_cpu_quota, &hive.agent_memory_max).await?;
|
||||
systemd_daemon_reload().await
|
||||
}
|
||||
|
|
@ -44,16 +38,6 @@ async fn systemd_daemon_reload() -> Result<()> {
|
|||
crate::priv_client::daemon_reload().await
|
||||
}
|
||||
|
||||
/// Idempotently rewrite the lines in `/etc/nixos-containers/<container>.conf`
|
||||
/// that hive-c0re owns: `PRIVATE_NETWORK` (forced 0 so the agent's web UI port
|
||||
/// is reachable on the host) and `EXTRA_NSPAWN_FLAGS` (the runtime-dir bind).
|
||||
/// The start script expands `$EXTRA_NSPAWN_FLAGS` unquoted into the
|
||||
/// `systemd-nspawn` command.
|
||||
/// Where in the container's filesystem the manager sees its agents tree.
|
||||
/// Matches the `/agents` path that pre-Phase-8 hosts declared via
|
||||
/// `containers.root.bindMounts."/agents"`.
|
||||
pub const CONTAINER_MANAGER_AGENTS_MOUNT: &str = "/agents";
|
||||
|
||||
/// Where the manager sees the applied trees of every agent, read-only.
|
||||
/// Manager runs `git fetch /applied/<n>/.git refs/tags/*:refs/tags/applied/*`
|
||||
/// to learn what hive-c0re deployed (or rejected, or failed to
|
||||
|
|
@ -115,6 +99,11 @@ fn hive_load_credentials() -> Vec<CredentialMount> {
|
|||
out
|
||||
}
|
||||
|
||||
/// Idempotently rewrite the lines in `/etc/nixos-containers/<container>.conf`
|
||||
/// that hive-c0re owns: `PRIVATE_NETWORK` (forced 0 so the agent's web UI port
|
||||
/// is reachable on the host) and `EXTRA_NSPAWN_FLAGS` (the runtime-dir bind).
|
||||
/// The start script expands `$EXTRA_NSPAWN_FLAGS` unquoted into the
|
||||
/// `systemd-nspawn` command.
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
reason = "one contiguous nspawn-flag assembly block; the length is the flag \
|
||||
|
|
|
|||
|
|
@ -10,16 +10,12 @@ pub use git::{
|
|||
git, git_command, git_read_tree_reset, git_rev_parse, git_tag, git_tag_annotated,
|
||||
git_update_ref,
|
||||
};
|
||||
pub use host_config::{
|
||||
CONTAINER_MANAGER_AGENTS_MOUNT, CONTAINER_MANAGER_APPLIED_MOUNT, write_dropins,
|
||||
};
|
||||
pub use host_config::write_dropins;
|
||||
pub use setup::{
|
||||
ensure_agent_state_subvolume, ensure_claude_dir, ensure_state_dir, initial_flake_nix,
|
||||
setup_applied, setup_proposed,
|
||||
};
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use tokio::process::Command;
|
||||
|
||||
|
|
@ -164,23 +160,6 @@ pub fn agent_uid_gid(agent_name: &str) -> Option<(u32, u32)> {
|
|||
None
|
||||
}
|
||||
|
||||
/// Best-effort `chown(path, agent_uid, agent_gid)`. Resolves the agent's
|
||||
/// uid/gid via [`agent_uid_gid`] and shells out to `std::os::unix::fs::chown`.
|
||||
/// Silently no-ops when the container isn't built yet (`None` from
|
||||
/// [`agent_uid_gid`]) and logs at debug on chown syscall failure — the
|
||||
/// activation script in the harness user module is the steady-state safety
|
||||
/// net. Used by per-agent state writers in `forge` + `matrix` so the
|
||||
/// agent can read the file without waiting for the next container
|
||||
/// rebuild.
|
||||
pub fn chown_to_agent(name: &str, path: &Path, subsystem: &str) {
|
||||
let Some((uid, gid)) = agent_uid_gid(name) else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = std::os::unix::fs::chown(path, Some(uid), Some(gid)) {
|
||||
tracing::debug!(%name, %subsystem, path = %path.display(), error = %e, "chown to agent failed");
|
||||
}
|
||||
}
|
||||
|
||||
fn validate(name: &str) -> Result<()> {
|
||||
if name.is_empty() {
|
||||
bail!("agent name must not be empty");
|
||||
|
|
@ -239,11 +218,11 @@ pub async fn provision_container(name: &str, hive: &HiveEnv, paths: &AgentPaths)
|
|||
agent_web_port(name)
|
||||
);
|
||||
}
|
||||
setup_proposed(&paths.proposed_dir, name).await?;
|
||||
setup_applied(&paths.applied_dir, Some(&paths.proposed_dir), name).await?;
|
||||
setup_proposed(&paths.proposed, name).await?;
|
||||
setup_applied(&paths.applied, Some(&paths.proposed), name).await?;
|
||||
ensure_agent_state_subvolume(name).await?;
|
||||
ensure_claude_dir(&paths.claude_dir)?;
|
||||
ensure_state_dir(&paths.notes_dir)?;
|
||||
ensure_claude_dir(&paths.claude)?;
|
||||
ensure_state_dir(&paths.notes)?;
|
||||
// Meta flake gets the new agent's input + nixosConfiguration
|
||||
// before `nixos-container create` so the `--flake meta#<name>`
|
||||
// ref resolves.
|
||||
|
|
@ -278,10 +257,10 @@ pub async fn prepare_rebuild_dirs(name: &str, paths: &AgentPaths) -> Result<()>
|
|||
agent_web_port(name)
|
||||
);
|
||||
}
|
||||
setup_applied(&paths.applied_dir, None, name).await?;
|
||||
setup_applied(&paths.applied, None, name).await?;
|
||||
ensure_agent_state_subvolume(name).await?;
|
||||
ensure_claude_dir(&paths.claude_dir)?;
|
||||
ensure_state_dir(&paths.notes_dir)?;
|
||||
ensure_claude_dir(&paths.claude)?;
|
||||
ensure_state_dir(&paths.notes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -515,13 +494,6 @@ async fn start_with_fallback_inner(name: &str) -> Result<()> {
|
|||
.with_context(|| format!("cold-start fallback also failed for {name}"))
|
||||
}
|
||||
|
||||
/// Stop + start without regenerating any config. For "kick the container"
|
||||
/// without touching the flake or nspawn flags.
|
||||
pub async fn restart(name: &str) -> Result<()> {
|
||||
kill(name).await?;
|
||||
start(name).await
|
||||
}
|
||||
|
||||
/// True when the container's systemd unit is active. Used by the dashboard
|
||||
/// to gate stop/restart buttons.
|
||||
pub async fn is_running(name: &str) -> bool {
|
||||
|
|
|
|||
|
|
@ -4,23 +4,58 @@ use std::sync::Arc;
|
|||
use anyhow::{Context as _, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
// Every module hangs off the `hive_c0re` library (see `src/lib.rs`).
|
||||
// The daemon and the `hivectl` sibling binary share the same module
|
||||
// tree — no per-binary duplication. Enumerated rather than wildcard
|
||||
// so clippy stays happy + the lib surface this bin consumes is
|
||||
// explicit (any new daemon entry point reads off the next add).
|
||||
use hive_c0re::coordinator::{Coordinator, HiveEnv, ServeConfig};
|
||||
use hive_c0re::{
|
||||
agent_sockets, auto_update, broker, crash_watch, dashboard, dashboard_events, forge,
|
||||
host_stats, job_queue, knowledge, matrix, mcp_sockets, migrate, reminder_scheduler,
|
||||
scheduled_prompts_worker, server, socket_server, sweep_health, warnings,
|
||||
// `hive-c0re` is bin-only: this binary owns the whole daemon module
|
||||
// tree. The operator CLI moved to the standalone `hivectl` crate (it
|
||||
// talks to the daemon over the host admin socket), so there's no longer
|
||||
// a library shared between two binaries — the modules that used to live
|
||||
// in `src/lib.rs` are declared here directly.
|
||||
//
|
||||
// Cohesive clusters live in directory submodules (`stores`, `stats`,
|
||||
// `agent_config`, `workers`); each child is re-exported at the crate
|
||||
// root so `crate::broker::…` style paths keep resolving unchanged.
|
||||
mod actions;
|
||||
mod agent_config;
|
||||
mod container_view;
|
||||
mod coordinator;
|
||||
mod dashboard;
|
||||
mod dashboard_events;
|
||||
mod forge;
|
||||
mod gateway_nginx;
|
||||
mod job_queue;
|
||||
mod lifecycle;
|
||||
mod loose_ends;
|
||||
mod matrix;
|
||||
mod meta;
|
||||
mod migrate;
|
||||
mod paths;
|
||||
mod priv_client;
|
||||
mod questions;
|
||||
mod server;
|
||||
mod socket_server;
|
||||
mod stats;
|
||||
mod stores;
|
||||
mod webhook_secret;
|
||||
mod workers;
|
||||
|
||||
pub(crate) use agent_config::{capabilities, limits, tool_groups, topology};
|
||||
pub(crate) use stats::{
|
||||
container_stats, hive_stats, host_stats, otel_metrics, sweep_health, warnings,
|
||||
};
|
||||
pub(crate) use stores::{
|
||||
approvals, audit_log, broker, build_logs, db, operator_questions, power, scheduled_prompts,
|
||||
};
|
||||
pub(crate) use workers::{
|
||||
agent_sockets, auto_update, crash_watch, knowledge, mcp_sockets, reminder_scheduler,
|
||||
scheduled_prompts_worker,
|
||||
};
|
||||
|
||||
use coordinator::{Coordinator, HiveEnv, ServeConfig};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "hive-c0re", about = "hyperhive coordinator daemon and CLI")]
|
||||
#[command(name = "hive-c0re", about = "hyperhive coordinator daemon")]
|
||||
struct Cli {
|
||||
/// Path to the host admin socket.
|
||||
#[arg(long, global = true, default_value = hive_c0re::paths::HOST_SOCKET)]
|
||||
#[arg(long, global = true, default_value = crate::paths::HOST_SOCKET)]
|
||||
socket: PathBuf,
|
||||
|
||||
#[command(subcommand)]
|
||||
|
|
@ -32,7 +67,7 @@ enum Cmd {
|
|||
/// Run the coordinator daemon.
|
||||
Serve {
|
||||
/// Path to a JSON config file holding the host-level daemon config
|
||||
/// (the [`ServeConfig`](hive_c0re::coordinator::ServeConfig) shape:
|
||||
/// (the [`ServeConfig`](crate::coordinator::ServeConfig) shape:
|
||||
/// the container-injected `HiveEnv` fields + the hive-c0re-local
|
||||
/// `model_prices` table). Used as the base; any per-flag override
|
||||
/// below wins over the file. Absent → start from the built-in
|
||||
|
|
@ -42,7 +77,7 @@ enum Cmd {
|
|||
#[arg(long)]
|
||||
config: Option<PathBuf>,
|
||||
/// Path to the sqlite message store.
|
||||
#[arg(long, default_value = hive_c0re::paths::BROKER_DB)]
|
||||
#[arg(long, default_value = crate::paths::BROKER_DB)]
|
||||
db: PathBuf,
|
||||
/// Override: URL of the hyperhive flake. Inlined into each
|
||||
/// per-agent `flake.nix` as the `hyperhive` input.
|
||||
|
|
@ -174,7 +209,7 @@ async fn main() -> Result<()> {
|
|||
)]
|
||||
async fn cmd_serve(
|
||||
env: HiveEnv,
|
||||
model_prices: hive_c0re::hive_stats::PriceTable,
|
||||
model_prices: crate::hive_stats::PriceTable,
|
||||
build_slots: usize,
|
||||
db: std::path::PathBuf,
|
||||
socket: &std::path::Path,
|
||||
|
|
@ -183,7 +218,7 @@ async fn cmd_serve(
|
|||
// subdir (`db/`, `forge/`, `matrix/`, `run/`) BEFORE opening the
|
||||
// broker db — the broker + build-logs dbs are among the relocated
|
||||
// files. Idempotent; a no-op once migrated.
|
||||
hive_c0re::paths::relocate_legacy_state();
|
||||
crate::paths::relocate_legacy_state();
|
||||
// `dashboard_port` is consumed into the Coordinator below; capture the
|
||||
// Copy value first for the dashboard + knowledge-webhook tasks.
|
||||
let dashboard_port = env.dashboard_port;
|
||||
|
|
@ -208,7 +243,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(hive_c0re::lifecycle::sync_tmpfiles());
|
||||
tokio::spawn(crate::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.
|
||||
|
|
@ -228,7 +263,7 @@ async fn cmd_serve(
|
|||
// Webhook HMAC secret: load from state dir or generate on first run.
|
||||
// Used by both the webhook handlers (verification) and the Forgejo
|
||||
// hook registrations (so Forgejo signs deliveries with the same key).
|
||||
let webhook_secret: Option<String> = match hive_c0re::webhook_secret::load_or_generate() {
|
||||
let webhook_secret: Option<String> = match crate::webhook_secret::load_or_generate() {
|
||||
Ok(s) => Some(s),
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
|
|
@ -427,18 +462,18 @@ async fn cmd_serve(
|
|||
// + writable rootfs every ~5 min, cached so the 5s container-load
|
||||
// poll stays cheap cgroup-only reads. Feeds `disk_bytes` on the LOAD
|
||||
// tab. See container_stats::disk_sampler_loop.
|
||||
hive_c0re::container_stats::spawn_disk_sampler();
|
||||
crate::container_stats::spawn_disk_sampler();
|
||||
// Per-agent container-resource OTEL export: rides the same
|
||||
// cgroup gauges out to the configured OTLP endpoint, reusing the hive
|
||||
// `services.hyperhive.otel` config (endpoint + LoadCredential auth).
|
||||
// No-op when OTEL isn't configured.
|
||||
hive_c0re::otel_metrics::spawn_exporter();
|
||||
crate::otel_metrics::spawn_exporter();
|
||||
// build_logs.sqlite vacuum: c0re-side (single db). Failures kept
|
||||
// 30d, successes 24h — see `build_logs::vacuum` for the rule.
|
||||
hive_c0re::build_logs::spawn_vacuum(&coord);
|
||||
crate::build_logs::spawn_vacuum(&coord);
|
||||
// audit_log.sqlite vacuum: agent-initiated privileged-action trail,
|
||||
// 90d retention — see `audit_log::vacuum`.
|
||||
hive_c0re::audit_log::spawn_vacuum(&coord);
|
||||
crate::audit_log::spawn_vacuum(&coord);
|
||||
// Container crash watcher: emits HelperEvent::ContainerCrash
|
||||
// when a previously-running container goes away without an
|
||||
// operator-initiated transient state.
|
||||
|
|
|
|||
|
|
@ -283,12 +283,6 @@ pub fn agent_runtime_dir(name: &str) -> PathBuf {
|
|||
agent_runtime_root().join(name)
|
||||
}
|
||||
|
||||
/// `/run/hive-agent` — per-agent socket dir root (web + bound markers).
|
||||
#[must_use]
|
||||
pub fn agent_socket_dir() -> PathBuf {
|
||||
PathBuf::from(AGENT_SOCKET_DIR)
|
||||
}
|
||||
|
||||
/// Move any host-side state file still at its legacy flat location
|
||||
/// (directly under [`STATE_ROOT`]) into its new subdir. Idempotent and
|
||||
/// rename-based: a move only happens when the old path exists and the
|
||||
|
|
|
|||
|
|
@ -94,16 +94,6 @@ pub async fn kill_container(name: &str) -> Result<()> {
|
|||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update_container(name: &str) -> Result<(String, String)> {
|
||||
check(
|
||||
call(&PrivRequest::UpdateContainer {
|
||||
name: name.to_owned(),
|
||||
stream: false,
|
||||
})
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
/// Streaming variant: forward stdout/stderr lines to `on_line` as they
|
||||
/// arrive. Returns `Ok(())` on success; the callback is responsible for
|
||||
/// appending lines to `build_logs` or otherwise capturing the output.
|
||||
|
|
@ -121,16 +111,6 @@ pub async fn update_container_streaming(
|
|||
.await?)
|
||||
}
|
||||
|
||||
pub async fn create_container(name: &str) -> Result<(String, String)> {
|
||||
check(
|
||||
call(&PrivRequest::CreateContainer {
|
||||
name: name.to_owned(),
|
||||
stream: false,
|
||||
})
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
/// Streaming variant: forward stdout/stderr lines to `on_line` as they
|
||||
/// arrive. Returns `Ok(())` on success.
|
||||
pub async fn create_container_streaming(
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ pub(crate) async fn submit_merge_config_pr(
|
|||
}
|
||||
// Fetch the current PR head sha — becomes the "reviewed" sha.
|
||||
// Submitted together with the approval row (atomic single INSERT) so a
|
||||
// crash between submit and set_fetched_sha cannot leave a stranded row.
|
||||
// crash mid-submit cannot leave a stranded sha-less row.
|
||||
let sha = crate::forge::pr_head_sha(&repo, pr_number)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("fetch PR head sha for {agent} PR #{pr_number}: {e}"))?;
|
||||
|
|
|
|||
|
|
@ -93,12 +93,6 @@ impl SweepHealth {
|
|||
None => self.guard = Some(set_warning(self.kind, self.level, msg)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the banner warning is currently raised for this sweep.
|
||||
#[must_use]
|
||||
pub fn is_warning(&self) -> bool {
|
||||
self.guard.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact human-readable age (e.g. `"45s"`, `"12m"`, `"3h"`, `"2d"`) for a
|
||||
|
|
@ -135,7 +129,7 @@ mod tests {
|
|||
let mut h = SweepHealth::new("sh_below", "warn", 3);
|
||||
h.record_err(|_| "boom".to_owned());
|
||||
h.record_err(|_| "boom".to_owned());
|
||||
assert!(!h.is_warning());
|
||||
assert!(h.guard.is_none());
|
||||
assert!(banner("sh_below").is_none());
|
||||
}
|
||||
|
||||
|
|
@ -146,9 +140,9 @@ mod tests {
|
|||
assert!(banner("sh_raise").is_none(), "one miss < threshold");
|
||||
h.record_err(|c| format!("fail #{}", c.consecutive));
|
||||
assert_eq!(banner("sh_raise").as_deref(), Some("fail #2"));
|
||||
assert!(h.is_warning());
|
||||
assert!(h.guard.is_some());
|
||||
h.record_ok();
|
||||
assert!(!h.is_warning());
|
||||
assert!(h.guard.is_none());
|
||||
assert!(banner("sh_raise").is_none(), "success clears the banner");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,8 +75,7 @@ impl Approvals {
|
|||
/// when the sha is already known at submission time (e.g. `MergeConfigPr`
|
||||
/// fetches the PR head before inserting), making the insert + sha-set
|
||||
/// atomic. Pass `None` when the kind carries no sha (e.g. `Spawn` /
|
||||
/// `InitConfig`) or the sha is resolved after insertion, then call
|
||||
/// [`set_fetched_sha`] separately.
|
||||
/// `InitConfig`).
|
||||
pub fn submit_kind(
|
||||
&self,
|
||||
agent: &str,
|
||||
|
|
@ -126,17 +125,6 @@ impl Approvals {
|
|||
Ok(submitter)
|
||||
}
|
||||
|
||||
/// Record the canonical sha hive-c0re fetched from the proposed repo
|
||||
/// into applied at submission time. Idempotent on identical values.
|
||||
pub fn set_fetched_sha(&self, id: i64, sha: &str) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE approvals SET fetched_sha = ?1 WHERE id = ?2",
|
||||
params![sha, id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the `(id, fetched_sha)` of the pending `merge_config_pr`
|
||||
/// approval for `(agent, pr_number)`, if one exists. Drives
|
||||
/// `submit_merge_config_pr`'s idempotency + PR-drift handling: same
|
||||
|
|
@ -583,7 +571,7 @@ mod tests {
|
|||
fn fetched_sha_in_insert_is_readable_via_get() {
|
||||
// `submit_kind` with `Some(sha)` must store it atomically in the
|
||||
// INSERT — the `get()` row must reflect it without a separate
|
||||
// `set_fetched_sha` call. This is the MergeConfigPr path.
|
||||
// sha-set step. This is the MergeConfigPr path.
|
||||
let (_dir, _path, db) = open_temp();
|
||||
let sha = "abc1234567890abc1234567890abc1234567890ab";
|
||||
let id = db
|
||||
|
|
|
|||
|
|
@ -40,13 +40,6 @@ pub fn install(handle: Arc<AuditLog>) {
|
|||
let _ = GLOBAL.set(handle);
|
||||
}
|
||||
|
||||
/// Fetch the process-wide handle, or `None` if `install` hasn't run yet
|
||||
/// (early startup, or unit tests). Callers must gracefully no-op on `None`.
|
||||
#[must_use]
|
||||
pub fn global() -> Option<Arc<AuditLog>> {
|
||||
GLOBAL.get().cloned()
|
||||
}
|
||||
|
||||
/// Retain audit rows for 90 days. Longer than build-log retention — this
|
||||
/// is a security/accountability record, not debug noise; the operator may
|
||||
/// want to review "who restarted what" well after the fact.
|
||||
|
|
|
|||
Loading…
Reference in a new issue