Add `hivectl gateway {create-user,delete-user,list-users}` subcommands for
managing htpasswd files used by gateway Basic auth. Pure Rust bcrypt
(cost 12, $2y$ prefix nginx accepts). No external htpasswd binary required.
Also fix the NixOS module assertion: `cfg.auth ? htpasswdFile` is always
true in the module system (declared options always exist as keys); switch
to `nullOr path; default = null` + `!= null` check so the assertion
actually fires with a useful error when enable=true but no file is set.
Guard bind-mount and nginx config against null to prevent eval errors.
Update docs/gateway.md to show hivectl commands instead of raw htpasswd.
Replaces the earlier PAM+binary approach with nginx's built-in
`auth_basic` module. No new binary, no new systemd service, no PAM.
New option `services.hyperhive.gateway.auth`:
- `enable` — off by default
- `htpasswdFile` — host path to an htpasswd file (required when enable)
- `realm` — WWW-Authenticate realm string (default "hyperhive");
restricted to `strMatching "[^\"$]*"` to prevent nginx config injection
When enabled:
- the parent directory of `htpasswdFile` is bind-mounted read-only
into the gateway container at `/run/gateway-auth/`
- the `"/"` proxy location gets `auth_basic` + `auth_basic_user_file`
Create credentials: `htpasswd -Bc /path/to/file alice` (BCrypt).
See `docs/gateway.md` ("HTTP Basic auth") for the full setup guide.
Adds opt-in HTTP Basic auth to the hive-gateway backed by the host PAM
stack + group membership check.
New binary `hive-gateway-auth` (hive-c0re workspace):
- Axum HTTP service on 127.0.0.1:7002 (host loopback)
- Decodes Basic credentials, authenticates via pam_unix.so
- Checks membership in `hyperhive-operator` group (or custom)
- Returns 200 / 401 / 403; nginx `auth_request` consumes these
New options under `services.hyperhive.gateway.auth`:
- `enable` — off by default
- `port` — auth service port (default 7002)
- `realm` — WWW-Authenticate realm string (default "hyperhive")
- `group` — required host group (default "hyperhive-operator")
- `pamService` — PAM service name (default "hive-gateway")
Host-side NixOS wiring:
- `users.groups.hyperhive-operator` declared when default group used
- `/etc/pam.d/hive-gateway` emitted via `security.pam.services`
- `systemd.services.hive-gateway-auth` runs the auth binary as root
(needs /etc/shadow access for pam_unix.so)
Gateway container nginx wiring:
- `location = /__hive_gateway_auth` — internal proxy to auth service
- `auth_request /__hive_gateway_auth` on the `"/"` proxy location
- `@hive_auth_required` named location adds WWW-Authenticate: Basic
header on 401 so browsers display a login prompt
Workspace deps: pam = "0.8"; flake.nix: linux-pam added to
nativeBuildInputs so pkg-config can find libpam at build time.
On first agent deployment, the container boots before hive-c0re has
provisioned the forge-token. forge-avatar-sync was exiting early with
"no forge-token found", and RemainAfterExit=true prevented systemd
from ever re-running it — avatar never got uploaded until the next
container reboot.
Add a systemd.paths.forge-avatar-sync unit (PathExistsGlob on the
forge-token file) to re-fire the service once the token arrives, and
set RemainAfterExit=false to allow the re-fire. Mirrors the existing
matrix-avatar-sync pattern exactly.
Same cleanup as the previous pass (tabs.js, app.js): replace
innerHTML = '' with replaceChildren() and lowercase the one
uppercase 'HTTP' error in logs.js.
- Replace innerHTML = '' with replaceChildren() throughout tabs.js and
app.js (12 + 7 sites). paintAtomic already used replaceChildren; now
the direct-clear sites are consistent with it.
- Add missing .rqe-source-startup_sweep CSS rule (startup_sweep is a
valid QueueSource variant but had no badge style, falling through to
the base muted appearance with no explicit intent).
- Lowercase the one uppercase 'HTTP ' in the fetchAndRenderToolGroups
error path to match every other fetch error in the file.
All crates now pass `cargo clippy --workspace -- -D warnings` cleanly.
Fixes span six crates (hive-sh4re, hive-ag3nt, hive-c0re, hive-forge,
hive-priv, hive-matrix-mcp was already clean):
- doc_markdown: wrap snake_case, type names, constants in backticks
- collapsible_if / collapsible_match: fold nested ifs into let-chains
- duration_suboptimal_units: Duration::from_secs(N) → from_mins/from_hours
- implicit_hasher: allow on HashMap-param fns where generalization is risky
- items_after_statements: hoist use to function tops
- map(f).unwrap_or(x) → map_or(x, f); map(f).unwrap_or_else(g) → map_or_else
- is_ok_and / is_none_or in place of map().unwrap_or(bool)
- needless_continue: {} instead of continue in loop match arms
- match_same_arms: Ok(None) | Err(_) merged
- format_push_str: write!() instead of push_str(&format!())
- while let replaces loop { let Some(..) = x else { break } }
- struct_excessive_bools / dead_code: allow on purpose-built structs
- too_many_lines / too_many_arguments: allow where refactor not worth it
- unused_async: remove async from poll_once in bash_runner
- needless_borrow: fix &repo deref in hive-forge comments verb
- cast_possible_truncation: allow u64→usize in fetch_tail
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add white-space: nowrap to .tab-label so text like '◆ Y3R C4LL ◆'
never breaks at spaces. Add flex-shrink: 0 to .tabbar .tab so tabs
keep their natural width rather than compressing — overflow detection
then correctly moves tabs that don't fit into the ⋮ dropdown.
turn_stats.rs writes to harness_dir()/hyperhive-turn-stats.sqlite but
stats.rs was reading from state_dir()/hyperhive-turn-stats.sqlite.
These paths diverged when the harness/state split was introduced.
The read side must match the write side.
overflowBtn/overflowDrop/overflowWrap were declared after syncTabFromHash()
was invoked. activateTab() calls updateTabbarOverflow() which closes over
these consts — hitting them in the TDZ threw ReferenceError on every page
load, also preventing fetchAndRenderToolGroups() from running (tool-groups
section stuck at loading).
Selecting any option in the M0V3 dropdown now fires confirm+POST
immediately; no separate button needed. On cancel or after the
request completes, the select resets to the placeholder. Removes
the now-redundant btn-move button and its CSS rule.
Remove the separate '⇡ M0V3 → ROOT' button from the selection bar.
Add '(no parent)' as the first real option in the existing M0V3
dropdown — selecting it submits an empty new_parent, which the backend
already treats as 'promote to root'.
The select placeholder label changes to '⇢ M0V3 →' so the combined
control reads naturally without the old standalone button. The submit
button enables as soon as any option past the placeholder is chosen
(selectedIndex > 0), which correctly covers both '(no parent)' and
named-parent selections.
Section heading, element id, CSS classes, and JS functions all renamed
from 'capabilities'/'cap-*' to 'tool-groups'/'tg-*' to accurately
describe what the UI manages (tool-group permissions, not a capabilities
system).
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
Lists all variables declared in base.css with their hex values,
Catppuccin Mocha names, and intended use. Includes a common-mistakes
table mapping the wrong names (--text, --mauve, --surface0/1/2, etc.)
to the correct ones, plus a usage guide for the most common patterns
(dropdowns, hover states, active tabs, badges).
--mauve is not declared in base.css; the palette exposes the Catppuccin
Mocha mauve colour as --purple (#cba6f7). Affected rules:
.btn-move colour + border
.logs-back colour (back-link on logs page)
.logs-tab.logs-tab-active text colour (issue #1001 — tab shows no state)
border colour on a schedule input
Combined with the --surface1/2 fix from the previous commit, the active
log tab now shows a solid background (--border) with purple text (--purple).
dashboard.css used var(--surface0/1/2) and var(--text) throughout the
agent context menu, tabbar overflow dropdown, and logs toolbar — none
of which are declared in base.css (the palette only defines --bg-elev,
--border, --purple-dim, --fg etc). This caused all three dropdowns to
render with a transparent background (issue #996).
Replacements:
--surface0 → --bg-elev (dropdown background)
--surface1 → --border (hover / active state)
--surface2 → --purple-dim (border / separator)
--text → --fg (label colour)
Also addresses two argus yellows from the PR #997 review:
- Skip updateTabbarOverflow() early when the dropdown is open; avoids
DOM flicker from the 1s badge-update tick while the menu is visible.
- Fall back to 32px when overflowBtn.offsetWidth === 0 (hidden on the
first render) so the initial width measurement is not off.
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.
Add a ⋮ overflow button at the right end of the dashboard tab strip.
SETTINGS and LOGS always live in the overflow dropdown (marked
data-overflow="default" in index.html — never rendered in the main bar).
Dynamic overflow: when the bar is too narrow to fit all remaining tabs,
rightmost tabs spill into the dropdown right-to-left. Implemented via
JS measurement + ResizeObserver; no CSS-only relayout trick needed.
The ⋮ button:
- hidden when the dropdown is empty (wide screens with only the default
tabs overflowed, which are always there anyway → button always shown)
- gets .tabbar-overflow-active when the current hash tab is inside
- dropdown items clone the tab label + count badge from the original
- closes on outside click / Escape
MutationObserver on the tabbar catches P33RS/M4TR1X hidden-attribute
changes so the overflow recalculates when those tabs are shown/hidden.
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`.
- live detection: use !h.status (null while running) not === 'running'
- SSE: replace named addEventListener('chunk'/'done') with onmessage +
frame.done check — backend sends generic message events, no named type
- SSE payload: use frame.stdout_append/stderr_append not frame.text
- timestamps: started_at/finished_at are unix seconds, multiply by 1000
was already done implicitly — actually use unix arithmetic directly
to avoid new Date() confusion entirely
Also move the misplaced .flow-main-slim rule from the logs section to
the flow-shell section of dashboard.css.
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.
Per mara feedback: don't show disabled actions at all.
- restart/stop only rendered when agent is running
- start only rendered when agent is stopped
- removed .agent-menu-item:disabled CSS rule (no longer needed)
Also switch Escape handler to stopImmediatePropagation so the
selection-clear keydown listener doesn't co-fire when closing a menu.
Each agent card in the SW4RM tab now has a ⋮ button that reveals a
dropdown with single-agent lifecycle actions: R3ST4RT, ST0P, ST4RT,
R3BU1LD, DESTR0Y, PURG3. Items are state-aware (disabled when action
doesn't apply). The button is CSS-invisible until the row is hovered
or the menu is open — quiet rows stay clean. DESTR0Y and PURG3 are
hidden for the manager container.
Click-outside and Escape close the menu. A tree-rebuild (on any SSE
state update) also closes it to avoid stale DOM references.
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.