diff --git a/hive-ag3nt/src/bin/hive-ag3nt.rs b/hive-ag3nt/src/bin/hive-ag3nt.rs index ec17fc01..48def743 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -204,12 +204,10 @@ async fn serve( // responsiveness if recv() times out. tokio::time::sleep(interval).await; } - Ok( - AgentResponse::Ok - | AgentResponse::Status { .. } - | AgentResponse::Recent { .. } - | AgentResponse::QuestionQueued { .. }, - ) => { + Ok(AgentResponse::Ok + | AgentResponse::Status { .. } + | AgentResponse::Recent { .. } + | AgentResponse::QuestionQueued { .. }) => { tracing::warn!("recv produced unexpected response kind"); } Ok(AgentResponse::Err { message }) => { diff --git a/hive-ag3nt/src/bin/hive-m1nd.rs b/hive-ag3nt/src/bin/hive-m1nd.rs index d4bdd72f..89eab9ff 100644 --- a/hive-ag3nt/src/bin/hive-m1nd.rs +++ b/hive-ag3nt/src/bin/hive-m1nd.rs @@ -76,25 +76,11 @@ async fn main() -> Result<()> { )); match initial { LoginState::Online => { - serve( - &cli.socket, - Duration::from_millis(poll_ms), - bus, - &files, - turn_lock, - ) - .await + serve(&cli.socket, Duration::from_millis(poll_ms), bus, &files, turn_lock).await } LoginState::NeedsLogin => { turn::wait_for_login(&claude_dir, login_state, poll_ms).await; - serve( - &cli.socket, - Duration::from_millis(poll_ms), - bus, - &files, - turn_lock, - ) - .await + serve(&cli.socket, Duration::from_millis(poll_ms), bus, &files, turn_lock).await } } } diff --git a/hive-ag3nt/src/events.rs b/hive-ag3nt/src/events.rs index 979b5f4e..c8b0b08e 100644 --- a/hive-ag3nt/src/events.rs +++ b/hive-ag3nt/src/events.rs @@ -23,19 +23,15 @@ const HISTORY_CAPACITY: usize = 2000; /// Path to the persisted event db. Overridable via `HYPERHIVE_EVENTS_DB` /// for dev / tests; otherwise derived from the agent's state dir. fn events_db_path() -> PathBuf { - std::env::var_os("HYPERHIVE_EVENTS_DB").map_or_else( - || crate::paths::state_dir().join("hyperhive-events.sqlite"), - PathBuf::from, - ) + std::env::var_os("HYPERHIVE_EVENTS_DB") + .map_or_else(|| crate::paths::state_dir().join("hyperhive-events.sqlite"), PathBuf::from) } /// Path to the persisted model file. Overridable via `HYPERHIVE_MODEL_FILE` /// for dev / tests; otherwise derived from the agent's state dir. fn model_file_path() -> PathBuf { - std::env::var_os("HYPERHIVE_MODEL_FILE").map_or_else( - || crate::paths::state_dir().join("hyperhive-model"), - PathBuf::from, - ) + std::env::var_os("HYPERHIVE_MODEL_FILE") + .map_or_else(|| crate::paths::state_dir().join("hyperhive-model"), PathBuf::from) } fn load_model() -> Option { diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 542abfba..9fc3ddb2 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -482,10 +482,7 @@ impl ManagerServer { let (resp, retries) = self .dispatch(hive_sh4re::ManagerRequest::Start { name: args.name }) .await; - annotate_retries( - format_ack(resp, "start", format!("started {name}")), - retries, - ) + annotate_retries(format_ack(resp, "start", format!("started {name}")), retries) }) .await } @@ -654,7 +651,8 @@ pub const SERVER_NAME: &str = "hyperhive"; /// state and silently evaporates on /compact or session reset — agents /// should plan in /state notes instead. Edit later as our trust model /// evolves. -pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &["Bash", "Edit", "Glob", "Grep", "Read", "Write"]; +pub const ALLOWED_BUILTIN_TOOLS: &[&str] = + &["Bash", "Edit", "Glob", "Grep", "Read", "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 diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index 1ff442c5..c050ca34 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -108,7 +108,8 @@ pub async fn write_system_prompt( mcp::Flavor::Agent => include_str!("../prompts/agent.md"), mcp::Flavor::Manager => include_str!("../prompts/manager.md"), }; - let pronouns = std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned()); + let pronouns = + std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned()); let body = template .replace("{label}", label) .replace("{operator_pronouns}", &pronouns); diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index c3d70151..1e6da8d3 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -192,7 +192,9 @@ async fn run_apply_commit( // re-lock to the proposal commit on the prepare_deploy step // below. On build failure we roll main back to prev_main_sha so // a crash leaves the agent on its last-good tree. - if let Err(e) = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &proposal_ref).await { + if let Err(e) = + lifecycle::git_update_ref(applied_dir, "refs/heads/main", &proposal_ref).await + { return ( Err(anyhow::anyhow!("ff main to {proposal_ref}: {e:#}")), None, @@ -202,14 +204,20 @@ async fn run_apply_commit( // main is ahead; working tree didn't sync. Roll main back to // keep the two consistent before bailing. let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await; - return (Err(anyhow::anyhow!("read-tree to main: {e:#}")), None); + return ( + Err(anyhow::anyhow!("read-tree to main: {e:#}")), + None, + ); } // Phase 1 of the meta two-phase deploy: relock without committing. if let Err(e) = crate::meta::prepare_deploy(&approval.agent).await { let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await; let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await; - return (Err(anyhow::anyhow!("meta prepare_deploy: {e:#}")), None); + return ( + Err(anyhow::anyhow!("meta prepare_deploy: {e:#}")), + None, + ); } // Container-level rebuild against meta#. @@ -371,9 +379,13 @@ pub async fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<() { let tag_name = format!("denied/{id}"); let body = note.unwrap_or("").to_owned(); - if let Err(e) = - lifecycle::git_tag_annotated(&applied_dir, &tag_name, &proposal_ref, &body) - .await + if let Err(e) = lifecycle::git_tag_annotated( + &applied_dir, + &tag_name, + &proposal_ref, + &body, + ) + .await { tracing::warn!(%id, error = ?e, "plant denied tag failed"); } else { diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index 8ba71fc0..a4d33297 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -18,7 +18,11 @@ pub struct AgentSocket { pub handle: JoinHandle<()>, } -pub fn start(agent: &str, socket_path: &Path, coord: Arc) -> Result { +pub fn start( + agent: &str, + socket_path: &Path, + coord: Arc, +) -> Result { let agent = agent.to_owned(); if let Some(parent) = socket_path.parent() { std::fs::create_dir_all(parent) @@ -211,29 +215,21 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc) -> let now = std::time::SystemTime::now(); let future = match now.checked_add(std::time::Duration::from_secs(*seconds)) { Some(t) => t, - None => { - return AgentResponse::Err { - message: format!( - "InSeconds overflow: {seconds}s exceeds system time range" - ), - }; - } + None => return AgentResponse::Err { + message: format!("InSeconds overflow: {seconds}s exceeds system time range"), + }, }; let duration = match future.duration_since(std::time::UNIX_EPOCH) { Ok(d) => d, - Err(e) => { - return AgentResponse::Err { - message: format!("system time before UNIX_EPOCH: {e}"), - }; - } + Err(e) => return AgentResponse::Err { + message: format!("system time before UNIX_EPOCH: {e}"), + }, }; match i64::try_from(duration.as_secs()) { Ok(ts) => Ok(ts), - Err(e) => { - return AgentResponse::Err { - message: format!("unix timestamp exceeds i64 range: {e}"), - }; - } + Err(e) => return AgentResponse::Err { + message: format!("unix timestamp exceeds i64 range: {e}"), + }, } } ReminderTiming::At { unix_timestamp } => Ok(*unix_timestamp), diff --git a/hive-c0re/src/approvals.rs b/hive-c0re/src/approvals.rs index 2e9dad90..b5cbca99 100644 --- a/hive-c0re/src/approvals.rs +++ b/hive-c0re/src/approvals.rs @@ -103,13 +103,7 @@ impl Approvals { conn.execute( "INSERT INTO approvals (agent, kind, commit_ref, requested_at, status, description) VALUES (?1, ?2, ?3, ?4, 'pending', ?5)", - params![ - agent, - kind_to_str(kind), - commit_ref, - now_unix(), - description - ], + params![agent, kind_to_str(kind), commit_ref, now_unix(), description], )?; Ok(conn.last_insert_rowid()) } @@ -170,16 +164,8 @@ impl Approvals { /// approval so the caller can run the action and pass the agent name. pub fn mark_approved(&self, id: i64) -> Result { let conn = self.conn.lock().unwrap(); - let current: Option<( - String, - String, - String, - i64, - String, - Option, - Option, - )> = conn - .query_row( + let current: Option<(String, String, String, i64, String, Option, Option)> = + conn.query_row( "SELECT agent, kind, commit_ref, requested_at, status, fetched_sha, description FROM approvals WHERE id = ?1", params![id], diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 8617f05b..4a80218e 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -1023,8 +1023,8 @@ async fn run_meta_update(coord: &Arc, inputs: & touched_agents }; - let current_rev = - crate::auto_update::current_flake_rev(&coord.hyperhive_flake).unwrap_or_default(); + let current_rev = crate::auto_update::current_flake_rev(&coord.hyperhive_flake) + .unwrap_or_default(); // Sequential rebuild loop — the META_LOCK guards meta-side // races but parallel nix builds also serialise via nix-daemon, // so sequential is just as fast in practice and keeps logs diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index 228fd6ef..d9697763 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -43,7 +43,11 @@ fn token_path(name: &str) -> PathBuf { /// Probe whether `hive-forge` exists as a nixos-container. Cheap — /// `nixos-container list` is just a directory scan in /etc. pub async fn is_present() -> bool { - let Ok(out) = Command::new("nixos-container").arg("list").output().await else { + let Ok(out) = Command::new("nixos-container") + .arg("list") + .output() + .await + else { return false; }; if !out.status.success() { diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 345375ae..760d4b43 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -873,9 +873,7 @@ async fn run(args: &[&str]) -> Result<()> { // in the last few lines. let stderr_cmdline = cmdline.clone(); let stderr_tail: std::sync::Arc>> = - std::sync::Arc::new(std::sync::Mutex::new( - std::collections::VecDeque::with_capacity(32), - )); + std::sync::Arc::new(std::sync::Mutex::new(std::collections::VecDeque::with_capacity(32))); let stderr_tail_pump = stderr_tail.clone(); let pump_stderr = tokio::spawn(async move { let mut lines = BufReader::new(stderr).lines(); diff --git a/hive-c0re/src/manager_server.rs b/hive-c0re/src/manager_server.rs index c25cbb0b..5f90a573 100644 --- a/hive-c0re/src/manager_server.rs +++ b/hive-c0re/src/manager_server.rs @@ -228,7 +228,8 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc) -> ManagerResp message: "update: hyperhive_flake has no canonical path".into(), }; }; - let _guard = coord.transient_guard(name, crate::coordinator::TransientKind::Rebuilding); + let _guard = + coord.transient_guard(name, crate::coordinator::TransientKind::Rebuilding); let result = crate::auto_update::rebuild_agent(coord, name, ¤t_rev).await; drop(_guard); match result { @@ -361,20 +362,24 @@ async fn submit_apply_commit( ) .map_err(|e| anyhow::anyhow!("queue approval row: {e:#}"))?; let tag = format!("proposal/{id}"); - let sha = - match crate::lifecycle::git_fetch_to_tag(&applied_dir, &proposed_dir, commit_ref, &tag) - .await - { - Ok(s) => s, - Err(e) => { - // Surface the failure on the approval row so the - // dashboard reflects it instead of leaving a phantom - // pending entry. The note doubles as the operator-visible - // explanation of why the approval can't be approved. - let _ = coord.approvals.mark_failed(id, &format!("{e:#}")); - return Err(anyhow::anyhow!("git_fetch_to_tag: {e:#}")); - } - }; + let sha = match crate::lifecycle::git_fetch_to_tag( + &applied_dir, + &proposed_dir, + commit_ref, + &tag, + ) + .await + { + Ok(s) => s, + Err(e) => { + // Surface the failure on the approval row so the + // dashboard reflects it instead of leaving a phantom + // pending entry. The note doubles as the operator-visible + // explanation of why the approval can't be approved. + let _ = coord.approvals.mark_failed(id, &format!("{e:#}")); + return Err(anyhow::anyhow!("git_fetch_to_tag: {e:#}")); + } + }; coord .approvals .set_fetched_sha(id, &sha) diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index 8f5bb882..4aa79b10 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -252,7 +252,9 @@ fn render_flake( // Free-text operator string — escape backslash + double-quote so a // pronouns value like `he/him \ "rare"` round-trips into a valid // nix string literal without breaking the flake. - let pronouns_escaped = operator_pronouns.replace('\\', "\\\\").replace('"', "\\\""); + let pronouns_escaped = operator_pronouns + .replace('\\', "\\\\") + .replace('"', "\\\""); let _ = writeln!( out, " dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port }}:" diff --git a/hive-c0re/src/migrate.rs b/hive-c0re/src/migrate.rs index 9076aa52..bf5e9f26 100644 --- a/hive-c0re/src/migrate.rs +++ b/hive-c0re/src/migrate.rs @@ -68,24 +68,23 @@ pub async fn run(coord: &Arc) -> Result<()> { if let Err(e) = migrate_applied_repo(name).await { tracing::warn!(%name, error = ?e, "migration: applied repo rewrite failed"); } - if let Err(e) = - lifecycle::setup_proposed(&Coordinator::agent_proposed_dir(name), name).await + if let Err(e) = lifecycle::setup_proposed(&Coordinator::agent_proposed_dir(name), name) + .await { tracing::warn!(%name, error = ?e, "migration: setup_proposed failed"); } } // Phase 3: meta repo. - let agents = lifecycle::agents_for_meta_listing() + let agents = lifecycle::agents_for_meta_listing().await.unwrap_or_default(); + if let Err(e) = + meta::sync_agents( + &coord.hyperhive_flake, + coord.dashboard_port, + &coord.operator_pronouns, + &agents, + ) .await - .unwrap_or_default(); - if let Err(e) = meta::sync_agents( - &coord.hyperhive_flake, - coord.dashboard_port, - &coord.operator_pronouns, - &agents, - ) - .await { tracing::warn!(error = ?e, "migration: meta sync_agents failed"); } @@ -110,8 +109,7 @@ pub async fn run(coord: &Arc) -> Result<()> { all_ok = false; } } - if all_ok - && !names.is_empty() + if all_ok && !names.is_empty() && let Err(e) = std::fs::write(repoint_marker(), b"done\n") { tracing::warn!(error = ?e, "migration: write repoint marker failed"); @@ -144,7 +142,8 @@ async fn migrate_applied_repo(name: &str) -> Result<()> { return Ok(()); } let want = lifecycle::initial_flake_nix(); - std::fs::write(&flake_path, want).with_context(|| format!("write {}", flake_path.display()))?; + std::fs::write(&flake_path, want) + .with_context(|| format!("write {}", flake_path.display()))?; raw_git( &dir, &[ diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 639475e4..c11a8948 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -104,10 +104,9 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { } HostRequest::RequestSpawn { name } => { tracing::info!(%name, "request_spawn"); - let id = - coord - .approvals - .submit_kind(name, hive_sh4re::ApprovalKind::Spawn, "", None)?; + let id = coord + .approvals + .submit_kind(name, hive_sh4re::ApprovalKind::Spawn, "", None)?; tracing::info!(%id, %name, "spawn approval queued"); HostResponse::success() } diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index f6e7354a..116ad474 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -1,9 +1,4 @@ -{ - pkgs, - lib, - config, - ... -}: +{ pkgs, lib, config, ... }: { # Shared scaffolding for any hyperhive harness container — both # sub-agents (`agent-base.nix`) and the manager (`manager.nix`) extend @@ -13,10 +8,7 @@ options.hyperhive.allowedRecipients = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; - example = [ - "alice" - "manager" - ]; + example = [ "alice" "manager" ]; description = '' Names this agent is allowed to `send` to via `mcp__hyperhive__send`. Empty list (the default) means @@ -37,42 +29,37 @@ }; options.hyperhive.extraMcpServers = lib.mkOption { - type = lib.types.attrsOf ( - lib.types.submodule { - options = { - command = lib.mkOption { - type = lib.types.str; - description = "Absolute path to the MCP server binary. Use `\${pkgs.foo}/bin/foo` or `/run/current-system/sw/bin/foo`."; - }; - args = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = "Args passed to the MCP server binary."; - }; - env = lib.mkOption { - type = lib.types.attrsOf lib.types.str; - default = { }; - description = "Environment variables for the MCP server child process."; - }; - allowedTools = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ "*" ]; - example = [ - "send_message" - "join_room" - ]; - description = '' - Tool names this MCP server is auto-approved to call via - `--allowedTools`. Single entry `"*"` (the default) means - "every tool from this server" — convenient but trusting. - Tighten to a specific list when you only want a subset. - Names are bare (e.g. `send_message`); the harness prepends - `mcp____` at build time. - ''; - }; + type = lib.types.attrsOf (lib.types.submodule { + options = { + command = lib.mkOption { + type = lib.types.str; + description = "Absolute path to the MCP server binary. Use `\${pkgs.foo}/bin/foo` or `/run/current-system/sw/bin/foo`."; }; - } - ); + args = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Args passed to the MCP server binary."; + }; + env = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = "Environment variables for the MCP server child process."; + }; + allowedTools = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "*" ]; + example = [ "send_message" "join_room" ]; + description = '' + Tool names this MCP server is auto-approved to call via + `--allowedTools`. Single entry `"*"` (the default) means + "every tool from this server" — convenient but trusting. + Tighten to a specific list when you only want a subset. + Names are bare (e.g. `send_message`); the harness prepends + `mcp____` at build time. + ''; + }; + }; + }); default = { }; example = lib.literalExpression '' { @@ -111,32 +98,23 @@ options.hyperhive.claudeMarketplaces = lib.mkOption { type = lib.types.listOf lib.types.str; - default = [ "anthropics/claude-plugins-official" ]; - example = [ - "anthropics/claude-plugins-official" - "anthropics/claude-plugins-community" - ]; + default = [ ]; + example = [ "github:anthropics/claude-plugins-official" ]; description = '' Claude Code plugin marketplaces to add at harness boot. Each entry is passed to `claude plugin marketplace add ` - (`owner/repo`, full git URL, or local path). Idempotent — - re-adding an existing marketplace is treated as success. - Required before `hyperhive.claudePlugins` entries that - reference a marketplace (e.g. `foo@claude-plugins-official`). - Rendered to `/etc/hyperhive/claude-marketplaces.json`. - - Defaults to Anthropic's official marketplace; agents get it - out of the box without any per-agent.nix wiring. + (URL, path, or `github:owner/repo`). Idempotent — re-adding an + existing marketplace is treated as success. Required before + `hyperhive.claudePlugins` entries that reference a marketplace + (e.g. `foo@some-marketplace`). Rendered to + `/etc/hyperhive/claude-marketplaces.json`. ''; }; options.hyperhive.claudePlugins = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; - example = [ - "formatter@my-marketplace" - "thinking-tools@anthropics" - ]; + example = [ "formatter@my-marketplace" "thinking-tools@anthropics" ]; description = '' Claude Code plugins to install at harness boot. Each entry is passed verbatim to `claude plugin install ` once per @@ -150,7 +128,8 @@ }; config = { - environment.etc."hyperhive/extra-mcp.json".text = builtins.toJSON config.hyperhive.extraMcpServers; + environment.etc."hyperhive/extra-mcp.json".text = + builtins.toJSON config.hyperhive.extraMcpServers; environment.etc."hyperhive/send-allow.json".text = builtins.toJSON config.hyperhive.allowedRecipients; @@ -196,10 +175,7 @@ Type = "oneshot"; RemainAfterExit = true; }; - path = [ - pkgs.tea - pkgs.coreutils - ]; + path = [ pkgs.tea pkgs.coreutils ]; script = '' set -eu CONFIG=/root/.config/tea/config.yml