nix fmt + rustfmt sweep

This commit is contained in:
müde 2026-05-17 01:40:28 +02:00
parent 0cf120e9e9
commit 411cf86632
16 changed files with 171 additions and 133 deletions

View file

@ -204,10 +204,12 @@ async fn serve(
// responsiveness if recv() times out. // responsiveness if recv() times out.
tokio::time::sleep(interval).await; tokio::time::sleep(interval).await;
} }
Ok(AgentResponse::Ok Ok(
| AgentResponse::Status { .. } AgentResponse::Ok
| AgentResponse::Recent { .. } | AgentResponse::Status { .. }
| AgentResponse::QuestionQueued { .. }) => { | AgentResponse::Recent { .. }
| AgentResponse::QuestionQueued { .. },
) => {
tracing::warn!("recv produced unexpected response kind"); tracing::warn!("recv produced unexpected response kind");
} }
Ok(AgentResponse::Err { message }) => { Ok(AgentResponse::Err { message }) => {

View file

@ -76,11 +76,25 @@ async fn main() -> Result<()> {
)); ));
match initial { match initial {
LoginState::Online => { 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 => { LoginState::NeedsLogin => {
turn::wait_for_login(&claude_dir, login_state, poll_ms).await; 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
} }
} }
} }

View file

@ -23,15 +23,19 @@ const HISTORY_CAPACITY: usize = 2000;
/// Path to the persisted event db. Overridable via `HYPERHIVE_EVENTS_DB` /// Path to the persisted event db. Overridable via `HYPERHIVE_EVENTS_DB`
/// for dev / tests; otherwise derived from the agent's state dir. /// for dev / tests; otherwise derived from the agent's state dir.
fn events_db_path() -> PathBuf { fn events_db_path() -> PathBuf {
std::env::var_os("HYPERHIVE_EVENTS_DB") std::env::var_os("HYPERHIVE_EVENTS_DB").map_or_else(
.map_or_else(|| crate::paths::state_dir().join("hyperhive-events.sqlite"), PathBuf::from) || crate::paths::state_dir().join("hyperhive-events.sqlite"),
PathBuf::from,
)
} }
/// Path to the persisted model file. Overridable via `HYPERHIVE_MODEL_FILE` /// Path to the persisted model file. Overridable via `HYPERHIVE_MODEL_FILE`
/// for dev / tests; otherwise derived from the agent's state dir. /// for dev / tests; otherwise derived from the agent's state dir.
fn model_file_path() -> PathBuf { fn model_file_path() -> PathBuf {
std::env::var_os("HYPERHIVE_MODEL_FILE") std::env::var_os("HYPERHIVE_MODEL_FILE").map_or_else(
.map_or_else(|| crate::paths::state_dir().join("hyperhive-model"), PathBuf::from) || crate::paths::state_dir().join("hyperhive-model"),
PathBuf::from,
)
} }
fn load_model() -> Option<String> { fn load_model() -> Option<String> {

View file

@ -482,7 +482,10 @@ impl ManagerServer {
let (resp, retries) = self let (resp, retries) = self
.dispatch(hive_sh4re::ManagerRequest::Start { name: args.name }) .dispatch(hive_sh4re::ManagerRequest::Start { name: args.name })
.await; .await;
annotate_retries(format_ack(resp, "start", format!("started {name}")), retries) annotate_retries(
format_ack(resp, "start", format!("started {name}")),
retries,
)
}) })
.await .await
} }
@ -651,8 +654,7 @@ pub const SERVER_NAME: &str = "hyperhive";
/// state and silently evaporates on /compact or session reset — agents /// state and silently evaporates on /compact or session reset — agents
/// should plan in /state notes instead. Edit later as our trust model /// should plan in /state notes instead. Edit later as our trust model
/// evolves. /// evolves.
pub const ALLOWED_BUILTIN_TOOLS: &[&str] = pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &["Bash", "Edit", "Glob", "Grep", "Read", "Write"];
&["Bash", "Edit", "Glob", "Grep", "Read", "Write"];
/// Which MCP tool surface to advertise via `--allowedTools`. The agent /// Which MCP tool surface to advertise via `--allowedTools`. The agent
/// list is the strict subset of the manager list, so we just thread the /// list is the strict subset of the manager list, so we just thread the

