Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3f675e367 | ||
|
|
0058ed1e59 | ||
|
|
73684fb00a | ||
|
|
246dd19390 |
12 changed files with 377 additions and 118 deletions
86
flake.nix
86
flake.nix
|
|
@ -46,16 +46,32 @@
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
treefmt-eval = treefmt-nix.lib.evalModule pkgs treefmt-config;
|
treefmt-eval = treefmt-nix.lib.evalModule pkgs treefmt-config;
|
||||||
craneLib = crane.mkLib pkgs;
|
craneLib = crane.mkLib pkgs;
|
||||||
|
# Narrowed source tree the rust derivations consume.
|
||||||
|
# `cleanCargoSource` is crane's standard "everything cargo
|
||||||
|
# cares about" filter (Cargo.toml/Cargo.lock + *.rs). All
|
||||||
|
# non-rust runtime assets — branding + the claude system
|
||||||
|
# prompt template + claude-settings.json — live in the
|
||||||
|
# separate `hyperhive-assets` derivation (#555) and are
|
||||||
|
# loaded by the binaries at runtime from `$HIVE_ASSETS_DIR`.
|
||||||
|
# The unit tests in `hive-ag3nt::prompt` read the same
|
||||||
|
# `prompts/system.md` directly from the workspace tree at
|
||||||
|
# *test* runtime (via `env!("CARGO_MANIFEST_DIR")` — a
|
||||||
|
# compile-time string, no file open at compile), so the
|
||||||
|
# prompt template doesn't have to be in this fileset to
|
||||||
|
# keep `cargo test` honest. Net effect: tweaks to any
|
||||||
|
# non-`*.rs` / non-`Cargo.*` file (README, branding,
|
||||||
|
# nix modules, frontend tree, OR `hive-ag3nt/prompts/*`)
|
||||||
|
# do NOT bust this src hash, so the rust derivations
|
||||||
|
# stay fully cached.
|
||||||
|
cleanSrc = craneLib.cleanCargoSource ./.;
|
||||||
# Build the workspace's dependency tree once, cached as
|
# Build the workspace's dependency tree once, cached as
|
||||||
# its own derivation. `buildPackage` and `cargoClippy`
|
# its own derivation. `buildPackage` and `cargoClippy`
|
||||||
# both reuse this via `inherit cargoArtifacts;` so a
|
# both reuse this via `inherit cargoArtifacts;` so a
|
||||||
# workspace-only edit doesn't rebuild deps. Same
|
# workspace-only edit doesn't rebuild deps. All three
|
||||||
# `nativeBuildInputs` as the workspace build itself —
|
# derivations consume the same `cleanSrc` so the input
|
||||||
# build.rs runs during dep-build too (any deps with a
|
# hash stays consistent across the chain.
|
||||||
# build.rs need rsvg too if they transitively pull it
|
|
||||||
# in; harmless if they don't).
|
|
||||||
cargoArtifacts = craneLib.buildDepsOnly {
|
cargoArtifacts = craneLib.buildDepsOnly {
|
||||||
src = ./.;
|
src = cleanSrc;
|
||||||
# Workspace Cargo.toml is virtual (no `[package].name`),
|
# Workspace Cargo.toml is virtual (no `[package].name`),
|
||||||
# so crane can't auto-derive a name. Spell it out
|
# so crane can't auto-derive a name. Spell it out
|
||||||
# explicitly here and below — keeps the derivation name
|
# explicitly here and below — keeps the derivation name
|
||||||
|
|
@ -68,16 +84,16 @@
|
||||||
};
|
};
|
||||||
# Shared between buildDepsOnly + buildPackage + cargoClippy
|
# Shared between buildDepsOnly + buildPackage + cargoClippy
|
||||||
# so the three derivations see the same toolchain shape.
|
# so the three derivations see the same toolchain shape.
|
||||||
# librsvg: hive-c0re/build.rs invokes `rsvg-convert` to
|
|
||||||
# render branding/agent-configs.svg → PNG that the daemon
|
|
||||||
# `include_bytes!`s (#424); SVG stays source-of-truth.
|
|
||||||
# git: naersk used to auto-include it; crane is more
|
# git: naersk used to auto-include it; crane is more
|
||||||
# minimal, so we add it explicitly so hive-c0re's
|
# minimal, so we add it explicitly so hive-c0re's
|
||||||
# `lifecycle::tests::setup_proposed_*` (which shell out to
|
# `lifecycle::tests::setup_proposed_*` (which shell out to
|
||||||
# `git init` + commit) pass under `cargo test` in the
|
# `git init` + commit) pass under `cargo test` in the
|
||||||
# sandbox.
|
# sandbox.
|
||||||
|
# `librsvg` used to live here for `hive-c0re/build.rs`'s
|
||||||
|
# rsvg-convert call — that whole codepath moved into the
|
||||||
|
# `hyperhive-assets` derivation in #555, so the rust
|
||||||
|
# derivation no longer needs the dependency.
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
pkgs.librsvg
|
|
||||||
pkgs.git
|
pkgs.git
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -88,17 +104,30 @@
|
||||||
{
|
{
|
||||||
pkgs,
|
pkgs,
|
||||||
craneLib,
|
craneLib,
|
||||||
|
cleanSrc,
|
||||||
cargoArtifacts,
|
cargoArtifacts,
|
||||||
nativeBuildInputs,
|
nativeBuildInputs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
|
# Build the workspace binaries without running tests. Tests
|
||||||
|
# are run as a separate check (`checks.cargo-test`) that
|
||||||
|
# carries the `hyperhive-assets` build input — `hive-ag3nt::
|
||||||
|
# prompt::tests` reads the production prompt template at test
|
||||||
|
# runtime through `$HIVE_ASSETS_DIR`, so wiring the env var
|
||||||
|
# into the build phase here would make the prompt's hash a
|
||||||
|
# build input of `default` (defeats #555's cache goal: a
|
||||||
|
# prompt edit would still bust the binary derivation, even
|
||||||
|
# though no .rs file changed). Keeping tests in a separate
|
||||||
|
# check derivation localises the asset-rebuild blast radius
|
||||||
|
# to that one check — `nix flake check` still exercises them.
|
||||||
default = craneLib.buildPackage {
|
default = craneLib.buildPackage {
|
||||||
src = ./.;
|
src = cleanSrc;
|
||||||
inherit cargoArtifacts nativeBuildInputs;
|
inherit cargoArtifacts nativeBuildInputs;
|
||||||
pname = "hyperhive-workspace";
|
pname = "hyperhive-workspace";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
meta.description = "hyperhive workspace (hive-c0re, hive-ag3nt, hive-m1nd)";
|
meta.description = "hyperhive workspace (hive-c0re, hive-ag3nt, hive-m1nd)";
|
||||||
|
doCheck = false;
|
||||||
};
|
};
|
||||||
# Bundled browser assets — see ./nix/frontend.nix. Output is
|
# Bundled browser assets — see ./nix/frontend.nix. Output is
|
||||||
# $out/{dashboard,agent}/ which the Rust binaries serve via
|
# $out/{dashboard,agent}/ which the Rust binaries serve via
|
||||||
|
|
@ -106,6 +135,14 @@
|
||||||
frontend = pkgs.callPackage ./nix/frontend.nix {
|
frontend = pkgs.callPackage ./nix/frontend.nix {
|
||||||
branding-svg = ./branding/hyperhive.svg;
|
branding-svg = ./branding/hyperhive.svg;
|
||||||
};
|
};
|
||||||
|
# Static runtime assets the rust binaries read via
|
||||||
|
# `hive_sh4re::assets::*` (#555): branding/* + prompts/*,
|
||||||
|
# plus the rendered agent-configs.png. Split out of the
|
||||||
|
# rust derivation so a tweak to e.g. system.md doesn't bust
|
||||||
|
# the cargo cache. Build input of the `cargo-test` check but
|
||||||
|
# NOT of `packages.default`, so the binary derivation stays
|
||||||
|
# cached when a prompt edit ripples through.
|
||||||
|
assets = pkgs.callPackage ./nix/assets.nix { };
|
||||||
# Pre-built per-container system closures. Exposed as packages
|
# Pre-built per-container system closures. Exposed as packages
|
||||||
# so operators can `nix build .#agent-base-toplevel` (or wire
|
# so operators can `nix build .#agent-base-toplevel` (or wire
|
||||||
# them into their host system closure via the
|
# them into their host system closure via the
|
||||||
|
|
@ -137,6 +174,10 @@
|
||||||
# is applied (manager + agent containers both apply it via
|
# is applied (manager + agent containers both apply it via
|
||||||
# `mkContainer` further down).
|
# `mkContainer` further down).
|
||||||
hyperhive-frontend = self.packages.${prev.stdenv.hostPlatform.system}.frontend;
|
hyperhive-frontend = self.packages.${prev.stdenv.hostPlatform.system}.frontend;
|
||||||
|
# Static runtime assets (#555). Exposed alongside the binary
|
||||||
|
# so the harness module can wire $HIVE_ASSETS_DIR straight
|
||||||
|
# to `${pkgs.hyperhive-assets}/share/hyperhive`.
|
||||||
|
hyperhive-assets = self.packages.${prev.stdenv.hostPlatform.system}.assets;
|
||||||
};
|
};
|
||||||
claude-unstable =
|
claude-unstable =
|
||||||
final: prev:
|
final: prev:
|
||||||
|
|
@ -171,6 +212,7 @@
|
||||||
hive-c0re = import ./nix/modules/hive-c0re.nix {
|
hive-c0re = import ./nix/modules/hive-c0re.nix {
|
||||||
hyperhivePackage = system: self.packages.${system}.default;
|
hyperhivePackage = system: self.packages.${system}.default;
|
||||||
hyperhiveFrontend = system: self.packages.${system}.frontend;
|
hyperhiveFrontend = system: self.packages.${system}.frontend;
|
||||||
|
hyperhiveAssets = system: self.packages.${system}.assets;
|
||||||
hyperhiveFlake = "${self}";
|
hyperhiveFlake = "${self}";
|
||||||
# Per-container toplevels — wired into `system.extraDependencies`
|
# Per-container toplevels — wired into `system.extraDependencies`
|
||||||
# when `services.hive-c0re.preBuildAgentTemplates` is on so the
|
# when `services.hive-c0re.preBuildAgentTemplates` is on so the
|
||||||
|
|
@ -237,8 +279,11 @@
|
||||||
|
|
||||||
checks = forAllSystems (
|
checks = forAllSystems (
|
||||||
{
|
{
|
||||||
|
pkgs,
|
||||||
|
system,
|
||||||
treefmt-eval,
|
treefmt-eval,
|
||||||
craneLib,
|
craneLib,
|
||||||
|
cleanSrc,
|
||||||
cargoArtifacts,
|
cargoArtifacts,
|
||||||
nativeBuildInputs,
|
nativeBuildInputs,
|
||||||
...
|
...
|
||||||
|
|
@ -254,12 +299,29 @@
|
||||||
# separator, which is why the old wiring went through
|
# separator, which is why the old wiring went through
|
||||||
# overrideAttrs).
|
# overrideAttrs).
|
||||||
clippy = craneLib.cargoClippy {
|
clippy = craneLib.cargoClippy {
|
||||||
src = ./.;
|
src = cleanSrc;
|
||||||
inherit cargoArtifacts nativeBuildInputs;
|
inherit cargoArtifacts nativeBuildInputs;
|
||||||
pname = "hyperhive-workspace";
|
pname = "hyperhive-workspace";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings";
|
cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings";
|
||||||
};
|
};
|
||||||
|
# `cargo test --workspace` lifted out of `buildPackage` so the
|
||||||
|
# `hyperhive-assets` dep (which `hive-ag3nt::prompt::tests`
|
||||||
|
# needs via `HIVE_ASSETS_DIR` to assert against the actual
|
||||||
|
# production prompt template) is scoped to this one check
|
||||||
|
# instead of bleeding into the binary derivation's input
|
||||||
|
# hash. Net: editing `hive-ag3nt/prompts/system.md` still
|
||||||
|
# rebuilds this test check (correct — the tests assert
|
||||||
|
# against its wording), but `packages.default` and the
|
||||||
|
# per-container toplevels stay fully cached.
|
||||||
|
cargo-test = craneLib.cargoTest {
|
||||||
|
src = cleanSrc;
|
||||||
|
inherit cargoArtifacts nativeBuildInputs;
|
||||||
|
pname = "hyperhive-workspace";
|
||||||
|
version = "0.1.0";
|
||||||
|
cargoTestExtraArgs = "--workspace";
|
||||||
|
HIVE_ASSETS_DIR = "${self.packages.${system}.assets}/share/hyperhive";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,22 +29,24 @@
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
use crate::mcp::Flavor;
|
use crate::mcp::Flavor;
|
||||||
|
|
||||||
const TEMPLATE: &str = include_str!("../prompts/system.md");
|
|
||||||
|
|
||||||
/// Assemble the system prompt for a given flavor + label + pronouns.
|
/// Assemble the system prompt for a given flavor + label + pronouns.
|
||||||
/// Pure function — no I/O. Splits out from [`write_system_prompt`] so
|
/// Pure function — no I/O. Splits out from [`write_system_prompt`] so
|
||||||
/// the marker logic + substitution is unit-testable in isolation.
|
/// the marker logic + substitution is unit-testable in isolation.
|
||||||
|
/// The caller supplies the template body so tests can pass an inline
|
||||||
|
/// fixture and production reads it once at harness startup via
|
||||||
|
/// [`hive_sh4re::assets::prompt_template`] (`$HIVE_ASSETS_DIR/prompts/
|
||||||
|
/// system.md`).
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn render(flavor: Flavor, label: &str, operator_pronouns: &str) -> String {
|
pub fn render(template: &str, flavor: Flavor, label: &str, operator_pronouns: &str) -> String {
|
||||||
let target = match flavor {
|
let target = match flavor {
|
||||||
Flavor::Agent => "agent",
|
Flavor::Agent => "agent",
|
||||||
Flavor::Manager => "manager",
|
Flavor::Manager => "manager",
|
||||||
};
|
};
|
||||||
let body = filter_role_blocks(TEMPLATE, target);
|
let body = filter_role_blocks(template, target);
|
||||||
body.replace("{label}", label)
|
body.replace("{label}", label)
|
||||||
.replace("{operator_pronouns}", operator_pronouns)
|
.replace("{operator_pronouns}", operator_pronouns)
|
||||||
}
|
}
|
||||||
|
|
@ -115,15 +117,20 @@ fn parse_close_marker(line: &str) -> Option<&str> {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns an error if the system prompt file cannot be written.
|
/// Returns an error if the system prompt file cannot be written.
|
||||||
pub async fn write_system_prompt(
|
pub async fn write_system_prompt(socket: &Path, label: &str, flavor: Flavor) -> Result<PathBuf> {
|
||||||
socket: &Path,
|
|
||||||
label: &str,
|
|
||||||
flavor: Flavor,
|
|
||||||
) -> Result<PathBuf> {
|
|
||||||
let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive"));
|
let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive"));
|
||||||
tokio::fs::create_dir_all(parent).await.ok();
|
tokio::fs::create_dir_all(parent).await.ok();
|
||||||
let pronouns = std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned());
|
let pronouns = std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned());
|
||||||
let body = render(flavor, label, &pronouns);
|
let template_path = hive_sh4re::assets::prompt_template();
|
||||||
|
let template = tokio::fs::read_to_string(&template_path)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"read claude system prompt template from {}",
|
||||||
|
template_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let body = render(&template, flavor, label, &pronouns);
|
||||||
let path = parent.join("claude-system-prompt.md");
|
let path = parent.join("claude-system-prompt.md");
|
||||||
tokio::fs::write(&path, body).await?;
|
tokio::fs::write(&path, body).await?;
|
||||||
tracing::info!(path = %path.display(), "wrote claude system prompt");
|
tracing::info!(path = %path.display(), "wrote claude system prompt");
|
||||||
|
|
@ -133,6 +140,36 @@ pub async fn write_system_prompt(
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
// #555: the production template lives at
|
||||||
|
// `$HIVE_ASSETS_DIR/prompts/system.md` and is loaded at runtime.
|
||||||
|
// The unit tests below want to assert against the actual production
|
||||||
|
// wording (so the renderer + tool surface stay honest), so they
|
||||||
|
// resolve the same path at test runtime via two fallbacks:
|
||||||
|
// 1. `$HIVE_ASSETS_DIR/prompts/system.md` — the runtime contract
|
||||||
|
// production uses. The flake's `checks.cargo-test` derivation
|
||||||
|
// sets this to the `hyperhive-assets` output so `cargo test`
|
||||||
|
// inside the nix sandbox finds the file without needing
|
||||||
|
// `prompts/` in the cargo source tree. `packages.default`
|
||||||
|
// explicitly does NOT carry the assets dep, so a prompt edit
|
||||||
|
// doesn't bust the binary derivation — only this test check.
|
||||||
|
// 2. `env!("CARGO_MANIFEST_DIR")/prompts/system.md` — for plain
|
||||||
|
// `cargo test --workspace` from a checked-out repo where the
|
||||||
|
// env var isn't set; `env!` is a compile-time string lookup,
|
||||||
|
// no file open at compile, so this still doesn't pull
|
||||||
|
// `prompts/` into the build hash.
|
||||||
|
// The combined effect is that the flake's `cleanSrc` no longer
|
||||||
|
// unions `./hive-ag3nt/prompts` — tweaks to system.md don't bust
|
||||||
|
// the cargo cache anymore.
|
||||||
|
static PRODUCTION_TEMPLATE: LazyLock<String> = LazyLock::new(|| {
|
||||||
|
let path = match std::env::var("HIVE_ASSETS_DIR") {
|
||||||
|
Ok(v) if !v.is_empty() => format!("{v}/prompts/system.md"),
|
||||||
|
_ => concat!(env!("CARGO_MANIFEST_DIR"), "/prompts/system.md").to_owned(),
|
||||||
|
};
|
||||||
|
std::fs::read_to_string(&path)
|
||||||
|
.unwrap_or_else(|e| panic!("read production prompt template at {path}: {e}"))
|
||||||
|
});
|
||||||
|
|
||||||
const SAMPLE: &str = "\
|
const SAMPLE: &str = "\
|
||||||
shared opener
|
shared opener
|
||||||
|
|
@ -233,7 +270,7 @@ shared closer
|
||||||
// Real template's first agent line — keeps the renderer
|
// Real template's first agent line — keeps the renderer
|
||||||
// honest about the {label} / {operator_pronouns} pair the
|
// honest about the {label} / {operator_pronouns} pair the
|
||||||
// harness already relied on.
|
// harness already relied on.
|
||||||
let rendered = render(Flavor::Agent, "alice", "they/them");
|
let rendered = render(&PRODUCTION_TEMPLATE, Flavor::Agent, "alice", "they/them");
|
||||||
assert!(rendered.contains("hyperhive agent `alice`"));
|
assert!(rendered.contains("hyperhive agent `alice`"));
|
||||||
assert!(rendered.contains("**they/them** pronouns"));
|
assert!(rendered.contains("**they/them** pronouns"));
|
||||||
assert!(!rendered.contains("{label}"));
|
assert!(!rendered.contains("{label}"));
|
||||||
|
|
@ -246,7 +283,7 @@ shared closer
|
||||||
// kill, schedule_*) MUST NOT appear in the agent's rendered
|
// kill, schedule_*) MUST NOT appear in the agent's rendered
|
||||||
// prompt. Drift between flavor and tool surface bites every
|
// prompt. Drift between flavor and tool surface bites every
|
||||||
// time it happens (cf. #511 missing-allow-list bug).
|
// time it happens (cf. #511 missing-allow-list bug).
|
||||||
let rendered = render(Flavor::Agent, "alice", "she/her");
|
let rendered = render(&PRODUCTION_TEMPLATE, Flavor::Agent, "alice", "she/her");
|
||||||
assert!(!rendered.contains("request_init_config"));
|
assert!(!rendered.contains("request_init_config"));
|
||||||
assert!(!rendered.contains("request_apply_commit"));
|
assert!(!rendered.contains("request_apply_commit"));
|
||||||
assert!(!rendered.contains("get_logs"));
|
assert!(!rendered.contains("get_logs"));
|
||||||
|
|
@ -257,7 +294,7 @@ shared closer
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_manager_includes_manager_only_tools() {
|
fn render_manager_includes_manager_only_tools() {
|
||||||
let rendered = render(Flavor::Manager, "hm1nd", "she/her");
|
let rendered = render(&PRODUCTION_TEMPLATE, Flavor::Manager, "hm1nd", "she/her");
|
||||||
assert!(rendered.contains("request_init_config"));
|
assert!(rendered.contains("request_init_config"));
|
||||||
assert!(rendered.contains("request_apply_commit"));
|
assert!(rendered.contains("request_apply_commit"));
|
||||||
assert!(rendered.contains("get_logs"));
|
assert!(rendered.contains("get_logs"));
|
||||||
|
|
@ -269,9 +306,9 @@ shared closer
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_uses_correct_role_opener() {
|
fn render_uses_correct_role_opener() {
|
||||||
let agent = render(Flavor::Agent, "alice", "she/her");
|
let agent = render(&PRODUCTION_TEMPLATE, Flavor::Agent, "alice", "she/her");
|
||||||
assert!(agent.starts_with("You are hyperhive agent"));
|
assert!(agent.starts_with("You are hyperhive agent"));
|
||||||
let manager = render(Flavor::Manager, "hm1nd", "she/her");
|
let manager = render(&PRODUCTION_TEMPLATE, Flavor::Manager, "hm1nd", "she/her");
|
||||||
assert!(manager.starts_with("You are the hyperhive manager"));
|
assert!(manager.starts_with("You are the hyperhive manager"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
|
@ -18,15 +18,14 @@ use crate::events::{Bus, LiveEvent};
|
||||||
use crate::login::LoginState;
|
use crate::login::LoginState;
|
||||||
use crate::mcp;
|
use crate::mcp;
|
||||||
|
|
||||||
/// `--settings` JSON applied to every claude invocation. Lives as a
|
// `--settings` JSON is read at runtime from
|
||||||
/// properly-formatted file in `prompts/claude-settings.json` so it's easy
|
// `$HIVE_ASSETS_DIR/prompts/claude-settings.json` via
|
||||||
/// to read and edit; we ship it via `include_str!`. We turn off claude's
|
// `hive_sh4re::assets::claude_settings()` (#555). We turn off claude's
|
||||||
/// in-session auto-compaction and its cross-session auto-memory because
|
// in-session auto-compaction and its cross-session auto-memory because
|
||||||
/// hyperhive owns those concerns (`/compact` on overflow, notes
|
// hyperhive owns those concerns (`/compact` on overflow, notes
|
||||||
/// persistence under `/state`). Unknown keys are silently ignored by
|
// persistence under `/state`). Unknown keys are silently ignored by
|
||||||
/// claude-code; if a key gets renamed we'll spot it because the
|
// claude-code; if a key gets renamed we'll spot it because the
|
||||||
/// corresponding behavior will start firing mid-turn again.
|
// corresponding behavior will start firing mid-turn again.
|
||||||
const CLAUDE_SETTINGS: &str = include_str!("../prompts/claude-settings.json");
|
|
||||||
|
|
||||||
/// Regex-ish marker claude-code emits when context overflows. Same string
|
/// Regex-ish marker claude-code emits when context overflows. Same string
|
||||||
/// bitburner-agent watches for. Empirically reliable across claude-code
|
/// bitburner-agent watches for. Empirically reliable across claude-code
|
||||||
|
|
@ -159,7 +158,14 @@ pub async fn write_settings(socket: &Path) -> Result<PathBuf> {
|
||||||
let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive"));
|
let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive"));
|
||||||
tokio::fs::create_dir_all(parent).await.ok();
|
tokio::fs::create_dir_all(parent).await.ok();
|
||||||
let path = parent.join("claude-settings.json");
|
let path = parent.join("claude-settings.json");
|
||||||
tokio::fs::write(&path, CLAUDE_SETTINGS).await?;
|
// #555: source-of-truth is `$HIVE_ASSETS_DIR/prompts/claude-settings.json`;
|
||||||
|
// copy through the per-agent runtime dir so claude reads it from the
|
||||||
|
// same socket-adjacent location every time and so a future override
|
||||||
|
// (per-agent settings JSON layer) drops in cleanly.
|
||||||
|
let src = hive_sh4re::assets::claude_settings();
|
||||||
|
tokio::fs::copy(&src, &path)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("copy claude settings from {} to {}", src.display(), path.display()))?;
|
||||||
tracing::info!(path = %path.display(), "wrote claude settings");
|
tracing::info!(path = %path.display(), "wrote claude settings");
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
@ -508,10 +514,10 @@ pub async fn wait_for_login(
|
||||||
/// regular files + newest `mtime` across them. The two axes are both
|
/// regular files + newest `mtime` across them. The two axes are both
|
||||||
/// load-bearing for `wait_for_login`'s refresh check (`session_refreshed`):
|
/// load-bearing for `wait_for_login`'s refresh check (`session_refreshed`):
|
||||||
/// mtime catches the common case (re-login overwrites an existing
|
/// mtime catches the common case (re-login overwrites an existing
|
||||||
/// credentials file in-place), file_count catches the pathological case
|
/// credentials file in-place), `file_count` catches the pathological case
|
||||||
/// where `meta.modified()` errors on every file (exotic fs, NFS quirks)
|
/// where `meta.modified()` errors on every file (exotic fs, NFS quirks)
|
||||||
/// so the mtime axis stays `None` forever but new files still trigger a
|
/// so the mtime axis stays `None` forever but new files still trigger a
|
||||||
/// resume. Defaults to `{0, None}` on read_dir failure (missing or
|
/// resume. Defaults to `{0, None}` on `read_dir` failure (missing or
|
||||||
/// unreadable dir) — `wait_for_login` then resumes when files first
|
/// unreadable dir) — `wait_for_login` then resumes when files first
|
||||||
/// appear.
|
/// appear.
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
|
@ -542,9 +548,9 @@ fn snapshot_dir(dir: &Path) -> DirSnapshot {
|
||||||
/// Has the credentials dir been written since `prev`? Used as the
|
/// Has the credentials dir been written since `prev`? Used as the
|
||||||
/// exit condition for `wait_for_login`:
|
/// exit condition for `wait_for_login`:
|
||||||
///
|
///
|
||||||
/// - file_count changed → something was added or removed, treat as
|
/// - `file_count` changed → something was added or removed, treat as
|
||||||
/// refresh (covers the "all files have unreadable mtime" edge case).
|
/// refresh (covers the "all files have unreadable mtime" edge case).
|
||||||
/// - newest_mtime advanced → existing file was rewritten in place
|
/// - `newest_mtime` advanced → existing file was rewritten in place
|
||||||
/// (the common claude re-login path).
|
/// (the common claude re-login path).
|
||||||
/// - prev had no mtime (empty or all-unreadable) and now has one →
|
/// - prev had no mtime (empty or all-unreadable) and now has one →
|
||||||
/// first useful signal we've seen, treat as refresh.
|
/// first useful signal we've seen, treat as refresh.
|
||||||
|
|
|
||||||
|
|
@ -221,9 +221,15 @@ fn try_bind(addr: SocketAddr) -> std::io::Result<tokio::net::TcpListener> {
|
||||||
/// Always returns an image, so consumers (dashboard, favicon) can hit
|
/// Always returns an image, so consumers (dashboard, favicon) can hit
|
||||||
/// `/icon` unconditionally without probing whether one is configured.
|
/// `/icon` unconditionally without probing whether one is configured.
|
||||||
async fn serve_icon() -> impl IntoResponse {
|
async fn serve_icon() -> impl IntoResponse {
|
||||||
const DEFAULT_ICON: &str = include_str!("../../branding/hyperhive.svg");
|
// Per-agent icon overrides go through `/etc/hyperhive/icon.svg` (set
|
||||||
let body = std::fs::read_to_string("/etc/hyperhive/icon.svg")
|
// via the `hyperhive.icon` agent.nix option); the bundled default is
|
||||||
.unwrap_or_else(|_| DEFAULT_ICON.to_string());
|
// resolved at runtime from
|
||||||
|
// `$HIVE_ASSETS_DIR/branding/hyperhive.svg` (#555). If neither file
|
||||||
|
// can be read we serve an empty body — keeps the response a valid
|
||||||
|
// SVG content-type without a panic on a misconfigured container.
|
||||||
|
let body = std::fs::read_to_string("/etc/hyperhive/icon.svg").unwrap_or_else(|_| {
|
||||||
|
std::fs::read_to_string(hive_sh4re::assets::branding_svg()).unwrap_or_default()
|
||||||
|
});
|
||||||
([("content-type", "image/svg+xml")], body)
|
([("content-type", "image/svg+xml")], body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,6 @@
|
||||||
name = "hive-c0re"
|
name = "hive-c0re"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
# Render branding/agent-configs.svg → $OUT_DIR/agent-configs.png at
|
|
||||||
# compile time (#424). build.rs shells out to `rsvg-convert`
|
|
||||||
# (librsvg, pulled in via flake.nix' crane nativeBuildInputs); the
|
|
||||||
# baked PNG is included via include_bytes! from forge.rs so no
|
|
||||||
# raster gets checked into git.
|
|
||||||
build = "build.rs"
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
//! Render `branding/agent-configs.svg` → `$OUT_DIR/agent-configs.png`
|
|
||||||
//! at compile time so the daemon can `include_bytes!` the PNG without
|
|
||||||
//! checking the raster into git (#424 mara: "generate png on the fly
|
|
||||||
//! or in build"). The SVG is the source of truth; the PNG is a build
|
|
||||||
//! artifact.
|
|
||||||
//!
|
|
||||||
//! Uses `rsvg-convert` from PATH (librsvg, already available in
|
|
||||||
//! nixpkgs and added to the crane derivation's `nativeBuildInputs`
|
|
||||||
//! in `flake.nix`). For dev builds outside Nix, install librsvg via
|
|
||||||
//! your system package manager (Debian/Ubuntu: `librsvg2-bin`,
|
|
||||||
//! macOS: `brew install librsvg`).
|
|
||||||
|
|
||||||
use std::env;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
const SVG_PATH: &str = "../branding/agent-configs.svg";
|
|
||||||
const PNG_NAME: &str = "agent-configs.png";
|
|
||||||
// 300×300 to match the existing branding/hyperhive.png, which the
|
|
||||||
// Forgejo avatar endpoint accepts without resizing on upload.
|
|
||||||
const PX: &str = "300";
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// Re-run the build script when either the SVG itself or this
|
|
||||||
// script change. We deliberately don't watch every file in
|
|
||||||
// `branding/` — only the one PNG we generate.
|
|
||||||
println!("cargo:rerun-if-changed=build.rs");
|
|
||||||
println!("cargo:rerun-if-changed={SVG_PATH}");
|
|
||||||
|
|
||||||
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR set by cargo"));
|
|
||||||
let png_path = out_dir.join(PNG_NAME);
|
|
||||||
|
|
||||||
let status = Command::new("rsvg-convert")
|
|
||||||
.args(["--width", PX, "--height", PX, "-o"])
|
|
||||||
.arg(&png_path)
|
|
||||||
.arg(SVG_PATH)
|
|
||||||
.status();
|
|
||||||
|
|
||||||
match status {
|
|
||||||
Ok(s) if s.success() => {}
|
|
||||||
Ok(s) => panic!("rsvg-convert exited with {s} rendering {SVG_PATH}"),
|
|
||||||
Err(e) => panic!(
|
|
||||||
"failed to invoke rsvg-convert: {e}\n\
|
|
||||||
install librsvg (Debian/Ubuntu: librsvg2-bin, macOS: brew install librsvg, \
|
|
||||||
NixOS: pkgs.librsvg). The Nix derivation already pulls it in via \
|
|
||||||
flake.nix → craneLib.buildPackage.nativeBuildInputs.",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -41,20 +41,14 @@ const CORE_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-core-avatar-set";
|
||||||
/// Sibling marker for the `agent-configs` org avatar (#424). Same one-
|
/// Sibling marker for the `agent-configs` org avatar (#424). Same one-
|
||||||
/// shot semantics — delete to force the upload to re-run.
|
/// shot semantics — delete to force the upload to re-run.
|
||||||
const CONFIG_ORG_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-agent-configs-avatar-set";
|
const CONFIG_ORG_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-agent-configs-avatar-set";
|
||||||
/// Hyperhive logo bytes, baked into the daemon. Uploaded once via the
|
// Avatar PNGs are loaded at runtime from
|
||||||
/// admin avatar API so the `core` Forgejo user shows the project mark
|
// `$HIVE_ASSETS_DIR/branding/{hyperhive,agent-configs}.png` via the
|
||||||
/// next to commits in `agent-configs/*`, `core/meta`, etc. instead of
|
// helpers in `hive_sh4re::assets` (#555 — was `include_bytes!` of an
|
||||||
/// the default hash identicon.
|
// in-source path and an OUT_DIR-rendered sibling, both of which
|
||||||
const CORE_AVATAR_PNG: &[u8] = include_bytes!("../../branding/hyperhive.png");
|
// invalidated the crane src cache on any branding edit). The
|
||||||
/// `agent-configs` org logo bytes (#424). Sibling visual to the main
|
// `agent-configs.png` is rendered from its SVG during the
|
||||||
/// hyperhive mark — same dark base + outer ring + corner brackets,
|
// `hyperhive-assets` derivation's build (was `hive-c0re/build.rs`
|
||||||
/// with a stacked-config-files glyph in the centre so the operator
|
// + `rsvg-convert` on PATH; both gone now).
|
||||||
/// can distinguish the agent-configs namespace from the main
|
|
||||||
/// `hyperhive` org at a glance. Source-of-truth is
|
|
||||||
/// `branding/agent-configs.svg`; `hive-c0re/build.rs` renders it
|
|
||||||
/// into `$OUT_DIR/agent-configs.png` at compile time via
|
|
||||||
/// `rsvg-convert` so the raster never gets checked into git.
|
|
||||||
const CONFIG_ORG_AVATAR_PNG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/agent-configs.png"));
|
|
||||||
/// Forgejo org grouping every agent's applied config repo. Core is a
|
/// Forgejo org grouping every agent's applied config repo. Core is a
|
||||||
/// site admin and reads + writes every repo here; agents are NOT
|
/// site admin and reads + writes every repo here; agents are NOT
|
||||||
/// members and the repos are private, so no agent — not even the one
|
/// members and the repos are private, so no agent — not even the one
|
||||||
|
|
@ -298,9 +292,13 @@ async fn ensure_core_avatar(token: &str) -> Result<()> {
|
||||||
if marker.exists() {
|
if marker.exists() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
let png_path = hive_sh4re::assets::core_avatar_png();
|
||||||
|
let png_bytes = tokio::fs::read(&png_path)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("read core avatar PNG from {}", png_path.display()))?;
|
||||||
let body = format!(
|
let body = format!(
|
||||||
r#"{{"image":"{}"}}"#,
|
r#"{{"image":"{}"}}"#,
|
||||||
base64::engine::general_purpose::STANDARD.encode(CORE_AVATAR_PNG),
|
base64::engine::general_purpose::STANDARD.encode(&png_bytes),
|
||||||
);
|
);
|
||||||
let url = format!("{FORGE_HTTP}/api/v1/admin/users/core/avatar");
|
let url = format!("{FORGE_HTTP}/api/v1/admin/users/core/avatar");
|
||||||
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
||||||
|
|
@ -325,9 +323,13 @@ async fn ensure_config_org_avatar(token: &str) -> Result<()> {
|
||||||
if marker.exists() {
|
if marker.exists() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
let png_path = hive_sh4re::assets::config_org_avatar_png();
|
||||||
|
let png_bytes = tokio::fs::read(&png_path)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("read {CONFIG_ORG} avatar PNG from {}", png_path.display()))?;
|
||||||
let body = format!(
|
let body = format!(
|
||||||
r#"{{"image":"{}"}}"#,
|
r#"{{"image":"{}"}}"#,
|
||||||
base64::engine::general_purpose::STANDARD.encode(CONFIG_ORG_AVATAR_PNG),
|
base64::engine::general_purpose::STANDARD.encode(&png_bytes),
|
||||||
);
|
);
|
||||||
let url = format!("{FORGE_HTTP}/api/v1/orgs/{CONFIG_ORG}/avatar");
|
let url = format!("{FORGE_HTTP}/api/v1/orgs/{CONFIG_ORG}/avatar");
|
||||||
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
||||||
|
|
|
||||||
99
hive-sh4re/src/assets.rs
Normal file
99
hive-sh4re/src/assets.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
//! Resolve the on-disk path to hyperhive's static assets (branding +
|
||||||
|
//! claude prompts). Single source of truth for both the host daemon
|
||||||
|
//! (`hive-c0re`) and the in-container harness (`hive-ag3nt` /
|
||||||
|
//! `hive-m1nd`) so they agree on the lookup contract.
|
||||||
|
//!
|
||||||
|
//! At runtime, the path is read from `$HIVE_ASSETS_DIR`. In nix
|
||||||
|
//! builds that env var is set by the `hive-c0re` / `harness-base` modules
|
||||||
|
//! to `${pkgs.hyperhive-assets}/share/hyperhive` (see `nix/assets.nix`).
|
||||||
|
//! For `cargo run` outside nix, set it yourself:
|
||||||
|
//!
|
||||||
|
//! ```sh
|
||||||
|
//! HIVE_ASSETS_DIR=$(pwd)/dev-assets cargo run ...
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! where `dev-assets/` is laid out the same as the nix output:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! dev-assets/
|
||||||
|
//! branding/
|
||||||
|
//! hyperhive.{svg,png}
|
||||||
|
//! agent-configs.{svg,png}
|
||||||
|
//! prompts/
|
||||||
|
//! system.md
|
||||||
|
//! claude-settings.json
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! A convenience `cargo xtask seed-dev-assets` could materialise this
|
||||||
|
//! by copying `branding/` + `hive-ag3nt/prompts/` + rendering
|
||||||
|
//! `agent-configs.png` via rsvg-convert, but it's intentionally not
|
||||||
|
//! shipped yet — dev setups vary too much for one helper to fit.
|
||||||
|
//!
|
||||||
|
//! Each crate that wants a specific asset goes through one of the
|
||||||
|
//! typed helpers (`branding_svg()`, `prompt_template()`, …) so the
|
||||||
|
//! lookup contract is centralised. Missing files panic at first
|
||||||
|
//! call with a clear "set `HIVE_ASSETS_DIR` + put the file at …"
|
||||||
|
//! message — same failure shape as the old `include_*!` macros
|
||||||
|
//! (which would have failed at compile time, not runtime, but the
|
||||||
|
//! diagnostic value is identical).
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Read `$HIVE_ASSETS_DIR`. Panics with a clear remediation message
|
||||||
|
/// when unset — every code path that calls into this module is one
|
||||||
|
/// the binary cannot run without, so a hard early failure is the
|
||||||
|
/// right shape.
|
||||||
|
fn dir() -> PathBuf {
|
||||||
|
match std::env::var("HIVE_ASSETS_DIR") {
|
||||||
|
Ok(v) if !v.is_empty() => PathBuf::from(v),
|
||||||
|
_ => panic!(
|
||||||
|
"HIVE_ASSETS_DIR is not set. Inside the nix-built systemd \
|
||||||
|
units this is wired automatically from \
|
||||||
|
`pkgs.hyperhive-assets`; for `cargo run` outside nix, \
|
||||||
|
set it to a directory laid out like \
|
||||||
|
`nix/assets.nix`'s output (branding/ + prompts/ \
|
||||||
|
subdirs). See hive-sh4re/src/assets.rs for the contract.",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `$HIVE_ASSETS_DIR/branding/hyperhive.svg` — the project's primary
|
||||||
|
/// mark. Loaded by the per-agent web UI as its default icon
|
||||||
|
/// (`/agents/<n>/icon.svg` falls through here when the agent didn't
|
||||||
|
/// override `hyperhive.icon` in its `agent.nix`).
|
||||||
|
#[must_use]
|
||||||
|
pub fn branding_svg() -> PathBuf {
|
||||||
|
dir().join("branding/hyperhive.svg")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `$HIVE_ASSETS_DIR/branding/hyperhive.png` — the same mark
|
||||||
|
/// rasterised, used by `hive-c0re::forge::push_avatar` to upload
|
||||||
|
/// the forge org avatar.
|
||||||
|
#[must_use]
|
||||||
|
pub fn core_avatar_png() -> PathBuf {
|
||||||
|
dir().join("branding/hyperhive.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `$HIVE_ASSETS_DIR/branding/agent-configs.png` — secondary org
|
||||||
|
/// mark for the `agent-configs/` mirror org. Rendered from
|
||||||
|
/// `agent-configs.svg` at asset-build time (was rendered in
|
||||||
|
/// `hive-c0re/build.rs` before #555).
|
||||||
|
#[must_use]
|
||||||
|
pub fn config_org_avatar_png() -> PathBuf {
|
||||||
|
dir().join("branding/agent-configs.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `$HIVE_ASSETS_DIR/prompts/system.md` — the claude system prompt
|
||||||
|
/// template. `hive-ag3nt::prompt::render` filters the role markers
|
||||||
|
/// inside it per agent / manager flavor.
|
||||||
|
#[must_use]
|
||||||
|
pub fn prompt_template() -> PathBuf {
|
||||||
|
dir().join("prompts/system.md")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `$HIVE_ASSETS_DIR/prompts/claude-settings.json` — the static
|
||||||
|
/// `--settings` JSON every claude invocation reads.
|
||||||
|
#[must_use]
|
||||||
|
pub fn claude_settings() -> PathBuf {
|
||||||
|
dir().join("prompts/claude-settings.json")
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub mod assets;
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Host admin socket — /run/hyperhive/host.sock
|
// Host admin socket — /run/hyperhive/host.sock
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
77
nix/assets.nix
Normal file
77
nix/assets.nix
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
{
|
||||||
|
stdenv,
|
||||||
|
lib,
|
||||||
|
librsvg,
|
||||||
|
}:
|
||||||
|
|
||||||
|
# Static assets the rust workspace reads at runtime: the project's
|
||||||
|
# branding SVG/PNG family + the claude system-prompt template +
|
||||||
|
# claude-settings JSON. Lives as its own derivation so a tweak to
|
||||||
|
# branding/agent-configs.svg or hive-ag3nt/prompts/system.md doesn't
|
||||||
|
# invalidate the rust derivation's cargo cache (closes #555 follow-up
|
||||||
|
# to #538 — naersk previously paired with `src = ./.;` invalidating
|
||||||
|
# every rust build on any branding/prompt edit; crane inherited that
|
||||||
|
# coupling and this split breaks it cleanly).
|
||||||
|
#
|
||||||
|
# Output layout:
|
||||||
|
#
|
||||||
|
# $out/share/hyperhive/branding/{hyperhive.svg, hyperhive.png,
|
||||||
|
# agent-configs.svg, agent-configs.png}
|
||||||
|
# $out/share/hyperhive/prompts/{system.md, claude-settings.json}
|
||||||
|
#
|
||||||
|
# The agent-configs PNG is rendered at build time from the SVG via
|
||||||
|
# rsvg-convert — same shape as the old `hive-c0re/build.rs` rasteriser,
|
||||||
|
# just hoisted into nix so the librsvg dependency stays *here* instead
|
||||||
|
# of in the rust derivation's nativeBuildInputs.
|
||||||
|
|
||||||
|
stdenv.mkDerivation {
|
||||||
|
pname = "hyperhive-assets";
|
||||||
|
version = "0.1.0";
|
||||||
|
# `src` is intentionally narrow — only branding/ + the hive-ag3nt/prompts/
|
||||||
|
# subdir, NOT the whole tree. Keeps the input hash decoupled from
|
||||||
|
# rust source / docs / nix module edits.
|
||||||
|
srcs = [
|
||||||
|
../branding
|
||||||
|
../hive-ag3nt/prompts
|
||||||
|
];
|
||||||
|
# `unpackPhase` would normally extract each src to its own dir; we
|
||||||
|
# just want them side-by-side, so hand-roll a flat copy.
|
||||||
|
unpackPhase = ''
|
||||||
|
runHook preUnpack
|
||||||
|
cp -r ${../branding} branding
|
||||||
|
cp -r ${../hive-ag3nt/prompts} prompts
|
||||||
|
chmod -R u+w branding prompts
|
||||||
|
runHook postUnpack
|
||||||
|
'';
|
||||||
|
|
||||||
|
nativeBuildInputs = [ librsvg ];
|
||||||
|
|
||||||
|
# No real build step — just render the agent-configs PNG alongside
|
||||||
|
# its SVG. 300×300 matches branding/hyperhive.png, which is the size
|
||||||
|
# Forgejo's avatar endpoint accepts without resampling on upload (the
|
||||||
|
# same constraint hive-c0re/build.rs encoded).
|
||||||
|
buildPhase = ''
|
||||||
|
runHook preBuild
|
||||||
|
rsvg-convert --width 300 --height 300 \
|
||||||
|
-o branding/agent-configs.png \
|
||||||
|
branding/agent-configs.svg
|
||||||
|
runHook postBuild
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
mkdir -p $out/share/hyperhive
|
||||||
|
cp -r branding $out/share/hyperhive/branding
|
||||||
|
cp -r prompts $out/share/hyperhive/prompts
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Pure data — no executables to fixup, no shared libs to patchelf.
|
||||||
|
dontFixup = true;
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "hyperhive static assets (branding + claude prompts)";
|
||||||
|
homepage = "https://forge.darkest.space/hyperhive/hyperhive";
|
||||||
|
license = lib.licenses.mit;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
hyperhivePackage,
|
hyperhivePackage,
|
||||||
hyperhiveFrontend,
|
hyperhiveFrontend,
|
||||||
|
hyperhiveAssets,
|
||||||
hyperhiveFlake,
|
hyperhiveFlake,
|
||||||
agentBaseToplevel,
|
agentBaseToplevel,
|
||||||
managerToplevel,
|
managerToplevel,
|
||||||
|
|
@ -66,6 +67,20 @@ in
|
||||||
endpoints) is the source of truth for any replacement.
|
endpoints) is the source of truth for any replacement.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
assets = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
default = hyperhiveAssets pkgs.stdenv.hostPlatform.system;
|
||||||
|
defaultText = lib.literalExpression "hyperhive.packages.\${system}.assets";
|
||||||
|
description = ''
|
||||||
|
Bundled static runtime assets (see `./nix/assets.nix`): the
|
||||||
|
project's branding family + the claude system-prompt template +
|
||||||
|
claude-settings JSON. Output has `share/hyperhive/{branding,prompts}/`;
|
||||||
|
passed to hive-c0re's systemd unit via `HIVE_ASSETS_DIR`
|
||||||
|
(`hive_sh4re::assets::*` resolve paths underneath). Override to
|
||||||
|
ship customised branding or prompts without rebuilding the
|
||||||
|
rust derivation.
|
||||||
|
'';
|
||||||
|
};
|
||||||
hyperhiveFlake = lib.mkOption {
|
hyperhiveFlake = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = hyperhiveFlake;
|
default = hyperhiveFlake;
|
||||||
|
|
@ -196,6 +211,10 @@ in
|
||||||
# serves this via `tower_http::ServeDir` for any path it doesn't
|
# serves this via `tower_http::ServeDir` for any path it doesn't
|
||||||
# match against an API/action route.
|
# match against an API/action route.
|
||||||
HIVE_STATIC_DIR = "${cfg.frontend}/dashboard";
|
HIVE_STATIC_DIR = "${cfg.frontend}/dashboard";
|
||||||
|
# Path to the static runtime asset tree (branding + claude
|
||||||
|
# prompts). `hive_sh4re::assets::*` reads paths underneath.
|
||||||
|
# `forge.rs` reads the avatar PNGs from here on startup.
|
||||||
|
HIVE_ASSETS_DIR = "${cfg.assets}/share/hyperhive";
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs config.hyperhive.forge.enable {
|
// lib.optionalAttrs config.hyperhive.forge.enable {
|
||||||
# Agents poll this URL for Forgejo notifications. Derived from
|
# Agents poll this URL for Forgejo notifications. Derived from
|
||||||
|
|
|
||||||
|
|
@ -531,10 +531,15 @@
|
||||||
# HIVE_DEFAULT_MODEL seeds the initial model selection when no persisted
|
# HIVE_DEFAULT_MODEL seeds the initial model selection when no persisted
|
||||||
# model choice exists in the state dir. SHELL must be set so claude's
|
# model choice exists in the state dir. SHELL must be set so claude's
|
||||||
# Bash tool finds a POSIX shell.
|
# Bash tool finds a POSIX shell.
|
||||||
|
# HIVE_ASSETS_DIR points at the project's static runtime assets
|
||||||
|
# (branding + claude prompts; see `nix/assets.nix`). Set here so
|
||||||
|
# both the harness binary and any user-shell `cargo run` inside the
|
||||||
|
# container resolve them from the same path.
|
||||||
# HIVE_CONTEXT_WINDOW_TOKENS_* are injected by the meta flake from the
|
# HIVE_CONTEXT_WINDOW_TOKENS_* are injected by the meta flake from the
|
||||||
# host-level `services.hive-c0re.contextWindowTokens` option — not set here.
|
# host-level `services.hive-c0re.contextWindowTokens` option — not set here.
|
||||||
environment.variables = {
|
environment.variables = {
|
||||||
HIVE_DEFAULT_MODEL = config.hyperhive.model;
|
HIVE_DEFAULT_MODEL = config.hyperhive.model;
|
||||||
|
HIVE_ASSETS_DIR = "${pkgs.hyperhive-assets}/share/hyperhive";
|
||||||
SHELL = "${pkgs.bashInteractive}/bin/bash";
|
SHELL = "${pkgs.bashInteractive}/bin/bash";
|
||||||
} // lib.optionalAttrs (!config.hyperhive.autoCompact) {
|
} // lib.optionalAttrs (!config.hyperhive.autoCompact) {
|
||||||
# Zero watermark disables proactive compaction; the reactive path
|
# Zero watermark disables proactive compaction; the reactive path
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue