diff --git a/CLAUDE.md b/CLAUDE.md index 9b52b3f4..37322c3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,8 +32,9 @@ hand-maintained per-file tree drifts out of sync with the code. the meta flake, lifecycle (`nixos-container` shellouts), gateway / forge / matrix provisioning, per-container stats, and the axum operator dashboard (`dashboard.rs`). Largest crate. -- **`hive-ag3nt/`** — in-container harness; one `hive` binary for every - agent. Turn-loop *policy* layer (`turn.rs`) over the `hive-claude` +- **`hive-ag3nt/`** — in-container harness; three sibling binaries for + 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 (`mcp_config.rs`: tool-group/capability → `--allowedTools`, `--mcp-config` render), per-agent web UI (`web_ui/` module dir), event + turn-stats diff --git a/Cargo.lock b/Cargo.lock index 2e4e59c1..ebf70b9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -630,6 +630,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -862,6 +872,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -924,6 +935,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1057,6 +1077,27 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "forgejo-api" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c51fffa047d5836a4d89179a20b21b7e41ad97e713104752169d819b83c9158" +dependencies = [ + "base64ct", + "bytes", + "futures", + "reqwest", + "serde", + "serde_json", + "soft_assert", + "thiserror 1.0.69", + "time", + "tokio", + "url", + "urlencoding", + "zeroize", +] + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1338,6 +1379,7 @@ dependencies = [ "anyhow", "axum", "clap", + "forgejo-api", "futures-util", "hive-claude", "hive-sh4re", @@ -1348,11 +1390,13 @@ dependencies = [ "serde", "serde_json", "tempfile", + "time", "tokio", "tokio-stream", "tower-http", "tracing", "tracing-subscriber", + "url", ] [[package]] @@ -1384,6 +1428,7 @@ dependencies = [ "clap", "clap-markdown", "clap_complete", + "forgejo-api", "hive-sh4re", "libc", "listenfd", @@ -1398,6 +1443,7 @@ dependencies = [ "tokio-stream", "tracing", "tracing-subscriber", + "url", ] [[package]] @@ -1417,9 +1463,12 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "forgejo-api", "reqwest", "serde", "serde_json", + "time", + "url", ] [[package]] @@ -1631,9 +1680,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -3018,6 +3069,7 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", + "encoding_rs", "futures-channel", "futures-core", "futures-util", @@ -3030,6 +3082,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", "mime_guess", "percent-encoding", "pin-project-lite", @@ -3457,7 +3510,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3689,6 +3742,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "soft_assert" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5097ec7ea7218135541ad96348f1441d0c616537dd4ed9c47205920c35d7d97" + [[package]] name = "spki" version = "0.7.3" @@ -3786,6 +3845,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -4596,6 +4676,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index 873c0434..10737252 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,14 @@ reqwest = { version = "0.12", default-features = false, features = [ "json", "rustls-tls", ] } +forgejo-api = { version = "0.11", default-features = false, features = [ + "rustls-tls", +] } +url = "2" +time = { version = "0.3", default-features = false, features = [ + "formatting", + "parsing", +] } petgraph = { version = "0.8", default-features = false, features = ["std"] } matrix-sdk = { version = "0.14", default-features = false, features = [ "rustls-tls", diff --git a/docs/agent-hierarchy.md b/docs/agent-hierarchy.md index a11ac9a6..038a52c3 100644 --- a/docs/agent-hierarchy.md +++ b/docs/agent-hierarchy.md @@ -214,7 +214,8 @@ nspawn agent. Open questions, not yet wired: ## 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 is no longer a separate manager service name or role distinction in the harness — privilege differences live server-side in the broker @@ -259,7 +260,8 @@ bit set` regardless of `hyperhive.user.passwordlessSudo`. ### `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 resilient across transient crashes without thundering retries. - `RuntimeDirectory = "hive-config"` → `/run/hive-config/` owned by diff --git a/docs/forge.md b/docs/forge.md index 093b5860..83a2481c 100644 --- a/docs/forge.md +++ b/docs/forge.md @@ -71,7 +71,12 @@ Two things live in the `agent-configs` Forgejo organization: The `hive-forge` CLI (separate workspace crate, see [`README.md`](../README.md) file map) wraps the Forgejo REST API with the per-agent token; agents call it for issue / PR / comment -ops as if it were a peer. +ops as if it were a peer. All REST calls across the workspace +(`hive-forge` verbs, hive-c0re provisioning, this poller) go through +the typed `forgejo-api` crate; only non-`/api/v1` web-router routes +(attachment / artifact downloads, log streaming) and the poller's +enrichment fetches of server-provided subject URLs stay on raw +reqwest. ## Notification poller (`hive-ag3nt/src/forge_notify.rs`) @@ -146,11 +151,14 @@ one path still marked-read directly (no read-before-comment value). The poller starts disabled and stays that way for any of: -- `HIVE_FORGE_URL` not set (no forge configured for this hive). +- `HIVE_FORGE_URL` not set (no forge configured for this hive), or + not parseable as a URL. - `/forge-token` missing or empty (agent has no forge account — pre-provisioning or destroy-without-purge race). -- Initial `reqwest::Client::builder` fails (extremely unlikely; - treated as fatal-to-the-task only). +- Initial client construction fails (the typed `forgejo-api` client + for the API calls, or the plain reqwest client kept for the + best-effort enrichment fetches of server-provided subject URLs; + both extremely unlikely; treated as fatal-to-the-task only). Disabled = the spawned task returns immediately. All other failure modes (HTTP errors, parse errors, mark-read failures) are diff --git a/docs/turn-loop.md b/docs/turn-loop.md index 30b5390f..2a16037e 100644 --- a/docs/turn-loop.md +++ b/docs/turn-loop.md @@ -5,7 +5,8 @@ claude has access to in return. ## 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 (`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 -One `hive` binary for all agents. The earlier split into -`hive-ag3nt` + `hive-m1nd` was collapsed because the privilege -boundary lives server-side at the broker socket -(`/run/hive/mcp.sock`): `ManagerRequest` calls are refused by the -standard agent socket regardless of who sends them. +Three 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 +refused by the standard agent socket regardless of who sends them.) -Three subcommands: - -- `serve` — long-running harness loop (the inbox poll + +- `hive-agent` — long-running harness loop (the inbox poll + claude-pump + ack/requeue cycle described above). -- `mcp` — MCP server. Default: stdio child claude spawns via - `--mcp-config` per turn. With `--http `, runs as a persistent - streamable-HTTP daemon instead (used by the `hive-mcp-http` - systemd unit when `hyperhive.mcp.httpPort` is set). -- `wake --from --body ` — push a message into our own - inbox so the next turn fires with the given body. Used by - co-process daemons (matrix bridge, scraper, webhook listeners) - to nudge claude on external events. `--body -` reads from stdin. +- `hive-agent-mcp` — MCP server. Default: stdio child claude spawns + via `--mcp-config` per turn (the serve loop renders the config to + point at this sibling of its own `/proc/self/exe`). With + `--http `, runs as a persistent streamable-HTTP daemon + instead (used by the `hive-mcp-http` systemd unit when + `hyperhive.mcp.httpPort` is set). +- `hive-agent-wake --from --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 `AgentRequest` / `AgentResponse` (= `ManagerRequest` / `ManagerResponse` — type aliases) are the wire types. There is one role: agent. -`bin/hive.rs` factors the turn loop through a `Surface` trait with one -zero-sized impl (`AgentSurface`) wrapping: +`bin/hive-agent.rs` factors the turn loop through a `Surface` trait +with one zero-sized impl (`AgentSurface`) wrapping: - One async method per wire op: `ack_turn`, `requeue_inflight`, - `inbox_unread`, `post_turn_counts`, `send_to_parent`, - `recv_next`, `wake_external`. + `inbox_unread`, `post_turn_counts`, `send_to_parent`, `recv_next`. `main()` calls `serve_main::` 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 diff --git a/docs/turn-loop/mcp.md b/docs/turn-loop/mcp.md index 02d707ed..2552e64a 100644 --- a/docs/turn-loop/mcp.md +++ b/docs/turn-loop/mcp.md @@ -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 socket at `/run/hive/mcp.sock`. Two equivalent paths: -- **Shell out to `hive wake --from