refactor(c0re): pass hive-c0re serve config via a --config file, shrink ExecStart
The systemd ExecStart carried every host-level setting as its own flag — nine of them, including two escaped JSON blobs (the context-window map and the model-price table). Collapse them into a single `--config <file>` JSON. - Reuse the existing HiveEnv as the container-injected config shape (add Deserialize + Default), and add a ServeConfig wrapper = flattened HiveEnv plus the hive-c0re-local model_prices table (kept out of HiveEnv since it is never injected into containers). serde(default) lets any field be omitted and fall back to its canonical default. - clap: add --config; the per-setting flags become optional overrides (config file is the base, explicit flags win — preserves hivectl/debug ergonomics and bare `hive-c0re serve`). - Coordinator::open and cmd_serve now take the bundled HiveEnv, which drops their too_many_arguments clippy allows. cmd_serve keeps a single too_many_lines allow (inherent daemon-boot orchestration, not arg-driven). - nix: write the config as JSON to the store + pass --config, so ExecStart is one short line. - Add a round-trip test proving the flatten + per-field defaults work. Closes the ExecStart-length issue.
This commit is contained in:
parent
09787dd1c3
commit
98660d134a
3 changed files with 223 additions and 120 deletions
|
|
@ -158,7 +158,14 @@ pub struct Coordinator {
|
|||
/// Cloned from `Coordinator` via [`Coordinator::hive_env`]. All fields
|
||||
/// are cheap to clone (small strings + small map); lifecycle ops are
|
||||
/// infrequent enough that the copy cost is irrelevant.
|
||||
#[derive(Clone, Debug)]
|
||||
///
|
||||
/// Also the container-injected subset of the `hive-c0re serve --config`
|
||||
/// JSON (see [`ServeConfig`]): `#[serde(default)]` lets a partial config
|
||||
/// fall back to [`Default`] (the canonical defaults that used to live in
|
||||
/// the clap `default_value`s), and bare `hive-c0re serve` with no
|
||||
/// `--config` boots from `Default`.
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct HiveEnv {
|
||||
pub hyperhive_flake: String,
|
||||
pub nixpkgs_flake: String,
|
||||
|
|
@ -172,6 +179,78 @@ pub struct HiveEnv {
|
|||
pub agent_memory_max: String,
|
||||
}
|
||||
|
||||
impl Default for HiveEnv {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hyperhive_flake: "/etc/hyperhive".to_string(),
|
||||
nixpkgs_flake: String::new(),
|
||||
nixpkgs_unstable_flake: String::new(),
|
||||
dashboard_port: 7000,
|
||||
operator_pronouns: "she/her".to_string(),
|
||||
context_window_tokens: std::collections::HashMap::from([
|
||||
("haiku".to_string(), 200_000),
|
||||
("sonnet".to_string(), 1_000_000),
|
||||
("opus".to_string(), 1_000_000),
|
||||
]),
|
||||
agent_cpu_quota: "200%".to_string(),
|
||||
agent_memory_max: "4G".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// On-disk shape of the `hive-c0re serve --config <file>` JSON: the
|
||||
/// container-injected [`HiveEnv`] (flattened) plus the hive-c0re-local
|
||||
/// `model_prices` table (read only by the `/api/stats-hive` handler, never
|
||||
/// injected into containers — hence kept out of `HiveEnv`). The NixOS
|
||||
/// module writes this so the `ExecStart` carries one `--config` flag
|
||||
/// instead of every host-level setting as its own JSON-blob argument.
|
||||
/// `#[serde(default)]` lets any field be omitted and fall back to its
|
||||
/// canonical default.
|
||||
#[derive(Clone, Debug, Default, serde::Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct ServeConfig {
|
||||
#[serde(flatten)]
|
||||
pub env: HiveEnv,
|
||||
pub model_prices: crate::hive_stats::PriceTable,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod serve_config_tests {
|
||||
use super::ServeConfig;
|
||||
|
||||
#[test]
|
||||
fn deserialises_flat_with_per_field_defaults() {
|
||||
// The flattened HiveEnv fields + the local model_prices all live at
|
||||
// the JSON top level. Present fields are read; omitted ones fall
|
||||
// back to their canonical defaults (so a partial config is valid).
|
||||
let sc: ServeConfig = serde_json::from_str(
|
||||
r#"{
|
||||
"hyperhive_flake": "/x",
|
||||
"dashboard_port": 1234,
|
||||
"model_prices": {
|
||||
"opus": {"input": 5.0, "output": 25.0, "cache_read": 0.5, "cache_write": 10.0}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("flat ServeConfig should deserialise");
|
||||
assert_eq!(sc.env.hyperhive_flake, "/x");
|
||||
assert_eq!(sc.env.dashboard_port, 1234);
|
||||
// omitted → HiveEnv::default()
|
||||
assert_eq!(sc.env.operator_pronouns, "she/her");
|
||||
assert_eq!(sc.env.agent_cpu_quota, "200%");
|
||||
assert_eq!(sc.env.context_window_tokens.get("sonnet"), Some(&1_000_000));
|
||||
assert!(sc.model_prices.contains_key("opus"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_object_is_all_defaults() {
|
||||
let sc: ServeConfig = serde_json::from_str("{}").expect("empty config valid");
|
||||
assert_eq!(sc.env.dashboard_port, 7000);
|
||||
assert_eq!(sc.env.hyperhive_flake, "/etc/hyperhive");
|
||||
assert!(sc.model_prices.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-agent filesystem paths that lifecycle operations write to.
|
||||
/// Assembled from `Coordinator`'s static path helpers so callers
|
||||
/// don't repeat the same `Coordinator::agent_*_dir(name)` calls.
|
||||
|
|
@ -271,24 +350,21 @@ impl TransientKind {
|
|||
}
|
||||
|
||||
impl Coordinator {
|
||||
#[allow(
|
||||
clippy::too_many_arguments,
|
||||
reason = "constructor wiring host-level config (flakes, ports, pronouns, \
|
||||
context-window + resource limits) into the coordinator; bundling \
|
||||
into a struct would just move the same fields one level out"
|
||||
)]
|
||||
pub fn open(
|
||||
db_path: &Path,
|
||||
hyperhive_flake: String,
|
||||
nixpkgs_flake: String,
|
||||
nixpkgs_unstable_flake: String,
|
||||
dashboard_port: u16,
|
||||
operator_pronouns: String,
|
||||
context_window_tokens: std::collections::HashMap<String, u64>,
|
||||
agent_cpu_quota: String,
|
||||
agent_memory_max: String,
|
||||
env: HiveEnv,
|
||||
model_prices: crate::hive_stats::PriceTable,
|
||||
) -> Result<Self> {
|
||||
let HiveEnv {
|
||||
hyperhive_flake,
|
||||
nixpkgs_flake,
|
||||
nixpkgs_unstable_flake,
|
||||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
agent_cpu_quota,
|
||||
agent_memory_max,
|
||||
} = env;
|
||||
let broker = Broker::open(db_path).context("open broker")?;
|
||||
let approvals = Approvals::open(db_path).context("open approvals")?;
|
||||
let questions = OperatorQuestions::open(db_path).context("open operator_questions")?;
|
||||
|
|
|
|||
Loading…
Reference in a new issue