diff --git a/hive-ag3nt/src/events.rs b/hive-ag3nt/src/events.rs index e16e3b7..f7e9c89 100644 --- a/hive-ag3nt/src/events.rs +++ b/hive-ag3nt/src/events.rs @@ -310,22 +310,27 @@ pub enum TurnState { Compacting, } -/// Default claude model when nothing's been set at runtime. Overridable -/// via the `HIVE_DEFAULT_MODEL` env var (set from `hyperhive.model` in -/// the container's `agent.nix`). The operator can also switch at runtime -/// via `/model ` in the web terminal; the chosen model is persisted -/// to the state dir so it survives restarts. +/// Compiled-in fallback model used when neither `HIVE_DEFAULT_MODEL` nor a +/// persisted runtime override is present. pub const DEFAULT_MODEL: &str = "haiku"; -/// Return the initial default model name: `HIVE_DEFAULT_MODEL` env var if -/// set to a non-empty string, otherwise `DEFAULT_MODEL`. +/// Return the model declared in `HIVE_DEFAULT_MODEL` (set from +/// `hyperhive.model` in `agent.nix`), or `None` if the env var is absent / +/// empty. When `Some`, this takes precedence over any persisted runtime +/// override so that nix config changes always take effect on rebuild. #[must_use] -pub fn default_model() -> &'static str { +pub fn configured_model() -> Option<&'static str> { // Leak once at startup — acceptable for a single config value. std::env::var("HIVE_DEFAULT_MODEL") .ok() .filter(|s| !s.trim().is_empty()) - .map_or(DEFAULT_MODEL, |s| Box::leak(s.into_boxed_str())) + .map(|s| &*Box::leak(s.into_boxed_str())) +} + +/// Return the model to use when no config and no persisted override exist. +#[must_use] +pub fn default_model() -> &'static str { + configured_model().unwrap_or(DEFAULT_MODEL) } /// Context-window size in tokens for a given model name. @@ -441,7 +446,13 @@ impl Bus { } }; let (tx, _) = broadcast::channel(CHANNEL_CAPACITY); - let initial_model = load_model().unwrap_or_else(|| default_model().to_owned()); + // Priority: HIVE_DEFAULT_MODEL (from hyperhive.model in agent.nix) > + // persisted runtime override > compiled-in DEFAULT_MODEL. + // The nix config always wins on rebuild; the persisted file is kept + // for within-session tracking only (see persist_model / set_model). + let initial_model = configured_model() + .map(str::to_owned) + .unwrap_or_else(|| load_model().unwrap_or_else(|| DEFAULT_MODEL.to_owned())); // Restore rate_limited from the sentinel file — if the harness // crashed while parked, we should still show the right status on // cold load until the next turn clears it. diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 9127ac8..64a8b02 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -2,6 +2,11 @@ pkgs, lib, config, + # Flake inputs routed through _module.args by the agent flake.nix. + # Default to {} so the module evaluates cleanly even when the agent + # flake doesn't set up the routing pattern (e.g. during standalone + # nixos-rebuild without a flake wrapper). + flakeInputs ? { }, ... }: { @@ -20,13 +25,12 @@ default = "haiku"; example = "sonnet"; description = '' - Default claude model for this agent. Sets the `HIVE_DEFAULT_MODEL` - environment variable consumed by the harness at boot; if no - persisted model choice exists in the agent's state dir the harness - falls back to this value. The operator can still switch the model at - runtime via the per-agent web UI — that choice is persisted to the - state dir and takes precedence over this default until the agent is - purged. + Claude model for this agent. Sets the `HIVE_DEFAULT_MODEL` + environment variable; the harness applies it at boot and it takes + priority over any persisted runtime override. The operator can still + switch the model at runtime via the per-agent web UI — that choice + is tracked in the state dir for the current session but is reset by + any rebuild that changes this option. Valid values are the short model names that `claude --model` accepts: `"haiku"`, `"sonnet"`, `"opus"` (or any future identifier). Context @@ -320,6 +324,42 @@ }; config = { + assertions = [ + # Guard the inputs-routed-as-output pattern: the agent flake.nix is + # expected to set `_module.args.flakeInputs = builtins.removeAttrs inputs ["self"]`. + # If `self` leaks into flakeInputs the agent gets a spurious attrset + # entry that can shadow real inputs and is almost certainly a bug. + { + assertion = !(builtins.hasAttr "self" flakeInputs); + message = '' + hyperhive: `flakeInputs` must not contain "self". + In your agent flake.nix, use: + _module.args.flakeInputs = builtins.removeAttrs inputs [ "self" ]; + ''; + } + # hyperhive.model must be a non-empty string — an empty value causes + # the harness to pass an invalid model flag to claude. + { + assertion = config.hyperhive.model != ""; + message = "hyperhive.model must not be empty (set it to e.g. \"haiku\" or \"sonnet\")"; + } + # hyperhive.forge.url must look like an HTTP URL when non-default. + { + assertion = + config.hyperhive.forge.url == "" + || lib.hasPrefix "http://" config.hyperhive.forge.url + || lib.hasPrefix "https://" config.hyperhive.forge.url; + message = "hyperhive.forge.url must be an http:// or https:// URL (got: \"${config.hyperhive.forge.url}\")"; + } + # hyperhive.icon must reference an SVG file when set. + { + assertion = + config.hyperhive.icon == null + || lib.hasSuffix ".svg" (toString config.hyperhive.icon); + message = "hyperhive.icon must point to an .svg file"; + } + ]; + environment.etc."hyperhive/extra-mcp.json".text = builtins.toJSON config.hyperhive.extraMcpServers; # Operator-set per-agent icon (hyperhive.icon). When configured, the