View file

@ -108,8 +108,7 @@ pub async fn write_system_prompt(
mcp::Flavor::Agent => include_str!("../prompts/agent.md"), mcp::Flavor::Agent => include_str!("../prompts/agent.md"),
mcp::Flavor::Manager => include_str!("../prompts/manager.md"), mcp::Flavor::Manager => include_str!("../prompts/manager.md"),
}; };
let pronouns = let pronouns = std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned());
std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned());
let body = template let body = template
.replace("{label}", label) .replace("{label}", label)
.replace("{operator_pronouns}", &pronouns); .replace("{operator_pronouns}", &pronouns);

View file

@ -192,9 +192,7 @@ async fn run_apply_commit(
// re-lock to the proposal commit on the prepare_deploy step // re-lock to the proposal commit on the prepare_deploy step
// below. On build failure we roll main back to prev_main_sha so // below. On build failure we roll main back to prev_main_sha so
// a crash leaves the agent on its last-good tree. // a crash leaves the agent on its last-good tree.
if let Err(e) = if let Err(e) = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &proposal_ref).await {
lifecycle::git_update_ref(applied_dir, "refs/heads/main", &proposal_ref).await
{
return ( return (
Err(anyhow::anyhow!("ff main to {proposal_ref}: {e:#}")), Err(anyhow::anyhow!("ff main to {proposal_ref}: {e:#}")),
None, None,
@ -204,20 +202,14 @@ async fn run_apply_commit(
// main is ahead; working tree didn't sync. Roll main back to // main is ahead; working tree didn't sync. Roll main back to
// keep the two consistent before bailing. // keep the two consistent before bailing.
let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await; let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await;
return ( return (Err(anyhow::anyhow!("read-tree to main: {e:#}")), None);
Err(anyhow::anyhow!("read-tree to main: {e:#}")),
None,
);
} }
// Phase 1 of the meta two-phase deploy: relock without committing. // Phase 1 of the meta two-phase deploy: relock without committing.
if let Err(e) = crate::meta::prepare_deploy(&approval.agent).await { 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_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await;
let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await; let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await;
return ( return (Err(anyhow::anyhow!("meta prepare_deploy: {e:#}")), None);
Err(anyhow::anyhow!("meta prepare_deploy: {e:#}")),
None,
);
} }
// Container-level rebuild against meta#<name>. // Container-level rebuild against meta#<name>.
@ -379,13 +371,9 @@ pub async fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<()
{ {
let tag_name = format!("denied/{id}"); let tag_name = format!("denied/{id}");
let body = note.unwrap_or("").to_owned(); let body = note.unwrap_or("").to_owned();
if let Err(e) = lifecycle::git_tag_annotated( if let Err(e) =
&applied_dir, lifecycle::git_tag_annotated(&applied_dir, &tag_name, &proposal_ref, &body)
&tag_name, .await
&proposal_ref,
&body,
)
.await
{ {
tracing::warn!(%id, error = ?e, "plant denied tag failed"); tracing::warn!(%id, error = ?e, "plant denied tag failed");
} else { } else {

View file

@ -18,11 +18,7 @@ pub struct AgentSocket {
pub handle: JoinHandle<()>, pub handle: JoinHandle<()>,
} }
pub fn start( pub fn start(agent: &str, socket_path: &Path, coord: Arc<Coordinator>) -> Result<AgentSocket> {
agent: &str,
socket_path: &Path,
coord: Arc<Coordinator>,
) -> Result<AgentSocket> {
let agent = agent.to_owned(); let agent = agent.to_owned();
if let Some(parent) = socket_path.parent() { if let Some(parent) = socket_path.parent() {
std::fs::create_dir_all(parent) std::fs::create_dir_all(parent)
@ -215,21 +211,29 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
let now = std::time::SystemTime::now(); let now = std::time::SystemTime::now();
let future = match now.checked_add(std::time::Duration::from_secs(*seconds)) { let future = match now.checked_add(std::time::Duration::from_secs(*seconds)) {
Some(t) => t, Some(t) => t,
None => return AgentResponse::Err { None => {
message: format!("InSeconds overflow: {seconds}s exceeds system time range"), return AgentResponse::Err {
}, message: format!(
"InSeconds overflow: {seconds}s exceeds system time range"
),
};
}
}; };
let duration = match future.duration_since(std::time::UNIX_EPOCH) { let duration = match future.duration_since(std::time::UNIX_EPOCH) {
Ok(d) => d, Ok(d) => d,
Err(e) => return AgentResponse::Err { Err(e) => {
message: format!("system time before UNIX_EPOCH: {e}"), return AgentResponse::Err {
}, message: format!("system time before UNIX_EPOCH: {e}"),
};
}
}; };
match i64::try_from(duration.as_secs()) { match i64::try_from(duration.as_secs()) {
Ok(ts) => Ok(ts), Ok(ts) => Ok(ts),
Err(e) => return AgentResponse::Err { Err(e) => {
message: format!("unix timestamp exceeds i64 range: {e}"), return AgentResponse::Err {
}, message: format!("unix timestamp exceeds i64 range: {e}"),
};
}
} }
} }
ReminderTiming::At { unix_timestamp } => Ok(*unix_timestamp), ReminderTiming::At { unix_timestamp } => Ok(*unix_timestamp),

View file

@ -103,7 +103,13 @@ impl Approvals {
conn.execute( conn.execute(
"INSERT INTO approvals (agent, kind, commit_ref, requested_at, status, description) "INSERT INTO approvals (agent, kind, commit_ref, requested_at, status, description)
VALUES (?1, ?2, ?3, ?4, 'pending', ?5)", 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()) Ok(conn.last_insert_rowid())
} }
@ -164,8 +170,16 @@ impl Approvals {
/// approval so the caller can run the action and pass the agent name. /// approval so the caller can run the action and pass the agent name.
pub fn mark_approved(&self, id: i64) -> Result<Approval> { pub fn mark_approved(&self, id: i64) -> Result<Approval> {
let conn = self.conn.lock().unwrap(); let conn = self.conn.lock().unwrap();
let current: Option<(String, String, String, i64, String, Option<String>, Option<String>)> = let current: Option<(
conn.query_row( String,
String,
String,
i64,
String,
Option<String>,
Option<String>,
)> = conn
.query_row(
"SELECT agent, kind, commit_ref, requested_at, status, fetched_sha, description "SELECT agent, kind, commit_ref, requested_at, status, fetched_sha, description
FROM approvals WHERE id = ?1", FROM approvals WHERE id = ?1",
params![id], params![id],

View file

@ -1023,8 +1023,8 @@ async fn run_meta_update(coord: &Arc<crate::coordinator::Coordinator>, inputs: &
touched_agents touched_agents
}; };
let current_rev = crate::auto_update::current_flake_rev(&coord.hyperhive_flake) let current_rev =
.unwrap_or_default(); crate::auto_update::current_flake_rev(&coord.hyperhive_flake).unwrap_or_default();
// Sequential rebuild loop — the META_LOCK guards meta-side // Sequential rebuild loop — the META_LOCK guards meta-side
// races but parallel nix builds also serialise via nix-daemon, // races but parallel nix builds also serialise via nix-daemon,
// so sequential is just as fast in practice and keeps logs // so sequential is just as fast in practice and keeps logs

View file

@ -43,11 +43,7 @@ fn token_path(name: &str) -> PathBuf {
/// Probe whether `hive-forge` exists as a nixos-container. Cheap — /// Probe whether `hive-forge` exists as a nixos-container. Cheap —
/// `nixos-container list` is just a directory scan in /etc. /// `nixos-container list` is just a directory scan in /etc.
pub async fn is_present() -> bool { pub async fn is_present() -> bool {
let Ok(out) = Command::new("nixos-container") let Ok(out) = Command::new("nixos-container").arg("list").output().await else {
.arg("list")
.output()
.await
else {
return false; return false;
}; };
if !out.status.success() { if !out.status.success() {

View file

@ -873,7 +873,9 @@ async fn run(args: &[&str]) -> Result<()> {
// in the last few lines. // in the last few lines.
let stderr_cmdline = cmdline.clone(); let stderr_cmdline = cmdline.clone();
let stderr_tail: std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<String>>> = let stderr_tail: std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<String>>> =
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 stderr_tail_pump = stderr_tail.clone();
let pump_stderr = tokio::spawn(async move { let pump_stderr = tokio::spawn(async move {
let mut lines = BufReader::new(stderr).lines(); let mut lines = BufReader::new(stderr).lines();

View file

@ -228,8 +228,7 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
message: "update: hyperhive_flake has no canonical path".into(), message: "update: hyperhive_flake has no canonical path".into(),
}; };
}; };
let _guard = let _guard = coord.transient_guard(name, crate::coordinator::TransientKind::Rebuilding);
coord.transient_guard(name, crate::coordinator::TransientKind::Rebuilding);
let result = crate::auto_update::rebuild_agent(coord, name, &current_rev).await; let result = crate::auto_update::rebuild_agent(coord, name, &current_rev).await;
drop(_guard); drop(_guard);
match result { match result {
@ -362,24 +361,20 @@ async fn submit_apply_commit(
) )
.map_err(|e| anyhow::anyhow!("queue approval row: {e:#}"))?; .map_err(|e| anyhow::anyhow!("queue approval row: {e:#}"))?;
let tag = format!("proposal/{id}"); let tag = format!("proposal/{id}");
let sha = match crate::lifecycle::git_fetch_to_tag( let sha =
&applied_dir, match crate::lifecycle::git_fetch_to_tag(&applied_dir, &proposed_dir, commit_ref, &tag)
&proposed_dir, .await
commit_ref, {
&tag, Ok(s) => s,
) Err(e) => {
.await // Surface the failure on the approval row so the
{ // dashboard reflects it instead of leaving a phantom
Ok(s) => s, // pending entry. The note doubles as the operator-visible
Err(e) => { // explanation of why the approval can't be approved.
// Surface the failure on the approval row so the let _ = coord.approvals.mark_failed(id, &format!("{e:#}"));
// dashboard reflects it instead of leaving a phantom return Err(anyhow::anyhow!("git_fetch_to_tag: {e:#}"));
// 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 coord
.approvals .approvals
.set_fetched_sha(id, &sha) .set_fetched_sha(id, &sha)

View file

@ -252,9 +252,7 @@ fn render_flake(
// Free-text operator string — escape backslash + double-quote so a // Free-text operator string — escape backslash + double-quote so a
// pronouns value like `he/him \ "rare"` round-trips into a valid // pronouns value like `he/him \ "rare"` round-trips into a valid
// nix string literal without breaking the flake. // nix string literal without breaking the flake.
let pronouns_escaped = operator_pronouns let pronouns_escaped = operator_pronouns.replace('\\', "\\\\").replace('"', "\\\"");
.replace('\\', "\\\\")
.replace('"', "\\\"");
let _ = writeln!( let _ = writeln!(
out, out,
" dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port }}:" " dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port }}:"

View file

@ -68,23 +68,24 @@ pub async fn run(coord: &Arc<Coordinator>) -> Result<()> {
if let Err(e) = migrate_applied_repo(name).await { if let Err(e) = migrate_applied_repo(name).await {
tracing::warn!(%name, error = ?e, "migration: applied repo rewrite failed"); tracing::warn!(%name, error = ?e, "migration: applied repo rewrite failed");
} }
if let Err(e) = lifecycle::setup_proposed(&Coordinator::agent_proposed_dir(name), name) if let Err(e) =
.await lifecycle::setup_proposed(&Coordinator::agent_proposed_dir(name), name).await
{ {
tracing::warn!(%name, error = ?e, "migration: setup_proposed failed"); tracing::warn!(%name, error = ?e, "migration: setup_proposed failed");
} }
} }
// Phase 3: meta repo. // Phase 3: meta repo.
let agents = lifecycle::agents_for_meta_listing().await.unwrap_or_default(); let agents = lifecycle::agents_for_meta_listing()
if let Err(e) =
meta::sync_agents(
&coord.hyperhive_flake,
coord.dashboard_port,
&coord.operator_pronouns,
&agents,
)
.await .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"); tracing::warn!(error = ?e, "migration: meta sync_agents failed");
} }
@ -109,7 +110,8 @@ pub async fn run(coord: &Arc<Coordinator>) -> Result<()> {
all_ok = false; 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") && let Err(e) = std::fs::write(repoint_marker(), b"done\n")
{ {
tracing::warn!(error = ?e, "migration: write repoint marker failed"); tracing::warn!(error = ?e, "migration: write repoint marker failed");
@ -142,8 +144,7 @@ async fn migrate_applied_repo(name: &str) -> Result<()> {
return Ok(()); return Ok(());
} }
let want = lifecycle::initial_flake_nix(); let want = lifecycle::initial_flake_nix();
std::fs::write(&flake_path, want) std::fs::write(&flake_path, want).with_context(|| format!("write {}", flake_path.display()))?;
.with_context(|| format!("write {}", flake_path.display()))?;
raw_git( raw_git(
&dir, &dir,
&[ &[

View file

@ -104,9 +104,10 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
} }
HostRequest::RequestSpawn { name } => { HostRequest::RequestSpawn { name } => {
tracing::info!(%name, "request_spawn"); tracing::info!(%name, "request_spawn");
let id = coord let id =
.approvals coord
.submit_kind(name, hive_sh4re::ApprovalKind::Spawn, "", None)?; .approvals
.submit_kind(name, hive_sh4re::ApprovalKind::Spawn, "", None)?;
tracing::info!(%id, %name, "spawn approval queued"); tracing::info!(%id, %name, "spawn approval queued");
HostResponse::success() HostResponse::success()
} }

View file

@ -1,4 +1,9 @@
{ pkgs, lib, config, ... }: {
pkgs,
lib,
config,
...
}:
{ {
# Shared scaffolding for any hyperhive harness container — both # Shared scaffolding for any hyperhive harness container — both
# sub-agents (`agent-base.nix`) and the manager (`manager.nix`) extend # sub-agents (`agent-base.nix`) and the manager (`manager.nix`) extend
@ -8,7 +13,10 @@
options.hyperhive.allowedRecipients = lib.mkOption { options.hyperhive.allowedRecipients = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
default = [ ]; default = [ ];
example = [ "alice" "manager" ]; example = [
"alice"
"manager"
];
description = '' description = ''
Names this agent is allowed to `send` to via Names this agent is allowed to `send` to via
`mcp__hyperhive__send`. Empty list (the default) means `mcp__hyperhive__send`. Empty list (the default) means
@ -29,37 +37,42 @@
}; };
options.hyperhive.extraMcpServers = lib.mkOption { options.hyperhive.extraMcpServers = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule { type = lib.types.attrsOf (
options = { lib.types.submodule {
command = lib.mkOption { options = {
type = lib.types.str; command = lib.mkOption {
description = "Absolute path to the MCP server binary. Use `\${pkgs.foo}/bin/foo` or `/run/current-system/sw/bin/foo`."; 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__<server-key>__` at build time.
'';
};
}; };
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__<server-key>__` at build time.
'';
};
};
});
default = { }; default = { };
example = lib.literalExpression '' example = lib.literalExpression ''
{ {
@ -120,7 +133,10 @@
options.hyperhive.claudePlugins = lib.mkOption { options.hyperhive.claudePlugins = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
default = [ ]; default = [ ];
example = [ "formatter@my-marketplace" "thinking-tools@anthropics" ]; example = [
"formatter@my-marketplace"
"thinking-tools@anthropics"
];
description = '' description = ''
Claude Code plugins to install at harness boot. Each entry is Claude Code plugins to install at harness boot. Each entry is
passed verbatim to `claude plugin install <spec>` once per passed verbatim to `claude plugin install <spec>` once per
@ -134,8 +150,7 @@
}; };
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;
environment.etc."hyperhive/send-allow.json".text = environment.etc."hyperhive/send-allow.json".text =
builtins.toJSON config.hyperhive.allowedRecipients; builtins.toJSON config.hyperhive.allowedRecipients;
@ -181,7 +196,10 @@
Type = "oneshot"; Type = "oneshot";
RemainAfterExit = true; RemainAfterExit = true;
}; };
path = [ pkgs.tea pkgs.coreutils ]; path = [
pkgs.tea
pkgs.coreutils
];
script = '' script = ''
set -eu set -eu
CONFIG=/root/.config/tea/config.yml CONFIG=/root/.config/tea/config.yml