agent flake.nix: forward inputs as flakeInputs module arg

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.<name>.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/<id> 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.
This commit is contained in:
müde 2026-05-16 02:23:43 +02:00
parent 66a69d0c7f
commit 3db33b0fe5
4 changed files with 44 additions and 55 deletions

View file

@ -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/<name>/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/<name>/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/<id>` 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:

View file

@ -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.<name>.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<()> {

View file

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

View file

@ -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<Coordinator>) -> Result<()> {
if std::env::var(KILL_SWITCH).is_ok() {