Watch
0
0
Fork
You've already forked hyperhive
0

remove the forge links from the hive dashboard and the agent page

Each agent row on the hive dashboard, and the agent page's link menu, linked
to the agent's forge profile and its config repo. Both are gone, with what
only they used:

- hive-agent no longer builds the two links, the `Forge` link kind, or the
  `forge_public_url` field of its state, and no longer reads
  `HIVE_FORGE_PUBLIC_URL`. The agent page's link menu and the dashboard row's
  icon strip (which skips a forge link an agent not yet rebuilt still sends)
  drop their forge handling.
- hive-c0re's dashboard state no longer carries `forge_public_url`.
- `HIVE_FORGE_PUBLIC_URL` is no longer forwarded to agents through the meta
  flake (the forwarded-variable list, its test fixture, the controller's
  settings and the VM test), so the next meta flake render differs by that
  one line. hive-c0re still reads it for the forge URL a hive publishes, so
  `swarm.forge.publicUrl` stays, with its description corrected.
- The docs stop describing the links.
This commit is contained in:
iris 2026-11-07 12:44:07 +01:00 • committed by mara
commit d516bb890c
16 changed files with 49 additions and 148 deletions

View file

@ -81,14 +81,11 @@ at a given viewport width. Two columns:
- **Links badge** (`🔗`): opens a popover listing `↑ dashboard`
(back-link to the host dashboard, `${dashboardBase}`)
followed by this agent's `agent_links()`-sourced navigation (stats,
screen when VNC is enabled, forge profile + config-repo mirror when
the agent has a forge account, any `services.hyperhive.agent.dashboardLinks`
screen when VNC is enabled, any `services.hyperhive.agent.dashboardLinks`
extras) — all as real `<a>` elements, not a `Dropdown`-style
command list, so ctrl/middle-click and "copy link address" keep
working. Each `AgentLink.kind` resolves differently: `container` →
same-origin path; `forge` → `state.forge_public_url + url`, and
hive-agent drops the link entirely when that's unset (never guessed from
`<host>:3000`); `external` → already absolute. Same source
same-origin path; `external` → already absolute. Same source
(`GET /api/state`'s `links` field) also feeds
`DashboardState.links` for the dashboard card's icon strip —
`agent_links()` in hive-agent is the single source of truth for

View file

@ -45,8 +45,8 @@ covers it.
**Check on an agent.** 4G3NTS shows every container as a row: name, whether
it's running, what it's doing (a live status pill such as `rebuilding…` or
`starting…` while something's in flight), and quick links (stats, screen,
forge profile). Select the name to open its terminal and watch it work.
`starting…` while something's in flight), and quick links (stats, screen).
Select the name to open its terminal and watch it work.
**Review a config change.** Agent config changes (new packages, env vars, MCP
servers) are pull requests on the agent's `agent-configs` repo. Review and

View file

@ -197,9 +197,8 @@ fetch entirely.
**icon-only nav strip** plus live agent-owned state, all populated
async from a single `GET /api/dashboard-state` call to the agent's
own hive-agent. The response (`DashboardState`) carries: `links` (nav
strip entries — `📊 stats`, `🖥 screen` when GUI is enabled, `⬡ forge
profile`, `↳ agent-configs mirror`, plus any agent-declared
`dashboardLinks` extras), `status_text` / `status_set_at` (agent
strip entries — `📊 stats`, `🖥 screen` when GUI is enabled, plus any
agent-declared `dashboardLinks` extras), `status_text` / `status_set_at` (agent
self-reported status — the `(set N ago)` chip carries a `data-set-at`
stamp and ticks every 30s to stay fresh across the long-lived keyed row
cache), `rate_limited`, `ctx_tokens` / `context_window_tokens`
@ -216,11 +215,9 @@ per-agent harness — TCP or unix-domain depending on the agent's
`docs/4-implementation/networking/gateway.md::Per-agent unix-socket upstream`).
Gateway-off is the flag-absent fallback (a dashboard served outside a
hyperhive deploy): base URL is
`http://<host>:<container.port>` (direct TCP). Forge
links resolve against `state.forge_public_url` (sourced from
`services.hyperhive.swarm.forge.publicUrl`) and disappear entirely when
that's unset — never guessed from `<host>:3000`. External links are
already absolute. The same base URL drives the primary agent-name
`http://<host>:<container.port>` (direct TCP). External links are
already absolute. The row has no forge links: an agent's forge profile
and config repo are on the forge itself. The same base URL drives the primary agent-name
link + favicon fetch, so the whole row routes through the gateway
as a unit.
<!-- vale write-good.Passive = YES -->

