refactor: remove hyperhive.role option — there is only one role: agent

This commit is contained in:
damocles 2026-06-04 12:40:24 +02:00 committed by mara
commit 41eb3f806c
23 changed files with 105 additions and 229 deletions

View file

@ -328,11 +328,12 @@ nix/
`services.hyperhive.domain`; gateway serves
`.well-known/matrix/{client,server}` for
auto-discovery; federation on, e2ee deferred
templates/harness-base.nix shared harness for all containers; `hyperhive.role`
(`"agent"` | `"manager"`) drives service unit + forge
defaults; `hyperhive.model` option (HIVE_DEFAULT_MODEL)
templates/agent-base.nix thin role-setter (`hyperhive.role = "agent"`)
templates/manager.nix thin role-setter (`hyperhive.role = "manager"`)
templates/harness-base.nix shared harness for all containers; single
`hive-ag3nt` service unit; `hyperhive.model`
option (HIVE_DEFAULT_MODEL)
templates/agent-base.nix entry-point for sub-agent containers
templates/manager.nix entry-point for ruth; adds forge noise-reduction
defaults (keepSubscriptions=false etc.)
templates/weston-vnc.nix optional `hyperhive.gui.enable`
— weston + VNC backend systemd unit; writes
/etc/hyperhive/gui.json (vnc_port + auth) for

View file

@ -202,46 +202,19 @@ nspawn agent. Open questions, not yet wired:
- Filesystem: share parent's `/state` RW, or a sub-dir?
- Identity: distinct broker recipient name, or address the parent?
## Harness systemd unit shape (per-role)
## Harness systemd unit shape
One harness binary (`hive`), one `harness-base.nix` template, two
systemd units depending on `hyperhive.role`:
One harness binary (`hive`), one `harness-base.nix` template, one
service unit (`systemd.services.hive-ag3nt`) for all agents. There
is no longer a separate manager service name or role distinction in
the harness — privilege differences live server-side in the broker
socket (which tool groups and manager-surface calls each agent
receives).
- `agent-base.nix` (`role = "agent"`) → `systemd.services.hive-ag3nt`
- `manager.nix` (`role = "manager"`) → `systemd.services.hive-m1nd`
The unit names diverge but the binary is the same. `HIVE_ROLE` env
var picks the surface at startup (agent vs manager); naming the
units after the historical per-role binaries keeps dashboard log
queries, ExecStartPre paths, and ancestor PR diffs working without a
rename cascade.
### Manager-only defaults
`harness-base.nix` flips these when `hyperhive.role == "manager"`,
via `lib.mkDefault` so any agent can invert if needed:
- `hyperhive.forge.keepSubscriptions = false`
- `hyperhive.forge.skipNotifyReasons = [ "subscribed" "participating" ]`
Skips the subscription / participation firehose so the manager's
inbox only carries direct mentions, reviews, and assignments. Sub-
agents keep the noisier defaults so they see anything aimed at the
repos they're working on.
### Standalone-eval fallbacks
`nixosConfigurations.manager` must build standalone (without the
meta-flake's per-agent flake.nix wrapper). For the manager unit
that means a hardcoded `HIVE_LABEL` env value:
- `HIVE_LABEL = "ruth"` — logical agent name; matches what `meta.rs`
injects at deploy time.
Real deploys never read these — `meta::render_flake` overrides them
via the generated wrapper. They exist so the manager
`nixosConfigurations` evaluates cleanly even outside the meta-flake
boundary.
`agent-base.nix` and `manager.nix` both import `harness-base.nix`.
`manager.nix` additionally sets forge defaults to suppress the
subscription/participation firehose so ruth's inbox stays focused
on direct mentions, reviews, and assignments.
### Environment variables set on the unit
@ -256,8 +229,6 @@ boundary.
directly on the unit, **not** via `environment.variables`, because
the latter only populates `/etc/profile` which systemd services
don't inherit.
- `HIVE_ROLE = config.hyperhive.role` — picks the binary surface
(agent / manager) at startup.
### `PATH` setup (the wrapper-dir trick)
@ -278,8 +249,7 @@ bit set` regardless of `hyperhive.user.passwordlessSudo`.
### `serviceConfig` highlights
- `ExecStart = pkgs.hyperhive/bin/hive serve` — single binary,
surface picked from `HIVE_ROLE`.
- `ExecStart = pkgs.hyperhive/bin/hive serve` — single binary.
- `Restart = on-failure`, `RestartSec = 2` — keeps the harness
resilient across transient crashes without thundering retries.
- `RuntimeDirectory = "hive-config"``/run/hive-config/` owned by

View file

@ -5,8 +5,7 @@ claude has access to in return.
## The loop
Each agent harness (`hive serve`, role set via `$HIVE_ROLE` — always
`"agent"`, one binary) runs:
Each agent harness (`hive serve` — one binary for all agents) runs:
1. Long-poll `Recv` on its socket. The host-side broker
(`broker.rs::recv_blocking_batch`) returns immediately if there's
@ -57,15 +56,11 @@ Each agent harness (`hive serve`, role set via `$HIVE_ROLE` — always
## Harness binary shape
One `hive` binary serves both roles. The split into
One `hive` binary for all agents. The earlier split into
`hive-ag3nt` + `hive-m1nd` was collapsed because the privilege
boundary lives server-side at the broker socket
(`/run/hive/mcp.sock`): an agent-flavor socket refuses
`ManagerRequest` calls regardless of who sends them, so there's no
escalation risk in shipping the same code to both. `main()` reads
`$HIVE_ROLE` (set by `harness-base.nix` from `hyperhive.role`;
defaults to `"agent"` for standalone `nix run` invocations) and
dispatches.
(`/run/hive/mcp.sock`): `ManagerRequest` calls are refused by the
standard agent socket regardless of who sends them.
Three subcommands:

View file

@ -28,10 +28,8 @@ tracing-subscriber.workspace = true
tempfile = "3"
[[bin]]
# Unified harness binary: both `agent` and `manager` code paths live
# here; the binary picks its role at startup from `HIVE_ROLE` (set by
# `harness-base.nix` from `hyperhive.role`). The privilege boundary is
# enforced server-side at the socket, so shipping both surfaces in one
# binary is safe. See `docs/turn-loop.md::Harness binary shape`.
# Unified harness binary for all agents. Privilege boundary is
# enforced server-side at the socket (tool groups / manager surface).
# See `docs/turn-loop.md::Harness binary shape`.
name = "hive"
path = "src/bin/hive.rs"

View file

@ -198,7 +198,6 @@ trait Surface {
struct AgentSurface;
impl Surface for AgentSurface {
async fn ack_turn(socket: &Path) {
match client::request::<_, AgentResponse>(socket, &AgentRequest::AckTurn).await {
Ok(AgentResponse::Ok) => {}

View file

@ -269,13 +269,7 @@ shared closer
// Real template's first agent line — keeps the renderer
// honest about the {label} / {operator_pronouns} pair the
// harness already relied on.
let rendered = render(
&PRODUCTION_TEMPLATE,
"alice",
"they/them",
None,
None,
);
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "they/them", None, None);
assert!(rendered.contains("hyperhive agent `alice`"));
assert!(rendered.contains("**they/them** pronouns"));
assert!(!rendered.contains("{label}"));
@ -285,13 +279,7 @@ shared closer
#[test]
fn render_no_role_markers_in_output() {
// No raw role markers should survive into the rendered prompt.
let rendered = render(
&PRODUCTION_TEMPLATE,
"alice",
"she/her",
None,
None,
);
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None);
assert!(!rendered.contains("<!-- role:"));
assert!(!rendered.contains("<!-- /role:"));
// Shared tools appear.
@ -301,13 +289,7 @@ shared closer
#[test]
fn render_uses_agent_opener() {
let rendered = render(
&PRODUCTION_TEMPLATE,
"alice",
"she/her",
None,
None,
);
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None);
assert!(rendered.starts_with("You are hyperhive agent"));
}
@ -326,13 +308,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
#[test]
fn render_substitutes_hive_identity_when_set() {
let rendered = render(
IDENTITY_FIXTURE,
"alice",
"she/her",
Some("pr1ma"),
None,
);
let rendered = render(IDENTITY_FIXTURE, "alice", "she/her", Some("pr1ma"), None);
assert!(rendered.contains("on hive `pr1ma`"), "{rendered}");
// swarm clause stays absent when only hive is set.
assert!(!rendered.contains("in swarm"));

View file

@ -62,7 +62,6 @@ struct AppState {
gui_vnc_port: Option<u16>,
}
/// Bind the per-container web listener and serve the SPA.
///
/// `HIVE_WEB_SOCKET` opt-in selects unix-socket vs TCP binding; the
@ -737,7 +736,11 @@ async fn events_history(
// window is "drop buffered events you've already seen in history",
// never "lose an event that fired between the read and the seq."
// On paginated loads (`before` is set) seq is not needed.
let seq = if is_initial { Some(state.bus.current_seq()) } else { None };
let seq = if is_initial {
Some(state.bus.current_seq())
} else {
None
};
let (events, min_id, has_more) = state.bus.history_page(before, limit);
let mut resp = serde_json::json!({

View file

@ -306,17 +306,18 @@ async fn run_task(mut task: TaskFile, socket: &Path) {
let out_path = paths::task_out(&id);
let err_path = paths::task_err(&id);
let (timed_out, exit_code) = match exec_cmd(&task.cmd, &out_path, &err_path, task.timeout_secs).await {
Ok((code, false)) => (false, Some(code)),
Ok((_, true)) => {
tracing::warn!(id = %id, "bash_runner: task timed out");
(true, None)
}
Err(e) => {
tracing::warn!(id = %id, error = ?e, "bash_runner: exec error");
(false, None)
}
};
let (timed_out, exit_code) =
match exec_cmd(&task.cmd, &out_path, &err_path, task.timeout_secs).await {
Ok((code, false)) => (false, Some(code)),
Ok((_, true)) => {
tracing::warn!(id = %id, "bash_runner: task timed out");
(true, None)
}
Err(e) => {
tracing::warn!(id = %id, error = ?e, "bash_runner: exec error");
(false, None)
}
};
let stdout_tail = tail_file(&out_path, SUMMARY_BYTES);
let stderr_tail = tail_file(&err_path, SUMMARY_BYTES);

View file

@ -547,12 +547,9 @@ async fn run_apply_commit(
// "nixos-container update" label for the whole multi-minute window.
let hive = coord.hive_env();
let paths = Coordinator::agent_paths(&approval.agent, agent_dir.to_path_buf());
let build_result = lifecycle::rebuild_no_meta(
&approval.agent,
&hive,
&paths,
&|step| coord.set_queue_step(queue_entry_id, step),
)
let build_result = lifecycle::rebuild_no_meta(&approval.agent, &hive, &paths, &|step| {
coord.set_queue_step(queue_entry_id, step)
})
.await;
match build_result {

View file

@ -463,7 +463,7 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
Err(e) => {
return AgentResponse::Err {
message: format!("list containers failed: {e:#}"),
}
};
}
};
// Walk the full topology and collect every descendant.
@ -497,7 +497,8 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
};
}
tracing::info!(%agent, %name, "agent: request_init_config for child");
match crate::manager_server::submit_init_config(coord, name, description.clone()).await {
match crate::manager_server::submit_init_config(coord, name, description.clone()).await
{
Ok(_id) => AgentResponse::Ok,
Err(e) => AgentResponse::Err {
message: format!("{e:#}"),

View file

@ -84,12 +84,9 @@ pub async fn rebuild_agent(
// lifecycle_action; this catches the auto-update scan + any
// other direct caller.
let guard = coord.transient_guard(name, crate::coordinator::TransientKind::Rebuilding);
let result = lifecycle::rebuild(
name,
&hive,
&paths,
&|step| coord.set_queue_step(queue_entry_id, step),
)
let result = lifecycle::rebuild(name, &hive, &paths, &|step| {
coord.set_queue_step(queue_entry_id, step)
})
.await;
drop(guard);
match &result {

View file

@ -478,8 +478,7 @@ async fn matrix_reset_password(name: &str) -> Result<()> {
);
}
let admin_token = hive_c0re::matrix::read_admin_token()?;
let new_password =
hive_c0re::matrix::random_password().context("generate random password")?;
let new_password = hive_c0re::matrix::random_password().context("generate random password")?;
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
@ -496,8 +495,7 @@ async fn matrix_reset_password(name: &str) -> Result<()> {
)
.await
.with_context(|| format!("matrix reset-password {name}"))?;
let pw_path = PathBuf::from("/var/lib/hyperhive/matrix-creds")
.join(format!("{name}-password"));
let pw_path = PathBuf::from("/var/lib/hyperhive/matrix-creds").join(format!("{name}-password"));
println!("matrix: password for @{name}:{server_name} reset");
println!("password persisted at: {}", pw_path.display());
println!("next: hivectl matrix create-user {name} # mints a fresh access token");

View file

@ -3037,18 +3037,17 @@ struct PushWebhookRepo {
async fn post_webhook_knowledge(
axum::extract::Json(payload): axum::extract::Json<PushWebhookPayload>,
) -> Response {
let expected_repo = format!(
"{}/{}",
crate::knowledge::ORG,
crate::knowledge::REPO
);
let expected_repo = format!("{}/{}", crate::knowledge::ORG, crate::knowledge::REPO);
let full_name = payload
.repository
.as_ref()
.and_then(|r| r.full_name.as_deref())
.unwrap_or("");
if full_name != expected_repo {
tracing::debug!(full_name, "webhook/knowledge: ignoring push from unexpected repo");
tracing::debug!(
full_name,
"webhook/knowledge: ignoring push from unexpected repo"
);
return (StatusCode::OK, "ignored").into_response();
}
let git_ref = payload.git_ref.as_deref().unwrap_or("");

View file

@ -355,7 +355,12 @@ pub(crate) async fn submit_init_config(
}
let id = coord
.approvals
.submit_kind(name, hive_sh4re::ApprovalKind::InitConfig, "", description.as_deref())
.submit_kind(
name,
hive_sh4re::ApprovalKind::InitConfig,
"",
description.as_deref(),
)
.map_err(|e| anyhow::anyhow!("queue approval row: {e:#}"))?;
tracing::info!(%id, %name, "init_config approval queued");
coord.emit_approval_added(id, name, "init_config", None, None, description);

View file

@ -340,9 +340,7 @@ fn backfill_manager_tool_groups(names: &[String]) {
}
let existing = tool_groups::groups_for(MANAGER_NAME);
if !existing.is_empty() {
tracing::debug!(
"migration: ruth already has explicit tool groups — skipping backfill"
);
tracing::debug!("migration: ruth already has explicit tool groups — skipping backfill");
return;
}
let all_groups: Vec<String> = hive_sh4re::ToolGroup::MANAGER_DEFAULT

View file

@ -202,7 +202,6 @@ fn known_agents(_coord: &Coordinator) -> std::collections::HashSet<String> {
if let Some(name) = raw.strip_prefix(crate::lifecycle::AGENT_PREFIX) {
out.insert(name.to_owned());
}
}
}
Err(e) => {
@ -378,7 +377,6 @@ async fn known_agents_async() -> std::collections::HashSet<String> {
if let Some(name) = raw.strip_prefix(crate::lifecycle::AGENT_PREFIX) {
out.insert(name.to_owned());
}
}
}
Err(e) => {

View file

@ -82,8 +82,7 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
let agent_dir = coord.ensure_runtime(name)?;
let hive = coord.hive_env();
let paths = Coordinator::agent_paths(name, agent_dir);
match lifecycle::spawn(name, &hive, &paths).await
{
match lifecycle::spawn(name, &hive, &paths).await {
Ok(()) => {
coord.notify_manager(&hive_sh4re::HelperEvent::Spawned {
agent: name.clone(),

View file

@ -54,21 +54,13 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
/// Submit a review event (APPROVED / REQUEST_CHANGES / COMMENT) and print
/// a compact summary of the created review.
fn submit_review(
client: &Client,
number: u64,
event: &str,
body: Option<String>,
) -> Result<()> {
fn submit_review(client: &Client, number: u64, event: &str, body: Option<String>) -> Result<()> {
let repo = client.repo();
let payload = json!({
"event": event,
"body": body.unwrap_or_default(),
});
let v = client.post_json(
&format!("/repos/{repo}/pulls/{number}/reviews"),
&payload,
)?;
let v = client.post_json(&format!("/repos/{repo}/pulls/{number}/reviews"), &payload)?;
print_json(&json!({
"id": v.get("id"),
"state": v.get("state"),
@ -81,7 +73,9 @@ fn submit_review(
/// gracefully.
fn fetch_inline_comments(client: &Client, repo: &str, pr: u64, review_id: u64) -> Vec<Value> {
client
.get_json(&format!("/repos/{repo}/pulls/{pr}/reviews/{review_id}/comments"))
.get_json(&format!(
"/repos/{repo}/pulls/{pr}/reviews/{review_id}/comments"
))
.ok()
.and_then(|v| v.as_array().cloned())
.unwrap_or_default()
@ -100,12 +94,7 @@ fn list_reviews(client: &Client, number: u64) -> Result<()> {
}
/// JSON output: one object per review, with an inline `comments` array.
fn list_reviews_json(
client: &Client,
repo: &str,
number: u64,
reviews: &[Value],
) -> Result<()> {
fn list_reviews_json(client: &Client, repo: &str, number: u64, reviews: &[Value]) -> Result<()> {
let trimmed: Vec<Value> = reviews
.iter()
.map(|r| {
@ -140,12 +129,7 @@ fn list_reviews_json(
/// Human-readable output: Markdown-style heading per review, inline
/// comments as `[path:line] body` (line omitted for PR-level comments).
fn list_reviews_text(
client: &Client,
repo: &str,
number: u64,
reviews: &[Value],
) -> Result<()> {
fn list_reviews_text(client: &Client, repo: &str, number: u64, reviews: &[Value]) -> Result<()> {
if reviews.is_empty() {
println!("(no reviews)");
return Ok(());

View file

@ -936,7 +936,9 @@ impl ToolGroup {
Self::Inbox => {
"get_loose_ends, cancel_loose_end, remind, request_next_turn — self-scheduling"
}
Self::Lifecycle => "kill, start, restart, update, list_containers — container lifecycle (privileged)",
Self::Lifecycle => {
"kill, start, restart, update, list_containers — container lifecycle (privileged)"
}
Self::Approvals => {
"request_init_config, request_apply_commit, request_update_meta_inputs — config change flow (privileged)"
}
@ -946,7 +948,9 @@ impl ToolGroup {
Self::Diagnostics => {
"get_logs — read a sub-agent container's systemd journal (privileged)"
}
Self::Execution => "run, status — run shell commands via mcp__bash__run / mcp__bash__status",
Self::Execution => {
"run, status — run shell commands via mcp__bash__run / mcp__bash__status"
}
Self::WebTools => "WebFetch, WebSearch — Claude built-in web egress; not MCP tools",
}
}

View file

@ -197,8 +197,7 @@ in
'';
}
{
assertion =
!config.services.hyperhive.forge.enable || config.services.hyperhive.gateway.enable;
assertion = !config.services.hyperhive.forge.enable || config.services.hyperhive.gateway.enable;
message = ''
services.hyperhive.network.isolateContainers = true with
services.hyperhive.forge.enable = true requires

View file

@ -1,11 +1,7 @@
{ ... }:
{
imports = [ ./harness-base.nix ];
# Sub-agent role: the role-driven `systemd.services.hive-ag3nt` plus
# the default forge notification surface live in `harness-base.nix`.
# This file is the bare entry-point referenced from `flake.nix`
# Entry-point for sub-agent containers. Referenced from `flake.nix`
# (`nixosConfigurations.agent-base`) and the meta-flake's
# `applied/<name>/flake.nix` for sub-agent containers.
hyperhive.role = "agent";
# `applied/<name>/flake.nix`.
}

View file

@ -19,10 +19,9 @@ let
homeDir = "/home/${userName}";
in
{
# Shared scaffolding for any hyperhive harness container — both
# sub-agents (`agent-base.nix`) and the manager (`manager.nix`) extend
# this. The systemd service that actually runs the harness binary
# differs per role and lives in the child module.
# Shared scaffolding for every hyperhive harness container.
# `agent-base.nix` and `manager.nix` both import this; all agents
# use the same service unit regardless of which entry-point they came from.
# Optional feature modules. Each declares its own `hyperhive.*`
# option(s), default-off, so every agent has them available but
@ -86,28 +85,6 @@ in
'';
};
options.hyperhive.role = lib.mkOption {
type = lib.types.enum [
"agent"
"manager"
];
default = "agent";
example = "manager";
description = ''
Whether this container runs as a sub-agent (`"agent"`, the
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
and leaves it at the default for every sub-agent. Agents
and `agent.nix` files don't normally touch this option;
it's exposed so a standalone `nixos-rebuild` against
`nixosConfigurations.manager` keeps working without the
meta-flake wrapper around it.
'';
};
options.hyperhive.model = lib.mkOption {
type = lib.types.str;
default = "haiku";
@ -292,9 +269,7 @@ in
Computed: the merged static tree consumed by the harness via
`HIVE_STATIC_DIR`. Composed at evaluation time by copying
`hyperhive.frontend.dist`'s `agent/` subdir as the base, then
layering each `extraFiles` entry on top. Read-only
consumers (`agent-base.nix`, `manager.nix`) reference this in
their systemd service environment; do not set directly.
layering each `extraFiles` entry on top. Read-only do not set directly.
'';
};
@ -1357,18 +1332,6 @@ in
};
};
# Manager-only forge defaults: subscription/participation
# firehose stays off so the manager's inbox isn't drowned in
# noise. Full rationale + sub-agent contrast:
# docs/agent-hierarchy.md::Manager-only defaults.
hyperhive.forge = lib.mkIf (config.hyperhive.role == "manager") {
keepSubscriptions = lib.mkDefault false;
skipNotifyReasons = lib.mkDefault [
"subscribed"
"participating"
];
};
# 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
@ -1376,11 +1339,10 @@ in
# appends /bin to every entry.
systemd.services.hive-ag3nt =
let
isManager = config.hyperhive.role == "manager";
binary = "hive";
in
{
description = "${binary}${lib.optionalString isManager " manager"} harness";
description = "${binary} harness";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
# `/run/wrappers` before `/run/current-system/sw` so setuid
@ -1395,16 +1357,11 @@ in
HOME = homeDir;
HIVE_STATIC_DIR = "${config.hyperhive.frontend.mergedDist}";
HIVE_ASSETS_DIR = "${pkgs.hyperhive-assets}/share/hyperhive";
HIVE_ROLE = config.hyperhive.role;
# Unix-socket path for the harness web UI. All agents (sub-agents
# and manager) always bind here; TCP fallback is removed. Path
# matches `hive_c0re::agent_sockets::socket_path_for(name)` so
# lifecycle bind-mounts and gateway upstream config stay in sync.
# Unix-socket path for the harness web UI. All agents always bind
# here; TCP fallback is removed. Path matches
# `hive_c0re::agent_sockets::socket_path_for(name)` so lifecycle
# bind-mounts and gateway upstream config stay in sync.
HIVE_WEB_SOCKET = "/run/hive-agent/${userName}/web.sock";
}
// lib.optionalAttrs isManager {
# Standalone-eval fallback; meta.rs overrides at deploy time.
HIVE_LABEL = "ruth";
};
serviceConfig = {
ExecStart = "${pkgs.hyperhive}/bin/${binary} serve";

View file

@ -2,14 +2,15 @@
{
imports = [ ./harness-base.nix ];
# 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
# Entry-point for the privileged root agent (ruth). Referenced from
# `flake.nix` (`nixosConfigurations.ruth`) and the meta-flake's
# `applied/ruth/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";
# `applied/ruth/flake.nix`. Forge subscription/participation firehose
# stays off so ruth's inbox isn't drowned in noise.
hyperhive.forge = {
keepSubscriptions = false;
skipNotifyReasons = [
"subscribed"
"participating"
];
};
}