harness: declarative claude plugin marketplaces

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.
This commit is contained in:
müde 2026-05-17 01:36:18 +02:00
parent 608de57924
commit 597351ca4e
2 changed files with 66 additions and 0 deletions

View file

@ -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 <source>`. 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 `<plugin>@<marketplace>` 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<String> = 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")

View file

@ -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 <source>`
(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