Compare commits
3 changed files with 26 additions and 257 deletions
|
|
@ -234,79 +234,6 @@ State lives at `/var/lib/nixos-containers/hive-forge/var/lib/forgejo/`
|
||||||
and survives container restart / host reboot. To wipe, destroy the
|
and survives container restart / host reboot. To wipe, destroy the
|
||||||
container.
|
container.
|
||||||
|
|
||||||
## Per-agent static frontend split
|
|
||||||
|
|
||||||
When `services.hyperhive.frontend` is configured, hive-c0re injects
|
|
||||||
`HIVE_AGENT_FRONTEND_DIR = "${cfg.frontend}/agent"` into its service
|
|
||||||
environment. The nginx include generator (`gateway_nginx::write`) reads
|
|
||||||
this variable and, when set, emits split location blocks per agent
|
|
||||||
instead of the legacy single-proxy block.
|
|
||||||
|
|
||||||
**Location priority for `/agent/<name>/...`:**
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
# 1. Compiled assets — content-addressed nix store path, cache forever
|
|
||||||
location ^~ /agent/<name>/static/ {
|
|
||||||
alias <frontend>/static/;
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable, max-age=31536000";
|
|
||||||
}
|
|
||||||
|
|
||||||
# 2. Static dist + proxy fallback for dynamic paths
|
|
||||||
location /agent/<name>/ {
|
|
||||||
alias <frontend>/;
|
|
||||||
try_files $uri $uri.html $uri/index.html @<name>_dynamic;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 3. Proxy catchall — API, events, icon, send, login, …
|
|
||||||
location @<name>_dynamic {
|
|
||||||
proxy_pass <upstream>;
|
|
||||||
proxy_set_header X-Forwarded-Prefix /agent/<name>;
|
|
||||||
proxy_intercept_errors on;
|
|
||||||
error_page 502 503 504 = /__hive_agent_unreachable;
|
|
||||||
# … (full proxy header block)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**`try_files` resolution** (nginx applies the `alias` mapping before
|
|
||||||
checking file existence):
|
|
||||||
|
|
||||||
| request | resolved | outcome |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `/agent/iris/` | `<frontend>/index.html` | main agent page |
|
|
||||||
| `/agent/iris/stats` | `<frontend>/stats.html` | stats page |
|
|
||||||
| `/agent/iris/screen` | `<frontend>/screen.html` | screen page |
|
|
||||||
| `/agent/iris/static/app.js` | caught by `^~` block first | served with immutable cache |
|
|
||||||
| `/agent/iris/api/state` | no file match → `@iris_dynamic` | proxied to agent daemon |
|
|
||||||
| `/agent/iris/events/live` | no file match → `@iris_dynamic` | proxied (SSE) |
|
|
||||||
|
|
||||||
Adding a new HTML page to the frontend dist (`dist/<page>.html`)
|
|
||||||
automatically makes it reachable at `/agent/<name>/<page>` — no
|
|
||||||
generator change needed.
|
|
||||||
|
|
||||||
**Why `^~` for `/static/`**: the `^~` prefix gives this block higher
|
|
||||||
priority than the plain prefix `location /agent/<name>/`, so compiled
|
|
||||||
JS/CSS assets skip `try_files` entirely and get the immutable cache
|
|
||||||
headers. Nix store paths are content-addressed — the hash changes on
|
|
||||||
any content change — so `max-age=31536000` is safe.
|
|
||||||
|
|
||||||
**Why nix store is reachable from the gateway container**: nspawn
|
|
||||||
containers bind-mount `/nix/store` read-only by default. The
|
|
||||||
`HIVE_AGENT_FRONTEND_DIR` path is a nix store path baked in at
|
|
||||||
hive-c0re build time — the same path is visible to both c0re (writing
|
|
||||||
`agents.conf`) and the gateway nginx (serving files from it).
|
|
||||||
|
|
||||||
**Graceful degradation**: if `HIVE_AGENT_FRONTEND_DIR` is empty or
|
|
||||||
unset (e.g. a build that predates `cfg.frontend`), each agent gets the
|
|
||||||
legacy single-proxy block and all traffic is forwarded to the agent
|
|
||||||
daemon as before.
|
|
||||||
|
|
||||||
**`extraFiles`**: per-agent `hyperhive.frontend.extraFiles` are in
|
|
||||||
`mergedDist`, not in the base `cfg.frontend` dist. They are not under
|
|
||||||
the nix-store `alias` path, so requests for them fall through
|
|
||||||
`try_files` to `@<name>_dynamic` and are served by the agent daemon
|
|
||||||
as before.
|
|
||||||
|
|
||||||
## Per-agent error pages
|
## Per-agent error pages
|
||||||
|
|
||||||
`/agent/<name>/` requests hit two failure modes; both get static
|
`/agent/<name>/` requests hit two failure modes; both get static
|
||||||
|
|
|
||||||
|
|
@ -40,18 +40,9 @@ const PROXY_HEADER_BLOCK: &str = " proxy_http_version 1.1;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;";
|
proxy_set_header X-Forwarded-Proto $scheme;";
|
||||||
|
|
||||||
/// Render the nginx include body for `names`. Manager is filtered out.
|
/// Render the nginx include body for `names`. Manager is filtered out.
|
||||||
///
|
/// Each remaining agent gets a `location /agent/<name>/` block whose
|
||||||
/// When `frontend_dir` is `Some(path)` each agent gets split location
|
/// `proxy_pass` targets the unix socket when the `.bound` marker
|
||||||
/// blocks:
|
/// exists, or TCP loopback otherwise.
|
||||||
/// - Exact HTML pages (`/`, `/stats`, `/screen`) → `alias` from the nix
|
|
||||||
/// store dist. These load even while the agent daemon is restarting.
|
|
||||||
/// - Static assets (`/static/`) → `alias` from the nix store with cache
|
|
||||||
/// headers. Served by nginx directly, no agent socket round-trip.
|
|
||||||
/// - Everything else (all `/api/*`, `/events/*`, `/screen/ws`, `/icon`,
|
|
||||||
/// `/send`, `/login/*`) → proxied to the agent daemon as before.
|
|
||||||
///
|
|
||||||
/// When `frontend_dir` is `None` (legacy) each agent gets a single
|
|
||||||
/// `location /agent/<name>/` proxy block.
|
|
||||||
///
|
///
|
||||||
/// Output is deterministic for the same (sorted) set of names +
|
/// Output is deterministic for the same (sorted) set of names +
|
||||||
/// `.bound` state: no timestamps, no UUIDs. `BTreeMap` would give
|
/// `.bound` state: no timestamps, no UUIDs. `BTreeMap` would give
|
||||||
|
|
@ -59,7 +50,7 @@ const PROXY_HEADER_BLOCK: &str = " proxy_http_version 1.1;
|
||||||
/// they care about diff stability, but the gateway treats the blocks
|
/// they care about diff stability, but the gateway treats the blocks
|
||||||
/// as unordered by nginx's longest-prefix-match rules so ordering only
|
/// as unordered by nginx's longest-prefix-match rules so ordering only
|
||||||
/// affects human readability.
|
/// affects human readability.
|
||||||
fn render(names: &[String], frontend_dir: Option<&str>) -> String {
|
fn render(names: &[String]) -> String {
|
||||||
let mut out = String::from(
|
let mut out = String::from(
|
||||||
"# Generated by hive-c0re \u{2014} do not edit.\
|
"# Generated by hive-c0re \u{2014} do not edit.\
|
||||||
\n# Refreshed on every topology change + when agents bind/drop their unix sockets.\
|
\n# Refreshed on every topology change + when agents bind/drop their unix sockets.\
|
||||||
|
|
@ -78,69 +69,18 @@ fn render(names: &[String], frontend_dir: Option<&str>) -> String {
|
||||||
} else {
|
} else {
|
||||||
format!("http://127.0.0.1:{port}/")
|
format!("http://127.0.0.1:{port}/")
|
||||||
};
|
};
|
||||||
|
let _ = write!(
|
||||||
if let Some(frontend) = frontend_dir {
|
out,
|
||||||
// Split mode: try to serve files from the nix-store dist first;
|
"\nlocation /agent/{name}/ {{\n\
|
||||||
// fall through to the agent daemon for anything not found there.
|
{PROXY_HEADER_BLOCK}\n\
|
||||||
//
|
\n proxy_pass {upstream};\n\
|
||||||
// `frontend` is a nix store path injected at build time by hive-c0re.nix
|
\n proxy_set_header X-Forwarded-Prefix /agent/{name};\n\
|
||||||
// (`HIVE_AGENT_FRONTEND_DIR = "${cfg.frontend}/agent"`). Nix store paths
|
\n proxy_buffering off;\n\
|
||||||
// are of the form `/nix/store/<hash>-<name>` — only [a-z0-9/._-], never
|
\n proxy_read_timeout 1d;\n\
|
||||||
// shell metacharacters — so interpolating directly into the nginx config
|
\n proxy_intercept_errors on;\n\
|
||||||
// string is safe. We do NOT accept user-controlled input here.
|
\n error_page 502 503 504 = /__hive_agent_unreachable;\n\
|
||||||
//
|
}}\n",
|
||||||
// Location priority for a request to /agent/<name>/...:
|
);
|
||||||
// ^~ /agent/<name>/static/ — highest priority; matches compiled assets.
|
|
||||||
// Nix store paths are content-addressed and immutable — cache forever.
|
|
||||||
// /agent/<name>/ — try_files from dist, proxy fallback for the rest.
|
|
||||||
//
|
|
||||||
// try_files resolution (nginx applies the alias mapping before checking):
|
|
||||||
// $uri — exact file match (/static/app.js → static/app.js)
|
|
||||||
// $uri.html — bare-path .html fallback (/stats → stats.html)
|
|
||||||
// $uri/index.html — directory index (/ → index.html)
|
|
||||||
// @<name>_dynamic — proxy catchall: api, events, icon, login, …
|
|
||||||
//
|
|
||||||
// Adding pages to the frontend dist works automatically — no generator
|
|
||||||
// change needed. Per-agent extraFiles (in mergedDist, not in the base
|
|
||||||
// dist path) still proxy to the agent daemon via @<name>_dynamic.
|
|
||||||
let _ = write!(
|
|
||||||
out,
|
|
||||||
"\n# Compiled JS/CSS assets — content-addressed nix store path, cache forever\n\
|
|
||||||
location ^~ /agent/{name}/static/ {{\n\
|
|
||||||
\n alias {frontend}/static/;\n\
|
|
||||||
\n expires 1y;\n\
|
|
||||||
\n add_header Cache-Control \"public, immutable, max-age=31536000\";\n\
|
|
||||||
}}\n\
|
|
||||||
\n# Static dist + proxy fallback for dynamic paths\n\
|
|
||||||
location /agent/{name}/ {{\n\
|
|
||||||
\n alias {frontend}/;\n\
|
|
||||||
\n try_files $uri $uri.html $uri/index.html @{name}_dynamic;\n\
|
|
||||||
}}\n\
|
|
||||||
\nlocation @{name}_dynamic {{\n\
|
|
||||||
{PROXY_HEADER_BLOCK}\n\
|
|
||||||
\n proxy_pass {upstream};\n\
|
|
||||||
\n proxy_set_header X-Forwarded-Prefix /agent/{name};\n\
|
|
||||||
\n proxy_buffering off;\n\
|
|
||||||
\n proxy_read_timeout 1d;\n\
|
|
||||||
\n proxy_intercept_errors on;\n\
|
|
||||||
\n error_page 502 503 504 = /__hive_agent_unreachable;\n\
|
|
||||||
}}\n",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Legacy mode: proxy everything to the agent daemon.
|
|
||||||
let _ = write!(
|
|
||||||
out,
|
|
||||||
"\nlocation /agent/{name}/ {{\n\
|
|
||||||
{PROXY_HEADER_BLOCK}\n\
|
|
||||||
\n proxy_pass {upstream};\n\
|
|
||||||
\n proxy_set_header X-Forwarded-Prefix /agent/{name};\n\
|
|
||||||
\n proxy_buffering off;\n\
|
|
||||||
\n proxy_read_timeout 1d;\n\
|
|
||||||
\n proxy_intercept_errors on;\n\
|
|
||||||
\n error_page 502 503 504 = /__hive_agent_unreachable;\n\
|
|
||||||
}}\n",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
@ -164,10 +104,7 @@ fn render(names: &[String], frontend_dir: Option<&str>) -> String {
|
||||||
/// housekeeping restart or the next manual reload; the host's agent
|
/// housekeeping restart or the next manual reload; the host's agent
|
||||||
/// topology has already been written correctly.
|
/// topology has already been written correctly.
|
||||||
pub fn write(names: &[String]) -> Result<()> {
|
pub fn write(names: &[String]) -> Result<()> {
|
||||||
let frontend_dir = std::env::var("HIVE_AGENT_FRONTEND_DIR")
|
let body = render(names);
|
||||||
.ok()
|
|
||||||
.filter(|s| !s.is_empty());
|
|
||||||
let body = render(names, frontend_dir.as_deref());
|
|
||||||
let path = host_conf_path();
|
let path = host_conf_path();
|
||||||
if std::fs::read_to_string(&path).ok().as_deref() == Some(&body) {
|
if std::fs::read_to_string(&path).ok().as_deref() == Some(&body) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
@ -236,11 +173,9 @@ fn reload_gateway_nginx() {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
// ── legacy mode (frontend_dir = None) ──────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_empty_is_header_only() {
|
fn render_empty_is_header_only() {
|
||||||
let body = render(&[], None);
|
let body = render(&[]);
|
||||||
assert!(body.starts_with("# Generated by hive-c0re"));
|
assert!(body.starts_with("# Generated by hive-c0re"));
|
||||||
// No location blocks when no agents.
|
// No location blocks when no agents.
|
||||||
assert!(!body.contains("location"));
|
assert!(!body.contains("location"));
|
||||||
|
|
@ -252,7 +187,9 @@ mod tests {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| (*s).to_owned())
|
.map(|s| (*s).to_owned())
|
||||||
.collect();
|
.collect();
|
||||||
let body = render(&names, None);
|
// Use build_map_with so we don't need real .bound files.
|
||||||
|
// For render, we call it directly with names.
|
||||||
|
let body = render(&names);
|
||||||
assert!(!body.contains(&format!("/agent/{MANAGER_NAME}/")));
|
assert!(!body.contains(&format!("/agent/{MANAGER_NAME}/")));
|
||||||
assert!(body.contains("/agent/iris/"));
|
assert!(body.contains("/agent/iris/"));
|
||||||
}
|
}
|
||||||
|
|
@ -261,7 +198,7 @@ mod tests {
|
||||||
fn render_tcp_upstream_when_no_bound_marker() {
|
fn render_tcp_upstream_when_no_bound_marker() {
|
||||||
// No .bound file on disk → falls back to TCP loopback.
|
// No .bound file on disk → falls back to TCP loopback.
|
||||||
let names = vec!["iris".to_owned()];
|
let names = vec!["iris".to_owned()];
|
||||||
let body = render(&names, None);
|
let body = render(&names);
|
||||||
let port = lifecycle::agent_web_port("iris");
|
let port = lifecycle::agent_web_port("iris");
|
||||||
assert!(
|
assert!(
|
||||||
body.contains(&format!("proxy_pass http://127.0.0.1:{port}/")),
|
body.contains(&format!("proxy_pass http://127.0.0.1:{port}/")),
|
||||||
|
|
@ -272,7 +209,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn render_includes_proxy_headers() {
|
fn render_includes_proxy_headers() {
|
||||||
let names = vec!["iris".to_owned()];
|
let names = vec!["iris".to_owned()];
|
||||||
let body = render(&names, None);
|
let body = render(&names);
|
||||||
// All required proxy headers present (WebSocket upgrade + forwarded-for).
|
// All required proxy headers present (WebSocket upgrade + forwarded-for).
|
||||||
assert!(body.contains("proxy_http_version 1.1"));
|
assert!(body.contains("proxy_http_version 1.1"));
|
||||||
assert!(body.contains("proxy_set_header Upgrade $http_upgrade"));
|
assert!(body.contains("proxy_set_header Upgrade $http_upgrade"));
|
||||||
|
|
@ -282,7 +219,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn render_includes_forwarded_prefix() {
|
fn render_includes_forwarded_prefix() {
|
||||||
let names = vec!["atlas".to_owned()];
|
let names = vec!["atlas".to_owned()];
|
||||||
let body = render(&names, None);
|
let body = render(&names);
|
||||||
assert!(
|
assert!(
|
||||||
body.contains("X-Forwarded-Prefix /agent/atlas"),
|
body.contains("X-Forwarded-Prefix /agent/atlas"),
|
||||||
"expected X-Forwarded-Prefix for atlas, got:\n{body}"
|
"expected X-Forwarded-Prefix for atlas, got:\n{body}"
|
||||||
|
|
@ -292,7 +229,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn render_includes_error_pages() {
|
fn render_includes_error_pages() {
|
||||||
let names = vec!["iris".to_owned()];
|
let names = vec!["iris".to_owned()];
|
||||||
let body = render(&names, None);
|
let body = render(&names);
|
||||||
assert!(body.contains("proxy_intercept_errors on"));
|
assert!(body.contains("proxy_intercept_errors on"));
|
||||||
assert!(body.contains("__hive_agent_unreachable"));
|
assert!(body.contains("__hive_agent_unreachable"));
|
||||||
}
|
}
|
||||||
|
|
@ -300,95 +237,8 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn render_includes_buffering_and_timeout() {
|
fn render_includes_buffering_and_timeout() {
|
||||||
let names = vec!["iris".to_owned()];
|
let names = vec!["iris".to_owned()];
|
||||||
let body = render(&names, None);
|
let body = render(&names);
|
||||||
assert!(body.contains("proxy_buffering off"));
|
assert!(body.contains("proxy_buffering off"));
|
||||||
assert!(body.contains("proxy_read_timeout 1d"));
|
assert!(body.contains("proxy_read_timeout 1d"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── split mode (frontend_dir = Some) ───────────────────────────────
|
|
||||||
|
|
||||||
const FAKE_FRONTEND: &str = "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-frontend/agent";
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn split_mode_has_immutable_cache_for_static_assets() {
|
|
||||||
let names = vec!["iris".to_owned()];
|
|
||||||
let body = render(&names, Some(FAKE_FRONTEND));
|
|
||||||
// ^~ prefix block for /static/ with immutable cache headers
|
|
||||||
assert!(
|
|
||||||
body.contains("location ^~ /agent/iris/static/ {"),
|
|
||||||
"expected ^~ static location, got:\n{body}"
|
|
||||||
);
|
|
||||||
assert!(body.contains(&format!("alias {FAKE_FRONTEND}/static/;")));
|
|
||||||
assert!(body.contains("expires 1y;"));
|
|
||||||
assert!(body.contains("immutable"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn split_mode_has_alias_and_try_files() {
|
|
||||||
let names = vec!["iris".to_owned()];
|
|
||||||
let body = render(&names, Some(FAKE_FRONTEND));
|
|
||||||
// Plain prefix location with alias + try_files
|
|
||||||
assert!(
|
|
||||||
body.contains("location /agent/iris/ {"),
|
|
||||||
"expected try_files location, got:\n{body}"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
body.contains(&format!("alias {FAKE_FRONTEND}/;")),
|
|
||||||
"expected alias, got:\n{body}"
|
|
||||||
);
|
|
||||||
// try_files handles exact file, bare-path .html, directory index
|
|
||||||
assert!(
|
|
||||||
body.contains("try_files $uri $uri.html $uri/index.html @iris_dynamic"),
|
|
||||||
"expected try_files with .html fallback, got:\n{body}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn split_mode_has_named_proxy_location() {
|
|
||||||
let names = vec!["iris".to_owned()];
|
|
||||||
let body = render(&names, Some(FAKE_FRONTEND));
|
|
||||||
// Named location for dynamic proxy
|
|
||||||
assert!(
|
|
||||||
body.contains("location @iris_dynamic {"),
|
|
||||||
"expected named dynamic location, got:\n{body}"
|
|
||||||
);
|
|
||||||
assert!(body.contains("proxy_pass"));
|
|
||||||
assert!(body.contains("proxy_intercept_errors on"));
|
|
||||||
assert!(body.contains("__hive_agent_unreachable"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn split_mode_filters_manager() {
|
|
||||||
let names: Vec<String> = [MANAGER_NAME, "iris"]
|
|
||||||
.iter()
|
|
||||||
.map(|s| (*s).to_owned())
|
|
||||||
.collect();
|
|
||||||
let body = render(&names, Some(FAKE_FRONTEND));
|
|
||||||
assert!(!body.contains(&format!("/agent/{MANAGER_NAME}/")));
|
|
||||||
assert!(body.contains("/agent/iris/"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn split_mode_proxy_includes_forwarded_prefix() {
|
|
||||||
let names = vec!["atlas".to_owned()];
|
|
||||||
let body = render(&names, Some(FAKE_FRONTEND));
|
|
||||||
assert!(
|
|
||||||
body.contains("X-Forwarded-Prefix /agent/atlas"),
|
|
||||||
"expected X-Forwarded-Prefix for atlas in split mode, got:\n{body}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn split_mode_named_location_uses_agent_name() {
|
|
||||||
// Named location must be unique per agent to avoid nginx conflicts
|
|
||||||
let names = vec!["damocles".to_owned(), "iris".to_owned()];
|
|
||||||
let body = render(&names, Some(FAKE_FRONTEND));
|
|
||||||
assert!(body.contains("@damocles_dynamic"));
|
|
||||||
assert!(body.contains("@iris_dynamic"));
|
|
||||||
// Each agent gets exactly one named dynamic location block.
|
|
||||||
let damocles_count = body.matches("@damocles_dynamic {").count();
|
|
||||||
let iris_count = body.matches("@iris_dynamic {").count();
|
|
||||||
assert_eq!(damocles_count, 1, "expected exactly one damocles_dynamic block");
|
|
||||||
assert_eq!(iris_count, 1, "expected exactly one iris_dynamic block");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -302,14 +302,6 @@ 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 base agent frontend dist. hive-c0re's
|
|
||||||
# gateway_nginx.rs uses this to generate split location
|
|
||||||
# blocks in agents.conf — static HTML/CSS/JS served from the
|
|
||||||
# nix store directly; dynamic API paths still proxied to the
|
|
||||||
# agent daemon. The nix store is shared across nspawn
|
|
||||||
# containers, so this path is reachable from inside the
|
|
||||||
# gateway container's nginx.
|
|
||||||
HIVE_AGENT_FRONTEND_DIR = "${cfg.frontend}/agent";
|
|
||||||
# Path to the static runtime asset tree (branding + claude
|
# Path to the static runtime asset tree (branding + claude
|
||||||
# prompts). `hive_sh4re::assets::*` reads paths underneath.
|
# prompts). `hive_sh4re::assets::*` reads paths underneath.
|
||||||
# `forge.rs` reads the avatar PNGs from here on startup.
|
# `forge.rs` reads the avatar PNGs from here on startup.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue