diff --git a/hive-ag3nt/src/bin/hive-ag3nt.rs b/hive-ag3nt/src/bin/hive-ag3nt.rs index e421780f..fb993c38 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -80,13 +80,7 @@ async fn main() -> Result<()> { }); match initial { LoginState::Online => { - serve( - &cli.socket, - Duration::from_millis(poll_ms), - login_state, - bus, - ) - .await + serve(&cli.socket, Duration::from_millis(poll_ms), login_state, bus).await } LoginState::NeedsLogin => { // Partial-run mode: keep the harness alive (so the web UI @@ -158,7 +152,8 @@ async fn serve( body: body.clone(), }); let prompt = format_wake_prompt(&label, &from, &body); - let outcome = drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Agent).await; + let outcome = + drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Agent).await; emit_turn_end(&bus, &outcome); } Ok(AgentResponse::Empty) => {} diff --git a/hive-ag3nt/src/bin/hive-m1nd.rs b/hive-ag3nt/src/bin/hive-m1nd.rs index a4c2634a..eb2035a3 100644 --- a/hive-ag3nt/src/bin/hive-m1nd.rs +++ b/hive-ag3nt/src/bin/hive-m1nd.rs @@ -90,7 +90,9 @@ async fn main() -> Result<()> { } }); match initial { - LoginState::Online => serve(&cli.socket, Duration::from_millis(poll_ms), bus).await, + LoginState::Online => { + serve(&cli.socket, Duration::from_millis(poll_ms), bus).await + } LoginState::NeedsLogin => { tracing::warn!( claude_dir = %claude_dir.display(), @@ -172,7 +174,8 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> { body: body.clone(), }); let prompt = format_wake_prompt(&label, &from, &body); - let outcome = drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Manager).await; + let outcome = + drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Manager).await; emit_turn_end(&bus, &outcome); } Ok(ManagerResponse::Empty) => {} diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 6d3a6535..f73040f0 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -21,8 +21,10 @@ use std::path::PathBuf; use anyhow::Result; use rmcp::{ - ServerHandler, ServiceExt, handler::server::wrapper::Parameters, schemars, tool, tool_handler, - tool_router, transport::stdio, + ServerHandler, ServiceExt, + handler::server::wrapper::Parameters, + schemars, tool, tool_handler, tool_router, + transport::stdio, }; use crate::client; @@ -31,7 +33,12 @@ use crate::client; /// a status line → post-log. Free function so both `AgentServer` and /// `ManagerServer` use the same shape; the per-server `status_line` /// closure is what differs (different `Status` wire types). -pub async fn run_tool_envelope(tool: &'static str, args: String, status: S, body: F) -> String +pub async fn run_tool_envelope( + tool: &'static str, + args: String, + status: S, + body: F, +) -> String where F: Future, S: Future, @@ -216,10 +223,8 @@ impl ManagerServer { #[tool_router] impl ManagerServer { - #[tool( - description = "Send a message to a sub-agent (by logical name), to another agent, \ - or to the operator (recipient `operator`, surfaces in the dashboard)." - )] + #[tool(description = "Send a message to a sub-agent (by logical name), to another agent, \ + or to the operator (recipient `operator`, surfaces in the dashboard).")] async fn send(&self, Parameters(args): Parameters) -> String { let log = format!("{args:?}"); let to = args.to.clone(); @@ -240,10 +245,8 @@ impl ManagerServer { .await } - #[tool( - description = "Pop one message from the manager inbox. Returns sender + body, or \ - empty." - )] + #[tool(description = "Pop one message from the manager inbox. Returns sender + body, or \ + empty.")] async fn recv(&self, Parameters(args): Parameters) -> String { let log = format!("{args:?}"); run_tool_envelope("recv", log, self.status_line(), async move { @@ -253,9 +256,7 @@ impl ManagerServer { format!("from: {from}\n\n{body}") } Ok(hive_sh4re::ManagerResponse::Empty) => "(empty)".into(), - Ok(hive_sh4re::ManagerResponse::Err { message }) => { - format!("recv failed: {message}") - } + Ok(hive_sh4re::ManagerResponse::Err { message }) => format!("recv failed: {message}"), Ok(other) => format!("recv unexpected response: {other:?}"), Err(e) => format!("recv transport error: {e:#}"), } @@ -263,10 +264,8 @@ impl ManagerServer { .await } - #[tool( - description = "Queue a Spawn approval for a brand-new sub-agent. The operator \ - approves on the dashboard before the container is actually created." - )] + #[tool(description = "Queue a Spawn approval for a brand-new sub-agent. The operator \ + approves on the dashboard before the container is actually created.")] async fn request_spawn(&self, Parameters(args): Parameters) -> String { let log = format!("{args:?}"); let name = args.name.clone(); @@ -284,10 +283,8 @@ impl ManagerServer { .await } - #[tool( - description = "Stop a sub-agent container (graceful). The state dir is kept; \ - recreating reuses prior config + Claude credentials." - )] + #[tool(description = "Stop a sub-agent container (graceful). The state dir is kept; \ + recreating reuses prior config + Claude credentials.")] async fn kill(&self, Parameters(args): Parameters) -> String { let log = format!("{args:?}"); let name = args.name.clone(); @@ -295,9 +292,7 @@ impl ManagerServer { let req = hive_sh4re::ManagerRequest::Kill { name: args.name }; match client::request::<_, hive_sh4re::ManagerResponse>(&self.socket, &req).await { Ok(hive_sh4re::ManagerResponse::Ok) => format!("killed {name}"), - Ok(hive_sh4re::ManagerResponse::Err { message }) => { - format!("kill failed: {message}") - } + Ok(hive_sh4re::ManagerResponse::Err { message }) => format!("kill failed: {message}"), Ok(other) => format!("kill unexpected response: {other:?}"), Err(e) => format!("kill transport error: {e:#}"), } @@ -305,11 +300,9 @@ impl ManagerServer { .await } - #[tool( - description = "Submit a config change for operator approval. Pass the agent name \ + #[tool(description = "Submit a config change for operator approval. Pass the agent name \ (e.g. `alice` or `hm1nd` for the manager's own config) and a commit sha in that \ - agent's proposed config repo. On approval hive-c0re rebuilds the container." - )] + agent's proposed config repo. On approval hive-c0re rebuilds the container.")] async fn request_apply_commit( &self, Parameters(args): Parameters, @@ -317,27 +310,22 @@ impl ManagerServer { let log = format!("{args:?}"); let agent = args.agent.clone(); let commit_ref = args.commit_ref.clone(); - run_tool_envelope( - "request_apply_commit", - log, - self.status_line(), - async move { - let req = hive_sh4re::ManagerRequest::RequestApplyCommit { - agent: args.agent, - commit_ref: args.commit_ref, - }; - match client::request::<_, hive_sh4re::ManagerResponse>(&self.socket, &req).await { - Ok(hive_sh4re::ManagerResponse::Ok) => { - format!("apply approval queued for {agent} @ {commit_ref}") - } - Ok(hive_sh4re::ManagerResponse::Err { message }) => { - format!("request_apply_commit failed: {message}") - } - Ok(other) => format!("request_apply_commit unexpected response: {other:?}"), - Err(e) => format!("request_apply_commit transport error: {e:#}"), + run_tool_envelope("request_apply_commit", log, self.status_line(), async move { + let req = hive_sh4re::ManagerRequest::RequestApplyCommit { + agent: args.agent, + commit_ref: args.commit_ref, + }; + match client::request::<_, hive_sh4re::ManagerResponse>(&self.socket, &req).await { + Ok(hive_sh4re::ManagerResponse::Ok) => { + format!("apply approval queued for {agent} @ {commit_ref}") } - }, - ) + Ok(hive_sh4re::ManagerResponse::Err { message }) => { + format!("request_apply_commit failed: {message}") + } + Ok(other) => format!("request_apply_commit unexpected response: {other:?}"), + Err(e) => format!("request_apply_commit transport error: {e:#}"), + } + }) .await } } @@ -362,8 +350,15 @@ pub const SERVER_NAME: &str = "hyperhive"; /// (`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", "Read", "TodoWrite", "Write"]; +pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &[ + "Bash", + "Edit", + "Glob", + "Grep", + "Read", + "TodoWrite", + "Write", +]; /// Which MCP tool surface to advertise via `--allowedTools`. The agent /// list is the strict subset of the manager list, so we just thread the @@ -381,13 +376,7 @@ pub enum Flavor { pub fn allowed_mcp_tools(flavor: Flavor) -> Vec { let names: &[&str] = match flavor { Flavor::Agent => &["send", "recv"], - Flavor::Manager => &[ - "send", - "recv", - "request_spawn", - "kill", - "request_apply_commit", - ], + Flavor::Manager => &["send", "recv", "request_spawn", "kill", "request_apply_commit"], }; names .iter() @@ -399,10 +388,7 @@ pub fn allowed_mcp_tools(flavor: Flavor) -> Vec { /// both the built-ins and the MCP surface. #[must_use] pub fn allowed_tools_arg(flavor: Flavor) -> String { - let mut all: Vec = ALLOWED_BUILTIN_TOOLS - .iter() - .map(|s| (*s).to_owned()) - .collect(); + let mut all: Vec = ALLOWED_BUILTIN_TOOLS.iter().map(|s| (*s).to_owned()).collect(); all.extend(allowed_mcp_tools(flavor)); all.join(",") } diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index d6cd09f9..1997265e 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -323,9 +323,9 @@ async fn events_stream( let rx = state.bus.subscribe(); // Drop a "hello" note into the bus so every new subscriber sees at // least one event immediately and can clear the connecting placeholder. - state.bus.emit(crate::events::LiveEvent::Note( - "live stream attached".into(), - )); + state + .bus + .emit(crate::events::LiveEvent::Note("live stream attached".into())); let stream = BroadcastStream::new(rx).filter_map(|res| { let ev = res.ok()?; let json = serde_json::to_string(&ev).ok()?; @@ -356,7 +356,10 @@ struct CodeForm { code: String, } -async fn post_login_code(State(state): State, Form(form): Form) -> Response { +async fn post_login_code( + State(state): State, + Form(form): Form, +) -> Response { let session = state.session.lock().unwrap().clone(); let Some(session) = session else { return error_response("no login session running"); diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index e2cfd366..5b0dd7cb 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -6,9 +6,7 @@ use std::sync::Arc; use anyhow::{Result, bail}; -use hive_sh4re::{ - ApprovalKind, ApprovalStatus, HelperEvent, MANAGER_AGENT, Message, SYSTEM_SENDER, -}; +use hive_sh4re::{ApprovalKind, ApprovalStatus, HelperEvent, MANAGER_AGENT, Message, SYSTEM_SENDER}; use crate::coordinator::{Coordinator, TransientKind}; use crate::lifecycle::{self, MANAGER_NAME}; @@ -85,11 +83,7 @@ pub async fn approve(coord: Arc, id: i64) -> Result<()> { } } -fn finish_approval( - coord: &Coordinator, - approval: &hive_sh4re::Approval, - result: Result<()>, -) -> Result<()> { +fn finish_approval(coord: &Coordinator, approval: &hive_sh4re::Approval, result: Result<()>) -> Result<()> { match result { Ok(()) => { notify_manager( diff --git a/hive-c0re/src/auto_update.rs b/hive-c0re/src/auto_update.rs index 646609d7..bfcc464a 100644 --- a/hive-c0re/src/auto_update.rs +++ b/hive-c0re/src/auto_update.rs @@ -72,6 +72,7 @@ pub async fn rebuild_agent(coord: &Arc, name: &str, current_rev: &s Ok(()) } + /// Auto-create the manager container on startup if it isn't already there. /// hive-c0re manages hm1nd end-to-end (Phase 8 follow-up): operators no /// longer declare `containers.hm1nd` in their host NixOS config. Bypasses diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 3602119e..3a5337b2 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -75,11 +75,7 @@ pub fn is_manager(name: &str) -> bool { /// extends. Manager → `manager`; everyone else → `agent-base`. #[must_use] pub fn flake_base(name: &str) -> &'static str { - if is_manager(name) { - "manager" - } else { - "agent-base" - } + if is_manager(name) { "manager" } else { "agent-base" } } fn validate(name: &str) -> Result<()> { @@ -243,10 +239,13 @@ pub async fn setup_applied( modules = [ ./agent.nix {{ - programs.git.config.user = {{ - name = "{name}"; - email = "{name}@hyperhive"; - }}; + environment.etc."gitconfig".text = '' + [user] + name = {name} + email = {name}@hyperhive + [init] + defaultBranch = main + ''; systemd.services.{service}.environment = {{ HIVE_PORT = "{port}"; HIVE_LABEL = "{name}"; diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index a5a534d8..2d13954a 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -218,9 +218,7 @@ pub enum ManagerRequest { Status, /// Operator-injected message TO the manager (from the manager's own web /// UI). Same shape as `AgentRequest::OperatorMsg`. - OperatorMsg { - body: String, - }, + OperatorMsg { body: String }, /// Submit a spawn request for the user to approve. On approval the host /// creates and starts the container. Brand-new agent names only — if an /// agent of the same name already exists, the approval will fail. diff --git a/nix/templates/agent-base.nix b/nix/templates/agent-base.nix index 4fb5345e..4683023a 100644 --- a/nix/templates/agent-base.nix +++ b/nix/templates/agent-base.nix @@ -1,18 +1,31 @@ { pkgs, ... }: { - imports = [ ./harness-base.nix ]; + boot.isNspawnContainer = true; + + nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ]; + + environment.systemPackages = with pkgs; [ + hyperhive + claude-code + bashInteractive + git + coreutils-full + ]; + # claude's Bash tool refuses to run without a POSIX shell + $SHELL set. + environment.variables.SHELL = "${pkgs.bashInteractive}/bin/bash"; systemd.services.hive-ag3nt = { description = "hive-ag3nt harness"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; - # systemd units get a minimal PATH by default and don't inherit - # `environment.systemPackages`. Pointing at `/run/current-system/sw` - # gives the harness (and any tools claude shells out to via Bash) - # access to everything declared in `systemPackages` — including - # anything an agent adds to its own `agent.nix` — without having to - # touch the service definition. - path = [ "/run/current-system/sw" ]; + # The harness shells out to `claude` (turn loop + login flow). systemd + # units get a minimal PATH by default, so we have to put claude-code on + # it explicitly even though it's in environment.systemPackages above. + # bash is on PATH so claude's Bash tool can spawn `$SHELL`. + path = [ + pkgs.claude-code + pkgs.bashInteractive + ]; environment.SHELL = "${pkgs.bashInteractive}/bin/bash"; serviceConfig = { ExecStart = "${pkgs.hyperhive}/bin/hive-ag3nt serve"; @@ -20,4 +33,6 @@ RestartSec = 2; }; }; + + system.stateVersion = "25.11"; } diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix deleted file mode 100644 index 81402444..00000000 --- a/nix/templates/harness-base.nix +++ /dev/null @@ -1,40 +0,0 @@ -{ pkgs, ... }: -{ - # Shared scaffolding for any hyperhive harness container — both - # sub-agents (`agent-base.nix`) and the manager (`manager.nix`) extend - # this. The systemd service that actually runs the harness binary - # differs per role and lives in the child module. - - boot.isNspawnContainer = true; - - nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ]; - - environment.systemPackages = with pkgs; [ - hyperhive - claude-code - bashInteractive - coreutils-full - ]; - - # Git is needed by claude's Bash tool (for the agent <-> manager config - # request flow) and by hive-c0re's own setup_applied / setup_proposed. - # `programs.git.enable` installs the binary + manages `/etc/gitconfig` - # declaratively so the inline module in `applied//flake.nix` can - # override `user.name` / `user.email` per agent without fighting a raw - # `environment.etc."gitconfig"` block. - programs.git = { - enable = true; - config = { - user = { - name = "hyperhive"; - email = "hyperhive@local"; - }; - init.defaultBranch = "main"; - }; - }; - - # claude's Bash tool refuses to run without a POSIX shell + $SHELL set. - environment.variables.SHELL = "${pkgs.bashInteractive}/bin/bash"; - - system.stateVersion = "25.11"; -} diff --git a/nix/templates/manager.nix b/nix/templates/manager.nix index f88e2fba..0859e0b1 100644 --- a/nix/templates/manager.nix +++ b/nix/templates/manager.nix @@ -1,11 +1,27 @@ { pkgs, ... }: { - imports = [ ./harness-base.nix ]; + boot.isNspawnContainer = true; + + nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ]; + + environment.systemPackages = with pkgs; [ + hyperhive + claude-code + bashInteractive + git + coreutils-full + ]; + # claude's Bash tool refuses to run without a POSIX shell + $SHELL set. + environment.variables.SHELL = "${pkgs.bashInteractive}/bin/bash"; + + environment.etc."gitconfig".text = '' + [user] + name = hm1nd + email = hm1nd@hyperhive + [init] + defaultBranch = main + ''; - # HIVE_PORT/HIVE_LABEL/gitconfig are also injected by the generated - # `applied/hm1nd/flake.nix` (see `lifecycle::setup_applied`); the values - # here are the base config so the container stays sensible if anyone - # ever evaluates `nixosConfigurations.manager` standalone. systemd.services.hive-m1nd = { description = "hive-m1nd manager harness"; wantedBy = [ "multi-user.target" ]; @@ -13,17 +29,20 @@ environment = { HIVE_PORT = "8000"; HIVE_LABEL = "hm1nd"; - SHELL = "${pkgs.bashInteractive}/bin/bash"; }; - # See note in agent-base.nix — `/run/current-system/sw` makes the - # harness service PATH track `environment.systemPackages` so anything - # an agent adds to its own `agent.nix` is visible without editing the - # service definition. - path = [ "/run/current-system/sw" ]; + # See note in agent-base.nix — `claude` and a POSIX shell have to be on + # the service PATH explicitly for the harness + claude's Bash tool. + path = [ + pkgs.claude-code + pkgs.bashInteractive + ]; + environment.SHELL = "${pkgs.bashInteractive}/bin/bash"; serviceConfig = { ExecStart = "${pkgs.hyperhive}/bin/hive-m1nd serve"; Restart = "on-failure"; RestartSec = 2; }; }; + + system.stateVersion = "25.11"; }