From 597351ca4e0221d7237ea90acbea6a903cbd9ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Sun, 17 May 2026 01:36:18 +0200 Subject: [PATCH] harness: declarative claude plugin marketplaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit new `hyperhive.claudeMarketplaces` option (list of strings — URL, path, or github:owner/repo). harness boot adds each via `claude plugin marketplace add` before updating + installing the configured plugins, so specs like `foo@some-marketplace` resolve on a fresh container. idempotent: 'already exists' stderr is treated as success. --- hive-ag3nt/src/plugins.rs | 48 ++++++++++++++++++++++++++++++++++ nix/templates/harness-base.nix | 18 +++++++++++++ 2 files changed, 66 insertions(+) diff --git a/hive-ag3nt/src/plugins.rs b/hive-ag3nt/src/plugins.rs index 00254f9..7530656 100644 --- a/hive-ag3nt/src/plugins.rs +++ b/hive-ag3nt/src/plugins.rs @@ -18,6 +18,53 @@ use tokio::process::Command; use crate::client; const PLUGINS_PATH: &str = "/etc/hyperhive/claude-plugins.json"; +const MARKETPLACES_PATH: &str = "/etc/hyperhive/claude-marketplaces.json"; + +/// Add every marketplace from `/etc/hyperhive/claude-marketplaces.json` +/// via `claude plugin marketplace add `. Idempotent: re-add of +/// an existing marketplace is treated as success (claude prints an +/// "already exists" message and exits non-zero on some versions). +/// Required before any `@` install can resolve. +async fn add_marketplaces() { + let raw = match tokio::fs::read_to_string(MARKETPLACES_PATH).await { + Ok(s) => s, + Err(_) => return, + }; + let sources: Vec = match serde_json::from_str(&raw) { + Ok(v) => v, + Err(e) => { + tracing::warn!(path = MARKETPLACES_PATH, error = ?e, "claude-marketplaces spec parse failed; skipping"); + return; + } + }; + for source in sources { + match Command::new("claude") + .args(["plugin", "marketplace", "add", &source]) + .output() + .await + { + Ok(out) if out.status.success() => { + tracing::info!(source = %source, "claude plugin marketplace add ok"); + } + Ok(out) => { + let stderr = String::from_utf8_lossy(&out.stderr); + if stderr.contains("already") { + tracing::debug!(source = %source, "marketplace already added"); + } else { + tracing::warn!( + source = %source, + status = ?out.status, + stderr = %stderr, + "claude plugin marketplace add failed (non-fatal)", + ); + } + } + Err(e) => { + tracing::warn!(source = %source, error = ?e, "claude plugin marketplace add spawn failed"); + } + } + } +} /// Update all configured plugin marketplaces. Non-fatal — logs a warning /// on failure but does not abort the install sequence. @@ -64,6 +111,7 @@ pub async fn install_configured(socket: &Path, notify_recipient: Option<&str>) { if specs.is_empty() { return; } + add_marketplaces().await; update_marketplaces().await; for spec in specs { match Command::new("claude") diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index cd23ae4..116ad47 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -96,6 +96,21 @@ ''; }; + options.hyperhive.claudeMarketplaces = lib.mkOption { + type = lib.types.listOf lib.types.str; + 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 ` + (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 = [ ]; @@ -122,6 +137,9 @@ environment.etc."hyperhive/claude-plugins.json".text = builtins.toJSON config.hyperhive.claudePlugins; + environment.etc."hyperhive/claude-marketplaces.json".text = + builtins.toJSON config.hyperhive.claudeMarketplaces; + boot.isNspawnContainer = true; # `claude-code` is unfree. Each per-agent container's nixosConfiguration