hive-ag3nt + docs: extract harness binary shape prose (#716 batch 5)
`bin/hive.rs` carries ~15 attribution cookies (#598, #676, #692, #693, #778, #788, #682, #688) plus structural prose about the single-binary architecture (role-from-env), the `Surface` trait pattern (zero-sized type tags + generic dispatch), boot wiring (label fallback, plugin install → send_to_parent failure routing), and turn-outcome branching (ack / requeue / wait_for_login / parent notify). Migrated to a new `docs/turn-loop.md::Harness binary shape` subsection covering: - Single-binary rationale (server-side privilege boundary on the broker socket means no escalation risk in shipping both wire surfaces in the same process). - Three subcommands (`serve` / `mcp` / `wake`). - Surface trait + zero-sized type tags (AgentSurface, ManagerSurface) — picks per-role FLAVOR / FORGE_IS_MANAGER and exposes the broker wire ops behind trait methods so the turn loop is written once. - Boot wiring (HIVE_PORT, HIVE_LABEL fallback, plugin failures routed via send_to_parent, web_ui + forge_notify spawn). - Turn outcomes table (Ok/Compacted/RateLimited/AuthFailed/Failed) + continue-sentinel pickup. In-code rustdocs reduced to 1-line summaries + doc pointers; inline cookies stripped from boot wiring + plugin install + sentinel cleanup paths. 15 → 0 cookies in bin/hive.rs; cargo check -p hive-ag3nt passes.
This commit is contained in:
parent
2ce8bb5b77
commit
94c110fd5f
2 changed files with 114 additions and 53 deletions
|
|
@ -1,26 +1,11 @@
|
|||
//! Unified hyperhive harness binary (#598).
|
||||
//! Unified hyperhive harness binary. Picks role from `HIVE_ROLE`
|
||||
//! (`"agent"` | `"manager"`), dispatches one of three subcommands
|
||||
//! (`serve` / `mcp` / `wake`), and runs the turn loop through a
|
||||
//! generic `Surface` trait so both wire surfaces stay in lockstep.
|
||||
//!
|
||||
//! Replaces the pre-#598 `hive-ag3nt` + `hive-m1nd` split. One binary,
|
||||
//! picks its role at startup from `HIVE_ROLE` (set by post-#676
|
||||
//! `harness-base.nix` from `hyperhive.role` — `"agent"` or
|
||||
//! `"manager"`). Both agent + manager wire surfaces live in the same
|
||||
//! binary because the privilege boundary is enforced server-side at
|
||||
//! the socket (`/run/hive/mcp.sock`): an agent socket refuses
|
||||
//! `ManagerRequest` calls regardless of who sends them, so there's no
|
||||
//! escalation risk in shipping the same code to both.
|
||||
//!
|
||||
//! `HIVE_ROLE` defaults to `"agent"` if unset to keep the standalone
|
||||
//! `nix run .#hive-ag3nt` shape working without env plumbing.
|
||||
//!
|
||||
//! Post-#692 the entire turn loop (`serve_main` / `serve_loop` /
|
||||
//! `handle_turn` / `wake`) is one generic implementation parameterised
|
||||
//! by a `Surface` trait. Two zero-sized impls (`AgentSurface` /
|
||||
//! `ManagerSurface`) wrap the per-role wire enums + boot-time defaults
|
||||
//! (label fallback, MCP flavor, plugins arg, forge-notify mode); the
|
||||
//! turn logic itself is written exactly once. Lets `main`'s dispatch
|
||||
//! pick between `serve_main::<AgentSurface>` and
|
||||
//! `serve_main::<ManagerSurface>` and keep both code paths in lockstep
|
||||
//! by construction.
|
||||
//! Architecture (single-binary rationale, Surface-trait + zero-sized
|
||||
//! type tags, boot wiring, turn-outcome branch) lives in
|
||||
//! [`docs/turn-loop.md::Harness binary shape`](../../../docs/turn-loop.md).
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
|
@ -69,9 +54,8 @@ enum Cmd {
|
|||
/// turn fires with the given body. Intended for extra MCP servers
|
||||
/// / helpers (matrix bridge, scraper, webhook listener, etc.) that
|
||||
/// need to nudge claude on external events. Available on both
|
||||
/// agent and manager roles — closes #693, mirrors the
|
||||
/// `AgentRequest::Wake` / `ManagerRequest::Wake` pair already on
|
||||
/// the wire.
|
||||
/// agent and manager roles; mirrors the `AgentRequest::Wake` /
|
||||
/// `ManagerRequest::Wake` pair already on the wire.
|
||||
Wake {
|
||||
#[arg(long)]
|
||||
from: String,
|
||||
|
|
@ -107,10 +91,9 @@ async fn main() -> Result<()> {
|
|||
let cli = Cli::parse();
|
||||
let role = resolve_role()?;
|
||||
|
||||
// Generic dispatch (#692): one `serve_main` / `wake` body, two
|
||||
// monomorphisations driven by the `Surface` type parameter. The
|
||||
// wire-type-disjoint enums live behind the trait methods; the
|
||||
// turn loop itself is identical regardless of role.
|
||||
// Generic dispatch: one `serve_main` / `wake` body, two
|
||||
// monomorphisations driven by the `Surface` type parameter. See
|
||||
// `docs/turn-loop.md::Surface trait + zero-sized type tags`.
|
||||
match (role, cli.cmd) {
|
||||
(Role::Agent, Cmd::Serve { poll_ms }) => {
|
||||
serve_main::<AgentSurface>(&cli.socket, poll_ms).await
|
||||
|
|
@ -132,10 +115,10 @@ async fn main() -> Result<()> {
|
|||
// ---------- shared turn helpers ----------
|
||||
|
||||
/// Surface a `SYSTEM_SENDER` message in the live event bus + tracing
|
||||
/// log. Was manager-only pre-#692; agents receive `QuestionAnswered`,
|
||||
/// `ContainerCrash`, reparent notifications, and friends the same way
|
||||
/// the manager does, so the parse + log path is identical too. Quiet
|
||||
/// no-op when `from` isn't `SYSTEM_SENDER`.
|
||||
/// log. Both agents and the manager receive `QuestionAnswered`,
|
||||
/// `ContainerCrash`, reparent notifications, and friends; the parse
|
||||
/// + log path is identical. Quiet no-op when `from` isn't
|
||||
/// `SYSTEM_SENDER`.
|
||||
fn log_system_event(bus: &Bus, from: &str, body: &str) {
|
||||
if from != SYSTEM_SENDER {
|
||||
return;
|
||||
|
|
@ -178,7 +161,7 @@ fn consume_continue_sentinel() -> bool {
|
|||
true
|
||||
}
|
||||
|
||||
// ---------- surface trait (#692) ----------
|
||||
// ---------- surface trait ----------
|
||||
|
||||
/// What a `Recv` long-poll returned. Decoupled from the per-role
|
||||
/// Response enum so `serve_loop` can pattern-match without seeing
|
||||
|
|
@ -208,11 +191,9 @@ trait Surface {
|
|||
/// `is_manager` flag passed to `forge_notify::run`. Picks which
|
||||
/// wire enum (`AgentRequest::Wake` vs `ManagerRequest::Wake`) the
|
||||
/// poller uses to push notifications into the harness inbox — the
|
||||
/// per-role broker socket rejects the wrong type. Not actually
|
||||
/// about "mentions only" (the skip-reasons drop-list is a separate
|
||||
/// nix-side option). Real fix is to lift `Surface` into the lib
|
||||
/// crate and make `forge_notify::run` generic; deferred to its
|
||||
/// own issue.
|
||||
/// per-role broker socket rejects the wrong type. Lifting
|
||||
/// `Surface` into the lib crate to make `forge_notify::run`
|
||||
/// generic is deferred to its own issue.
|
||||
const FORGE_IS_MANAGER: bool;
|
||||
|
||||
/// Ack the in-flight turn. Logs warnings on transport/broker
|
||||
|
|
@ -259,7 +240,7 @@ trait Surface {
|
|||
) -> impl Future<Output = Result<()>>;
|
||||
}
|
||||
|
||||
// ---------- AgentSurface (#692) ----------
|
||||
// ---------- AgentSurface ----------
|
||||
|
||||
/// Zero-sized type tag for the sub-agent wire surface.
|
||||
/// Talks `AgentRequest` / `AgentResponse`.
|
||||
|
|
@ -397,7 +378,7 @@ impl Surface for AgentSurface {
|
|||
}
|
||||
}
|
||||
|
||||
// ---------- ManagerSurface (#692) ----------
|
||||
// ---------- ManagerSurface ----------
|
||||
|
||||
/// Zero-sized type tag for the manager wire surface.
|
||||
/// Talks `ManagerRequest` / `ManagerResponse`.
|
||||
|
|
@ -540,11 +521,12 @@ impl Surface for ManagerSurface {
|
|||
}
|
||||
}
|
||||
|
||||
// ---------- generic turn loop (#692) ----------
|
||||
// ---------- generic turn loop ----------
|
||||
|
||||
/// Per-role boot — wires up the web UI, login state, stats, plugins,
|
||||
/// forge notifier, and either drops into `serve_loop` directly
|
||||
/// (`Online`) or parks on the login flow first (`NeedsLogin`).
|
||||
/// (`Online`) or parks on the login flow first (`NeedsLogin`). See
|
||||
/// `docs/turn-loop.md::Boot wiring`.
|
||||
async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
|
||||
let port = std::env::var("HIVE_PORT")
|
||||
.ok()
|
||||
|
|
@ -553,10 +535,8 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
|
|||
// `HIVE_LABEL` is set unconditionally by the meta-flake envelope
|
||||
// for any container-deployed agent; the `"hive"` fallback here
|
||||
// covers standalone `nix run .#hive` invocations and pre-meta
|
||||
// dev shells. Previously this was per-role (`"hive-ag3nt"` /
|
||||
// `"hm1nd"`) — closing #788 collapsed it: there's no semantic
|
||||
// reason for the fallback to differ when the env var is missing,
|
||||
// and both branches are unreachable in production.
|
||||
// dev shells. Role-independent: no semantic reason for the
|
||||
// fallback to differ when the env var is missing.
|
||||
let label = std::env::var("HIVE_LABEL").unwrap_or_else(|_| "hive".into());
|
||||
let claude_dir = login::default_dir();
|
||||
let initial = LoginState::from_dir(&claude_dir);
|
||||
|
|
@ -572,11 +552,11 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
|
|||
}
|
||||
let files = turn::TurnFiles::prepare(socket, &label, S::FLAVOR).await?;
|
||||
let turn_lock: TurnLock = Arc::new(tokio::sync::Mutex::new(()));
|
||||
// Plugin install runs role-agnostic now (#778 v3, mara): failures
|
||||
// come back as a Vec<String> and we route each through `<parent>`
|
||||
// via the same `send_to_parent` failure-notify path the turn loop
|
||||
// uses. Manager failures now reach operator via root → operator
|
||||
// fallback (improvement on the pre-#778 silent-drop on manager).
|
||||
// Plugin install runs role-agnostic: failures come back as a
|
||||
// Vec<String> and we route each through `<parent>` via the same
|
||||
// `send_to_parent` failure-notify path the turn loop uses. The
|
||||
// broker resolves `<parent>` per `topology::parent_of`; root
|
||||
// agents and the manager fall through to operator.
|
||||
for failure in plugins::install_configured(socket).await {
|
||||
S::send_to_parent(socket, failure).await;
|
||||
}
|
||||
|
|
@ -597,7 +577,8 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
|
|||
turn::wait_for_login(&claude_dir, login_state.clone(), &bus, poll_ms).await;
|
||||
} else {
|
||||
// Clear any stale `hyperhive-needs-login` sentinel left over
|
||||
// from a prior boot (closes #682, see #688).
|
||||
// from a prior boot — `online` status writes the sentinel
|
||||
// cleanup in `Bus::emit_status`.
|
||||
bus.emit_status("online");
|
||||
}
|
||||
serve_loop::<S>(
|
||||
|
|
|
|||
Loading…
Reference in a new issue