refactor: remove hyperhive.role option — there is only one role: agent
This commit is contained in:
parent
d35408a18d
commit
41eb3f806c
23 changed files with 105 additions and 229 deletions
11
CLAUDE.md
11
CLAUDE.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) => {}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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!({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:#}"),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(());
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue