fold hive-c0re module tree into the daemon binary + drop dead pub items surfaced by bin-only (#2513)

This commit is contained in:
damocles 2026-07-15 23:54:43 +02:00 committed by mara
commit 673aea4e50
15 changed files with 95 additions and 250 deletions

View file

@ -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.

View file

@ -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),
}
}

View file

@ -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

View file

@ -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(),

View file

@ -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,

View file

@ -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,
};

View file

@ -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 \

View file

@ -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 {

View file

@ -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.

View file

@ -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

View file

@ -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(

View file

@ -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}"))?;

View file

@ -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");
}

View file

@ -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

View file

@ -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.