From 3db33b0fe5aabfd4e93424b343f7b993e622b1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Sat, 16 May 2026 02:23:43 +0200 Subject: [PATCH] agent flake.nix: forward inputs as flakeInputs module arg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit new boilerplate wraps agent.nix as a sub-module + passes every flake input (minus self) through to it via _module.args.flake Inputs. manager edits the inputs block of flake.nix to pull in out-of-tree flakes (MCP servers etc.) and references them in agent.nix as flakeInputs..packages.${pkgs.system}.default — the new input's pinned sha lands in the agent's own flake .lock (already tracked + part of the proposal flow), and transitively rolls up into meta's lock. migrate's MODULE_FLAKE_MARKER swaps to _module.args.flakeInputs so existing agents on the old 'nixosModules.default = import ./agent.nix' template get re-rendered onto the new shape on next hive-c0re start. manager_server's flake.nix tamper-check goes away — the build path's failed/ annotated tag already provides the safety net when a manager edit breaks the flake; enforcing 'no flake.nix edits at all' was overly strict (blocks the inputs- addition pattern that's the whole point of this change). manager prompt updated with a worked example for adding an MCP-server flake input + wiring it through agent.nix. --- hive-ag3nt/prompts/manager.md | 26 ++++++++++++++++- hive-c0re/src/lifecycle.rs | 16 +++++++++-- hive-c0re/src/manager_server.rs | 50 --------------------------------- hive-c0re/src/migrate.rs | 7 ++++- 4 files changed, 44 insertions(+), 55 deletions(-) diff --git a/hive-ag3nt/prompts/manager.md b/hive-ag3nt/prompts/manager.md index 74e774c..37485e3 100644 --- a/hive-ag3nt/prompts/manager.md +++ b/hive-ag3nt/prompts/manager.md @@ -14,7 +14,31 @@ Tools (hyperhive surface): Approval boundary: lifecycle ops on *existing* sub-agents (`kill`, `start`, `restart`) are at your discretion — no operator approval. *Creating* a new agent (`request_spawn`) and *changing* any agent's config (`request_apply_commit`) still go through the approval queue. The operator only signs off on changes; you run the day-to-day. -Your own editable config lives at `/agents/hm1nd/config/`; every sub-agent's lives at `/agents//config/`. `agent.nix` is a plain NixOS module function — `{ config, pkgs, lib, ... }: { ... }`. Add packages, services, imports, sibling `.nix` files; the whole committed tree gets deployed together. **Do not edit `flake.nix`** — it's a fixed boilerplate that exports `agent.nix` as `nixosModules.default`; the hive-c0re-owned meta flake at `/meta/` provides the NixOS base and wires identity / `HIVE_PORT` / `HIVE_LABEL` itself. +Your own editable config lives at `/agents/hm1nd/config/`; every sub-agent's lives at `/agents//config/`. `agent.nix` is a plain NixOS module function — `{ config, pkgs, lib, flakeInputs, ... }: { ... }`. Add packages, services, imports, sibling `.nix` files; the whole committed tree gets deployed together. + +`flake.nix` is mostly boilerplate (it exports `agent.nix` as `nixosModules.default` and forwards every flake input to the module as `flakeInputs`). **Don't touch the outputs block** — but you *can* edit the `inputs` block to pull in other flakes, which is the supported way to depend on out-of-tree packages (MCP servers, scrapers, anything not in nixpkgs): + +```nix +# flake.nix (manager-edited, inputs side only) +inputs.mcp-matrix.url = "github:foo/mcp-matrix"; +inputs.mcp-matrix.inputs.nixpkgs.follows = "nixpkgs"; # optional, reduce closure +``` + +```nix +# agent.nix — reference the input via flakeInputs +{ pkgs, flakeInputs, ... }: +let matrixPkg = flakeInputs.mcp-matrix.packages.${pkgs.system}.default; +in { + environment.systemPackages = [ matrixPkg ]; + hyperhive.extraMcpServers.matrix = { + command = "${matrixPkg}/bin/mcp-matrix"; + args = [ "--config" "/state/matrix.toml" ]; + allowedTools = [ "send_message" "join_room" ]; + }; +} +``` + +The new input's pinned sha lands in the agent's `flake.lock` (also tracked + part of the proposal). Build failures from a broken `flake.nix` surface as a `failed/` annotated tag, so the worst case is a rejected deploy — not a silently-broken agent. Each proposed repo has an `applied` git remote pre-configured pointing at the read-only mirror of what's deployed. Useful patterns: diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 96a1bd9..fe3432d 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -485,10 +485,20 @@ fn initial_agent_nix(name: &str) -> String { /// Module-only flake exposed by every agent's repo. Consumed by the /// hive-c0re-owned meta flake at `/var/lib/hyperhive/meta/` as a flake -/// input. Identity injection (`HIVE_PORT` / `HIVE_LABEL` / dashboard -/// port / git committer) lives in the meta flake's wrapper, not here. +/// input. The wrapper is intentionally permissive: +/// +/// - Manager edits `inputs.* = …` to add other flakes (e.g. an MCP +/// server's own flake) — the lock for those lands in the agent's +/// own `flake.lock` and rolls up into meta's lock transitively. +/// - The outputs block forwards every input (minus `self`) into +/// `agent.nix` as the `flakeInputs` module argument, so the +/// manager just references `flakeInputs..packages.${pkgs.system}.default` +/// without further plumbing. +/// +/// Identity injection (`HIVE_PORT` / `HIVE_LABEL` / dashboard port / +/// git committer) still lives in the meta flake's wrapper. pub fn initial_flake_nix() -> &'static str { - "{\n description = \"hyperhive agent\";\n inputs = { };\n outputs = { self }: {\n nixosModules.default = import ./agent.nix;\n };\n}\n" + "{\n description = \"hyperhive agent\";\n inputs = { };\n outputs =\n { self, ... }@inputs:\n {\n nixosModules.default = {\n imports = [ ./agent.nix ];\n _module.args.flakeInputs = builtins.removeAttrs inputs [ \"self\" ];\n };\n };\n}\n" } async fn git_commit(dir: &Path, message: &str) -> Result<()> { diff --git a/hive-c0re/src/manager_server.rs b/hive-c0re/src/manager_server.rs index 6913d09..907c612 100644 --- a/hive-c0re/src/manager_server.rs +++ b/hive-c0re/src/manager_server.rs @@ -324,26 +324,6 @@ async fn submit_apply_commit( return Err(anyhow::anyhow!("git_fetch_to_tag: {e:#}")); } }; - // Reject proposals that touch flake.nix — that file is fixed - // boilerplate the meta flake depends on (it exports - // `nixosModules.default = import ./agent.nix`); a manager edit - // there silently breaks the nixosConfiguration. Prompt tells the - // manager not to; this is the host-side belt. - let proposal_ref = format!("refs/tags/{tag}"); - match proposal_modifies(&applied_dir, &proposal_ref, "flake.nix").await { - Ok(true) => { - let note = "proposal modifies flake.nix — that file is hive-c0re-owned \ - boilerplate; edit only agent.nix (and sibling modules)"; - let _ = coord.approvals.mark_failed(id, note); - return Err(anyhow::anyhow!(note)); - } - Ok(false) => {} - Err(e) => { - // Diff itself failed — log + continue. The build will - // surface concrete errors if flake.nix is actually borked. - tracing::warn!(error = ?e, %tag, "flake.nix tamper-check failed; allowing"); - } - } coord .approvals .set_fetched_sha(id, &sha) @@ -351,36 +331,6 @@ async fn submit_apply_commit( Ok((id, sha)) } -/// True iff `proposal_ref`'s tree differs from `refs/heads/main` at -/// `path`. Used to enforce that proposals don't touch hive-c0re-owned -/// files in the applied repo. -async fn proposal_modifies( - applied_dir: &std::path::Path, - proposal_ref: &str, - path: &str, -) -> anyhow::Result { - let out = crate::lifecycle::git_command() - .current_dir(applied_dir) - .args([ - "diff", - "--name-only", - &format!("refs/heads/main..{proposal_ref}"), - "--", - path, - ]) - .output() - .await - .map_err(|e| anyhow::anyhow!("spawn git diff: {e}"))?; - if !out.status.success() { - anyhow::bail!( - "git diff exited {}: {}", - out.status, - String::from_utf8_lossy(&out.stderr).trim() - ); - } - Ok(!out.stdout.iter().all(u8::is_ascii_whitespace)) -} - /// On `AskOperator { ttl_seconds: Some(n) }`, sleep n seconds and then /// try to resolve the question with `[expired]`. If the operator (or /// any other path) already answered it, `answer()` returns Err and diff --git a/hive-c0re/src/migrate.rs b/hive-c0re/src/migrate.rs index 48e4169..33a7787 100644 --- a/hive-c0re/src/migrate.rs +++ b/hive-c0re/src/migrate.rs @@ -37,7 +37,12 @@ fn repoint_marker() -> PathBuf { PathBuf::from("/var/lib/hyperhive/.meta-migration-done") } -const MODULE_FLAKE_MARKER: &str = "nixosModules.default = import ./agent.nix"; +/// Substring that identifies the *current* agent flake boilerplate. +/// Bumped whenever the template changes so the startup migration +/// re-renders existing agents onto the new shape. Today the marker +/// is the `flakeInputs` module-arg forwarding line — older templates +/// (raw `import ./agent.nix`) get rewritten on next hive-c0re start. +const MODULE_FLAKE_MARKER: &str = "_module.args.flakeInputs"; pub async fn run(coord: &Arc) -> Result<()> { if std::env::var(KILL_SWITCH).is_ok() {