auto-install claude plugins at harness boot
new hyperhive.claudePlugins NixOS option (list of strings) rendered to /etc/hyperhive/claude-plugins.json. both hive-ag3nt and hive-m1nd shell out 'claude plugin install <spec>' for each entry once at startup before the turn loop opens. failures log a warning but don't abort boot.
This commit is contained in:
parent
8e7405db13
commit
6dd17864ac
5 changed files with 72 additions and 2 deletions
|
|
@ -6,7 +6,7 @@ use anyhow::Result;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use hive_ag3nt::events::{Bus, LiveEvent, TurnState};
|
use hive_ag3nt::events::{Bus, LiveEvent, TurnState};
|
||||||
use hive_ag3nt::login::{self, LoginState};
|
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};
|
use hive_sh4re::{AgentRequest, AgentResponse};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
|
@ -71,6 +71,7 @@ async fn main() -> Result<()> {
|
||||||
let login_state = Arc::new(Mutex::new(initial));
|
let login_state = Arc::new(Mutex::new(initial));
|
||||||
let bus = Bus::new();
|
let bus = Bus::new();
|
||||||
let files = turn::TurnFiles::prepare(&cli.socket, &label, mcp::Flavor::Agent).await?;
|
let files = turn::TurnFiles::prepare(&cli.socket, &label, mcp::Flavor::Agent).await?;
|
||||||
|
plugins::install_configured().await;
|
||||||
tokio::spawn(web_ui::serve(
|
tokio::spawn(web_ui::serve(
|
||||||
label,
|
label,
|
||||||
port,
|
port,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use anyhow::Result;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use hive_ag3nt::events::{Bus, LiveEvent, TurnState};
|
use hive_ag3nt::events::{Bus, LiveEvent, TurnState};
|
||||||
use hive_ag3nt::login::{self, LoginState};
|
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};
|
use hive_sh4re::{HelperEvent, ManagerRequest, ManagerResponse, SYSTEM_SENDER};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
|
@ -61,6 +61,7 @@ async fn main() -> Result<()> {
|
||||||
let login_state = Arc::new(Mutex::new(initial));
|
let login_state = Arc::new(Mutex::new(initial));
|
||||||
let bus = Bus::new();
|
let bus = Bus::new();
|
||||||
let files = turn::TurnFiles::prepare(&cli.socket, &label, mcp::Flavor::Manager).await?;
|
let files = turn::TurnFiles::prepare(&cli.socket, &label, mcp::Flavor::Manager).await?;
|
||||||
|
plugins::install_configured().await;
|
||||||
tokio::spawn(web_ui::serve(
|
tokio::spawn(web_ui::serve(
|
||||||
label,
|
label,
|
||||||
port,
|
port,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ pub mod events;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod login_session;
|
pub mod login_session;
|
||||||
pub mod mcp;
|
pub mod mcp;
|
||||||
|
pub mod plugins;
|
||||||
pub mod turn;
|
pub mod turn;
|
||||||
pub mod web_ui;
|
pub mod web_ui;
|
||||||
|
|
||||||
|
|
|
||||||
48
hive-ag3nt/src/plugins.rs
Normal file
48
hive-ag3nt/src/plugins.rs
Normal file
|
|
@ -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 <spec>` 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<String> = 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 <spec>` 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 = {
|
config = {
|
||||||
environment.etc."hyperhive/extra-mcp.json".text =
|
environment.etc."hyperhive/extra-mcp.json".text =
|
||||||
builtins.toJSON config.hyperhive.extraMcpServers;
|
builtins.toJSON config.hyperhive.extraMcpServers;
|
||||||
|
|
@ -89,6 +105,9 @@
|
||||||
environment.etc."hyperhive/send-allow.json".text =
|
environment.etc."hyperhive/send-allow.json".text =
|
||||||
builtins.toJSON config.hyperhive.allowedRecipients;
|
builtins.toJSON config.hyperhive.allowedRecipients;
|
||||||
|
|
||||||
|
environment.etc."hyperhive/claude-plugins.json".text =
|
||||||
|
builtins.toJSON config.hyperhive.claudePlugins;
|
||||||
|
|
||||||
boot.isNspawnContainer = true;
|
boot.isNspawnContainer = true;
|
||||||
|
|
||||||
# `claude-code` is unfree. Each per-agent container's nixosConfiguration
|
# `claude-code` is unfree. Each per-agent container's nixosConfiguration
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue