diff --git a/CLAUDE.md b/CLAUDE.md index 7bb81807..a2a08753 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -156,41 +156,6 @@ docs/damocles-migration.md options for moving damocles onto hyperhive marks them `failed` with note `"agent state dir missing"` so they fall out of `pending`. They stay in sqlite for audit. -## Agent MCP surface + turn loop - -The harness ships an embedded MCP server (rmcp 1.7) that claude launches as -a stdio child via `--mcp-config`. Subcommand: `hive-ag3nt mcp`. Tools: -- `mcp__hyperhive__send(to, body)` — message a peer or the operator. -- `mcp__hyperhive__recv()` — drain one inbox message. - -Both translate to `AgentRequest::Send`/`Recv` against the agent's own -`/run/hive/mcp.sock` (the existing hyperhive socket). The MCP surface is -just claude's view of that socket — same authority, friendlier protocol. - -The turn loop in `hive-ag3nt serve` writes -`/run/hive/claude-mcp-config.json` at boot pointing at -`/proc/self/exe mcp` (the running hive-ag3nt binary's nix store path). -Each turn invokes: - -``` -claude --print --mcp-config --tools --allowedTools -``` - -**Tool whitelist** (see `ALLOWED_BUILTIN_TOOLS` in `hive-ag3nt::mcp`): -- Allowed built-ins: `Bash`, `Edit`, `Glob`, `Grep`, `NotebookEdit`, `Read`, - `TodoWrite`, `Write`. -- Denied by omission: `WebFetch`, `WebSearch`, `Task` — no external egress - or nested-agent spawning until we have a real policy story. -- Allowed MCP tools: `mcp__hyperhive__send`, `mcp__hyperhive__recv`. - -`Bash` is on the allow-list "for now" — pending a finer-grained allow-list -system for command patterns (`Bash(git *)`-style). When that lands, the -`builtin_tools_arg` shape will probably change to a setting / hooks -combo per claude-code's permissions plumbing. - -Manager will get its own subcommand later with `request_spawn`, `kill`, -`request_apply_commit` added to the TOOLS list. - ## Manager (hm1nd) is hive-c0re-managed The manager container runs through the **same lifecycle as sub-agents** — @@ -394,27 +359,6 @@ with `./agent.nix` plus an inline module that sets `environment.etc."gitconfig".text` (committer identity = the agent's name) and `systemd.services.hive-ag3nt.environment.HIVE_PORT`/`HIVE_LABEL`. -## Security backlog - -- **Unprivileged containers (userns mapping).** Today the nspawn container - runs as a fully privileged root. Goal: `PrivateUsersChown=yes` (or the - nixos-container equivalent) so uid 0 inside maps to an unprivileged uid - on the host, and a container-root compromise lands the attacker on an - ordinary user account, not the host's root. Requires per-agent state - dirs to be chown'd to that uid on the host side. -- **Bash command allow-list.** Replace the blanket `Bash` allow with a - pattern allow-list (`Bash(git *)`, `Bash(nix build .*)`, etc.) per - claude-code's `--allowedTools` extended grammar. Likely lives in - `agent.nix` so each agent can scope its own shell surface. - -## Per-agent settings backlog - -- **Model override.** Hard-coded to `claude-haiku-4-5-20251001` in the turn - loop right now. Surface it as a per-agent override: operator via - dashboard, manager via `request_apply_commit` setting an attr on the - agent's flake (most natural place since the flake already carries - per-agent env/identity). - ## Polish backlog Not phased — pick when relevant: diff --git a/Cargo.lock b/Cargo.lock index 4286126f..87769024 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,15 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" version = "1.0.0" @@ -76,29 +67,12 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - [[package]] name = "axum" version = "0.8.9" @@ -157,12 +131,6 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - [[package]] name = "bytes" version = "1.11.1" @@ -185,18 +153,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link", -] - [[package]] name = "clap" version = "4.6.1" @@ -243,52 +199,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - [[package]] name = "errno" version = "0.3.14" @@ -332,21 +242,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" version = "0.3.32" @@ -354,7 +249,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", - "futures-sink", ] [[package]] @@ -363,34 +257,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "futures-sink" version = "0.3.32" @@ -409,13 +275,8 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "futures-channel", "futures-core", - "futures-io", - "futures-macro", - "futures-sink", "futures-task", - "memchr", "pin-project-lite", "slab", ] @@ -452,8 +313,6 @@ dependencies = [ "axum", "clap", "hive-sh4re", - "rmcp", - "schemars", "serde", "serde_json", "tokio", @@ -566,36 +425,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -608,18 +437,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "js-sys" -version = "0.3.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -696,15 +513,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -717,12 +525,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "pastey" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" - [[package]] name = "percent-encoding" version = "2.3.2" @@ -759,26 +561,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "regex-automata" version = "0.4.14" @@ -796,40 +578,6 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" -[[package]] -name = "rmcp" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e" -dependencies = [ - "async-trait", - "chrono", - "futures", - "pastey", - "pin-project-lite", - "rmcp-macros", - "schemars", - "serde", - "serde_json", - "thiserror", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "rmcp-macros" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aefac48c364756e97f04c0401ba3231e8607882c7c1d92da0437dc16307904d" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "serde_json", - "syn", -] - [[package]] name = "rusqlite" version = "0.37.0" @@ -844,44 +592,12 @@ dependencies = [ "smallvec", ] -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - [[package]] name = "ryu" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "chrono", - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn", -] - [[package]] name = "serde" version = "1.0.228" @@ -912,17 +628,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "serde_json" version = "1.0.149" @@ -1035,26 +740,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "thread_local" version = "1.1.9" @@ -1236,110 +921,12 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasm-bindgen" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.61.2" diff --git a/Cargo.toml b/Cargo.toml index 2f4c26f5..947b0508 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,18 +19,11 @@ anyhow = "1" axum = "0.8" clap = { version = "4", features = ["derive"] } hive-sh4re = { path = "hive-sh4re" } -rmcp = { version = "1.7", default-features = false, features = [ - "server", - "macros", - "transport-io", -] } rusqlite = { version = "0.37", features = ["bundled"] } -schemars = "1.0" serde = { version = "1", features = ["derive"] } serde_json = "1" similar = "2" tokio = { version = "1", features = [ - "fs", "io-util", "macros", "net", diff --git a/hive-ag3nt/Cargo.toml b/hive-ag3nt/Cargo.toml index ae1af3ef..09ae0911 100644 --- a/hive-ag3nt/Cargo.toml +++ b/hive-ag3nt/Cargo.toml @@ -11,8 +11,6 @@ anyhow.workspace = true axum.workspace = true clap.workspace = true hive-sh4re.workspace = true -rmcp.workspace = true -schemars.workspace = true serde.workspace = true serde_json.workspace = true tokio.workspace = true diff --git a/hive-ag3nt/src/bin/hive-ag3nt.rs b/hive-ag3nt/src/bin/hive-ag3nt.rs index 3ac2b252..0dd9d5c6 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -5,7 +5,7 @@ use std::time::Duration; use anyhow::{Result, bail}; use clap::{Parser, Subcommand}; use hive_ag3nt::login::{self, LoginState}; -use hive_ag3nt::{DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, mcp, web_ui}; +use hive_ag3nt::{DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, web_ui}; use hive_sh4re::{AgentRequest, AgentResponse}; use tokio::process::Command; @@ -33,10 +33,6 @@ enum Cmd { Send { to: String, body: String }, /// Pop one message from the inbox. Recv, - /// Run the agent's MCP server on stdio. Spawned by `claude` via - /// `--mcp-config`; tools dispatch through `/run/hive/mcp.sock` back into - /// the hyperhive broker. - Mcp, } #[tokio::main] @@ -92,7 +88,6 @@ async fn main() -> Result<()> { render(&resp)?; check(&resp) } - Cmd::Mcp => mcp::serve_stdio(cli.socket).await, } } @@ -122,7 +117,6 @@ async fn needs_login_loop( async fn serve(socket: &Path, interval: Duration, state: Arc>) -> Result<()> { tracing::info!(socket = %socket.display(), "hive-ag3nt serve"); let _ = state; // reserved for future state transitions (turn-loop -> needs-login) - let mcp_config = write_mcp_config(socket).await?; loop { let recv: Result = client::request(socket, &AgentRequest::Recv).await; match recv { @@ -132,7 +126,7 @@ async fn serve(socket: &Path, interval: Duration, state: Arc>) // both ends are falling back to echo. Real loop control is the // manager's job (Phase 4+). if !body.starts_with("echo: ") { - let reply = compute_reply(&body, &mcp_config).await; + let reply = compute_reply(&body).await; let send: Result = client::request( socket, &AgentRequest::Send { @@ -161,8 +155,8 @@ async fn serve(socket: &Path, interval: Duration, state: Arc>) } } -async fn compute_reply(prompt: &str, mcp_config: &Path) -> String { - match invoke_claude(prompt, mcp_config).await { +async fn compute_reply(prompt: &str) -> String { + match invoke_claude(prompt).await { Ok(s) => s, Err(e) => { tracing::warn!(error = %format!("{e:#}"), "claude failed; falling back to echo"); @@ -171,26 +165,9 @@ async fn compute_reply(prompt: &str, mcp_config: &Path) -> String { } } -async fn invoke_claude(prompt: &str, mcp_config: &Path) -> Result { - // Whitelist model: `--tools` restricts which built-ins exist in the - // session (omitting WebFetch/WebSearch/Task means claude literally - // can't invoke them); `--allowedTools` auto-approves the same set - // plus the hyperhive MCP surface so there's no permission prompt - // mid-turn. A finer-grained allow-list system for Bash command - // patterns is on the backlog (PLAN.md polish). - // Hard-coded to Haiku 4.5 for now to keep cost predictable while we - // iterate. TODO: make this overridable per-agent — op via dashboard, - // manager via `request_apply_commit` setting on the agent's flake. +async fn invoke_claude(prompt: &str) -> Result { let out = Command::new("claude") .arg("--print") - .arg("--model") - .arg("claude-haiku-4-5-20251001") - .arg("--mcp-config") - .arg(mcp_config) - .arg("--tools") - .arg(mcp::builtin_tools_arg()) - .arg("--allowedTools") - .arg(mcp::allowed_tools_arg()) .arg(prompt) .output() .await?; @@ -208,26 +185,6 @@ async fn invoke_claude(prompt: &str, mcp_config: &Path) -> Result { Ok(text) } -/// Drop the per-agent MCP config on disk so the turn loop can hand its path -/// to `claude --mcp-config`. Lives under `/run/hive/` (the bind-mounted -/// per-agent runtime dir) so it's ephemeral and isolated per container. -/// Returns the config path. -async fn write_mcp_config(socket: &Path) -> Result { - let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive")); - tokio::fs::create_dir_all(parent).await.ok(); - let path = parent.join("claude-mcp-config.json"); - // `/proc/self/exe` resolves to the running hive-ag3nt binary's nix store - // path, which the spawned child can re-invoke as the MCP server. Avoids - // needing claude-code's $PATH to contain hive-ag3nt. - let exe = std::env::current_exe() - .ok() - .map_or_else(|| "hive-ag3nt".into(), |p| p.display().to_string()); - let body = mcp::render_claude_config(&exe, socket); - tokio::fs::write(&path, body).await?; - tracing::info!(path = %path.display(), "wrote claude MCP config"); - Ok(path) -} - fn render(resp: &AgentResponse) -> Result<()> { println!("{}", serde_json::to_string_pretty(resp)?); Ok(()) diff --git a/hive-ag3nt/src/lib.rs b/hive-ag3nt/src/lib.rs index 4b7a62e0..b601724a 100644 --- a/hive-ag3nt/src/lib.rs +++ b/hive-ag3nt/src/lib.rs @@ -4,7 +4,6 @@ pub mod client; pub mod login; pub mod login_session; -pub mod mcp; pub mod web_ui; /// Default socket path inside the container — bind-mounted by `hive-c0re`. diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs deleted file mode 100644 index 5e99ebff..00000000 --- a/hive-ag3nt/src/mcp.rs +++ /dev/null @@ -1,169 +0,0 @@ -//! Embedded MCP server. Claude Code (running inside the agent container) -//! launches this as a stdio child via `--mcp-config`; tool calls land here -//! and are translated to `AgentRequest::Send`/`Recv` against hyperhive's -//! own per-agent unix socket at `/run/hive/mcp.sock`. -//! -//! Two protocols, two surfaces: -//! - **hyperhive socket** at `/run/hive/mcp.sock` — JSON-line, our -//! broker-routed Send/Recv. Unaffected by this module. -//! - **MCP stdio** owned by this module — what claude actually speaks. -//! -//! The agent surface today is intentionally tiny (send/recv); the manager -//! surface (Phase 8 follow-up) will add `request_spawn`, `request_kill`, -//! `request_apply_commit`. - -use std::path::PathBuf; - -use anyhow::Result; -use rmcp::{ - ServerHandler, ServiceExt, - handler::server::wrapper::Parameters, - schemars, tool, tool_handler, tool_router, - transport::stdio, -}; - -use crate::client; - -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct SendArgs { - /// Logical agent name to deliver the message to (e.g. `"manager"`, - /// `"alice"`, or the literal `"operator"` for the dashboard's T4LK box). - pub to: String, - /// Message body. Plain text; the broker doesn't parse it. - pub body: String, -} - -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct RecvArgs {} - -/// Per-agent tool surface. Holds the socket path so each tool call doesn't -/// re-derive it; the socket itself is the per-container `/run/hive/mcp.sock`. -#[derive(Debug, Clone)] -pub struct AgentServer { - socket: PathBuf, -} - -impl AgentServer { - #[must_use] - pub fn new(socket: PathBuf) -> Self { - Self { socket } - } -} - -#[tool_router] -impl AgentServer { - #[tool( - description = "Send a message to another hyperhive agent (or to the operator). \ - Use this to talk to peers or to surface output for the human at the dashboard." - )] - async fn send(&self, Parameters(args): Parameters) -> String { - let req = hive_sh4re::AgentRequest::Send { - to: args.to.clone(), - body: args.body, - }; - match client::request::<_, hive_sh4re::AgentResponse>(&self.socket, &req).await { - Ok(hive_sh4re::AgentResponse::Ok) => format!("sent to {}", args.to), - Ok(hive_sh4re::AgentResponse::Err { message }) => format!("send failed: {message}"), - Ok(other) => format!("send unexpected response: {other:?}"), - Err(e) => format!("send transport error: {e:#}"), - } - } - - #[tool( - description = "Pop one message from this agent's inbox. Returns the sender and body, \ - or an empty marker if nothing is waiting." - )] - async fn recv(&self, Parameters(_): Parameters) -> String { - let req = hive_sh4re::AgentRequest::Recv; - match client::request::<_, hive_sh4re::AgentResponse>(&self.socket, &req).await { - Ok(hive_sh4re::AgentResponse::Message { from, body }) => { - format!("from: {from}\n\n{body}") - } - Ok(hive_sh4re::AgentResponse::Empty) => "(empty)".into(), - Ok(hive_sh4re::AgentResponse::Err { message }) => format!("recv failed: {message}"), - Ok(other) => format!("recv unexpected response: {other:?}"), - Err(e) => format!("recv transport error: {e:#}"), - } - } -} - -#[tool_handler( - instructions = "You are a hyperhive agent. Use `send` to talk to peers (by their logical \ - name) or to the operator (recipient `operator`). Use `recv` to drain your inbox one \ - message at a time." -)] -impl ServerHandler for AgentServer {} - -/// Run the MCP server over stdio. Returns when the client disconnects. -pub async fn serve_stdio(socket: PathBuf) -> Result<()> { - let server = AgentServer::new(socket); - let service = server.serve(stdio()).await?; - service.waiting().await?; - Ok(()) -} - -/// Name of the hyperhive MCP server inside claude's view. Claude prefixes -/// tools as `mcp____` (e.g. `mcp__hyperhive__send`). -pub const SERVER_NAME: &str = "hyperhive"; - -/// Built-in claude tools the turn loop enables via `--tools`. Anything not -/// in this list literally doesn't exist in the session (claude won't even -/// try to call it). Web egress (`WebFetch`/`WebSearch`) and nested agents -/// (`Task`) are intentionally omitted for now; `Bash` is allowed pending a -/// finer-grained allow-list system for shell command patterns. Edit later -/// as our trust model evolves. -pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &[ - "Bash", - "Edit", - "Glob", - "Grep", - "NotebookEdit", - "Read", - "TodoWrite", - "Write", -]; - -/// MCP tools claude is allowed to call without prompting. Mirrors the -/// hyperhive surface so a new tool added below propagates to claude's -/// allow-list automatically. -#[must_use] -pub fn allowed_mcp_tools() -> Vec { - ["send", "recv"] - .iter() - .map(|t| format!("mcp__{SERVER_NAME}__{t}")) - .collect() -} - -/// Combined allow-list passed to `--allowedTools` (auto-approve) — covers -/// both the built-ins and the MCP surface. -#[must_use] -pub fn allowed_tools_arg() -> String { - let mut all: Vec = ALLOWED_BUILTIN_TOOLS.iter().map(|s| (*s).to_owned()).collect(); - all.extend(allowed_mcp_tools()); - all.join(",") -} - -/// Built-in tools list for `--tools` (which built-ins exist in this -/// session). Same as `ALLOWED_BUILTIN_TOOLS` but joined comma-separated. -#[must_use] -pub fn builtin_tools_arg() -> String { - ALLOWED_BUILTIN_TOOLS.join(",") -} - -/// Render the MCP config blob claude reads from `--mcp-config `. -/// `agent_binary` is the path (or PATH-resolvable name) of the `hive-ag3nt` -/// executable; `socket` is the hyperhive per-agent socket bind-mounted into -/// the container (forwarded to the child as `--socket `). -#[must_use] -pub fn render_claude_config(agent_binary: &str, socket: &std::path::Path) -> String { - let config = serde_json::json!({ - "mcpServers": { - SERVER_NAME: { - "command": agent_binary, - "args": ["--socket", socket.display().to_string(), "mcp"], - "env": {} - } - } - }); - serde_json::to_string_pretty(&config).unwrap_or_else(|_| "{}".into()) -} diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index 98548bce..689e8ea1 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -61,7 +61,7 @@ async fn index(State(state): State) -> Html { (LoginState::NeedsLogin, Some(session)) => render_login_in_progress(&session), }; Html(format!( - "\n\n\n\n{label} // hyperhive\n{STYLE}\n\n\n
░▒▓█▓▒░  {label}  ░▒▓█▓▒░  hyperhive ag3nt  ░▒▓█▓▒░
\n

◆ {label} ◆

\n
══════════════════════════════════════════════════════════════
\n{body}\n\n\n", + "\n\n\n\n\n{label} // hyperhive\n{STYLE}\n\n\n
░▒▓█▓▒░  {label}  ░▒▓█▓▒░  hyperhive ag3nt  ░▒▓█▓▒░
\n

◆ {label} ◆

\n
══════════════════════════════════════════════════════════════
\n{body}\n\n\n", label = state.label, )) }