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
|
`services.hyperhive.domain`; gateway serves
|
||||||
`.well-known/matrix/{client,server}` for
|
`.well-known/matrix/{client,server}` for
|
||||||
auto-discovery; federation on, e2ee deferred
|
auto-discovery; federation on, e2ee deferred
|
||||||
templates/harness-base.nix shared harness for all containers; `hyperhive.role`
|
templates/harness-base.nix shared harness for all containers; single
|
||||||
(`"agent"` | `"manager"`) drives service unit + forge
|
`hive-ag3nt` service unit; `hyperhive.model`
|
||||||
defaults; `hyperhive.model` option (HIVE_DEFAULT_MODEL)
|
option (HIVE_DEFAULT_MODEL)
|
||||||
templates/agent-base.nix thin role-setter (`hyperhive.role = "agent"`)
|
templates/agent-base.nix entry-point for sub-agent containers
|
||||||
templates/manager.nix thin role-setter (`hyperhive.role = "manager"`)
|
templates/manager.nix entry-point for ruth; adds forge noise-reduction
|
||||||
|
defaults (keepSubscriptions=false etc.)
|
||||||
templates/weston-vnc.nix optional `hyperhive.gui.enable`
|
templates/weston-vnc.nix optional `hyperhive.gui.enable`
|
||||||
— weston + VNC backend systemd unit; writes
|
— weston + VNC backend systemd unit; writes
|
||||||
/etc/hyperhive/gui.json (vnc_port + auth) for
|
/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?
|
- Filesystem: share parent's `/state` RW, or a sub-dir?
|
||||||
- Identity: distinct broker recipient name, or address the parent?
|
- 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
|
One harness binary (`hive`), one `harness-base.nix` template, one
|
||||||
systemd units depending on `hyperhive.role`:
|
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`
|
`agent-base.nix` and `manager.nix` both import `harness-base.nix`.
|
||||||
- `manager.nix` (`role = "manager"`) → `systemd.services.hive-m1nd`
|
`manager.nix` additionally sets forge defaults to suppress the
|
||||||
|
subscription/participation firehose so ruth's inbox stays focused
|
||||||
The unit names diverge but the binary is the same. `HIVE_ROLE` env
|
on direct mentions, reviews, and assignments.
|
||||||
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.
|
|
||||||
|
|
||||||
### Environment variables set on the unit
|
### Environment variables set on the unit
|
||||||
|
|
||||||
|
|
@ -256,8 +229,6 @@ boundary.
|
||||||
directly on the unit, **not** via `environment.variables`, because
|
directly on the unit, **not** via `environment.variables`, because
|
||||||
the latter only populates `/etc/profile` which systemd services
|
the latter only populates `/etc/profile` which systemd services
|
||||||
don't inherit.
|
don't inherit.
|
||||||
- `HIVE_ROLE = config.hyperhive.role` — picks the binary surface
|
|
||||||
(agent / manager) at startup.
|
|
||||||
|
|
||||||
### `PATH` setup (the wrapper-dir trick)
|
### `PATH` setup (the wrapper-dir trick)
|
||||||
|
|
||||||
|
|
@ -278,8 +249,7 @@ bit set` regardless of `hyperhive.user.passwordlessSudo`.
|
||||||
|
|
||||||
### `serviceConfig` highlights
|
### `serviceConfig` highlights
|
||||||
|
|
||||||
- `ExecStart = pkgs.hyperhive/bin/hive serve` — single binary,
|
- `ExecStart = pkgs.hyperhive/bin/hive serve` — single binary.
|
||||||
surface picked from `HIVE_ROLE`.
|
|
||||||
- `Restart = on-failure`, `RestartSec = 2` — keeps the harness
|
- `Restart = on-failure`, `RestartSec = 2` — keeps the harness
|
||||||
resilient across transient crashes without thundering retries.
|
resilient across transient crashes without thundering retries.
|
||||||
- `RuntimeDirectory = "hive-config"` → `/run/hive-config/` owned by
|
- `RuntimeDirectory = "hive-config"` → `/run/hive-config/` owned by
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@ claude has access to in return.
|
||||||
|
|
||||||
## The loop
|
## The loop
|
||||||
|
|
||||||
Each agent harness (`hive serve`, role set via `$HIVE_ROLE` — always
|
Each agent harness (`hive serve` — one binary for all agents) runs:
|
||||||
`"agent"`, one binary) runs:
|
|
||||||
|
|
||||||
1. Long-poll `Recv` on its socket. The host-side broker
|
1. Long-poll `Recv` on its socket. The host-side broker
|
||||||
(`broker.rs::recv_blocking_batch`) returns immediately if there's
|
(`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
|
## 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
|
`hive-ag3nt` + `hive-m1nd` was collapsed because the privilege
|
||||||
boundary lives server-side at the broker socket
|
boundary lives server-side at the broker socket
|
||||||
(`/run/hive/mcp.sock`): an agent-flavor socket refuses
|
(`/run/hive/mcp.sock`): `ManagerRequest` calls are refused by the
|
||||||
`ManagerRequest` calls regardless of who sends them, so there's no
|
standard agent socket regardless of who sends them.
|
||||||
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.
|
|
||||||
|
|
||||||
Three subcommands:
|
Three subcommands:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,8 @@ tracing-subscriber.workspace = true
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
# Unified harness binary: both `agent` and `manager` code paths live
|
# Unified harness binary for all agents. Privilege boundary is
|
||||||
# here; the binary picks its role at startup from `HIVE_ROLE` (set by
|
# enforced server-side at the socket (tool groups / manager surface).
|
||||||
# `harness-base.nix` from `hyperhive.role`). The privilege boundary is
|
# See `docs/turn-loop.md::Harness binary shape`.
|
||||||
# enforced server-side at the socket, so shipping both surfaces in one
|
|
||||||
# binary is safe. See `docs/turn-loop.md::Harness binary shape`.
|
|
||||||
name = "hive"
|
name = "hive"
|
||||||
path = "src/bin/hive.rs"
|
path = "src/bin/hive.rs"
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,6 @@ trait Surface {
|
||||||
struct AgentSurface;
|
struct AgentSurface;
|
||||||
|
|
||||||
impl Surface for AgentSurface {
|
impl Surface for AgentSurface {
|
||||||
|
|
||||||
async fn ack_turn(socket: &Path) {
|
async fn ack_turn(socket: &Path) {
|
||||||
match client::request::<_, AgentResponse>(socket, &AgentRequest::AckTurn).await {
|
match client::request::<_, AgentResponse>(socket, &AgentRequest::AckTurn).await {
|
||||||
Ok(AgentResponse::Ok) => {}
|
Ok(AgentResponse::Ok) => {}
|
||||||
|
|
|
||||||
|
|
@ -269,13 +269,7 @@ shared closer
|
||||||
// Real template's first agent line — keeps the renderer
|
// Real template's first agent line — keeps the renderer
|
||||||
// honest about the {label} / {operator_pronouns} pair the
|
// honest about the {label} / {operator_pronouns} pair the
|
||||||
// harness already relied on.
|
// harness already relied on.
|
||||||
let rendered = render(
|
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "they/them", None, None);
|
||||||
&PRODUCTION_TEMPLATE,
|
|
||||||
"alice",
|
|
||||||
"they/them",
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
assert!(rendered.contains("hyperhive agent `alice`"));
|
assert!(rendered.contains("hyperhive agent `alice`"));
|
||||||
assert!(rendered.contains("**they/them** pronouns"));
|
assert!(rendered.contains("**they/them** pronouns"));
|
||||||
assert!(!rendered.contains("{label}"));
|
assert!(!rendered.contains("{label}"));
|
||||||
|
|
@ -285,13 +279,7 @@ shared closer
|
||||||
#[test]
|
#[test]
|
||||||
fn render_no_role_markers_in_output() {
|
fn render_no_role_markers_in_output() {
|
||||||
// No raw role markers should survive into the rendered prompt.
|
// No raw role markers should survive into the rendered prompt.
|
||||||
let rendered = render(
|
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None);
|
||||||
&PRODUCTION_TEMPLATE,
|
|
||||||
"alice",
|
|
||||||
"she/her",
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
assert!(!rendered.contains("<!-- role:"));
|
assert!(!rendered.contains("<!-- role:"));
|
||||||
assert!(!rendered.contains("<!-- /role:"));
|
assert!(!rendered.contains("<!-- /role:"));
|
||||||
// Shared tools appear.
|
// Shared tools appear.
|
||||||
|
|
@ -301,13 +289,7 @@ shared closer
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_uses_agent_opener() {
|
fn render_uses_agent_opener() {
|
||||||
let rendered = render(
|
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None);
|
||||||
&PRODUCTION_TEMPLATE,
|
|
||||||
"alice",
|
|
||||||
"she/her",
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
assert!(rendered.starts_with("You are hyperhive agent"));
|
assert!(rendered.starts_with("You are hyperhive agent"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -326,13 +308,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_substitutes_hive_identity_when_set() {
|
fn render_substitutes_hive_identity_when_set() {
|
||||||
let rendered = render(
|
let rendered = render(IDENTITY_FIXTURE, "alice", "she/her", Some("pr1ma"), None);
|
||||||
IDENTITY_FIXTURE,
|
|
||||||
"alice",
|
|
||||||
"she/her",
|
|
||||||
Some("pr1ma"),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
assert!(rendered.contains("on hive `pr1ma`"), "{rendered}");
|
assert!(rendered.contains("on hive `pr1ma`"), "{rendered}");
|
||||||
// swarm clause stays absent when only hive is set.
|
// swarm clause stays absent when only hive is set.
|
||||||
assert!(!rendered.contains("in swarm"));
|
assert!(!rendered.contains("in swarm"));
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ struct AppState {
|
||||||
gui_vnc_port: Option<u16>,
|
gui_vnc_port: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Bind the per-container web listener and serve the SPA.
|
/// Bind the per-container web listener and serve the SPA.
|
||||||
///
|
///
|
||||||
/// `HIVE_WEB_SOCKET` opt-in selects unix-socket vs TCP binding; the
|
/// `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",
|
// window is "drop buffered events you've already seen in history",
|
||||||
// never "lose an event that fired between the read and the seq."
|
// never "lose an event that fired between the read and the seq."
|
||||||
// On paginated loads (`before` is set) seq is not needed.
|
// 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 (events, min_id, has_more) = state.bus.history_page(before, limit);
|
||||||
let mut resp = serde_json::json!({
|
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 out_path = paths::task_out(&id);
|
||||||
let err_path = paths::task_err(&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 {
|
let (timed_out, exit_code) =
|
||||||
Ok((code, false)) => (false, Some(code)),
|
match exec_cmd(&task.cmd, &out_path, &err_path, task.timeout_secs).await {
|
||||||
Ok((_, true)) => {
|
Ok((code, false)) => (false, Some(code)),
|
||||||
tracing::warn!(id = %id, "bash_runner: task timed out");
|
Ok((_, true)) => {
|
||||||
(true, None)
|
tracing::warn!(id = %id, "bash_runner: task timed out");
|
||||||
}
|
(true, None)
|
||||||
Err(e) => {
|
}
|
||||||
tracing::warn!(id = %id, error = ?e, "bash_runner: exec error");
|
Err(e) => {
|
||||||
(false, None)
|
tracing::warn!(id = %id, error = ?e, "bash_runner: exec error");
|
||||||
}
|
(false, None)
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let stdout_tail = tail_file(&out_path, SUMMARY_BYTES);
|
let stdout_tail = tail_file(&out_path, SUMMARY_BYTES);
|
||||||
let stderr_tail = tail_file(&err_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.
|
// "nixos-container update" label for the whole multi-minute window.
|
||||||
let hive = coord.hive_env();
|
let hive = coord.hive_env();
|
||||||
let paths = Coordinator::agent_paths(&approval.agent, agent_dir.to_path_buf());
|
let paths = Coordinator::agent_paths(&approval.agent, agent_dir.to_path_buf());
|
||||||
let build_result = lifecycle::rebuild_no_meta(
|
let build_result = lifecycle::rebuild_no_meta(&approval.agent, &hive, &paths, &|step| {
|
||||||
&approval.agent,
|
coord.set_queue_step(queue_entry_id, step)
|
||||||
&hive,
|
})
|
||||||
&paths,
|
|
||||||
&|step| coord.set_queue_step(queue_entry_id, step),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match build_result {
|
match build_result {
|
||||||
|
|
|
||||||
|
|
@ -463,7 +463,7 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return AgentResponse::Err {
|
return AgentResponse::Err {
|
||||||
message: format!("list containers failed: {e:#}"),
|
message: format!("list containers failed: {e:#}"),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Walk the full topology and collect every descendant.
|
// 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");
|
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,
|
Ok(_id) => AgentResponse::Ok,
|
||||||
Err(e) => AgentResponse::Err {
|
Err(e) => AgentResponse::Err {
|
||||||
message: format!("{e:#}"),
|
message: format!("{e:#}"),
|
||||||
|
|
|
||||||
|
|
@ -84,12 +84,9 @@ pub async fn rebuild_agent(
|
||||||
// lifecycle_action; this catches the auto-update scan + any
|
// lifecycle_action; this catches the auto-update scan + any
|
||||||
// other direct caller.
|
// other direct caller.
|
||||||
let guard = coord.transient_guard(name, crate::coordinator::TransientKind::Rebuilding);
|
let guard = coord.transient_guard(name, crate::coordinator::TransientKind::Rebuilding);
|
||||||
let result = lifecycle::rebuild(
|
let result = lifecycle::rebuild(name, &hive, &paths, &|step| {
|
||||||
name,
|
coord.set_queue_step(queue_entry_id, step)
|
||||||
&hive,
|
})
|
||||||
&paths,
|
|
||||||
&|step| coord.set_queue_step(queue_entry_id, step),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
drop(guard);
|
drop(guard);
|
||||||
match &result {
|
match &result {
|
||||||
|
|
|
||||||
|
|
@ -478,8 +478,7 @@ async fn matrix_reset_password(name: &str) -> Result<()> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let admin_token = hive_c0re::matrix::read_admin_token()?;
|
let admin_token = hive_c0re::matrix::read_admin_token()?;
|
||||||
let new_password =
|
let new_password = hive_c0re::matrix::random_password().context("generate random password")?;
|
||||||
hive_c0re::matrix::random_password().context("generate random password")?;
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(30))
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -496,8 +495,7 @@ async fn matrix_reset_password(name: &str) -> Result<()> {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("matrix reset-password {name}"))?;
|
.with_context(|| format!("matrix reset-password {name}"))?;
|
||||||
let pw_path = PathBuf::from("/var/lib/hyperhive/matrix-creds")
|
let pw_path = PathBuf::from("/var/lib/hyperhive/matrix-creds").join(format!("{name}-password"));
|
||||||
.join(format!("{name}-password"));
|
|
||||||
println!("matrix: password for @{name}:{server_name} reset");
|
println!("matrix: password for @{name}:{server_name} reset");
|
||||||
println!("password persisted at: {}", pw_path.display());
|
println!("password persisted at: {}", pw_path.display());
|
||||||
println!("next: hivectl matrix create-user {name} # mints a fresh access token");
|
println!("next: hivectl matrix create-user {name} # mints a fresh access token");
|
||||||
|
|
|
||||||
|
|
@ -3037,18 +3037,17 @@ struct PushWebhookRepo {
|
||||||
async fn post_webhook_knowledge(
|
async fn post_webhook_knowledge(
|
||||||
axum::extract::Json(payload): axum::extract::Json<PushWebhookPayload>,
|
axum::extract::Json(payload): axum::extract::Json<PushWebhookPayload>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let expected_repo = format!(
|
let expected_repo = format!("{}/{}", crate::knowledge::ORG, crate::knowledge::REPO);
|
||||||
"{}/{}",
|
|
||||||
crate::knowledge::ORG,
|
|
||||||
crate::knowledge::REPO
|
|
||||||
);
|
|
||||||
let full_name = payload
|
let full_name = payload
|
||||||
.repository
|
.repository
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|r| r.full_name.as_deref())
|
.and_then(|r| r.full_name.as_deref())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
if full_name != expected_repo {
|
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();
|
return (StatusCode::OK, "ignored").into_response();
|
||||||
}
|
}
|
||||||
let git_ref = payload.git_ref.as_deref().unwrap_or("");
|
let git_ref = payload.git_ref.as_deref().unwrap_or("");
|
||||||
|
|
|
||||||
|
|
@ -355,7 +355,12 @@ pub(crate) async fn submit_init_config(
|
||||||
}
|
}
|
||||||
let id = coord
|
let id = coord
|
||||||
.approvals
|
.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:#}"))?;
|
.map_err(|e| anyhow::anyhow!("queue approval row: {e:#}"))?;
|
||||||
tracing::info!(%id, %name, "init_config approval queued");
|
tracing::info!(%id, %name, "init_config approval queued");
|
||||||
coord.emit_approval_added(id, name, "init_config", None, None, description);
|
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);
|
let existing = tool_groups::groups_for(MANAGER_NAME);
|
||||||
if !existing.is_empty() {
|
if !existing.is_empty() {
|
||||||
tracing::debug!(
|
tracing::debug!("migration: ruth already has explicit tool groups — skipping backfill");
|
||||||
"migration: ruth already has explicit tool groups — skipping backfill"
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let all_groups: Vec<String> = hive_sh4re::ToolGroup::MANAGER_DEFAULT
|
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) {
|
if let Some(name) = raw.strip_prefix(crate::lifecycle::AGENT_PREFIX) {
|
||||||
out.insert(name.to_owned());
|
out.insert(name.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
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) {
|
if let Some(name) = raw.strip_prefix(crate::lifecycle::AGENT_PREFIX) {
|
||||||
out.insert(name.to_owned());
|
out.insert(name.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
|
|
@ -82,8 +82,7 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
||||||
let agent_dir = coord.ensure_runtime(name)?;
|
let agent_dir = coord.ensure_runtime(name)?;
|
||||||
let hive = coord.hive_env();
|
let hive = coord.hive_env();
|
||||||
let paths = Coordinator::agent_paths(name, agent_dir);
|
let paths = Coordinator::agent_paths(name, agent_dir);
|
||||||
match lifecycle::spawn(name, &hive, &paths).await
|
match lifecycle::spawn(name, &hive, &paths).await {
|
||||||
{
|
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
coord.notify_manager(&hive_sh4re::HelperEvent::Spawned {
|
coord.notify_manager(&hive_sh4re::HelperEvent::Spawned {
|
||||||
agent: name.clone(),
|
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
|
/// Submit a review event (APPROVED / REQUEST_CHANGES / COMMENT) and print
|
||||||
/// a compact summary of the created review.
|
/// a compact summary of the created review.
|
||||||
fn submit_review(
|
fn submit_review(client: &Client, number: u64, event: &str, body: Option<String>) -> Result<()> {
|
||||||
client: &Client,
|
|
||||||
number: u64,
|
|
||||||
event: &str,
|
|
||||||
body: Option<String>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let repo = client.repo();
|
let repo = client.repo();
|
||||||
let payload = json!({
|
let payload = json!({
|
||||||
"event": event,
|
"event": event,
|
||||||
"body": body.unwrap_or_default(),
|
"body": body.unwrap_or_default(),
|
||||||
});
|
});
|
||||||
let v = client.post_json(
|
let v = client.post_json(&format!("/repos/{repo}/pulls/{number}/reviews"), &payload)?;
|
||||||
&format!("/repos/{repo}/pulls/{number}/reviews"),
|
|
||||||
&payload,
|
|
||||||
)?;
|
|
||||||
print_json(&json!({
|
print_json(&json!({
|
||||||
"id": v.get("id"),
|
"id": v.get("id"),
|
||||||
"state": v.get("state"),
|
"state": v.get("state"),
|
||||||
|
|
@ -81,7 +73,9 @@ fn submit_review(
|
||||||
/// gracefully.
|
/// gracefully.
|
||||||
fn fetch_inline_comments(client: &Client, repo: &str, pr: u64, review_id: u64) -> Vec<Value> {
|
fn fetch_inline_comments(client: &Client, repo: &str, pr: u64, review_id: u64) -> Vec<Value> {
|
||||||
client
|
client
|
||||||
.get_json(&format!("/repos/{repo}/pulls/{pr}/reviews/{review_id}/comments"))
|
.get_json(&format!(
|
||||||
|
"/repos/{repo}/pulls/{pr}/reviews/{review_id}/comments"
|
||||||
|
))
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|v| v.as_array().cloned())
|
.and_then(|v| v.as_array().cloned())
|
||||||
.unwrap_or_default()
|
.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.
|
/// JSON output: one object per review, with an inline `comments` array.
|
||||||
fn list_reviews_json(
|
fn list_reviews_json(client: &Client, repo: &str, number: u64, reviews: &[Value]) -> Result<()> {
|
||||||
client: &Client,
|
|
||||||
repo: &str,
|
|
||||||
number: u64,
|
|
||||||
reviews: &[Value],
|
|
||||||
) -> Result<()> {
|
|
||||||
let trimmed: Vec<Value> = reviews
|
let trimmed: Vec<Value> = reviews
|
||||||
.iter()
|
.iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
|
|
@ -140,12 +129,7 @@ fn list_reviews_json(
|
||||||
|
|
||||||
/// Human-readable output: Markdown-style heading per review, inline
|
/// Human-readable output: Markdown-style heading per review, inline
|
||||||
/// comments as `[path:line] body` (line omitted for PR-level comments).
|
/// comments as `[path:line] body` (line omitted for PR-level comments).
|
||||||
fn list_reviews_text(
|
fn list_reviews_text(client: &Client, repo: &str, number: u64, reviews: &[Value]) -> Result<()> {
|
||||||
client: &Client,
|
|
||||||
repo: &str,
|
|
||||||
number: u64,
|
|
||||||
reviews: &[Value],
|
|
||||||
) -> Result<()> {
|
|
||||||
if reviews.is_empty() {
|
if reviews.is_empty() {
|
||||||
println!("(no reviews)");
|
println!("(no reviews)");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
|
||||||
|
|
@ -936,7 +936,9 @@ impl ToolGroup {
|
||||||
Self::Inbox => {
|
Self::Inbox => {
|
||||||
"get_loose_ends, cancel_loose_end, remind, request_next_turn — self-scheduling"
|
"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 => {
|
Self::Approvals => {
|
||||||
"request_init_config, request_apply_commit, request_update_meta_inputs — config change flow (privileged)"
|
"request_init_config, request_apply_commit, request_update_meta_inputs — config change flow (privileged)"
|
||||||
}
|
}
|
||||||
|
|
@ -946,7 +948,9 @@ impl ToolGroup {
|
||||||
Self::Diagnostics => {
|
Self::Diagnostics => {
|
||||||
"get_logs — read a sub-agent container's systemd journal (privileged)"
|
"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",
|
Self::WebTools => "WebFetch, WebSearch — Claude built-in web egress; not MCP tools",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -197,8 +197,7 @@ in
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
assertion =
|
assertion = !config.services.hyperhive.forge.enable || config.services.hyperhive.gateway.enable;
|
||||||
!config.services.hyperhive.forge.enable || config.services.hyperhive.gateway.enable;
|
|
||||||
message = ''
|
message = ''
|
||||||
services.hyperhive.network.isolateContainers = true with
|
services.hyperhive.network.isolateContainers = true with
|
||||||
services.hyperhive.forge.enable = true requires
|
services.hyperhive.forge.enable = true requires
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
imports = [ ./harness-base.nix ];
|
imports = [ ./harness-base.nix ];
|
||||||
|
# Entry-point for sub-agent containers. Referenced from `flake.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`
|
|
||||||
# (`nixosConfigurations.agent-base`) and the meta-flake's
|
# (`nixosConfigurations.agent-base`) and the meta-flake's
|
||||||
# `applied/<name>/flake.nix` for sub-agent containers.
|
# `applied/<name>/flake.nix`.
|
||||||
hyperhive.role = "agent";
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,9 @@ let
|
||||||
homeDir = "/home/${userName}";
|
homeDir = "/home/${userName}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Shared scaffolding for any hyperhive harness container — both
|
# Shared scaffolding for every hyperhive harness container.
|
||||||
# sub-agents (`agent-base.nix`) and the manager (`manager.nix`) extend
|
# `agent-base.nix` and `manager.nix` both import this; all agents
|
||||||
# this. The systemd service that actually runs the harness binary
|
# use the same service unit regardless of which entry-point they came from.
|
||||||
# differs per role and lives in the child module.
|
|
||||||
|
|
||||||
# Optional feature modules. Each declares its own `hyperhive.*`
|
# Optional feature modules. Each declares its own `hyperhive.*`
|
||||||
# option(s), default-off, so every agent has them available but
|
# 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 {
|
options.hyperhive.model = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "haiku";
|
default = "haiku";
|
||||||
|
|
@ -292,9 +269,7 @@ in
|
||||||
Computed: the merged static tree consumed by the harness via
|
Computed: the merged static tree consumed by the harness via
|
||||||
`HIVE_STATIC_DIR`. Composed at evaluation time by copying
|
`HIVE_STATIC_DIR`. Composed at evaluation time by copying
|
||||||
`hyperhive.frontend.dist`'s `agent/` subdir as the base, then
|
`hyperhive.frontend.dist`'s `agent/` subdir as the base, then
|
||||||
layering each `extraFiles` entry on top. Read-only —
|
layering each `extraFiles` entry on top. Read-only — do not set directly.
|
||||||
consumers (`agent-base.nix`, `manager.nix`) reference this in
|
|
||||||
their systemd service environment; 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,
|
# Harness systemd unit. Unit shape (PATH wrapper-dir trick, env vars,
|
||||||
# RuntimeDirectory, User=, standalone-eval fallbacks):
|
# RuntimeDirectory, User=, standalone-eval fallbacks):
|
||||||
# docs/agent-hierarchy.md::Harness systemd unit shape. PATH /bin
|
# docs/agent-hierarchy.md::Harness systemd unit shape. PATH /bin
|
||||||
|
|
@ -1376,11 +1339,10 @@ in
|
||||||
# appends /bin to every entry.
|
# appends /bin to every entry.
|
||||||
systemd.services.hive-ag3nt =
|
systemd.services.hive-ag3nt =
|
||||||
let
|
let
|
||||||
isManager = config.hyperhive.role == "manager";
|
|
||||||
binary = "hive";
|
binary = "hive";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
description = "${binary}${lib.optionalString isManager " manager"} harness";
|
description = "${binary} harness";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
after = [ "network.target" ];
|
after = [ "network.target" ];
|
||||||
# `/run/wrappers` before `/run/current-system/sw` so setuid
|
# `/run/wrappers` before `/run/current-system/sw` so setuid
|
||||||
|
|
@ -1395,16 +1357,11 @@ in
|
||||||
HOME = homeDir;
|
HOME = homeDir;
|
||||||
HIVE_STATIC_DIR = "${config.hyperhive.frontend.mergedDist}";
|
HIVE_STATIC_DIR = "${config.hyperhive.frontend.mergedDist}";
|
||||||
HIVE_ASSETS_DIR = "${pkgs.hyperhive-assets}/share/hyperhive";
|
HIVE_ASSETS_DIR = "${pkgs.hyperhive-assets}/share/hyperhive";
|
||||||
HIVE_ROLE = config.hyperhive.role;
|
# Unix-socket path for the harness web UI. All agents always bind
|
||||||
# Unix-socket path for the harness web UI. All agents (sub-agents
|
# here; TCP fallback is removed. Path matches
|
||||||
# and manager) always bind here; TCP fallback is removed. Path
|
# `hive_c0re::agent_sockets::socket_path_for(name)` so lifecycle
|
||||||
# matches `hive_c0re::agent_sockets::socket_path_for(name)` so
|
# bind-mounts and gateway upstream config stay in sync.
|
||||||
# lifecycle bind-mounts and gateway upstream config stay in sync.
|
|
||||||
HIVE_WEB_SOCKET = "/run/hive-agent/${userName}/web.sock";
|
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 = {
|
serviceConfig = {
|
||||||
ExecStart = "${pkgs.hyperhive}/bin/${binary} serve";
|
ExecStart = "${pkgs.hyperhive}/bin/${binary} serve";
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,15 @@
|
||||||
{
|
{
|
||||||
imports = [ ./harness-base.nix ];
|
imports = [ ./harness-base.nix ];
|
||||||
|
|
||||||
# Manager role: the `systemd.services.hive-ag3nt` unit plus
|
# Entry-point for the privileged root agent (ruth). Referenced from
|
||||||
# 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.ruth`) and the meta-flake's
|
# `flake.nix` (`nixosConfigurations.ruth`) and the meta-flake's
|
||||||
# `applied/ruth/flake.nix`. HIVE_PORT / HIVE_LABEL are injected by
|
# `applied/ruth/flake.nix`. Forge subscription/participation firehose
|
||||||
# the meta-flake at deploy time and have manager-only standalone-eval
|
# stays off so ruth's inbox isn't drowned in noise.
|
||||||
# fallbacks in `harness-base.nix`.
|
hyperhive.forge = {
|
||||||
hyperhive.role = "manager";
|
keepSubscriptions = false;
|
||||||
|
skipNotifyReasons = [
|
||||||
|
"subscribed"
|
||||||
|
"participating"
|
||||||
|
];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue