diff --git a/hive-ag3nt/src/bin/hive-ag3nt.rs b/hive-ag3nt/src/bin/hive-ag3nt.rs index 65dd0a1..8d6741b 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -6,7 +6,7 @@ use anyhow::Result; use clap::{Parser, Subcommand}; use hive_ag3nt::events::{Bus, LiveEvent, TurnState}; use hive_ag3nt::login::{self, LoginState}; -use hive_ag3nt::{DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, mcp, turn, web_ui}; +use hive_ag3nt::{DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, mcp, plugins, turn, web_ui}; use hive_sh4re::{AgentRequest, AgentResponse}; #[derive(Parser)] @@ -71,6 +71,7 @@ async fn main() -> Result<()> { let login_state = Arc::new(Mutex::new(initial)); let bus = Bus::new(); let files = turn::TurnFiles::prepare(&cli.socket, &label, mcp::Flavor::Agent).await?; + plugins::install_configured().await; tokio::spawn(web_ui::serve( label, port, diff --git a/hive-ag3nt/src/bin/hive-m1nd.rs b/hive-ag3nt/src/bin/hive-m1nd.rs index 42062e5..b56e9b4 100644 --- a/hive-ag3nt/src/bin/hive-m1nd.rs +++ b/hive-ag3nt/src/bin/hive-m1nd.rs @@ -10,7 +10,7 @@ use anyhow::Result; use clap::{Parser, Subcommand}; use hive_ag3nt::events::{Bus, LiveEvent, TurnState}; use hive_ag3nt::login::{self, LoginState}; -use hive_ag3nt::{DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, mcp, turn, web_ui}; +use hive_ag3nt::{DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, mcp, plugins, turn, web_ui}; use hive_sh4re::{HelperEvent, ManagerRequest, ManagerResponse, SYSTEM_SENDER}; #[derive(Parser)] @@ -61,6 +61,7 @@ async fn main() -> Result<()> { let login_state = Arc::new(Mutex::new(initial)); let bus = Bus::new(); let files = turn::TurnFiles::prepare(&cli.socket, &label, mcp::Flavor::Manager).await?; + plugins::install_configured().await; tokio::spawn(web_ui::serve( label, port, diff --git a/hive-ag3nt/src/lib.rs b/hive-ag3nt/src/lib.rs index 234939b..d29f393 100644 --- a/hive-ag3nt/src/lib.rs +++ b/hive-ag3nt/src/lib.rs @@ -6,6 +6,7 @@ pub mod events; pub mod login; pub mod login_session; pub mod mcp; +pub mod plugins; pub mod turn; pub mod web_ui; diff --git a/hive-ag3nt/src/plugins.rs b/hive-ag3nt/src/plugins.rs new file mode 100644 index 0000000..70d2045 --- /dev/null +++ b/hive-ag3nt/src/plugins.rs @@ -0,0 +1,48 @@ +//! Boot-time `claude plugin install` driver. Reads the list declared +//! via the `hyperhive.claudePlugins` NixOS option (rendered to +//! `/etc/hyperhive/claude-plugins.json` by the harness module) and +//! shells out `claude plugin install ` for each entry. Runs once +//! per harness boot before the turn loop; `claude plugin install` +//! is expected to be idempotent so reinstalling on each container +//! recreate is fine. Failures log a warning but do not abort boot — +//! we'd rather start without a plugin than refuse to serve. + +use tokio::process::Command; + +const PLUGINS_PATH: &str = "/etc/hyperhive/claude-plugins.json"; + +pub async fn install_configured() { + let raw = match tokio::fs::read_to_string(PLUGINS_PATH).await { + Ok(s) => s, + Err(_) => return, + }; + let specs: Vec = match serde_json::from_str(&raw) { + Ok(v) => v, + Err(e) => { + tracing::warn!(path = PLUGINS_PATH, error = ?e, "claude-plugins spec parse failed; skipping"); + return; + } + }; + for spec in specs { + match Command::new("claude") + .args(["plugin", "install", &spec]) + .output() + .await + { + Ok(out) if out.status.success() => { + tracing::info!(spec = %spec, "claude plugin install ok"); + } + Ok(out) => { + tracing::warn!( + spec = %spec, + status = ?out.status, + stderr = %String::from_utf8_lossy(&out.stderr), + "claude plugin install failed", + ); + } + Err(e) => { + tracing::warn!(spec = %spec, error = ?e, "claude plugin install spawn failed"); + } + } + } +} diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 2e682fa..c9b4b26 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -82,6 +82,22 @@ ''; }; + options.hyperhive.claudePlugins = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + 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 + container start, before the turn loop opens. `claude plugin + install` is expected to be idempotent, so reinstalling on every + boot is cheap. Failures log a warning but do not abort boot — a + missing plugin is preferable to a non-serving agent. Rendered to + `/etc/hyperhive/claude-plugins.json`; the harness reads it via + `plugins::install_configured`. + ''; + }; + config = { environment.etc."hyperhive/extra-mcp.json".text = builtins.toJSON config.hyperhive.extraMcpServers; @@ -89,6 +105,9 @@ environment.etc."hyperhive/send-allow.json".text = builtins.toJSON config.hyperhive.allowedRecipients; + environment.etc."hyperhive/claude-plugins.json".text = + builtins.toJSON config.hyperhive.claudePlugins; + boot.isNspawnContainer = true; # `claude-code` is unfree. Each per-agent container's nixosConfiguration