remove hive-agent-wake — no shipped consumer
This commit is contained in:
parent
65a0686297
commit
2316287327
18 changed files with 25 additions and 295 deletions
|
|
@ -38,8 +38,8 @@ hand-maintained per-file tree drifts out of sync with the code.
|
|||
destroy|rebuild|restart|list|set-parent|…>`, `approvals <pending|
|
||||
approve|deny>`, `forge`/`matrix`/`github`/`gateway` provisioning,
|
||||
`choom`, `stop`/`start`, `wg`/`peer-config`.
|
||||
- **`hive-agent/`**, **`hive-agent-mcp/`**, **`hive-agent-wake/`** —
|
||||
in-container harness, three sibling crates for every agent (not a
|
||||
- **`hive-agent/`**, **`hive-agent-mcp/`** —
|
||||
in-container harness, two sibling crates for every agent (not a
|
||||
single `hive-ag3nt/` dir — that's the runtime/binary-family nickname,
|
||||
not a directory).
|
||||
- **`hive-agent/`** — the serve-loop binary: turn-loop *policy* layer
|
||||
|
|
@ -50,8 +50,6 @@ hand-maintained per-file tree drifts out of sync with the code.
|
|||
streamable-http listener, `hive-mcp-http` systemd unit) + its claude
|
||||
launch-config layer (tool-group/capability → `--allowedTools`,
|
||||
`--mcp-config` render).
|
||||
- **`hive-agent-wake/`** — small external wake CLI for extra MCP
|
||||
servers/helpers to nudge claude on external events.
|
||||
- **`hive-claude/`** — reusable, app-agnostic driver for headless
|
||||
`claude --print`: spawns the CLI, streams + classifies stream-json,
|
||||
parses per-turn `Telemetry`, and drives a durable self-compacting
|
||||
|
|
|
|||
14
Cargo.lock
generated
14
Cargo.lock
generated
|
|
@ -1574,20 +1574,6 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hive-agent-wake"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"hive-core-agent-sock",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hive-bash-mcp"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ members = [
|
|||
"hive-agent-mcp",
|
||||
"hive-agent-sock",
|
||||
"hive-core-agent-sock",
|
||||
"hive-agent-wake",
|
||||
"hive-bash-mcp",
|
||||
"hive-c0re",
|
||||
"hive-claude",
|
||||
|
|
|
|||
|
|
@ -213,8 +213,8 @@ nspawn agent. Open questions, not yet wired:
|
|||
|
||||
## Harness systemd unit shape
|
||||
|
||||
One harness serve binary (`hive-agent`, with its `hive-agent-mcp` /
|
||||
`hive-agent-wake` siblings), one shared `nix/agent-modules/` tree, one
|
||||
One harness serve binary (`hive-agent`, with its `hive-agent-mcp`
|
||||
sibling), one shared `nix/agent-modules/` tree, one
|
||||
service unit (`systemd.services.hive-agent`) 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
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ agents) runs:
|
|||
|
||||
## Harness binary shape
|
||||
|
||||
Three sibling binaries out of the one `hive-ag3nt` crate, all
|
||||
Two sibling binaries out of the one `hive-ag3nt` crate, all
|
||||
role-agnostic. (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`): `ManagerRequest` calls are
|
||||
|
|
@ -66,10 +66,6 @@ refused by the standard agent socket regardless of who sends them.)
|
|||
8790); claude connects to its URL via `--mcp-config`. HTTP is the sole
|
||||
transport — no per-turn stdio child (eliminates the re-registration
|
||||
race).
|
||||
- `hive-agent-wake --from <name> --body <body>` — push a message into
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -160,18 +160,14 @@ at_unix_timestamp?)`, `request_next_turn()`.
|
|||
|
||||
External MCP servers (and any other in-container process) can
|
||||
inject a wake-up event into the agent's inbox via the per-agent
|
||||
socket at `/run/hive/mcp.sock`. Two equivalent paths:
|
||||
|
||||
- **Shell out to `hive-agent-wake --from <label> --body <text>`**
|
||||
(use `--body -` to read body from stdin). Already on the
|
||||
container's `PATH` since the harness binary is in
|
||||
`systemPackages`. Convenient for shell-script integrations and
|
||||
co-process daemons (matrix bridge, webhook listeners, scrapers).
|
||||
|
||||
- **Speak the wire protocol directly** — JSON-line over the
|
||||
unix socket: `{"cmd":"wake","from":"matrix","body":"new dm
|
||||
from @alice"}\n`. Same shape as any other `AgentRequest`;
|
||||
see `hive-sh4re::AgentRequest::Wake`.
|
||||
socket at `/run/hive/mcp.sock`. Speak the wire protocol directly —
|
||||
JSON-line over the unix socket: `{"cmd":"wake","from":"matrix","body":
|
||||
"new dm from @alice"}\n`. Same shape as any other `AgentRequest`; see
|
||||
`hive-sh4re::AgentRequest::Wake`. (An earlier `hive-agent-wake` CLI
|
||||
wrapper existed for this but was removed — no shipped co-process
|
||||
daemon actually shelled out to it; every one that wakes the harness
|
||||
(matrix, bash) dials the socket directly, so the raw protocol is the
|
||||
only path now.)
|
||||
|
||||
The wake event lands in the broker as `{from:<label>,
|
||||
to:<agent>, body}`, waking whatever `recv` call the harness
|
||||
|
|
|
|||
|
|
@ -86,7 +86,6 @@
|
|||
inherit (self.packages.${pkgs.stdenv.hostPlatform.system})
|
||||
hive-agent
|
||||
hive-agent-mcp
|
||||
hive-agent-wake
|
||||
hive-bash-daemon
|
||||
hive-forge
|
||||
hive-matrix-daemon
|
||||
|
|
|
|||
|
|
@ -30,8 +30,5 @@ or their own daemon instead of living here.
|
|||
harness's own `paths.rs` conventions.
|
||||
|
||||
Sibling of `hive-agent` (the serve loop that renders the
|
||||
`--mcp-config` blob pointing here) and `hive-agent-wake` (the small
|
||||
CLI extra MCP daemons use to nudge claude on external events, since
|
||||
they don't have a tool surface of their own to call `remind`/`send`
|
||||
through). Standalone bin crate so the always-on MCP server doesn't
|
||||
need to link the whole turn-loop lib.
|
||||
`--mcp-config` blob pointing here). Standalone bin crate so the
|
||||
always-on MCP server doesn't need to link the whole turn-loop lib.
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@
|
|||
//! hyperhive broker. claude reconnects to the stable URL each turn via
|
||||
//! `--mcp-config`, avoiding the per-turn re-registration race. HTTP is the
|
||||
//! sole transport — there is no stdio mode. Sibling of `hive-agent` (the
|
||||
//! serve loop that renders the `--mcp-config` blob pointing here) and
|
||||
//! `hive-agent-wake`.
|
||||
//! serve loop that renders the `--mcp-config` blob pointing here).
|
||||
//!
|
||||
//! Standalone bin crate: the MCP surface (`mcp/`) plus its small support
|
||||
//! modules (socket client, send allow-list, loose-end scanner, path
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
[package]
|
||||
name = "hive-agent-wake"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
readme = "README.md"
|
||||
|
||||
[[bin]]
|
||||
name = "hive-agent-wake"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
hive-core-agent-sock.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# hive-agent-wake
|
||||
|
||||
A tiny CLI that injects a wake-up event into an agent's own harness
|
||||
inbox, so the next turn fires with a given `--from`/`--body`. That's
|
||||
the whole crate.
|
||||
|
||||
## When to use it
|
||||
|
||||
Reach for this when you're wiring up an `extraMcpServers` daemon
|
||||
(scraper, webhook listener, custom integration) that needs to nudge
|
||||
claude on some external event — a new item arrived, a long job
|
||||
finished, whatever. The built-in daemons (matrix, bash) don't use
|
||||
this: they talk to the per-agent MCP socket directly instead. This CLI
|
||||
exists so a helper author only needs one small, dependency-light crate
|
||||
to get the same effect without linking `hive-agent-mcp` or the full
|
||||
`hive-agent` harness lib.
|
||||
|
||||
## Shape
|
||||
|
||||
One binary, `src/main.rs`: parses `--socket` (defaults to
|
||||
`/run/hive/mcp.sock`, the per-agent MCP socket bind-mounted from the
|
||||
host), `--from`, and `--body` (pass `-` to read the body from stdin),
|
||||
dials the socket with `hive-core-agent-sock`'s `Request`/`Response`
|
||||
wire types, sends a `Wake` request, and exits. It carries its own copy
|
||||
of the retrying request client rather than depending on `hive-agent-mcp`'s.
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
//! 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.
|
||||
//!
|
||||
//! Standalone bin crate: it dials the per-agent MCP socket directly and
|
||||
//! carries its own copy of the retrying request client (below), so it
|
||||
//! does not link the whole `hive-agent` harness lib — a helper author
|
||||
//! wiring up an `extraMcpServers` binary only needs this one small crate.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use hive_core_agent_sock::{Request, Response};
|
||||
|
||||
/// Per-agent MCP socket, bind-mounted from the host into every container.
|
||||
const DEFAULT_SOCKET: &str = "/run/hive/mcp.sock";
|
||||
|
||||
#[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: Response = client::request(
|
||||
&cli.socket,
|
||||
&Request::Wake {
|
||||
from: cli.from,
|
||||
body,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
match resp {
|
||||
Response::Ok => Ok(()),
|
||||
Response::Err { message } => anyhow::bail!("wake: {message}"),
|
||||
other => anyhow::bail!("wake: unexpected response {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Self-contained retrying unix-socket JSON request client. A trimmed
|
||||
/// copy of `hive_agent::client` (no `request_retried` variant — the wake
|
||||
/// CLI never needs the retry-count) so this bin does not link the harness
|
||||
/// lib. Keeping the retry matters: the socket can be briefly absent while
|
||||
/// hive-c0re restarts under an operator redeploy, and a helper firing a
|
||||
/// wake shouldn't spuriously fail then.
|
||||
mod client {
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
/// Backoff schedule between attempts. Five entries → up to 5 retries on
|
||||
/// top of the initial attempt; total wall-clock cap = 2+4+8+16+30 = 60s.
|
||||
/// Sized to ride out a hive-c0re restart (systemd usually has the unix
|
||||
/// socket back inside ~5s) without the caller having to handle the
|
||||
/// transient itself.
|
||||
const RETRY_BACKOFFS_MS: &[u64] = &[2_000, 4_000, 8_000, 16_000, 30_000];
|
||||
|
||||
/// Send `req` over the unix socket and decode the single-line JSON
|
||||
/// response, retrying transient connect/IO failures on the backoff
|
||||
/// schedule above.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the socket is unreachable after all retries, or
|
||||
/// if serialization / deserialization of the request or response fails.
|
||||
pub async fn request<Req, Resp>(socket: &Path, req: &Req) -> Result<Resp>
|
||||
where
|
||||
Req: Serialize + ?Sized,
|
||||
Resp: DeserializeOwned,
|
||||
{
|
||||
let mut last_err: Option<anyhow::Error> = None;
|
||||
// One attempt per backoff entry, plus a final attempt with no sleep
|
||||
// after it — so `len + 1` tries, retrying only transient failures.
|
||||
for attempt in 0..=RETRY_BACKOFFS_MS.len() {
|
||||
match try_once::<Req, Resp>(socket, req).await {
|
||||
Ok(resp) => return Ok(resp),
|
||||
Err(RequestError::Fatal(e)) => return Err(e),
|
||||
Err(RequestError::Transient(e)) => {
|
||||
if let Some(&sleep_ms) = RETRY_BACKOFFS_MS.get(attempt) {
|
||||
tracing::warn!(
|
||||
attempt = attempt + 1,
|
||||
sleep_ms,
|
||||
error = %e,
|
||||
"hive socket attempt failed; retrying"
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(sleep_ms)).await;
|
||||
}
|
||||
last_err = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// The final iteration always sets `last_err` on a transient failure.
|
||||
Err(last_err.expect("a transient failure on the final attempt set last_err"))
|
||||
}
|
||||
|
||||
/// Transient = connect / IO error worth a retry (server restart, broken
|
||||
/// pipe). Fatal = serialization / deserialization / protocol error
|
||||
/// where retrying would just repeat the same failure.
|
||||
enum RequestError {
|
||||
Transient(anyhow::Error),
|
||||
Fatal(anyhow::Error),
|
||||
}
|
||||
|
||||
async fn try_once<Req, Resp>(socket: &Path, req: &Req) -> Result<Resp, RequestError>
|
||||
where
|
||||
Req: Serialize + ?Sized,
|
||||
Resp: DeserializeOwned,
|
||||
{
|
||||
let stream = match UnixStream::connect(socket).await {
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
// A refused or missing socket usually means hive-c0re is
|
||||
// mid-restart (operator redeploy / rebuild) — the socket is
|
||||
// recreated on its boot and the retry loop rides it out.
|
||||
let restarting = matches!(
|
||||
e.kind(),
|
||||
std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound
|
||||
);
|
||||
let mut err =
|
||||
anyhow::Error::new(e).context(format!("connect to {}", socket.display()));
|
||||
if restarting {
|
||||
err = err.context(
|
||||
"hive-c0re may be restarting (e.g. an operator redeploy); \
|
||||
the CLI already retried ~60s before surfacing this",
|
||||
);
|
||||
}
|
||||
return Err(RequestError::Transient(err));
|
||||
}
|
||||
};
|
||||
let (read, mut write) = stream.into_split();
|
||||
|
||||
let mut payload = serde_json::to_string(req).map_err(|e| RequestError::Fatal(e.into()))?;
|
||||
payload.push('\n');
|
||||
write
|
||||
.write_all(payload.as_bytes())
|
||||
.await
|
||||
.map_err(|e| RequestError::Transient(e.into()))?;
|
||||
write
|
||||
.flush()
|
||||
.await
|
||||
.map_err(|e| RequestError::Transient(e.into()))?;
|
||||
|
||||
let mut reader = BufReader::new(read);
|
||||
let mut line = String::new();
|
||||
let read_bytes = reader
|
||||
.read_line(&mut line)
|
||||
.await
|
||||
.map_err(|e| RequestError::Transient(e.into()))?;
|
||||
if read_bytes == 0 || line.is_empty() {
|
||||
return Err(RequestError::Transient(anyhow!(
|
||||
"server closed connection without responding"
|
||||
)));
|
||||
}
|
||||
serde_json::from_str(line.trim()).map_err(|e| RequestError::Fatal(e.into()))
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ tracing-subscriber.workspace = true
|
|||
tempfile = "3"
|
||||
|
||||
# Single harness serve-loop binary: `hive-agent` (from `src/main.rs`).
|
||||
# The sibling MCP server + external wake CLI are their own bin crates
|
||||
# now (`hive-agent-mcp`, `hive-agent-wake`). Privilege boundary is
|
||||
# enforced server-side at the socket (tool groups / manager surface).
|
||||
# The sibling MCP server is its own bin crate now (`hive-agent-mcp`).
|
||||
# Privilege boundary is enforced server-side at the socket (tool
|
||||
# groups / manager surface).
|
||||
# See `docs/turn-loop.md::Harness binary shape`.
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ map of the module tree.
|
|||
- **`paths.rs`** — canonical path resolution for state/harness dirs and
|
||||
the harness-local sqlite files.
|
||||
|
||||
Siblings: **`hive-agent-mcp`** (the MCP server this loop points claude
|
||||
at every turn) and **`hive-agent-wake`** (external wake CLI for extra
|
||||
MCP daemons). All three are described together in
|
||||
Sibling: **`hive-agent-mcp`** (the MCP server this loop points claude
|
||||
at every turn). Both are described together in
|
||||
[`docs/turn-loop.md::Harness binary shape`](../docs/turn-loop.md).
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! Harness serve-loop binary. Long-polls the broker inbox and drives one
|
||||
//! claude turn per message. There is one role: agent. The `Surface`
|
||||
//! trait + `AgentSurface` zero-sized type tag keeps the turn loop
|
||||
//! generic and testable. Siblings: `hive-agent-mcp` (the MCP server this
|
||||
//! loop points claude at) and `hive-agent-wake` (external wake CLI).
|
||||
//! generic and testable. Sibling: `hive-agent-mcp` (the MCP server this
|
||||
//! loop points claude at).
|
||||
//! Architecture lives in
|
||||
//! [`docs/turn-loop.md::Harness binary shape`](../../../docs/turn-loop.md).
|
||||
//!
|
||||
|
|
|
|||
|
|
@ -176,13 +176,11 @@
|
|||
# the harness execs (hive-agent{,-mcp}, hive-bash-daemon,
|
||||
# hive-matrix-daemon, hive-matrix-mcp) are wired via their own
|
||||
# ExecStart/command lines in the sibling modules — they don't need
|
||||
# to be on PATH too. Only these two are actually looked up on PATH
|
||||
# to be on PATH too. Only this one is actually looked up on PATH
|
||||
# by claude/shell code inside the container:
|
||||
# `hive-agent-wake` (external wake CLI, docs/turn-loop/mcp.md) and
|
||||
# `hive-metric` (agent-emitted custom metrics CLI,
|
||||
# docs/observability.md).
|
||||
environment.systemPackages = [
|
||||
config.hyperhive.packages.hive-agent-wake
|
||||
config.hyperhive.packages.hive-metric
|
||||
]
|
||||
++ (with pkgs; [
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
description = ''
|
||||
hyperhive package outputs consumed by the harness modules: the
|
||||
per-binary daemon/CLI packages (`hive-agent`, `hive-agent-mcp`,
|
||||
`hive-agent-wake`, `hive-bash-daemon`,
|
||||
`hive-bash-daemon`,
|
||||
`hive-forge`, `hive-matrix-daemon`,
|
||||
`hive-metric`, `hive-screen-mcp`) plus the `assets`, `frontend` and
|
||||
`reference-docs` trees. Wired by the flake's agent-base/ruth
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ let
|
|||
hive-priv = "hyperhive privileged root helper";
|
||||
hive-agent = "hyperhive in-container agent harness serve loop";
|
||||
hive-agent-mcp = "hyperhive agent-surface MCP server";
|
||||
hive-agent-wake = "hyperhive external wake CLI — push a message into an agent's own inbox";
|
||||
hive-bash-daemon = "hyperhive per-agent bash-task runner daemon (serves its MCP tools directly over streamable-http)";
|
||||
hive-matrix-daemon = "hyperhive per-agent matrix-sdk daemon (serves its MCP tools directly over streamable-http)";
|
||||
hive-metric = "hyperhive agent-emitted custom metrics CLI";
|
||||
|
|
|
|||
Loading…
Reference in a new issue