View file

@ -431,9 +431,7 @@ when `StateSnapshot.gateway_enabled` is true, and direct
every hive, so the direct shape is a fallback for the variable being unset.
Three render sites follow it: the agent-name link, the favicon fetch
(`<url>/icon`), and the nav-strip `container`-kind links from
`DashboardState.links` (`GET /api/dashboard-state`). Forge links come from
`swarm.forge.publicUrl`; with it unset the dashboard hides them rather than
guess. See `docs/2-handbook/hives/dashboard.md::Container row` for the frontend side.
`DashboardState.links` (`GET /api/dashboard-state`). See `docs/2-handbook/hives/dashboard.md::Container row` for the frontend side.
### Dialing another vhost by name (`verifiedProxyTo`)

View file

@ -141,7 +141,7 @@ export function Root() {
count={todos.length}
onClick={() => setOpenPanel("todos")}
/>
{/* Needs a loaded snapshot for its data (links/forge_public_url,
{/* Needs a loaded snapshot for its data (links,
* dashboard_port) — omitted until the first /api/state resolves,
* same as the old page's populateOverflowMenu (now deleted —
* dashboard-link moved here, rebuild removed outright, see
@ -149,7 +149,6 @@ export function Root() {
{state ? (
<MetaNav
links={state.links}
forgePublicUrl={state.forge_public_url}
dashboardBase={resolveDashboardBase(state.dashboard_port)}
/>
) : null}

View file

@ -196,7 +196,7 @@ h3 {
object-fit: cover;
}
/* Meta-nav links (stats / screen / forge / dashboard / extras) —
/* Meta-nav links (stats / screen / dashboard / extras) —
no underline; hover lights with cyan glow + subtle background
tint. Reads as a row of soft tabs rather than default-styled
inline anchors. */

View file

@ -2,7 +2,7 @@
// (`@hive/shared/icons.js`'s `LinkIcon` — an emoji glyph can't match a
// neighbouring icon's size/weight on any platform, see that file's
// comment) in the pills cluster that opens a popover listing
// stats/screen/forge/config + any `services.hyperhive.agent.dashboardLinks` extras,
// stats/screen + any `services.hyperhive.agent.dashboardLinks` extras,
// sourced from the backend's `agent_links()` (the single source of
// truth — same list also feeds the dashboard card's icon strip) — plus
// a `🧭 dashboard` back-link.
@ -28,17 +28,12 @@ import "./MetaNav.css";
export interface MetaNavProps {
links: AgentLink[];
forgePublicUrl: string | null;
/** Absolute base URL of the host dashboard (`resolveDashboardBase`)
* — the `🧭 dashboard` item links to `${dashboardBase}`. */
dashboardBase: string;
}
export function MetaNav({
links,
forgePublicUrl,
dashboardBase,
}: MetaNavProps) {
export function MetaNav({ links, dashboardBase }: MetaNavProps) {
const [open, setOpen] = useState(false);
const rootRef = useRef<HTMLDivElement>(null);
@ -64,11 +59,7 @@ export function MetaNav({
};
}, [open]);
// Same kind -> URL resolution as app.js's old refreshState meta-links
// loop: `forge` needs `forgePublicUrl` set or the link is dropped
// entirely (never guessed from `<host>:3000`); `external` is already
// absolute; `container` is a same-origin path.
const visible = links.filter((lnk) => lnk.kind !== "forge" || forgePublicUrl);
// `external` is already absolute; `container` is a same-origin path.
// No early-return-on-empty: the dashboard link below is always
// present, so the trigger always has at least one item.
@ -93,24 +84,20 @@ export function MetaNav({
>
🧭 dashboard
</a>
{visible.map((lnk) => {
const href =
lnk.kind === "forge" ? `${forgePublicUrl}${lnk.url}` : lnk.url;
return (
<a
key={lnk.url}
class="meta-nav-item"
href={href}
target="_blank"
rel="noopener"
role="menuitem"
onClick={() => setOpen(false)}
>
{lnk.icon ? <span aria-hidden="true">{lnk.icon}</span> : null}
{lnk.label}
</a>
);
})}
{links.map((lnk) => (
<a
key={lnk.url}
class="meta-nav-item"
href={lnk.url}
target="_blank"
rel="noopener"
role="menuitem"
onClick={() => setOpen(false)}
>
{lnk.icon ? <span aria-hidden="true">{lnk.icon}</span> : null}
{lnk.label}
</a>
))}
</div>
) : null}
</div>

View file

@ -28,11 +28,10 @@ export interface AgentState {
paused: boolean;
inbox: InboxRow[];
links: AgentLink[];
forge_public_url: string | null;
}
// Mirrors `hive_agent::web_ui::state::AgentLink` — one meta-nav entry
// in the agent page header (stats/screen/forge/config + any
// in the agent page header (stats/screen + any
// `services.hyperhive.agent.dashboardLinks` extras). `agent_links()` on the backend
// is the single source of truth for what links an agent exposes; see
// docs/2-handbook/agents/web-page.md::Header for the kind → URL resolution table.
@ -40,7 +39,7 @@ export interface AgentLink {
url: string;
icon: string;
label: string;
kind: "container" | "forge" | "external";
kind: "container" | "external";
}
export interface TokenUsage {

View file

@ -270,8 +270,7 @@ function containerRowFingerprint(
// changed (keyed cache), skipping the build + async dashboard-state
// fetch for stable rows.
function buildContainerLi(c, opts) {
const { pending, opRunning, selected, url, containerBase, forgeBase, s } =
opts;
const { pending, opRunning, selected, url, containerBase, s } = opts;
// A single `pending-running` class covers any active transient
// badge state (see the pending-badge derivation comment in
// renderContainers).
@ -342,8 +341,7 @@ function buildContainerLi(c, opts) {
// `GET /api/dashboard-state` (via gateway when enabled, direct
// TCP otherwise). The agent is the single source of truth for its
// link list: stats / screen (GUI agents only — c0re's disk-based
// fallback cannot detect this) / forge profile / agent-configs /
// extras. DOM-built — link strings come from the agent's process
// fallback cannot detect this) / extras. DOM-built — link strings come from the agent's process
// and must never reach the HTML parser.
const navStrip = el("span", { class: "nav-strip" });
head.append(navStrip);
@ -363,15 +361,13 @@ function buildContainerLi(c, opts) {
// -- nav strip -----------------------------------------------
if (Array.isArray(ds.links)) {
for (const lnk of ds.links) {
// No public forge URL to link to — hide rather than build
// a broken/guessed href (see forgeBase above).
if (lnk.kind === "forge" && !forgeBase) continue;
// An agent not yet rebuilt still sends forge links, which no
// page links to any more.
if (lnk.kind === "forge") continue;
const href =
lnk.kind === "forge"
? forgeBase + (lnk.url || "")
: lnk.kind === "external"
? lnk.url || ""
: /* container */ containerBase + "/" + (lnk.url || "");
lnk.kind === "external"
? lnk.url || ""
: /* container */ containerBase + "/" + (lnk.url || "");
const a = el("a", {
class: "nav-link",
href,
@ -634,12 +630,6 @@ export function renderContainers(s) {
// loopback port while the socket marker is absent). See
// `docs/2-handbook/hives/dashboard.md::Container row` + `docs/4-implementation/networking/gateway.md::Vhost map`.
const gatewayLinks = !!(s && s.gateway_enabled);
// Forge public URL: state.forge_public_url (set by the NixOS module
// from services.hyperhive.swarm.forge.publicUrl) or null — never guessed
// from "<hostname>:3000". `null` propagates down to
// buildContainerLi/buildAgentMenu, which already hide forge links
// rather than emit a possibly-wrong one.
const forgeBase = (s && s.forge_public_url) || null;
const ul = existingUl ?? el("ul", { class: "containers" });
const rows = sortedContainerRows(containers);
@ -691,7 +681,6 @@ export function renderContainers(s) {
selected,
url,
containerBase,
forgeBase,
s,
});
containerRowCache.set(c.name, { el: li, fingerprint: fp });

View file

@ -47,10 +47,7 @@ pub(super) async fn api_state(State(state): State<AppState>) -> axum::Json<State
context_window_tokens,
ctx_usage,
cost_usage,
links: agent_links(&state.label, state.gui_vnc_port.is_some()),
forge_public_url: std::env::var("HIVE_FORGE_PUBLIC_URL")
.ok()
.filter(|s| !s.is_empty()),
links: agent_links(state.gui_vnc_port.is_some()),
hive_name: crate::identity::hive_name(),
swarm_name: crate::identity::swarm_name(),
effort: shown.effort,
@ -87,7 +84,7 @@ pub(super) async fn api_dashboard_state(
ctx_tokens,
context_window_tokens,
rate_limited,
links: agent_links(&state.label, state.gui_vnc_port.is_some()),
links: agent_links(state.gui_vnc_port.is_some()),
})
}
@ -148,13 +145,6 @@ pub(super) struct StateSnapshot {
/// — single source of truth. See [`docs/2-handbook/hives/dashboard.md::Container row`]
/// for the frontend resolver + which links appear in which conditions.
links: Vec<AgentLink>,
/// Public URL of the forge (e.g.
/// `"https://forge.pr1ma.darkest.space"`). Sourced from
/// `HIVE_FORGE_PUBLIC_URL` (set from
/// `services.hyperhive.swarm.forge.publicUrl`); `None` when unset. The frontend uses this to build
/// forge nav-strip links, and **hides** the forge link entirely
/// when absent rather than guessing `<hostname>:3000`.
forge_public_url: Option<String>,
/// Human name of this hive instance (e.g. `"pr1ma"`). Sourced
/// from `HYPERHIVE_HIVE_NAME`; `None` when unset. The frontend
/// uses this for the page `<title>` and header label so browser
@ -190,7 +180,7 @@ pub(super) struct StateSnapshot {
/// of truth for what links an agent exposes.
#[derive(Serialize)]
struct AgentLink {
/// `kind = Container | Forge` → path; `kind = External` → full URL.
/// `kind = Container` → path; `kind = External` → full URL.
/// The frontend prepends the right base before rendering.
url: String,
icon: String,
@ -209,10 +199,6 @@ enum AgentLinkKind {
/// `/screen`). Agent page: same-origin path. Dashboard:
/// `http://<host>:<container.port><url>`.
Container,
/// `url` is a path on the local Forgejo (`/<label>`,
/// `/agent-configs/<label>`). Both surfaces:
/// `http://<host>:3000<url>`.
Forge,
/// `url` is already a fully-qualified absolute URL — use as-is.
/// Agent-declared `services.hyperhive.agent.dashboardLinks` extras arrive here.
External,
@ -272,7 +258,7 @@ fn read_own_status() -> (Option<String>, Option<i64>) {
}
/// Build the navigation link list for the agent page header. URLs
/// are paths (relative) for `Container`/`Forge` targets and absolute
/// are paths (relative) for `Container` targets and absolute
/// for `External`; the frontend resolves each against its `kind`
/// against the right base so the backend never has to guess the
/// operator's browser host. See
@ -282,16 +268,9 @@ fn read_own_status() -> (Option<String>, Option<i64>) {
/// Icons are real emoji, not unicode arrow/geometric glyphs — mara:
/// operator-declared `dashboardLinks` extras are already free-choice
/// emoji, so the built-ins should read as the same family rather than
/// mixing in plain text glyphs (which also don't reliably render as a
/// hexagon/arrow across fonts to begin with). `config` went through two
/// rounds on this: gear (`⚙`, with and without a variation selector)
/// still rendered broken in mara's browser either way — same
/// font-unreliable codepoint `icons.tsx` already documents avoiding.
/// Landed on the wrench (`🔧`) instead: a supplementary-plane pictograph
/// with `Emoji_Presentation=Yes`, same category as every other icon
/// here (`📊`/`🖥`/`🔨`/`🧭`), so it doesn't need a variation selector to
/// render as color emoji in the first place.
fn agent_links(label: &str, gui_enabled: bool) -> Vec<AgentLink> {
/// mixing in plain text glyphs (which also don't reliably render across
/// fonts to begin with).
fn agent_links(gui_enabled: bool) -> Vec<AgentLink> {
let mut links = Vec::new();
links.push(AgentLink {
@ -310,26 +289,6 @@ fn agent_links(label: &str, gui_enabled: bool) -> Vec<AgentLink> {
});
}
// Either copy of the forge token: the one fetched from the swarm secret
// store (`HIVE_FORGE_TOKEN_FILE`), or the state-dir file the hive used
// to write.
let fetched_token = std::env::var_os("HIVE_FORGE_TOKEN_FILE")
.is_some_and(|p| std::path::Path::new(&p).is_file());
if fetched_token || crate::paths::state_dir().join("forge-token").is_file() {
links.push(AgentLink {
url: format!("/{label}"),
icon: "🔨".to_owned(),
label: "forge".to_owned(),
kind: AgentLinkKind::Forge,
});
links.push(AgentLink {
url: format!("/agent-configs/{label}"),
icon: "🔧".to_owned(),
label: "config".to_owned(),
kind: AgentLinkKind::Forge,
});
}
// Agent-declared extras (`services.hyperhive.agent.dashboardLinks` → the
// `hive-dashboard-links` NixOS oneshot writes them to
// `{state_dir}/hyperhive-dashboard-links.json`). Shape on disk

View file

@ -66,15 +66,6 @@ pub(super) struct StateSnapshot {
/// `http://<hostname>:<port>/` TCP links) is retained as a defensive
/// fallback for the env being unset. See `docs/4-implementation/networking/gateway.md::Vhost map`.
gateway_enabled: bool,
/// Public URL of the forge (e.g.
/// `"https://forge.pr1ma.darkest.space"`). Sourced from the
/// `HIVE_FORGE_PUBLIC_URL` env var, which the c0re NixOS module
/// sets from `services.hyperhive.swarm.forge.publicUrl` (defaults to
/// the gateway vhost URL). `None` when absent — the frontend **hides**
/// forge links rather than guessing `http://<hostname>:3000`,
/// which is only right by accident on deployments that aren't
/// plain localhost.
forge_public_url: Option<String>,
/// Browser URL of the swarm UI (the swarm apex domain). Sourced from
/// `HIVE_SWARM_PUBLIC_URL`, which the c0re NixOS module sets from
/// `services.hyperhive.swarm.domain`. `None` when the domain is unset —
@ -197,9 +188,6 @@ pub(super) async fn api_state(
let s = v.to_string_lossy().to_ascii_lowercase();
matches!(s.as_str(), "1" | "true" | "yes")
}),
forge_public_url: std::env::var("HIVE_FORGE_PUBLIC_URL")
.ok()
.filter(|s| !s.is_empty()),
swarm_public_url: std::env::var("HIVE_SWARM_PUBLIC_URL")
.ok()
.filter(|s| !s.is_empty()),

View file

@ -69,7 +69,6 @@ pub const FORWARDED_VARS: &[&str] = &[
// The metrics store's origin, for `swarm-metrics`, for the same reason.
"HIVE_AGENT_METRICS_URL",
"HIVE_FORGE_URL",
"HIVE_FORGE_PUBLIC_URL",
"HIVE_MATRIX_URL",
"HYPERHIVE_SWARM_NAME",
];

View file

@ -33,7 +33,6 @@
HIVE_AGENT_LOGS_QUERY_URL = "@HIVE_AGENT_LOGS_QUERY_URL@";
HIVE_AGENT_METRICS_URL = "@HIVE_AGENT_METRICS_URL@";
HIVE_FORGE_URL = "@HIVE_FORGE_URL@";
HIVE_FORGE_PUBLIC_URL = "@HIVE_FORGE_PUBLIC_URL@";
HIVE_MATRIX_URL = "@HIVE_MATRIX_URL@";
};
in

View file

@ -78,17 +78,11 @@ in
defaultText = lib.literalExpression ''"https://''${domain}"'';
example = "https://forge.example.com";
description = ''
Browser-facing forge URL the dashboard uses to build clickable
forge links (the per-agent-row forge links) —
sourced into every agent container + hive-c0re as
`HIVE_FORGE_PUBLIC_URL`.
Browser-facing forge URL, sourced into hive-c0re as
`HIVE_FORGE_PUBLIC_URL` and published with the hive's URLs.
Defaults to `https://''${cfg.domain}`, the gateway vhost. When
`null`, the dashboard **hides** forge links rather than
guessing one — see `docs/2-handbook/hives/dashboard.md::Container row` for
the rationale (a link built from the operator's own browser
hostname + a container port is only an accident away from
wrong on any deployment that isn't plain localhost).
`null`, the hive publishes no forge URL rather than guessing one.
'';
};

View file

@ -267,9 +267,6 @@ let
vars = {
HIVE_FORGE_URL = "http://${forgeCfg.domain}";
}
// lib.optionalAttrs (forgeCfg.publicUrl != null) {
HIVE_FORGE_PUBLIC_URL = forgeCfg.publicUrl;
}
// lib.optionalAttrs (matrixCfg.gatewayHost != null) {
HIVE_MATRIX_URL = "http://${matrixCfg.gatewayHost}";
}

View file

@ -49,7 +49,6 @@ let
"HIVE_AGENT_BAO_SERVICES_PKI_MOUNT"
"HIVE_AGENT_LOGS_QUERY_URL"
"HIVE_AGENT_METRICS_URL"
"HIVE_FORGE_PUBLIC_URL"
"HIVE_FORGE_URL"
"HIVE_MATRIX_URL"
];