rust+nix: load static assets at runtime, drop build.rs (#555)
Cuts every `include_bytes!`/`include_str!` of a non-rust path in
the workspace over to runtime file loads from `$HIVE_ASSETS_DIR`
(the `hyperhive-assets` derivation introduced in the previous
commit). After this commit the rust derivation has no compile-time
dependency on `branding/*` or `hive-ag3nt/prompts/*` anymore.
Call-site flips:
- `hive-c0re/src/forge.rs::CORE_AVATAR_PNG` /
`CONFIG_ORG_AVATAR_PNG`: were `include_bytes!` of
`branding/hyperhive.png` and `$OUT_DIR/agent-configs.png`. Now
`ensure_core_avatar` / `ensure_config_org_avatar` `tokio::fs::read`
via `hive_sh4re::assets::{core_avatar_png, config_org_avatar_png}`
at startup. The `agent-configs.png` is now rendered by the
`hyperhive-assets` derivation's rsvg-convert step (was
`hive-c0re/build.rs` + librsvg on the rust derivation's
nativeBuildInputs — both gone in the next commit).
- `hive-ag3nt/src/prompt.rs::TEMPLATE`: `render` now takes the
template as an argument; `write_system_prompt` reads it once from
`$HIVE_ASSETS_DIR/prompts/system.md` before calling render. The
test module still `include_str!`s the production template so
`cargo test --workspace` doesn't need `HIVE_ASSETS_DIR` set —
this is the only remaining compile-time reference to the file
from the rust workspace, gated to `#[cfg(test)]`.
- `hive-ag3nt/src/turn.rs::CLAUDE_SETTINGS`: was `include_str!`'d
and written via `tokio::fs::write`; now `tokio::fs::copy` from
`$HIVE_ASSETS_DIR/prompts/claude-settings.json` into the
per-agent socket dir.
- `hive-ag3nt/src/web_ui.rs::DEFAULT_ICON`: was `include_str!`'d;
now read on-demand from `$HIVE_ASSETS_DIR/branding/hyperhive.svg`
inside `serve_icon`. Falls back to an empty body if missing so
the endpoint never panics on a misconfigured container (matches
the existing "per-agent icon.svg override" fallthrough).
`HIVE_ASSETS_DIR` wiring:
- Inside containers: `nix/templates/harness-base.nix`
`environment.variables` sets it to
`${pkgs.hyperhive-assets}/share/hyperhive` (resolved through
the default overlay applied in `mkContainer`). Verified by
building `agent-base-toplevel` and grepping the resulting
`/etc/set-environment`.
- Host-side: `nix/modules/hive-c0re.nix` adds an `assets` option
defaulting to `hyperhive.packages.${system}.assets`, threaded
in from the flake's nixosModules wiring, and sets the same env
var on the `hive-c0re` systemd unit so the daemon's
`forge::ensure_*_avatar` startup hooks find the PNGs.
`hive-c0re/build.rs` deleted entirely; `[package].build` removed
from `hive-c0re/Cargo.toml`; rsvg-convert dependency lives in the
assets derivation only.
Validated: `nix build .#default .#checks.x86_64-linux.clippy
.#agent-base-toplevel .#manager-toplevel --fallback` all succeed.
`/etc/set-environment` in the toplevel shows
`HIVE_ASSETS_DIR="/nix/store/.../hyperhive-assets-0.1.0/share/hyperhive"`.
This commit is contained in:
parent
246dd19390
commit
73684fb00a
11 changed files with 207 additions and 105 deletions
|
|
@ -29,22 +29,24 @@
|
|||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::mcp::Flavor;
|
||||
|
||||
const TEMPLATE: &str = include_str!("../prompts/system.md");
|
||||
|
||||
/// Assemble the system prompt for a given flavor + label + pronouns.
|
||||
/// Pure function — no I/O. Splits out from [`write_system_prompt`] so
|
||||
/// 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]
|
||||
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 {
|
||||
Flavor::Agent => "agent",
|
||||
Flavor::Manager => "manager",
|
||||
};
|
||||
let body = filter_role_blocks(TEMPLATE, target);
|
||||
let body = filter_role_blocks(template, target);
|
||||
body.replace("{label}", label)
|
||||
.replace("{operator_pronouns}", operator_pronouns)
|
||||
}
|
||||
|
|
@ -123,7 +125,11 @@ pub async fn write_system_prompt(
|
|||
let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive"));
|
||||
tokio::fs::create_dir_all(parent).await.ok();
|
||||
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");
|
||||
tokio::fs::write(&path, body).await?;
|
||||
tracing::info!(path = %path.display(), "wrote claude system prompt");
|
||||
|
|
@ -134,6 +140,18 @@ pub async fn write_system_prompt(
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// #555: production reads the system-prompt template from
|
||||
// `$HIVE_ASSETS_DIR/prompts/system.md` at startup. The test module
|
||||
// still `include_str!`s it directly because:
|
||||
// 1. the "real template still substitutes / still filters" tests
|
||||
// below need the actual production wording to be honest;
|
||||
// 2. embedding it at compile time keeps `cargo test --workspace`
|
||||
// runnable without setting `HIVE_ASSETS_DIR`;
|
||||
// 3. this is the ONLY remaining compile-time reference to
|
||||
// `prompts/system.md` from the rust workspace — production
|
||||
// code loads it at runtime.
|
||||
const PRODUCTION_TEMPLATE: &str = include_str!("../prompts/system.md");
|
||||
|
||||
const SAMPLE: &str = "\
|
||||
shared opener
|
||||
<!-- role:agent -->
|
||||
|
|
@ -233,7 +251,7 @@ shared closer
|
|||
// Real template's first agent line — keeps the renderer
|
||||
// honest about the {label} / {operator_pronouns} pair the
|
||||
// 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("**they/them** pronouns"));
|
||||
assert!(!rendered.contains("{label}"));
|
||||
|
|
@ -246,7 +264,7 @@ shared closer
|
|||
// kill, schedule_*) MUST NOT appear in the agent's rendered
|
||||
// prompt. Drift between flavor and tool surface bites every
|
||||
// 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_apply_commit"));
|
||||
assert!(!rendered.contains("get_logs"));
|
||||
|
|
@ -257,7 +275,7 @@ shared closer
|
|||
|
||||
#[test]
|
||||
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_apply_commit"));
|
||||
assert!(rendered.contains("get_logs"));
|
||||
|
|
@ -269,9 +287,9 @@ shared closer
|
|||
|
||||
#[test]
|
||||
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"));
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue