Compare commits

...
11 changed files with 46 additions and 58 deletions

View file

@ -139,7 +139,7 @@
inherit cargoArtifacts nativeBuildInputs;
pname = "hyperhive-workspace";
version = "0.1.0";
meta.description = "hyperhive workspace (hive-c0re, hive-ag3nt, hive-m1nd)";
meta.description = "hyperhive workspace (hive-c0re, hive-ag3nt, hive-root)";
doCheck = false;
};
# Bundled browser assets — see ./nix/frontend.nix. Output is
@ -173,7 +173,7 @@
# they're plain derivations, but `nix build` from a non-x86
# host would only succeed via a remote x86 builder.
agent-base-toplevel = self.nixosConfigurations.agent-base.config.system.build.toplevel;
manager-toplevel = self.nixosConfigurations.manager.config.system.build.toplevel;
root-toplevel = self.nixosConfigurations.root.config.system.build.toplevel;
# Auto-generated nix options reference for hyperhive (#616).
# `docs` bundles host + agent pages into one tree; the split
@ -223,7 +223,7 @@
nixosModules = {
agent-base = ./nix/templates/agent-base.nix;
manager = ./nix/templates/manager.nix;
root = ./nix/templates/manager.nix;
# The hive-c0re module wants `pkgs.hyperhive` for its default
# `services.hyperhive.c0re.package`. To avoid making operators apply an
# overlay (which would also pollute their host pkgs with our
@ -244,7 +244,7 @@
# extra deps gated so aarch64 hosts don't accidentally pull
# them in via cross-build.
agentBaseToplevel = self.packages.x86_64-linux.agent-base-toplevel;
managerToplevel = self.packages.x86_64-linux.manager-toplevel;
managerToplevel = self.packages.x86_64-linux.root-toplevel;
};
hive-ci = ./nix/modules/hive-ci.nix;
hive-forge = ./nix/modules/hive-forge.nix;
@ -277,7 +277,7 @@
in
{
agent-base = mkContainer self.nixosModules.agent-base;
manager = mkContainer self.nixosModules.manager;
root = mkContainer self.nixosModules.root;
};
devShells = forAllSystems (

View file

@ -8,7 +8,7 @@ You are the hyperhive manager `{label}` (qualified: `{qualified_label}`){hive_id
Tools (hyperhive surface):
- `mcp__hyperhive__recv(wait_seconds?, max?)` — drain inbox messages (returns `(empty)` if nothing pending). Without `wait_seconds` (or with `0`) it returns immediately — a cheap "anything pending?" peek you can sprinkle between tool calls. To **wait** for work when you have nothing else useful to do this turn, call with a long wait (e.g. `wait_seconds: 180`, the max) — incoming messages wake you instantly, otherwise the call returns empty at the timeout. That's strictly better than a fixed `sleep` shell command: lower latency on new work, no busy-loop. `max` (default 1, cap 32) drains several queued messages in one call — the wake prompt tells you the pending count.
- `mcp__hyperhive__send(to, body, in_reply_to?)` — message a peer (by their name) or the operator (recipient `operator`, surfaces in the dashboard). Use `to: "*"` to broadcast to all agents (they receive a hint that it's a broadcast and may not need action). Use `to: "<parent>"` to address your structural parent without hardcoding their name — hive-c0re rewrites it at delivery time per `topology.json`, falling back to `operator` if you're a root agent. Use `to: "<children>"` to fan-out to every direct child of yours per `topology.json` (no-op for leaf agents). Both sentinels let the operator reparent at runtime with zero change on your side. Optional `in_reply_to: <message-id>` threads this message under a prior one — the dashboard and per-agent inbox render it with a `↳ reply` link. Some agents have a per-agent allow-list (`hyperhive.allowedRecipients` in their `agent.nix`) — if so the tool refuses recipients outside the list with a clear error; route through the manager (`send(to: "manager", …)`) which is always reachable.
- `mcp__hyperhive__send(to, body, in_reply_to?)` — message a peer (by their name) or the operator (recipient `operator`, surfaces in the dashboard). Use `to: "*"` to broadcast to all agents (they receive a hint that it's a broadcast and may not need action). Use `to: "<parent>"` to address your structural parent without hardcoding their name — hive-c0re rewrites it at delivery time per `topology.json`, falling back to `operator` if you're a root agent. Use `to: "<children>"` to fan-out to every direct child of yours per `topology.json` (no-op for leaf agents). Both sentinels let the operator reparent at runtime with zero change on your side. Optional `in_reply_to: <message-id>` threads this message under a prior one — the dashboard and per-agent inbox render it with a `↳ reply` link. Some agents have a per-agent allow-list (`hyperhive.allowedRecipients` in their `agent.nix`) — if so the tool refuses recipients outside the list with a clear error; route through the manager (`send(to: "root", …)`) which is always reachable.
<!-- role:agent -->
- (some agents only) **extra MCP tools** surfaced as `mcp__<server>__<tool>` — these are agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. Treat them as first-class tools alongside the hyperhive surface; the operator already auto-approved them at deploy time.
<!-- /role:agent -->
@ -43,7 +43,7 @@ Tools (hyperhive surface):
<!-- role:agent -->
- `mcp__hyperhive__request_next_turn()` — ask the harness to start another turn immediately after this one ends, even if the inbox is empty. Use for multi-turn tasks (long builds, sequential steps) where you want to continue without waiting for an external message. The next turn starts with `from: "self"` and `body: "continue"`. No-op if new inbox messages arrive before this turn ends (the harness already loops immediately on pending messages). No args.
Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — message the manager (recipient `manager`) describing what you need + why. The manager evaluates the request (it doesn't rubber-stamp), edits `/agents/{label}/config/agent.nix` on your behalf, commits, and submits an approval that the operator can accept on the dashboard; on approve hive-c0re rebuilds your container with the new config.
Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — message the manager (recipient `root`) describing what you need + why. The manager evaluates the request (it doesn't rubber-stamp), edits `/agents/{label}/config/agent.nix` on your behalf, commits, and submits an approval that the operator can accept on the dashboard; on approve hive-c0re rebuilds your container with the new config.
Your config repo is mounted **read-only** at `/agents/{label}/config/``agent.nix` plus whatever extra files the manager has split the config into. Read it to see exactly what defines you (declared packages, env vars, MCP servers) before asking the manager for a change, so you can point at the precise file and line. You cannot write here; all changes flow through the manager.
<!-- /role:agent -->

View file

@ -154,8 +154,8 @@ pub async fn rebuild_agent(
}
/// Auto-create the manager container on startup if it isn't already there.
/// hive-c0re manages hm1nd end-to-end (Phase 8 follow-up): operators no
/// longer declare `containers.hm1nd` in their host NixOS config. Bypasses
/// hive-c0re manages `root` end-to-end: operators no
/// longer declare `containers.root` in their host NixOS config. Bypasses
/// the approval queue — manager is required infrastructure. Idempotent.
pub async fn ensure_manager(coord: &Arc<Coordinator>) -> Result<()> {
let existing = lifecycle::list().await.unwrap_or_default();

View file

@ -1157,9 +1157,8 @@ async fn get_journal(
])
.arg(lines.to_string());
if let Some(u) = q.unit.as_deref().filter(|s| !s.is_empty()) {
// accept hive-ag3nt[.service] / hive-m1nd[.service] — anything
// else we refuse, again to keep the shell-out tight.
let allowed = ["hive-ag3nt.service", "hive-m1nd.service"];
// accept hive-ag3nt[.service] — anything else refused.
let allowed = ["hive-ag3nt.service"];
let unit = if u.ends_with(".service") {
u.to_owned()
} else {

View file

@ -11,10 +11,10 @@ use tokio::process::Command;
pub const AGENT_PREFIX: &str = "h-";
pub const MAX_AGENT_NAME: usize = 9;
/// Container name of the manager. Lives in the same path scheme as sub-agents
/// (`/var/lib/hyperhive/agents/hm1nd/`, `/var/lib/hyperhive/applied/hm1nd/`),
/// (`/var/lib/hyperhive/agents/root/`, `/var/lib/hyperhive/applied/root/`),
/// but its container has no `h-` prefix and extends a different
/// nixosConfiguration (`manager`, not `agent-base`).
pub const MANAGER_NAME: &str = "hm1nd";
/// nixosConfiguration (`root`, not `agent-base`).
pub const MANAGER_NAME: &str = "root";
/// Mount point of the per-agent runtime directory inside the container.
pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive";
@ -23,7 +23,7 @@ pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive";
/// container. The harness service runs as a non-root unix user
/// whose home is `/home/<agent>/`, so the mount path varies per
/// agent — `container_claude_mount(name)` returns
/// `/home/<name>/.claude` for sub-agents and `/home/hm1nd/.claude`
/// `/home/<name>/.claude` for sub-agents and `/home/root/.claude`
/// for the manager. `claude` inside the container reads
/// `$HOME/.claude` and the service environment sets `HOME` to the
/// same path, so the OAuth session survives container restarts.
@ -1035,7 +1035,7 @@ async fn systemd_daemon_reload() -> Result<()> {
/// `systemd-nspawn` command.
/// Where in the container's filesystem the manager sees its agents tree.
/// Matches the `/agents` path that pre-Phase-8 hosts declared via
/// `containers.hm1nd.bindMounts."/agents"`.
/// `containers.root.bindMounts."/agents"`.
pub const CONTAINER_MANAGER_AGENTS_MOUNT: &str = "/agents";
/// Where the manager sees the applied trees of every agent, read-only.
@ -1252,7 +1252,7 @@ async fn run(args: &[&str]) -> Result<()> {
// Convention: `nixos-container <verb> <container> ...` — the
// verb is `args[0]` (kind) and the container is `args[1]`
// (h-<name> | hm1nd | hive-matrix | ...) for every long-running
// (h-<name> | root | hive-matrix | ...) for every long-running
// case we care about. Strip the `h-` prefix for sub-agents so the
// build_logs row's `agent` column matches the agent's bare name
// (`alice` rather than `h-alice`) — that's what the dashboard

View file

@ -23,7 +23,7 @@ pub fn start(coord: Arc<Coordinator>) -> Result<()> {
}
let listener = UnixListener::bind(&socket)
.with_context(|| format!("bind manager socket {}", socket.display()))?;
// 0666 so the in-container hm1nd user (non-root) can connect;
// 0666 so the in-container root user (non-root) can connect;
// the bind source dir is manager-only on host. See agent_server.rs.
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(&socket, std::fs::Permissions::from_mode(0o666))
@ -404,11 +404,9 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
let n = lines.unwrap_or(50);
// `journalctl -M` wants the *machine* name, not the
// logical agent name: `gui` → `h-gui`. `container_name`
// does that and passes `hm1nd` through unprefixed — but
// it doesn't know the broker-logical manager name
// `"manager"` (it'd wrongly produce `h-manager`), so
// handle that alias explicitly. Either manager spelling
// resolves to the unprefixed `hm1nd` machine.
// does that and passes the manager name through unprefixed.
// The explicit check here keeps parity with the MANAGER_AGENT
// constant so the two never diverge.
let machine = if agent == MANAGER_AGENT {
crate::lifecycle::MANAGER_NAME.to_owned()
} else {

View file

@ -448,10 +448,10 @@ where
out.push_str(
r#" let
base = if isManager
then hyperhive.nixosConfigurations.manager
then hyperhive.nixosConfigurations.root
else hyperhive.nixosConfigurations.agent-base;
input = inputs."agent-${name}";
service = if isManager then "hive-m1nd" else "hive-ag3nt";
service = "hive-ag3nt";
parentEnv = if parent == null then {} else { HIVE_PARENT = parent; };
toolGroupsEnv = if toolGroups == null then {} else { HIVE_TOOL_GROUPS = toolGroups; };
in
@ -461,7 +461,7 @@ where
{
# The harness service inside the container runs as a
# non-root unix user named after the agent (`damocles`,
# `iris`, `hm1nd`, ). UID auto-assigned by NixOS; the
# `iris`, `root`, ). UID auto-assigned by NixOS; the
# per-agent override here is what makes
# `hyperhive.user.name` match the agent's identity
# instead of the harness-base default of `"agent"`.

View file

@ -249,21 +249,14 @@ mod tests {
#[test]
fn manager_uses_container_name_prefix() {
// Manager's container view of its state is at
// `/agents/<MANAGER_NAME>/state/` (= `/agents/hm1nd/state/`),
// same as every other agent.
assert_eq!(container_state_prefix("manager"), "/agents/hm1nd/state/");
let p = resolve_host_path("manager", "/agents/hm1nd/state/reminders/x.md").unwrap();
// NB: the host path still resolves under `agents/manager/`
// (Coordinator::agent_notes_dir takes the broker LOGICAL name).
// That's a pre-existing manager-logical-vs-container-name
// discrepancy tracked separately; out of scope here.
// Manager's container view of its state is at `/agents/root/state/`.
assert_eq!(container_state_prefix("root"), "/agents/root/state/");
let p = resolve_host_path("root", "/agents/root/state/reminders/x.md").unwrap();
assert_eq!(
p,
PathBuf::from("/var/lib/hyperhive/agents/manager/state/reminders/x.md")
PathBuf::from("/var/lib/hyperhive/agents/root/state/reminders/x.md")
);
// And the legacy `/state/` prefix must NOT be accepted anymore.
assert!(resolve_host_path("manager", "/state/x.md").is_err());
assert!(resolve_host_path("root", "/state/x.md").is_err());
}
#[test]

View file

@ -578,7 +578,7 @@ fn default_true() -> bool {
// -----------------------------------------------------------------------------
/// Logical name the broker uses for the manager.
pub const MANAGER_AGENT: &str = "manager";
pub const MANAGER_AGENT: &str = "root";
/// Logical name the broker uses for the human operator. Messages with
/// `to = OPERATOR_RECIPIENT` accumulate in sqlite and surface on the
@ -693,7 +693,7 @@ pub enum HelperEvent {
/// (the dispatcher re-parses it on approve and inserts the schedule).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SchedulePromptPayload {
/// Names of recipient agents. Operator + `hm1nd` allowed.
/// Names of recipient agents. Operator + `root` allowed.
pub targets: Vec<String>,
/// Message body delivered to each target's inbox at fire time.
/// Same size budget as `Send.body` — soft cap at the broker level.

View file

@ -110,8 +110,8 @@ in
example = "manager";
description = ''
Whether this container runs as a sub-agent (`"agent"`, the
default invokes `hive-ag3nt serve`) or as the swarm's
manager (`"manager"` invokes `hive-m1nd serve` and
default) or as the swarm's manager (`"manager"` both invoke
`hive serve`;
defaults the forge notification surface to mentions-only).
meta.rs flips this to `"manager"` for the manager container
@ -690,7 +690,7 @@ in
}
];
# Per-agent unix user. Runs the hive-ag3nt / hive-m1nd harness +
# Per-agent unix user. Runs the hive harness +
# co-process daemons under a non-root principal. UID auto-assigned by
# NixOS. The container activation script (hive-agent-user-migrate)
# chowns the bind-mounted state dir — including credential files
@ -1277,14 +1277,12 @@ in
];
};
# Role-driven harness systemd unit: one binary, two unit names
# for log/ExecStartPre stability. Unit shape (PATH wrapper-dir
# trick, env vars, RuntimeDirectory, User=, standalone-eval
# fallbacks): docs/agent-hierarchy.md::Harness systemd unit
# shape (per-role). PATH /bin auto-append behaviour:
# docs/gotchas.md::systemd.services.*.path appends /bin to
# every entry.
systemd.services.${if config.hyperhive.role == "manager" then "hive-m1nd" else "hive-ag3nt"} =
# Harness systemd unit. Unit shape (PATH wrapper-dir trick, env vars,
# RuntimeDirectory, User=, standalone-eval fallbacks):
# docs/agent-hierarchy.md::Harness systemd unit shape. PATH /bin
# auto-append behaviour: docs/gotchas.md::systemd.services.*.path
# appends /bin to every entry.
systemd.services.hive-ag3nt =
let
isManager = config.hyperhive.role == "manager";
binary = "hive";
@ -1319,9 +1317,9 @@ in
}
// lib.optionalAttrs isManager {
# Standalone-eval fallbacks; meta.rs overrides at deploy time.
# HIVE_PORT = FNV-1a("hm1nd") % 900 + 8100.
HIVE_PORT = "8875";
HIVE_LABEL = "hm1nd";
# HIVE_PORT = FNV-1a("root") % 900 + 8100.
HIVE_PORT = "8273";
HIVE_LABEL = "root";
# Manager always uses a unix socket so the gateway can route
# /agent/<name>/ to it the same way it routes sub-agents.
HIVE_WEB_SOCKET = "/run/hive-agent/${userName}/web.sock";

View file

@ -2,13 +2,13 @@
{
imports = [ ./harness-base.nix ];
# Manager role: the role-driven `systemd.services.hive-m1nd` plus
# Manager role: the `systemd.services.hive-ag3nt` unit plus
# the manager-only forge defaults (`keepSubscriptions = false`,
# `skipNotifyReasons = [ "subscribed" "participating" ]`) live in
# `harness-base.nix` under `lib.mkIf (config.hyperhive.role ==
# "manager")`. This file is the bare entry-point referenced from
# `flake.nix` (`nixosConfigurations.manager`) and the meta-flake's
# `applied/hm1nd/flake.nix`. HIVE_PORT / HIVE_LABEL are injected by
# `flake.nix` (`nixosConfigurations.root`) and the meta-flake's
# `applied/root/flake.nix`. HIVE_PORT / HIVE_LABEL are injected by
# the meta-flake at deploy time and have manager-only standalone-eval
# fallbacks in `harness-base.nix`.
hyperhive.role = "manager";