From 8b9f7d21b7c2cf908fec9955386470b693b000e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Fri, 15 May 2026 21:05:40 +0200 Subject: [PATCH] model persisted to /state; stop auto-allowing claude-code unfree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit model persistence: /model now writes to /state/hyperhive-model (in-container), Bus::new reads it on init. operator override survives harness restart and container rebuild; gone on --purge like every other piece of agent state. path overridable via HYPERHIVE_MODEL_FILE for tests. failure to persist is a warn, not fatal — runtime override still applies, just won't survive a restart. unfree opt-in: drop the auto-allowUnfreePredicate from harness-base.nix and the claude-unstable overlay. operator now has to set nixpkgs.config.allowUnfree (or a predicate listing claude-code) in their own host config. silent unfree bypass was sketchy; this is honest. readme + gotchas updated to spell out the snippet. todo: drops model-persistence + container-crash + journald (all shipped); adds per-agent send allow-list (constrain who an agent can message). --- README.md | 11 +++++++++ TODO.md | 20 +++++++++------- docs/gotchas.md | 14 +++++++---- flake.nix | 7 +++++- hive-ag3nt/src/events.rs | 44 +++++++++++++++++++++++++++++++--- nix/templates/harness-base.nix | 7 +++++- 6 files changed, 84 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index de64cce..cb8191d 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,17 @@ hive-c0re will then: - auto-create the manager container (`hm1nd`) if missing, - auto-rebuild any managed container whose hyperhive rev is stale. +`claude-code` is unfree; hyperhive does not auto-allow it for you. +Add to your host config: + +```nix +nixpkgs.config.allowUnfreePredicate = + pkg: builtins.elem (nixpkgs.lib.getName pkg) [ "claude-code" ]; +``` + +(or `nixpkgs.config.allowUnfree = true`, your call). Each per-agent +container inherits this through the same nixpkgs evaluation. + ## Build / deploy ```sh diff --git a/TODO.md b/TODO.md index 6f0ab2a..4b495e5 100644 --- a/TODO.md +++ b/TODO.md @@ -3,6 +3,17 @@ Pick anything from here when relevant. Cross-cutting design notes live in [CLAUDE.md](CLAUDE.md); high-level project intro in [README.md](README.md). +## Permissions / policy + +- **Per-agent send allow-list.** Today any agent can `send` to any + other recipient (peer, manager, operator). Add a per-agent + policy that constrains the `to` field — declared in `agent.nix`, + e.g. `hyperhive.allowedRecipients = [ "manager" "alice" ]`. + Broker rejects with an `Err { message }` when the policy denies. + Default: unrestricted (back-compat). The manager can still + always send anywhere. Useful for sandboxing untrusted sub-agents + so they can only talk to the manager, not other sub-agents. + ## Security - **Unprivileged containers (userns mapping).** Today the nspawn container @@ -31,15 +42,6 @@ Pick anything from here when relevant. Cross-cutting design notes live in derived from the same config so the operator stays in control of what's exposed. -## Per-agent settings - -- **Model override persistence.** `/model ` already switches - the model at runtime via `Bus::set_model`; the chip on the agent - page reflects the current value. Override is in-memory only and - resets on harness restart — by design for now, but consider - optional persistence (`/state/model` file?) so an operator-set - model survives a rebuild. - ## UI / UX - **Terminal: `/model` slash command.** Operator-typeable model diff --git a/docs/gotchas.md b/docs/gotchas.md index e34863d..26a178c 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -53,11 +53,15 @@ socket without needing a clean reinstall. ## `claude-code` is unfree -`harness-base.nix` allow-list's it specifically. The flake pins it to -**nixpkgs-unstable** via `overlays.claude-unstable` (stable lags too -far). The overlay imports unstable with its own -`allowUnfreePredicate` so the access inside the overlay doesn't -itself trip. +The flake pins it to **nixpkgs-unstable** via +`overlays.claude-unstable` (stable lags too far). The overlay +imports unstable inheriting the user's `nixpkgs.config`, so the +operator must opt in by setting `allowUnfree = true` (or an +`allowUnfreePredicate` that whitelists `claude-code`) on their host +config. hyperhive deliberately does NOT auto-allow — silent unfree +bypass would be sketchy, and the error message is clear enough that +the operator can fix it once and forget about it. Same on the +per-agent containers (they inherit through the same nixpkgs). ## Claude credentials are per-agent diff --git a/flake.nix b/flake.nix index 9551efb..32d9a42 100644 --- a/flake.nix +++ b/flake.nix @@ -67,9 +67,14 @@ claude-unstable = final: prev: let + # Inherit the *user's* nixpkgs config so allowUnfree (or an + # `allowUnfreePredicate` they set on their flake) propagates + # into the unstable import. hyperhive does not silently + # bypass the unfree gate — if the operator hasn't opted in, + # this overlay's `claude-code` access fails honestly. unstable = import nixpkgs-unstable { inherit (prev.stdenv.hostPlatform) system; - config.allowUnfreePredicate = pkg: builtins.elem (prev.lib.getName pkg) [ "claude-code" ]; + config = prev.config; }; in { diff --git a/hive-ag3nt/src/events.rs b/hive-ag3nt/src/events.rs index 23b138f..9771217 100644 --- a/hive-ag3nt/src/events.rs +++ b/hive-ag3nt/src/events.rs @@ -24,6 +24,36 @@ const HISTORY_CAPACITY: usize = 2000; /// `HYPERHIVE_EVENTS_DB` env var (used in tests and one-shot tools). const DEFAULT_EVENTS_DB: &str = "/state/hyperhive-events.sqlite"; +/// Persisted model name file. Same lifecycle as the events db — +/// survives destroy/recreate, gone on purge. Empty / missing file +/// falls back to `DEFAULT_MODEL`. +const DEFAULT_MODEL_FILE: &str = "/state/hyperhive-model"; + +/// Path to the persisted model file. Overridable via +/// `HYPERHIVE_MODEL_FILE` for dev / tests. +fn model_file_path() -> PathBuf { + std::env::var_os("HYPERHIVE_MODEL_FILE") + .map_or_else(|| PathBuf::from(DEFAULT_MODEL_FILE), PathBuf::from) +} + +fn load_model() -> Option { + let s = std::fs::read_to_string(model_file_path()).ok()?; + let name = s.trim(); + if name.is_empty() { + None + } else { + Some(name.to_owned()) + } +} + +fn persist_model(name: &str) -> std::io::Result<()> { + let path = model_file_path(); + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + std::fs::write(path, format!("{name}\n")) +} + fn now_unix() -> i64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -177,11 +207,12 @@ impl Bus { } }; let (tx, _) = broadcast::channel(CHANNEL_CAPACITY); + let initial_model = load_model().unwrap_or_else(|| DEFAULT_MODEL.to_owned()); Self { tx: Arc::new(tx), store, state: Arc::new(Mutex::new((TurnState::Idle, now_unix()))), - model: Arc::new(Mutex::new(DEFAULT_MODEL.to_owned())), + model: Arc::new(Mutex::new(initial_model)), } } @@ -193,9 +224,16 @@ impl Bus { } /// Switch the model for future turns. The current turn (if any) - /// keeps the model it was already running. + /// keeps the model it was already running. Persisted to + /// `/state/hyperhive-model` so the override survives harness + /// restart and container rebuild (gone on `--purge`, matching + /// every other piece of agent state). pub fn set_model(&self, name: impl Into) { - *self.model.lock().unwrap() = name.into(); + let value: String = name.into(); + self.model.lock().unwrap().clone_from(&value); + if let Err(e) = persist_model(&value) { + tracing::warn!(error = ?e, "model: persist failed"); + } } /// Update the harness's authoritative turn-loop state. Records diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index a612a32..fc574be 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -7,7 +7,12 @@ boot.isNspawnContainer = true; - nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ]; + # `claude-code` is unfree. hyperhive intentionally does NOT auto-allow + # it — the operator opts in by setting + # `nixpkgs.config.allowUnfreePredicate` (or `allowUnfree = true`) in + # their own host config / agent.nix. Without that, the per-agent + # build fails on this package and the operator sees an honest "this + # is unfree, are you sure?" error. environment.systemPackages = with pkgs; [ hyperhive