diff --git a/CLAUDE.md b/CLAUDE.md index c94311c8..1744cd06 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -222,11 +222,10 @@ line (broker's `count_pending`). When adding new tools (manager surface, notes/state, etc.), use `run_tool` and they pick up the envelope for free. **Tool whitelist** (see `ALLOWED_BUILTIN_TOOLS` in `hive-ag3nt::mcp`): -- Allowed built-ins: `Bash`, `Edit`, `Glob`, `Grep`, `Read`, `TodoWrite`, - `Write`. -- Denied by omission: `WebFetch`, `WebSearch`, `Task`, `NotebookEdit` — - no external egress, nested-agent spawning, or Jupyter handling until we - have a real policy story. +- 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 diff --git a/hive-ag3nt/src/bin/hive-ag3nt.rs b/hive-ag3nt/src/bin/hive-ag3nt.rs index fb993c38..406e661d 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -217,9 +217,6 @@ fn emit_turn_end(bus: &Bus, outcome: &turn::TurnOutcome) { /// about it and the MCP tools, and is expected to drive any further /// recv/send itself. fn format_wake_prompt(label: &str, from: &str, body: &str) -> String { - // Manager broker name. Lifecycle calls it `hm1nd` (container), broker - // calls it `manager`. Sub-agents address the manager via `manager`. - let manager = hive_sh4re::MANAGER_AGENT; format!( "You are hyperhive agent `{label}` in a multi-agent system.\n\ \n\ @@ -234,14 +231,6 @@ fn format_wake_prompt(label: &str, from: &str, body: &str) -> String { - `mcp__hyperhive__send(to, body)` — message a peer (by their name) \ or the operator (recipient `operator`, surfaces in the dashboard).\n\ \n\ - Need new packages, env vars, or other NixOS config for yourself? \ - You can't edit your own config directly — message the manager \ - (recipient `{manager}`) describing what you need. The manager \ - edits `/agents/{label}/config/agent.nix` on your behalf, commits, \ - and submits an approval that the operator can accept on the \ - dashboard; on approve hive-c0re rebuilds your container with the \ - new config.\n\ - \n\ Handle the inbox, then stop. Don't narrate intent — act." ) } diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index f73040f0..a91cb8f9 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -355,6 +355,7 @@ pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &[ "Edit", "Glob", "Grep", + "NotebookEdit", "Read", "TodoWrite", "Write", diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index d4f22ea9..c09e929e 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -91,12 +91,8 @@ async fn index(State(state): State) -> Html { (LoginState::NeedsLogin, None) => render_needs_login_idle(), (LoginState::NeedsLogin, Some(session)) => render_login_in_progress(&session), }; - let dashboard_port = std::env::var("HIVE_DASHBOARD_PORT") - .ok() - .and_then(|s| s.parse::().ok()) - .unwrap_or(7000); Html(format!( - "\n\n\n\n{label} // hyperhive\n{STYLE}\n\n\n
░▒▓█▓▒░  {label}  ░▒▓█▓▒░  hyperhive ag3nt  ░▒▓█▓▒░
\n

◆ {label} ◆ ↻ R3BU1LD

\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, )) } @@ -439,19 +435,6 @@ const STYLE: &str = r#" .btn:hover { background: rgba(204, 102, 255, 0.1); } .btn-login { color: var(--amber); border-color: var(--amber); } .btn-cancel { color: #ff6b6b; border-color: #ff6b6b; font-size: 0.85em; padding: 0.15em 0.6em; } - .btn-rebuild { - color: var(--amber); - border: 1px solid var(--amber); - padding: 0.15em 0.6em; - font-size: 0.55em; - font-family: inherit; - text-decoration: none; - letter-spacing: 0.1em; - margin-left: 0.6em; - vertical-align: middle; - cursor: pointer; - } - .btn-rebuild:hover { background: rgba(255, 184, 77, 0.1); } .btn-send { color: var(--green); border-color: var(--green); } .sendform { display: flex; gap: 0.6em; margin-top: 0.5em; } .sendform input { diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 5b0dd7cb..212e2549 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -48,7 +48,6 @@ pub async fn approve(coord: Arc, id: i64) -> Result<()> { &agent_dir, &applied_dir, &claude_dir, - coord.dashboard_port, ) .await } @@ -70,7 +69,6 @@ pub async fn approve(coord: Arc, id: i64) -> Result<()> { &proposed_dir, &applied_dir, &claude_dir, - coord_bg.dashboard_port, ) .await; coord_bg.clear_transient(&agent_bg); diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index e030f81a..df1f5996 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -65,7 +65,7 @@ async fn serve(stream: UnixStream, agent: String, broker: Arc) -> Result return Ok(()); } let resp = match serde_json::from_str::(line.trim()) { - Ok(req) => dispatch(&req, &agent, &broker).await, + Ok(req) => dispatch(&req, &agent, &broker), Err(e) => AgentResponse::Err { message: format!("parse error: {e}"), }, @@ -77,11 +77,7 @@ async fn serve(stream: UnixStream, agent: String, broker: Arc) -> Result } } -/// How long the long-poll `Recv` holds a connection open waiting for new -/// mail. Set well below typical TCP/proxy idle limits. -const RECV_LONG_POLL: std::time::Duration = std::time::Duration::from_secs(30); - -async fn dispatch(req: &AgentRequest, agent: &str, broker: &Broker) -> AgentResponse { +fn dispatch(req: &AgentRequest, agent: &str, broker: &Broker) -> AgentResponse { match req { AgentRequest::Send { to, body } => { match broker.send(&Message { @@ -95,7 +91,7 @@ async fn dispatch(req: &AgentRequest, agent: &str, broker: &Broker) -> AgentResp }, } } - AgentRequest::Recv => match broker.recv_blocking(agent, RECV_LONG_POLL).await { + AgentRequest::Recv => match broker.recv(agent) { Ok(Some(msg)) => AgentResponse::Message { from: msg.from, body: msg.body, diff --git a/hive-c0re/src/auto_update.rs b/hive-c0re/src/auto_update.rs index bfcc464a..e970fdae 100644 --- a/hive-c0re/src/auto_update.rs +++ b/hive-c0re/src/auto_update.rs @@ -64,7 +64,6 @@ pub async fn rebuild_agent(coord: &Arc, name: &str, current_rev: &s &agent_dir, &applied_dir, &claude_dir, - coord.dashboard_port, ) .await?; std::fs::write(rev_marker_path(name), current_rev) @@ -79,26 +78,8 @@ pub async fn rebuild_agent(coord: &Arc, name: &str, current_rev: &s /// the approval queue — manager is required infrastructure. Idempotent. pub async fn ensure_manager(coord: &Arc) -> Result<()> { let existing = lifecycle::list().await.unwrap_or_default(); - let current_rev = current_flake_rev(&coord.hyperhive_flake); if existing.iter().any(|c| c == MANAGER_NAME) { - // Container exists already. If it predates the unified lifecycle - // (no applied flake on disk) we must rebuild — otherwise it's - // running whatever the host-declarative config was at create - // time, with a wrong systemd unit and port. - let applied_flake = Coordinator::agent_applied_dir(MANAGER_NAME).join("flake.nix"); - if !applied_flake.exists() - && let Some(rev) = current_rev.as_ref() - { - tracing::warn!( - "manager container exists but no applied flake — forcing rebuild to migrate" - ); - let coord_clone = coord.clone(); - if let Err(e) = rebuild_agent(&coord_clone, MANAGER_NAME, rev).await { - tracing::warn!(error = ?e, "manager migration rebuild failed"); - } - } else { - tracing::debug!("manager container already present"); - } + tracing::debug!("manager container already present"); return Ok(()); } tracing::info!("manager container missing — spawning"); @@ -113,10 +94,9 @@ pub async fn ensure_manager(coord: &Arc) -> Result<()> { &proposed, &applied, &claude_dir, - coord.dashboard_port, ) .await?; - if let Some(rev) = current_rev { + if let Some(rev) = current_flake_rev(&coord.hyperhive_flake) { let _ = std::fs::write(rev_marker_path(MANAGER_NAME), rev); } Ok(()) diff --git a/hive-c0re/src/broker.rs b/hive-c0re/src/broker.rs index 4ffc4022..eb8a2ecf 100644 --- a/hive-c0re/src/broker.rs +++ b/hive-c0re/src/broker.rs @@ -100,42 +100,6 @@ impl Broker { Ok(u64::try_from(n.max(0)).unwrap_or(0)) } - /// Long-poll variant of `recv`: returns immediately if there's a - /// pending message; otherwise waits up to `timeout` for the broker to - /// emit a `Sent { to: recipient }` event, then retries the pop. Lets - /// agents react to new mail without polling their socket on a fixed - /// interval. - pub async fn recv_blocking( - &self, - recipient: &str, - timeout: std::time::Duration, - ) -> Result> { - if let Some(m) = self.recv(recipient)? { - return Ok(Some(m)); - } - let mut rx = self.subscribe(); - let deadline = tokio::time::Instant::now() + timeout; - loop { - let Some(remaining) = deadline.checked_duration_since(tokio::time::Instant::now()) - else { - return Ok(None); - }; - match tokio::time::timeout(remaining, rx.recv()).await { - Err(_) => return Ok(None), - // Channel lagged or closed — fall back to a single direct - // pop (in case we missed our notification while behind). - Ok(Err(_)) => return self.recv(recipient), - Ok(Ok(MessageEvent::Sent { to, .. })) if to == recipient => { - if let Some(m) = self.recv(recipient)? { - return Ok(Some(m)); - } - // Lost a race (concurrent recv elsewhere). Keep waiting. - } - Ok(Ok(_)) => {} - } - } - } - pub fn recv(&self, recipient: &str) -> Result> { let conn = self.conn.lock().unwrap(); let row: Option<(i64, String, String, String)> = conn diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index f2a0f24d..8267ac52 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -29,10 +29,6 @@ pub struct Coordinator { /// URL of the hyperhive flake (no fragment). Inlined into per-agent /// `flake.nix` files as `inputs.hyperhive.url`. pub hyperhive_flake: String, - /// TCP port the host's hive-c0re dashboard listens on. Inlined into - /// each per-agent flake so the agent's web UI can build the right - /// rebuild-button URL pointing back at the dashboard. - pub dashboard_port: u16, agents: Mutex>, /// Agents whose lifecycle action (currently just spawn) is in flight. /// Read by the dashboard to render a spinner; cleared when the action @@ -55,14 +51,13 @@ pub enum TransientKind { } impl Coordinator { - pub fn open(db_path: &Path, hyperhive_flake: String, dashboard_port: u16) -> Result { + pub fn open(db_path: &Path, hyperhive_flake: String) -> Result { let broker = Broker::open(db_path).context("open broker")?; let approvals = Approvals::open(db_path).context("open approvals")?; Ok(Self { broker: Arc::new(broker), approvals: Arc::new(approvals), hyperhive_flake, - dashboard_port, agents: Mutex::new(HashMap::new()), transient: Mutex::new(HashMap::new()), }) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 3a5337b2..110079e7 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -101,11 +101,10 @@ pub async fn spawn( proposed_dir: &Path, applied_dir: &Path, claude_dir: &Path, - dashboard_port: u16, ) -> Result<()> { validate(name)?; setup_proposed(proposed_dir, name).await?; - setup_applied(applied_dir, name, hyperhive_flake, dashboard_port).await?; + setup_applied(applied_dir, name, hyperhive_flake).await?; ensure_claude_dir(claude_dir)?; let container = container_name(name); let flake_ref = format!("{}#default", applied_dir.display()); @@ -146,10 +145,9 @@ pub async fn rebuild( agent_dir: &Path, applied_dir: &Path, claude_dir: &Path, - dashboard_port: u16, ) -> Result<()> { validate(name)?; - setup_applied(applied_dir, name, hyperhive_flake, dashboard_port).await?; + setup_applied(applied_dir, name, hyperhive_flake).await?; ensure_claude_dir(claude_dir)?; let container = container_name(name); let flake_ref = format!("{}#default", applied_dir.display()); @@ -207,12 +205,7 @@ pub async fn setup_proposed(proposed_dir: &Path, name: &str) -> Result<()> { /// Maintain the authoritative applied repo. Rewrites `flake.nix` every call /// (so a new hyperhive flake URL propagates on rebuild); seeds `agent.nix` /// only on first call. `apply_commit` overwrites `agent.nix` later. -pub async fn setup_applied( - applied_dir: &Path, - name: &str, - hyperhive_flake: &str, - dashboard_port: u16, -) -> Result<()> { +pub async fn setup_applied(applied_dir: &Path, name: &str, hyperhive_flake: &str) -> Result<()> { std::fs::create_dir_all(applied_dir) .with_context(|| format!("create {}", applied_dir.display()))?; @@ -249,7 +242,6 @@ pub async fn setup_applied( systemd.services.{service}.environment = {{ HIVE_PORT = "{port}"; HIVE_LABEL = "{name}"; - HIVE_DASHBOARD_PORT = "{dashboard_port}"; }}; }} ]; diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 9de9bee3..d27ac4cb 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -85,7 +85,7 @@ async fn main() -> Result<()> { db, dashboard_port, } => { - let coord = Arc::new(Coordinator::open(&db, hyperhive_flake, dashboard_port)?); + let coord = Arc::new(Coordinator::open(&db, hyperhive_flake)?); manager_server::start(coord.clone())?; // Auto-create the manager container if it isn't there yet. Block // on this — without hm1nd the system has no manager harness. diff --git a/hive-c0re/src/manager_server.rs b/hive-c0re/src/manager_server.rs index ce21bfdd..38c60057 100644 --- a/hive-c0re/src/manager_server.rs +++ b/hive-c0re/src/manager_server.rs @@ -69,8 +69,6 @@ async fn serve(stream: UnixStream, coord: Arc) -> Result<()> { } } -const MANAGER_RECV_LONG_POLL: std::time::Duration = std::time::Duration::from_secs(30); - async fn dispatch(req: &ManagerRequest, coord: &Coordinator) -> ManagerResponse { match req { ManagerRequest::Send { to, body } => match coord.broker.send(&Message { @@ -99,11 +97,7 @@ async fn dispatch(req: &ManagerRequest, coord: &Coordinator) -> ManagerResponse message: format!("{e:#}"), }, }, - ManagerRequest::Recv => match coord - .broker - .recv_blocking(MANAGER_AGENT, MANAGER_RECV_LONG_POLL) - .await - { + ManagerRequest::Recv => match coord.broker.recv(MANAGER_AGENT) { Ok(Some(msg)) => ManagerResponse::Message { from: msg.from, body: msg.body, diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 4e6705ff..afbc2f1d 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -72,7 +72,6 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { &proposed_dir, &applied_dir, &claude_dir, - coord.dashboard_port, ) .await { @@ -111,7 +110,6 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { &agent_dir, &applied_dir, &claude_dir, - coord.dashboard_port, ) .await?; HostResponse::success()