refactor(hive-ag3nt): split hive bin into hive-agent / hive-agent-mcp / hive-agent-wake
This commit is contained in:
parent
2486251b32
commit
5b062dca55
14 changed files with 182 additions and 144 deletions
|
|
@ -32,8 +32,9 @@ hand-maintained per-file tree drifts out of sync with the code.
|
||||||
the meta flake, lifecycle (`nixos-container` shellouts), gateway /
|
the meta flake, lifecycle (`nixos-container` shellouts), gateway /
|
||||||
forge / matrix provisioning, per-container stats, and the axum
|
forge / matrix provisioning, per-container stats, and the axum
|
||||||
operator dashboard (`dashboard.rs`). Largest crate.
|
operator dashboard (`dashboard.rs`). Largest crate.
|
||||||
- **`hive-ag3nt/`** — in-container harness; one `hive` binary for every
|
- **`hive-ag3nt/`** — in-container harness; three sibling binaries for
|
||||||
agent. Turn-loop *policy* layer (`turn.rs`) over the `hive-claude`
|
every agent (`hive-agent` serve loop, `hive-agent-mcp`,
|
||||||
|
`hive-agent-wake`). Turn-loop *policy* layer (`turn.rs`) over the `hive-claude`
|
||||||
driver, embedded MCP server (`mcp.rs`) + its claude launch-config layer
|
driver, embedded MCP server (`mcp.rs`) + its claude launch-config layer
|
||||||
(`mcp_config.rs`: tool-group/capability → `--allowedTools`, `--mcp-config`
|
(`mcp_config.rs`: tool-group/capability → `--allowedTools`, `--mcp-config`
|
||||||
render), per-agent web UI (`web_ui/` module dir), event + turn-stats
|
render), per-agent web UI (`web_ui/` module dir), event + turn-stats
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,8 @@ nspawn agent. Open questions, not yet wired:
|
||||||
|
|
||||||
## Harness systemd unit shape
|
## Harness systemd unit shape
|
||||||
|
|
||||||
One harness binary (`hive`), one `harness-base.nix` template, one
|
One harness serve binary (`hive-agent`, with its `hive-agent-mcp` /
|
||||||
|
`hive-agent-wake` siblings), one `harness-base.nix` template, one
|
||||||
service unit (`systemd.services.hive-ag3nt`) for all agents. There
|
service unit (`systemd.services.hive-ag3nt`) for all agents. There
|
||||||
is no longer a separate manager service name or role distinction in
|
is no longer a separate manager service name or role distinction in
|
||||||
the harness — privilege differences live server-side in the broker
|
the harness — privilege differences live server-side in the broker
|
||||||
|
|
@ -259,7 +260,8 @@ bit set` regardless of `hyperhive.user.passwordlessSudo`.
|
||||||
|
|
||||||
### `serviceConfig` highlights
|
### `serviceConfig` highlights
|
||||||
|
|
||||||
- `ExecStart = pkgs.hyperhive/bin/hive serve` — single binary.
|
- `ExecStart = pkgs.hyperhive/bin/hive-agent` — same binary for
|
||||||
|
every agent.
|
||||||
- `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,7 +5,8 @@ claude has access to in return.
|
||||||
|
|
||||||
## The loop
|
## The loop
|
||||||
|
|
||||||
Each agent harness (`hive serve` — one binary for all agents) runs:
|
Each agent harness (`hive-agent` — one serve-loop binary for all
|
||||||
|
agents) 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
|
||||||
|
|
@ -51,38 +52,37 @@ Each agent harness (`hive serve` — one binary for all agents) runs:
|
||||||
|
|
||||||
## Harness binary shape
|
## Harness binary shape
|
||||||
|
|
||||||
One `hive` binary for all agents. The earlier split into
|
Three sibling binaries out of the one `hive-ag3nt` crate, all
|
||||||
`hive-ag3nt` + `hive-m1nd` was collapsed because the privilege
|
role-agnostic. (The earlier split into `hive-ag3nt` + `hive-m1nd`
|
||||||
boundary lives server-side at the broker socket
|
was collapsed because the privilege boundary lives server-side at
|
||||||
(`/run/hive/mcp.sock`): `ManagerRequest` calls are refused by the
|
the broker socket (`/run/hive/mcp.sock`): `ManagerRequest` calls are
|
||||||
standard agent socket regardless of who sends them.
|
refused by the standard agent socket regardless of who sends them.)
|
||||||
|
|
||||||
Three subcommands:
|
- `hive-agent` — long-running harness loop (the inbox poll +
|
||||||
|
|
||||||
- `serve` — long-running harness loop (the inbox poll +
|
|
||||||
claude-pump + ack/requeue cycle described above).
|
claude-pump + ack/requeue cycle described above).
|
||||||
- `mcp` — MCP server. Default: stdio child claude spawns via
|
- `hive-agent-mcp` — MCP server. Default: stdio child claude spawns
|
||||||
`--mcp-config` per turn. With `--http <addr>`, runs as a persistent
|
via `--mcp-config` per turn (the serve loop renders the config to
|
||||||
streamable-HTTP daemon instead (used by the `hive-mcp-http`
|
point at this sibling of its own `/proc/self/exe`). With
|
||||||
systemd unit when `hyperhive.mcp.httpPort` is set).
|
`--http <addr>`, runs as a persistent streamable-HTTP daemon
|
||||||
- `wake --from <name> --body <body>` — push a message into our own
|
instead (used by the `hive-mcp-http` systemd unit when
|
||||||
inbox so the next turn fires with the given body. Used by
|
`hyperhive.mcp.httpPort` is set).
|
||||||
co-process daemons (matrix bridge, scraper, webhook listeners)
|
- `hive-agent-wake --from <name> --body <body>` — push a message into
|
||||||
to nudge claude on external events. `--body -` reads from stdin.
|
our own inbox so the next turn fires with the given body. Used by
|
||||||
|
co-process helpers (scrapers, webhook listeners) to nudge claude on
|
||||||
|
external events. `--body -` reads from stdin.
|
||||||
|
|
||||||
### `Surface` trait + zero-sized type tags
|
### `Surface` trait + zero-sized type tags
|
||||||
|
|
||||||
`AgentRequest` / `AgentResponse` (= `ManagerRequest` / `ManagerResponse` —
|
`AgentRequest` / `AgentResponse` (= `ManagerRequest` / `ManagerResponse` —
|
||||||
type aliases) are the wire types. There is one role: agent.
|
type aliases) are the wire types. There is one role: agent.
|
||||||
`bin/hive.rs` factors the turn loop through a `Surface` trait with one
|
`bin/hive-agent.rs` factors the turn loop through a `Surface` trait
|
||||||
zero-sized impl (`AgentSurface`) wrapping:
|
with one zero-sized impl (`AgentSurface`) wrapping:
|
||||||
|
|
||||||
- One async method per wire op: `ack_turn`, `requeue_inflight`,
|
- One async method per wire op: `ack_turn`, `requeue_inflight`,
|
||||||
`inbox_unread`, `post_turn_counts`, `send_to_parent`,
|
`inbox_unread`, `post_turn_counts`, `send_to_parent`, `recv_next`.
|
||||||
`recv_next`, `wake_external`.
|
|
||||||
|
|
||||||
`main()` calls `serve_main::<AgentSurface>` for all roles. The turn
|
`main()` calls `serve_main::<AgentSurface>` for all roles. The turn
|
||||||
loop (`serve_loop` / `handle_turn` / `wake`) has no per-role branches.
|
loop (`serve_loop` / `handle_turn`) has no per-role branches.
|
||||||
|
|
||||||
### Boot wiring
|
### Boot wiring
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ External MCP servers (and any other in-container process) can
|
||||||
inject a wake-up event into the agent's inbox via the per-agent
|
inject a wake-up event into the agent's inbox via the per-agent
|
||||||
socket at `/run/hive/mcp.sock`. Two equivalent paths:
|
socket at `/run/hive/mcp.sock`. Two equivalent paths:
|
||||||
|
|
||||||
- **Shell out to `hive wake --from <label> --body <text>`**
|
- **Shell out to `hive-agent-wake --from <label> --body <text>`**
|
||||||
(use `--body -` to read body from stdin). Already on the
|
(use `--body -` to read body from stdin). Already on the
|
||||||
container's `PATH` since the harness binary is in
|
container's `PATH` since the harness binary is in
|
||||||
`systemPackages`. Convenient for shell-script integrations and
|
`systemPackages`. Convenient for shell-script integrations and
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,8 @@ tracing-subscriber.workspace = true
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
[[bin]]
|
# Three sibling harness binaries for all agents (auto-discovered from
|
||||||
# Unified harness binary for all agents. Privilege boundary is
|
# `src/bin/`): `hive-agent` (serve loop), `hive-agent-mcp` (MCP
|
||||||
|
# server), `hive-agent-wake` (wake CLI). Privilege boundary is
|
||||||
# enforced server-side at the socket (tool groups / manager surface).
|
# enforced server-side at the socket (tool groups / manager surface).
|
||||||
# See `docs/turn-loop.md::Harness binary shape`.
|
# See `docs/turn-loop.md::Harness binary shape`.
|
||||||
name = "hive"
|
|
||||||
path = "src/bin/hive.rs"
|
|
||||||
|
|
|
||||||
43
hive-ag3nt/src/bin/hive-agent-mcp.rs
Normal file
43
hive-ag3nt/src/bin/hive-agent-mcp.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
//! MCP-server binary. Default is stdio — spawned by `claude` via
|
||||||
|
//! `--mcp-config`; tools dispatch through `/run/hive/mcp.sock` back into
|
||||||
|
//! the hyperhive broker. Pass `--http <addr>` to instead run a long-lived
|
||||||
|
//! streamable-http listener (persistent daemon, the `hive-mcp-http`
|
||||||
|
//! systemd unit) that claude reconnects to each turn, avoiding the
|
||||||
|
//! per-turn stdio re-registration race. Sibling of `hive-agent` (the
|
||||||
|
//! serve loop that renders the `--mcp-config` blob pointing here) and
|
||||||
|
//! `hive-agent-wake`.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use hive_ag3nt::{DEFAULT_SOCKET, mcp};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "hive-agent-mcp", about = "hyperhive MCP server")]
|
||||||
|
struct Cli {
|
||||||
|
/// Path to the per-agent MCP socket (bind-mounted from the host).
|
||||||
|
#[arg(long, default_value = DEFAULT_SOCKET)]
|
||||||
|
socket: PathBuf,
|
||||||
|
|
||||||
|
/// Serve over streamable-http on this address (e.g.
|
||||||
|
/// `127.0.0.1:8790`) instead of stdio. Bind loopback only.
|
||||||
|
#[arg(long)]
|
||||||
|
http: Option<std::net::SocketAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let cli = Cli::parse();
|
||||||
|
match cli.http {
|
||||||
|
Some(addr) => mcp::serve_http(cli.socket, addr).await,
|
||||||
|
None => mcp::serve_agent_stdio(cli.socket).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
60
hive-ag3nt/src/bin/hive-agent-wake.rs
Normal file
60
hive-ag3nt/src/bin/hive-agent-wake.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
//! Wake CLI: inject a wake-up event into this container's harness inbox
|
||||||
|
//! so the next turn fires with the given body. Intended for extra MCP
|
||||||
|
//! servers / helpers (scraper, webhook listener, etc.) that need to
|
||||||
|
//! nudge claude on external events; the built-in daemons (matrix, bash)
|
||||||
|
//! talk to the socket directly instead. Sibling of `hive-agent` and
|
||||||
|
//! `hive-agent-mcp`.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use hive_ag3nt::{DEFAULT_SOCKET, client};
|
||||||
|
use hive_sh4re::{AgentRequest, AgentResponse};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "hive-agent-wake", about = "hyperhive harness wake signal")]
|
||||||
|
struct Cli {
|
||||||
|
/// Path to the per-agent MCP socket (bind-mounted from the host).
|
||||||
|
#[arg(long, default_value = DEFAULT_SOCKET)]
|
||||||
|
socket: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
from: String,
|
||||||
|
|
||||||
|
/// Body of the wake message. Pass `-` to read from stdin.
|
||||||
|
#[arg(long)]
|
||||||
|
body: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let body = if cli.body == "-" {
|
||||||
|
let mut buf = String::new();
|
||||||
|
std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf)?;
|
||||||
|
buf
|
||||||
|
} else {
|
||||||
|
cli.body
|
||||||
|
};
|
||||||
|
let resp: AgentResponse = client::request(
|
||||||
|
&cli.socket,
|
||||||
|
&AgentRequest::Wake {
|
||||||
|
from: cli.from,
|
||||||
|
body,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
match resp {
|
||||||
|
AgentResponse::Ok => Ok(()),
|
||||||
|
AgentResponse::Err { message } => anyhow::bail!("wake: {message}"),
|
||||||
|
other => anyhow::bail!("wake: unexpected response {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
//! Unified hyperhive harness binary. Dispatches one of three subcommands
|
//! Harness serve-loop binary. Long-polls the broker inbox and drives one
|
||||||
//! (`serve` / `mcp` / `wake`). There is one role: agent. The `Surface`
|
//! claude turn per message. There is one role: agent. The `Surface`
|
||||||
//! trait + `AgentSurface` zero-sized type tag keeps the turn loop
|
//! trait + `AgentSurface` zero-sized type tag keeps the turn loop
|
||||||
//! generic and testable. Architecture lives in
|
//! generic and testable. Siblings: `hive-agent-mcp` (the MCP server this
|
||||||
|
//! loop points claude at) and `hive-agent-wake` (external wake CLI).
|
||||||
|
//! Architecture lives in
|
||||||
//! [`docs/turn-loop.md::Harness binary shape`](../../../docs/turn-loop.md).
|
//! [`docs/turn-loop.md::Harness binary shape`](../../../docs/turn-loop.md).
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
@ -9,58 +11,23 @@ use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::Parser;
|
||||||
use hive_ag3nt::events::{Bus, LiveEvent, TurnState};
|
use hive_ag3nt::events::{Bus, LiveEvent, TurnState};
|
||||||
use hive_ag3nt::login::{self, LoginState};
|
use hive_ag3nt::login::{self, LoginState};
|
||||||
use hive_ag3nt::turn_stats::TurnStats;
|
use hive_ag3nt::turn_stats::TurnStats;
|
||||||
use hive_ag3nt::{
|
use hive_ag3nt::{DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, plugins, serve_common, turn, web_ui};
|
||||||
DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, mcp, plugins, serve_common, turn, web_ui,
|
|
||||||
};
|
|
||||||
use hive_sh4re::{AgentRequest, AgentResponse, HelperEvent, SYSTEM_SENDER};
|
use hive_sh4re::{AgentRequest, AgentResponse, HelperEvent, SYSTEM_SENDER};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "hive", about = "hyperhive harness")]
|
#[command(name = "hive-agent", about = "hyperhive harness serve loop")]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
/// Path to the per-agent MCP socket (bind-mounted from the host).
|
/// Path to the per-agent MCP socket (bind-mounted from the host).
|
||||||
#[arg(long, global = true, default_value = DEFAULT_SOCKET)]
|
#[arg(long, default_value = DEFAULT_SOCKET)]
|
||||||
socket: PathBuf,
|
socket: PathBuf,
|
||||||
|
|
||||||
#[command(subcommand)]
|
/// Inbox poll interval in milliseconds.
|
||||||
cmd: Cmd,
|
#[arg(long, default_value_t = 1000)]
|
||||||
}
|
poll_ms: u64,
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
enum Cmd {
|
|
||||||
/// Run the long-lived harness loop. Polls inbox; replies via
|
|
||||||
/// `claude --print` when available.
|
|
||||||
Serve {
|
|
||||||
/// Inbox poll interval in milliseconds.
|
|
||||||
#[arg(long, default_value_t = 1000)]
|
|
||||||
poll_ms: u64,
|
|
||||||
},
|
|
||||||
/// Run the MCP server. Default is stdio — spawned by `claude` via
|
|
||||||
/// `--mcp-config`; tools dispatch through `/run/hive/mcp.sock` back
|
|
||||||
/// into the hyperhive broker. Pass `--http <addr>` to instead run a
|
|
||||||
/// long-lived streamable-http listener (persistent daemon) that
|
|
||||||
/// claude reconnects to each turn, avoiding the per-turn stdio
|
|
||||||
/// re-registration race.
|
|
||||||
Mcp {
|
|
||||||
/// Serve over streamable-http on this address (e.g.
|
|
||||||
/// `127.0.0.1:8790`) instead of stdio. Bind loopback only.
|
|
||||||
#[arg(long)]
|
|
||||||
http: Option<std::net::SocketAddr>,
|
|
||||||
},
|
|
||||||
/// Inject a wake-up event into this harness's inbox so the next
|
|
||||||
/// 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.
|
|
||||||
Wake {
|
|
||||||
#[arg(long)]
|
|
||||||
from: String,
|
|
||||||
/// Body of the wake message. Pass `-` to read from stdin.
|
|
||||||
#[arg(long)]
|
|
||||||
body: String,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
@ -73,15 +40,7 @@ async fn main() -> Result<()> {
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
serve_main::<AgentSurface>(&cli.socket, cli.poll_ms).await
|
||||||
match cli.cmd {
|
|
||||||
Cmd::Serve { poll_ms } => serve_main::<AgentSurface>(&cli.socket, poll_ms).await,
|
|
||||||
Cmd::Mcp { http } => match http {
|
|
||||||
Some(addr) => mcp::serve_http(cli.socket, addr).await,
|
|
||||||
None => mcp::serve_agent_stdio(cli.socket).await,
|
|
||||||
},
|
|
||||||
Cmd::Wake { from, body } => wake::<AgentSurface>(&cli.socket, from, body).await,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- shared turn helpers ----------
|
// ---------- shared turn helpers ----------
|
||||||
|
|
@ -261,13 +220,6 @@ trait Surface {
|
||||||
/// generic `serve_loop` doesn't need the per-role Response enum
|
/// generic `serve_loop` doesn't need the per-role Response enum
|
||||||
/// at all.
|
/// at all.
|
||||||
fn recv_next(socket: &Path) -> impl Future<Output = RecvOutcome>;
|
fn recv_next(socket: &Path) -> impl Future<Output = RecvOutcome>;
|
||||||
|
|
||||||
/// External `wake` subcommand (the `hive wake` CLI command, used
|
|
||||||
/// by co-process daemons like matrix to push events into the
|
|
||||||
/// harness inbox). Errors out via `anyhow::bail!` so the calling
|
|
||||||
/// binary surfaces them on stderr.
|
|
||||||
fn wake_external(socket: &Path, from: String, body: String)
|
|
||||||
-> impl Future<Output = Result<()>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- AgentSurface ----------
|
// ---------- AgentSurface ----------
|
||||||
|
|
@ -383,16 +335,6 @@ impl Surface for AgentSurface {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wake_external(socket: &Path, from: String, body: String) -> Result<()> {
|
|
||||||
let resp: AgentResponse =
|
|
||||||
client::request(socket, &AgentRequest::Wake { from, body }).await?;
|
|
||||||
match resp {
|
|
||||||
AgentResponse::Ok => Ok(()),
|
|
||||||
AgentResponse::Err { message } => anyhow::bail!("wake: {message}"),
|
|
||||||
other => anyhow::bail!("wake: unexpected response {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- generic turn loop ----------
|
// ---------- generic turn loop ----------
|
||||||
|
|
@ -673,20 +615,6 @@ async fn handle_turn<S: Surface>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// External `hive wake` subcommand — push a message into our own
|
|
||||||
/// inbox so the next turn fires with the given body. Reads the body
|
|
||||||
/// from stdin when `body == "-"`.
|
|
||||||
async fn wake<S: Surface>(socket: &Path, from: String, body: String) -> Result<()> {
|
|
||||||
let body = if body == "-" {
|
|
||||||
let mut buf = String::new();
|
|
||||||
std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf)?;
|
|
||||||
buf
|
|
||||||
} else {
|
|
||||||
body
|
|
||||||
};
|
|
||||||
S::wake_external(socket, from, body).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod continue_tests {
|
mod continue_tests {
|
||||||
use super::{TurnControl, should_self_continue, synthetic_continue};
|
use super::{TurnControl, should_self_continue, synthetic_continue};
|
||||||
|
|
@ -52,7 +52,7 @@ const TOKEN_RETRY_MAX: u32 = 20;
|
||||||
/// claim it's "new" — see docs/forge.md, "new vs activity on".
|
/// claim it's "new" — see docs/forge.md, "new vs activity on".
|
||||||
const NEW_ITEM_TOLERANCE_SECS: i64 = 120;
|
const NEW_ITEM_TOLERANCE_SECS: i64 = 120;
|
||||||
|
|
||||||
/// Spawn point: called once from `hive serve`. Returns immediately if the forge is not
|
/// Spawn point: called once from the `hive-agent` serve loop. Returns immediately if the forge is not
|
||||||
/// configured. Otherwise loops forever, polling every
|
/// configured. Otherwise loops forever, polling every
|
||||||
/// `POLL_INTERVAL_SECS` seconds. Errors are never fatal.
|
/// `POLL_INTERVAL_SECS` seconds. Errors are never fatal.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
//! Shared in-container harness code for the single `hive` binary that
|
//! Shared in-container harness code for the sibling `hive-agent` /
|
||||||
//! serves every agent role (the manager is just an agent role, not a
|
//! `hive-agent-mcp` / `hive-agent-wake` binaries, which serve every
|
||||||
//! separate binary).
|
//! agent role (the manager is just an agent role, not a separate set
|
||||||
|
//! of binaries).
|
||||||
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
|
|
|
||||||
|
|
@ -330,13 +330,13 @@ fn load_extra_mcp() -> std::collections::BTreeMap<String, ExtraMcpServer> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render the MCP config blob claude reads from `--mcp-config <path>`.
|
/// Render the MCP config blob claude reads from `--mcp-config <path>`.
|
||||||
/// `agent_binary` is the path (or PATH-resolvable name) of the `hive-ag3nt`
|
/// `mcp_binary` is the path (or PATH-resolvable name) of the
|
||||||
/// executable; `socket` is the hyperhive per-agent socket bind-mounted into
|
/// `hive-agent-mcp` bridge executable; `socket` is the hyperhive per-agent
|
||||||
/// the container (forwarded to the child as `--socket <path>`). Merges in
|
/// socket bind-mounted into the container (forwarded to the child as
|
||||||
/// any extra MCP servers declared via `hyperhive.extraMcpServers` in the
|
/// `--socket <path>`). Merges in any extra MCP servers declared via
|
||||||
/// agent's NixOS config.
|
/// `hyperhive.extraMcpServers` in the agent's NixOS config.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn render_claude_config(agent_binary: &str, socket: &std::path::Path) -> String {
|
pub fn render_claude_config(mcp_binary: &str, socket: &std::path::Path) -> String {
|
||||||
let mut servers = serde_json::Map::new();
|
let mut servers = serde_json::Map::new();
|
||||||
// When the harness is configured to run the built-in server as a
|
// When the harness is configured to run the built-in server as a
|
||||||
// persistent streamable-http daemon (loopback port in
|
// persistent streamable-http daemon (loopback port in
|
||||||
|
|
@ -353,8 +353,8 @@ pub fn render_claude_config(agent_binary: &str, socket: &std::path::Path) -> Str
|
||||||
"url": format!("http://127.0.0.1:{port}/mcp"),
|
"url": format!("http://127.0.0.1:{port}/mcp"),
|
||||||
}),
|
}),
|
||||||
None => serde_json::json!({
|
None => serde_json::json!({
|
||||||
"command": agent_binary,
|
"command": mcp_binary,
|
||||||
"args": ["--socket", socket.display().to_string(), "mcp"],
|
"args": ["--socket", socket.display().to_string()],
|
||||||
"env": {}
|
"env": {}
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
//! Pure helpers factored out of the `hive` serve loop (`bin/hive.rs`).
|
//! Pure helpers factored out of the harness serve loop
|
||||||
|
//! (`bin/hive-agent.rs`).
|
||||||
//! Only functions with no wire-type dependency live here;
|
//! Only functions with no wire-type dependency live here;
|
||||||
//! request/response-flavored helpers (`requeue_inflight`, `ack_turn`, etc.)
|
//! request/response-flavored helpers (`requeue_inflight`, `ack_turn`, etc.)
|
||||||
//! stay in the binary because they touch the request enum variants directly.
|
//! stay in the binary because they touch the request enum variants directly.
|
||||||
|
|
@ -60,7 +61,7 @@ pub struct TurnRowArgs<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assemble a `TurnStatRow` from the harness's per-turn state. Lives here
|
/// Assemble a `TurnStatRow` from the harness's per-turn state. Lives here
|
||||||
/// (rather than inline in the `hive` serve loop) so it stays wire-type-free
|
/// (rather than inline in the serve loop) so it stays wire-type-free
|
||||||
/// and unit-testable; the binary just feeds it the post-turn counts.
|
/// and unit-testable; the binary just feeds it the post-turn counts.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn build_row(args: TurnRowArgs<'_>) -> TurnStatRow {
|
pub fn build_row(args: TurnRowArgs<'_>) -> TurnStatRow {
|
||||||
|
|
|
||||||
|
|
@ -108,8 +108,9 @@ impl TurnFiles {
|
||||||
|
|
||||||
/// Drop the MCP config blob claude reads from `--mcp-config <path>`.
|
/// Drop the MCP config blob claude reads from `--mcp-config <path>`.
|
||||||
/// `socket` is the hyperhive per-container socket (forwarded to the child
|
/// `socket` is the hyperhive per-container socket (forwarded to the child
|
||||||
/// as `--socket <path>`). The MCP subcommand is always `mcp` on the single
|
/// as `--socket <path>`). The MCP server is the `hive-agent-mcp` binary
|
||||||
/// `hive` binary resolved from `/proc/self/exe`.
|
/// installed next to the running `hive-agent` (resolved as a sibling of
|
||||||
|
/// `/proc/self/exe`; PATH-resolvable name as the fallback).
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
|
|
@ -120,7 +121,8 @@ pub async fn write_mcp_config(socket: &Path) -> Result<PathBuf> {
|
||||||
let path = parent.join("claude-mcp-config.json");
|
let path = parent.join("claude-mcp-config.json");
|
||||||
let exe = std::env::current_exe()
|
let exe = std::env::current_exe()
|
||||||
.ok()
|
.ok()
|
||||||
.map_or_else(|| "hive".into(), |p| p.display().to_string());
|
.and_then(|p| Some(p.parent()?.join("hive-agent-mcp")))
|
||||||
|
.map_or_else(|| "hive-agent-mcp".into(), |p| p.display().to_string());
|
||||||
let body = mcp_config::render_claude_config(&exe, socket);
|
let body = mcp_config::render_claude_config(&exe, socket);
|
||||||
tokio::fs::write(&path, body).await?;
|
tokio::fs::write(&path, body).await?;
|
||||||
tracing::info!(path = %path.display(), "wrote claude MCP config");
|
tracing::info!(path = %path.display(), "wrote claude MCP config");
|
||||||
|
|
@ -146,7 +148,7 @@ pub async fn write_system_prompt(socket: &Path, label: &str) -> Result<PathBuf>
|
||||||
/// `result_kind = "compacted"` in turn stats so the stats page can distinguish
|
/// `result_kind = "compacted"` in turn stats so the stats page can distinguish
|
||||||
/// those turns. Both `Ok(true)` and `Ok(false)` are ack'd; the error cases
|
/// those turns. Both `Ok(true)` and `Ok(false)` are ack'd; the error cases
|
||||||
/// each map to a distinct serve-loop action (see [`emit_turn_end`] and the
|
/// each map to a distinct serve-loop action (see [`emit_turn_end`] and the
|
||||||
/// `hive` serve loop).
|
/// `hive-agent` serve loop).
|
||||||
pub type TurnOutcome = std::result::Result<bool, TurnError>;
|
pub type TurnOutcome = std::result::Result<bool, TurnError>;
|
||||||
|
|
||||||
/// The ways a turn can end without a usable result. Each is deliberately *not*
|
/// The ways a turn can end without a usable result. Each is deliberately *not*
|
||||||
|
|
@ -287,7 +289,7 @@ pub fn make_session(bus: &Bus) -> AgentSession {
|
||||||
/// the whole turn is retried a single time before bubbling `AuthFailed` to
|
/// the whole turn is retried a single time before bubbling `AuthFailed` to
|
||||||
/// the serve loop (which parks for re-login).
|
/// the serve loop (which parks for re-login).
|
||||||
///
|
///
|
||||||
/// Called once per turn by the `hive` serve loop, which owns the shared
|
/// Called once per turn by the `hive-agent` serve loop, which owns the shared
|
||||||
/// `session` ([`make_session`]) and threads it in.
|
/// `session` ([`make_session`]) and threads it in.
|
||||||
pub async fn drive_turn(
|
pub async fn drive_turn(
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
|
|
|
||||||
|
|
@ -898,12 +898,13 @@ in
|
||||||
streamable-http daemon on this loopback port instead of the
|
streamable-http daemon on this loopback port instead of the
|
||||||
default per-turn stdio child.
|
default per-turn stdio child.
|
||||||
|
|
||||||
When null (the default) claude spawns a fresh `hive … mcp` stdio
|
When null (the default) claude spawns a fresh `hive-agent-mcp`
|
||||||
subprocess every turn — the shape that carries the per-turn MCP
|
stdio subprocess every turn — the shape that carries the per-turn
|
||||||
re-registration race (a resumed turn can emit its first tool call
|
MCP re-registration race (a resumed turn can emit its first tool
|
||||||
before that turn's async `initialize`/`tools-list` completes,
|
call before that turn's async `initialize`/`tools-list` completes,
|
||||||
stranding the agent with `No such tool`). When set, a long-lived
|
stranding the agent with `No such tool`). When set, a long-lived
|
||||||
`hive-mcp-http` systemd unit runs `hive mcp --http 127.0.0.1:<port>`
|
`hive-mcp-http` systemd unit runs
|
||||||
|
`hive-agent-mcp --http 127.0.0.1:<port>`
|
||||||
and `render_claude_config` points claude at the stable
|
and `render_claude_config` points claude at the stable
|
||||||
`http://127.0.0.1:<port>/mcp` URL, which survives the per-turn
|
`http://127.0.0.1:<port>/mcp` URL, which survives the per-turn
|
||||||
claude re-spawn (and a host-side hive-c0re restart — each tool call
|
claude re-spawn (and a host-side hive-c0re restart — each tool call
|
||||||
|
|
@ -1846,7 +1847,7 @@ in
|
||||||
# `render_claude_config`). Long-lived so claude reconnects to the
|
# `render_claude_config`). Long-lived so claude reconnects to the
|
||||||
# stable URL each turn instead of respawning + re-registering a stdio
|
# stable URL each turn instead of respawning + re-registering a stdio
|
||||||
# subprocess (the per-turn MCP registration race). It dials the
|
# subprocess (the per-turn MCP registration race). It dials the
|
||||||
# control socket (`/run/hive/mcp.sock`, the `hive` binary default)
|
# control socket (`/run/hive/mcp.sock`, the harness binaries' default)
|
||||||
# fresh on every tool call, so a host-side hive-c0re restart is
|
# fresh on every tool call, so a host-side hive-c0re restart is
|
||||||
# transparent. `before = hive-ag3nt` so the URL is already listening
|
# transparent. `before = hive-ag3nt` so the URL is already listening
|
||||||
# by the time the harness renders the first turn's config; the
|
# by the time the harness renders the first turn's config; the
|
||||||
|
|
@ -1858,7 +1859,7 @@ in
|
||||||
before = [ "hive-ag3nt.service" ];
|
before = [ "hive-ag3nt.service" ];
|
||||||
environment.RUST_LOG = "info";
|
environment.RUST_LOG = "info";
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = "${pkgs.hyperhive}/bin/hive mcp --http 127.0.0.1:${toString config.hyperhive.mcp.httpPort}";
|
ExecStart = "${pkgs.hyperhive}/bin/hive-agent-mcp --http 127.0.0.1:${toString config.hyperhive.mcp.httpPort}";
|
||||||
SyslogIdentifier = "hive-mcp-http";
|
SyslogIdentifier = "hive-mcp-http";
|
||||||
# `always` (not `on-failure`): this endpoint is load-bearing when
|
# `always` (not `on-failure`): this endpoint is load-bearing when
|
||||||
# `httpPort` is set — a down window is total hyperhive-MCP loss with
|
# `httpPort` is set — a down window is total hyperhive-MCP loss with
|
||||||
|
|
@ -1936,7 +1937,7 @@ in
|
||||||
# appends /bin to every entry.
|
# appends /bin to every entry.
|
||||||
systemd.services.hive-ag3nt =
|
systemd.services.hive-ag3nt =
|
||||||
let
|
let
|
||||||
binary = "hive";
|
binary = "hive-agent";
|
||||||
# OTEL is shipped declaratively via the managed claude settings
|
# OTEL is shipped declaratively via the managed claude settings
|
||||||
# json (`environment.etc."claude-code/managed-settings.json"`,
|
# json (`environment.etc."claude-code/managed-settings.json"`,
|
||||||
# `otelSettingsEnv` in the top-level let) — claude reads it for
|
# `otelSettingsEnv` in the top-level let) — claude reads it for
|
||||||
|
|
@ -1982,7 +1983,7 @@ in
|
||||||
HIVE_GUI_VNC_PORT = toString config.hyperhive.gui.vncPort;
|
HIVE_GUI_VNC_PORT = toString config.hyperhive.gui.vncPort;
|
||||||
};
|
};
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = "${pkgs.hyperhive}/bin/${binary} serve";
|
ExecStart = "${pkgs.hyperhive}/bin/${binary}";
|
||||||
# Pin the journal identity to the binary name (otherwise systemd
|
# Pin the journal identity to the binary name (otherwise systemd
|
||||||
# derives SyslogIdentifier from the ExecStart basename).
|
# derives SyslogIdentifier from the ExecStart basename).
|
||||||
SyslogIdentifier = binary;
|
SyslogIdentifier = binary;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue