Backend (hive-c0re/src/dashboard.rs):
GET /api/tool-groups — returns { groups: [...], assignments: {...} };
groups list comes from ToolGroup::ALL so the UI needs no change when
a new group is added (satisfies the 'no extend ui' requirement)
POST /api/tool-groups/{agent} — accepts { groups: [...] }, calls
set_groups() then enqueues a rebuild so the new HIVE_TOOL_GROUPS
env var takes effect immediately
hive-sh4re/src/lib.rs:
Added ToolGroup::ALL const (ordered slice of every group)
Added ToolGroup::as_str() — snake_case wire name, matches serde
Frontend:
SYST3M tab: new C4P4B1L1T13S section above K3PT ST4T3 with
#capabilities-section placeholder
tabs.js: fetchAndRenderCapabilities() + renderCapabilities() —
columns are built from the groups array returned by the API;
each row has one checkbox per group and a save button that POSTs
and re-fetches after 800ms; agents without explicit assignments
show a (default) label; triggered on each SYST3M tab activation
dashboard.css: .cap-table-wrap/.cap-table/.cap-row/.cap-agent-*
styles for the scrollable matrix table
topology::reconcile_roles writes roles.json to the meta dir, but
sync_agents never staged it. After #978 merged, roles.json showed up
as an untracked file in the meta repo (visible in `git status`) because
it followed the same pattern as topology.json and tool-groups.json but
was missed in the git add list.
Add the same conditional stage for roles.json: git add is a no-op when
the file is unchanged or absent, matching the existing pattern.
nginx -s reload signals a running master process. When nginx enters
failed state (start-limit-hit from repeated nginx -t failures on a
bad agents.conf), there is no master and the reload is a silent no-op.
c0re kept re-firing the same no-op reload forever via RELOAD_PENDING.
Fix: probe the nginx unit's ActiveState before sending the reload:
- active → nginx -s reload (existing zero-downtime path)
- failed → systemctl reset-failed nginx + systemctl start nginx
- other → systemctl start nginx
This makes c0re self-healing: once a corrected agents.conf is published,
the next reload_gateway_nginx call clears the start-limit and restarts
nginx automatically without operator intervention.
New helpers: nginx_active_state() (systemctl show --property=ActiveState
--value) and gateway_systemctl() (host-side systemctl --machine=hive-gateway).
When the token file is deleted but the homeserver account still exists,
register_user returns M_USER_IN_USE (HTTP 400) and the provisioning
sweep hard-fails, leaving the agent without a working matrix token.
Fix: persist the random password to matrix-password alongside the
access token on first registration. On subsequent attempts where
M_USER_IN_USE is returned, fall back to login_user (m.login.password)
using the stored password. If both files are gone, the error message
guides the operator to `hivectl matrix create-user <name> --password`.
Add /logs.html as a standalone page (same back-link pattern as flow.html):
- BUILD tab: all-agents build log history via new GET /api/build-logs endpoint
- AGENT tab: per-container journald viewer with agent selector + unit filter
- SYSTEM tab: host-side hive-c0re.service logs via new GET /api/journal-host endpoint
Remove inline log drill-ins from SW4RM container rows (buildJournalTrigger
and buildBuildLogsTrigger) — log viewing now lives on the dedicated page.
flow.html: strip the full dashboard tabbar, replace with a simple back link
matching the new logs page chrome.
index.html: add L0GS tab link to /logs.html in the tab strip.
Backend additions:
- build_logs::list_recent_all — cross-agent query (newest first, cap 100)
- GET /api/build-logs — all-agents variant backed by list_recent_all
- GET /api/journal-host — host journald (no -M container flag), restricted
to allow-listed units (hive-c0re.service)
Per mara's design: the role grants access to every agent with no parent
in the topology (parent=None), derived purely from structure. No agent
name is hardcoded. In normal operation this is just the manager; any
additional parentless agents the operator creates are also covered.
Update ROLE docstring, lifecycle.rs comment, and unit tests accordingly.
Add a multi-root test to document the behaviour with multiple parentless
agents.
Replace literal "root" with MANAGER_NAME constant in role tests;
update ROLE_CAN_MANAGE_TOP_LEVEL_AGENTS docstring to say "manager"
not "root/manager agent".
The previous parent=None heuristic was wrong — the manager is not
required to be the structural topology root (it can have a parent).
Delegate to children_of_in(MANAGER_NAME) directly; topology.rs is the
right place for this knowledge. Update comment in lifecycle.rs to say
"direct child of the manager" instead of "direct child of root".
Add topology::top_level_agents_in(topo) and top_level_agents() which
find the topology root by structure (parent=None) rather than by name,
then return its children. Lifecycle.rs role logic now uses this instead
of children_of(MANAGER_NAME), removing the hardcoded manager-name
reference from the bind-mount logic.
Also adds two unit tests for top_level_agents_in.
set_role previously removed empty entries after revoke, causing
reconcile_roles to re-seed the role on the next tick (absent key =
never seen = seed). Fix: never remove empty entries; an empty list
is a tombstone meaning "explicitly revoked".
Also adds unit tests for has_role_in, set_role revoke semantics, and
the reconcile_roles seed/no-seed distinction (pure in-memory, no disk).
Removes the MANAGER_NAME special-case from set_nspawn_flags in favour of
two general mechanisms:
1. Topology-driven child mounts: every agent now gets its direct children's
state, harness, and config dirs bind-mounted (RW). Root's children are
the top-level agents, so root gets the same access it did before via the
old /agents blob bind — but derived from topology, not a hardcoded name
check.
2. can_manage_top_level_agents role: agents holding this role additionally
get every top-level agent treated as a virtual child (same RW mounts)
plus /applied and /meta as RO. Designed for recovery: a role holder can
update a top-level agent's config even when that agent is down.
Root receives this role by default on first reconcile_roles call.
Operator can revoke it with set_role.
Every agent (including root) now gets its own state/harness/config dirs via
the standard path. Roles are stored in meta/roles.json (same dir as
topology.json); reconcile_roles is called from reconcile so both files stay
in sync.
All functional renames (MANAGER_NAME, MANAGER_AGENT → "root") were done in
earlier commits. This cleans up the stale `hm1nd` strings that remained in
doc comments, test fixture labels, and the manager MCP server instructions
(which told the manager its config lived at /agents/hm1nd/config/agent.nix).
nginx forbids a URI part on proxy_pass inside named locations, so the
split-mode `@<name>_dynamic` fallback (introduced when the gateway
started serving static dist directly) failed config-test with:
"proxy_pass" cannot have URI part in location given by regular
expression, or inside named location
Render two upstream forms: prefix locations keep the trailing slash so
nginx auto-strips the location prefix; the named-location path strips
`/agent/<name>/` via `rewrite ... break` and uses a bare upstream.
`@hyperhive` (no dot) is rejected by Forgejo's PATCH /admin/users API
with 422 Unprocessable Entity. Switch to `@hyperhive.local` across all
four files that construct hive-c0re/agent git emails and Forgejo account
emails, so both stay in sync and pass RFC-valid domain validation.
BindMount was imported at line 10 via the priv_proto glob-style import,
then re-exported again with `pub use` at line 69. E0252: two definitions
of BindMount in the same type namespace. Remove the redundant pub use —
callers that need BindMount should import from hive_sh4re::priv_proto.
gateway_nginx.rs reads HIVE_AGENT_FRONTEND_DIR (injected by hive-c0re.nix
as ${cfg.frontend}/agent). When set, agents.conf emits per-agent split
blocks instead of the old single proxy_pass:
# Compiled assets — immutable nix store path, cache 1y
location ^~ /agent/<name>/static/ {
alias <frontend>/static/;
expires 1y; add_header Cache-Control "public, immutable, ...";
}
# Static dist + proxy fallback
location /agent/<name>/ {
alias <frontend>/;
try_files $uri $uri.html $uri/index.html @<name>_dynamic;
}
location @<name>_dynamic {
proxy_pass <upstream>; # api, events, icon, login, …
…proxy headers unchanged…
}
try_files path resolution (nginx applies alias mapping first):
$uri — exact file (/static/app.js → static/app.js)
$uri.html — bare-path fallback (/stats → stats.html)
$uri/index.html — directory index (/ → index.html)
@<name>_dynamic — proxy catchall for anything not in the dist
Adding pages to the frontend dist works automatically — no generator
change needed. Per-agent extraFiles (in mergedDist, not in the base
nix-store path) continue to proxy to the agent daemon.
frontend is a nix store path injected at build time — only [a-z0-9/._-],
no shell metacharacters — safe to interpolate without sanitization;
comment added documenting this assumption.
Without HIVE_AGENT_FRONTEND_DIR the existing single-proxy block is
emitted unchanged — backward-compatible for deployments without the env.
render() takes frontend_dir as a parameter so tests exercise both code
paths safely in parallel. 13 tests: 7 legacy, 6 split-mode. No clippy
warnings in changed files.
nix/modules/hive-c0re.nix: inject HIVE_AGENT_FRONTEND_DIR = "${cfg.frontend}/agent".
Add a systemd.sockets.hive-c0re unit that holds /run/hyperhive/host.sock
before hive-c0re starts. hive-c0re serve() detects LISTEN_FDS via the
listenfd crate and accepts the systemd-handed fd instead of calling bind().
Falls back to the existing bind path when LISTEN_FDS is absent so direct
invocation and CI are unaffected.
Benefits: hivectl can connect the moment the socket unit activates (no
racy window), and a hive-c0re restart never drops the socket inode.