Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
625bae1b31 | ||
|
|
37efb0889f | ||
|
|
65a10a3c2b | ||
|
|
d9fa9c564e |
8 changed files with 697 additions and 6 deletions
56
CLAUDE.md
56
CLAUDE.md
|
|
@ -156,6 +156,41 @@ 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 <path> --tools <builtins> --allowedTools <builtins+mcp> <prompt>
|
||||
```
|
||||
|
||||
**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** —
|
||||
|
|
@ -359,6 +394,27 @@ 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:
|
||||
|
|
|
|||
413
Cargo.lock
generated
413
Cargo.lock
generated
|
|
@ -11,6 +11,15 @@ 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"
|
||||
|
|
@ -67,12 +76,29 @@ 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"
|
||||
|
|
@ -131,6 +157,12 @@ 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"
|
||||
|
|
@ -153,6 +185,18 @@ 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"
|
||||
|
|
@ -199,6 +243,52 @@ 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"
|
||||
|
|
@ -242,6 +332,21 @@ 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"
|
||||
|
|
@ -249,6 +354,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -257,6 +363,34 @@ 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"
|
||||
|
|
@ -275,8 +409,13 @@ 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",
|
||||
]
|
||||
|
|
@ -313,6 +452,8 @@ dependencies = [
|
|||
"axum",
|
||||
"clap",
|
||||
"hive-sh4re",
|
||||
"rmcp",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
|
|
@ -425,6 +566,36 @@ 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"
|
||||
|
|
@ -437,6 +608,18 @@ 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"
|
||||
|
|
@ -513,6 +696,15 @@ 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"
|
||||
|
|
@ -525,6 +717,12 @@ 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"
|
||||
|
|
@ -561,6 +759,26 @@ 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"
|
||||
|
|
@ -578,6 +796,40 @@ 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"
|
||||
|
|
@ -592,12 +844,44 @@ 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"
|
||||
|
|
@ -628,6 +912,17 @@ 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"
|
||||
|
|
@ -740,6 +1035,26 @@ 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"
|
||||
|
|
@ -921,12 +1236,110 @@ 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"
|
||||
|
|
|
|||
|
|
@ -19,11 +19,18 @@ 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",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ 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
|
||||
|
|
|
|||
|
|
@ -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, web_ui};
|
||||
use hive_ag3nt::{DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, mcp, web_ui};
|
||||
use hive_sh4re::{AgentRequest, AgentResponse};
|
||||
use tokio::process::Command;
|
||||
|
||||
|
|
@ -33,6 +33,10 @@ 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]
|
||||
|
|
@ -88,6 +92,7 @@ async fn main() -> Result<()> {
|
|||
render(&resp)?;
|
||||
check(&resp)
|
||||
}
|
||||
Cmd::Mcp => mcp::serve_stdio(cli.socket).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,6 +122,7 @@ async fn needs_login_loop(
|
|||
async fn serve(socket: &Path, interval: Duration, state: Arc<Mutex<LoginState>>) -> 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<AgentResponse> = client::request(socket, &AgentRequest::Recv).await;
|
||||
match recv {
|
||||
|
|
@ -126,7 +132,7 @@ async fn serve(socket: &Path, interval: Duration, state: Arc<Mutex<LoginState>>)
|
|||
// 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).await;
|
||||
let reply = compute_reply(&body, &mcp_config).await;
|
||||
let send: Result<AgentResponse> = client::request(
|
||||
socket,
|
||||
&AgentRequest::Send {
|
||||
|
|
@ -155,8 +161,8 @@ async fn serve(socket: &Path, interval: Duration, state: Arc<Mutex<LoginState>>)
|
|||
}
|
||||
}
|
||||
|
||||
async fn compute_reply(prompt: &str) -> String {
|
||||
match invoke_claude(prompt).await {
|
||||
async fn compute_reply(prompt: &str, mcp_config: &Path) -> String {
|
||||
match invoke_claude(prompt, mcp_config).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %format!("{e:#}"), "claude failed; falling back to echo");
|
||||
|
|
@ -165,9 +171,26 @@ async fn compute_reply(prompt: &str) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
async fn invoke_claude(prompt: &str) -> Result<String> {
|
||||
async fn invoke_claude(prompt: &str, mcp_config: &Path) -> Result<String> {
|
||||
// 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.
|
||||
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?;
|
||||
|
|
@ -185,6 +208,26 @@ async fn invoke_claude(prompt: &str) -> Result<String> {
|
|||
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<PathBuf> {
|
||||
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(())
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
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`.
|
||||
|
|
|
|||
169
hive-ag3nt/src/mcp.rs
Normal file
169
hive-ag3nt/src/mcp.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
//! 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<SendArgs>) -> 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<RecvArgs>) -> 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__<this>__<tool>` (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<String> {
|
||||
["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<String> = 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 <path>`.
|
||||
/// `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 <path>`).
|
||||
#[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())
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ async fn index(State(state): State<AppState>) -> Html<String> {
|
|||
(LoginState::NeedsLogin, Some(session)) => render_login_in_progress(&session),
|
||||
};
|
||||
Html(format!(
|
||||
"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta http-equiv=\"refresh\" content=\"3\">\n<title>{label} // hyperhive</title>\n{STYLE}\n</head>\n<body>\n<pre class=\"banner\">░▒▓█▓▒░ {label} ░▒▓█▓▒░ hyperhive ag3nt ░▒▓█▓▒░</pre>\n<h2>◆ {label} ◆</h2>\n<div class=\"divider\">══════════════════════════════════════════════════════════════</div>\n{body}\n</body>\n</html>\n",
|
||||
"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>{label} // hyperhive</title>\n{STYLE}\n</head>\n<body>\n<pre class=\"banner\">░▒▓█▓▒░ {label} ░▒▓█▓▒░ hyperhive ag3nt ░▒▓█▓▒░</pre>\n<h2>◆ {label} ◆</h2>\n<div class=\"divider\">══════════════════════════════════════════════════════════════</div>\n{body}\n</body>\n</html>\n",
|
||||
label = state.label,
|
||||
))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue