Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7e0675d22 | ||
|
|
77622c0585 | ||
|
|
829cea1a26 | ||
|
|
1d0d52b85f | ||
|
|
7772e29ec2 | ||
|
|
1b4e9d0e2b | ||
|
|
9654315a20 | ||
|
|
935f2011a0 | ||
|
|
4c2398a38e | ||
|
|
6ff2103a22 | ||
|
|
37b5335c27 |
11 changed files with 46 additions and 58 deletions
10
flake.nix
10
flake.nix
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"`.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Reference in a new issue