- hive-sh4re: UnreadMatrix gains summary: String field (per-room breakdown)
- hive-matrix-mcp/protocol: add RoomUnread struct + UnreadSummary request
- hive-matrix-mcp/handlers: collect_unread() fetches per-room data;
single-unread rooms include truncated last-message body + sender;
multi-unread rooms carry count only
- hive-matrix-mcp/wake: format_unread_summary() builds wake body from
RoomUnread slice; terse one-liner for single-room/single-message,
bulleted list for multi-room; always appends read-hint
- hive-matrix-mcp/timeline: wake body now covers all rooms with unread
at fire time, not just the triggering event; falls back to per-event
teaser if notification counts haven't updated yet
- hive-ag3nt/mcp: matrix_unread_summary() replaces matrix_unread_rooms();
UnreadMatrix loose end carries per-room summary lines; render shows
room breakdown with sender: body for single-unread rooms
get_agent_links was proxying to http://127.0.0.1:{port}/api/state.
Since all agents now bind exclusively to a unix socket (HIVE_WEB_SOCKET
set unconditionally in harness-base.nix), the TCP fetch always fails
silently, returning [] — so the nav-strip icons on every card disappear.
Replace with container_view::build_nav_links(), which reconstructs the
same link list from disk:
- stats.html always (container kind)
- /{name} and /agent-configs/{name} when forge-token is present (forge kind)
- extras from hyperhive-dashboard-links.json (external kind)
The GUI screen link is intentionally omitted — /etc/hyperhive/gui.json
lives inside the agent container and is not host-visible. GUI agents
are rare; the omission is acceptable until a host-visible marker lands.
No new dependencies. reqwest is no longer used in dashboard.rs for
this handler (still used by forge.rs and hivectl.rs).
RELOAD_PENDING stays true on any failed reload, and reload_if_pending
fires on every 10-second spawn_poll tick. When the gateway is down or
nginx config is bad this hammers systemctl indefinitely.
Fix: track LAST_FAILED_RELOAD (unix timestamp). reload_if_pending backs
off to one attempt per RELOAD_RETRY_SECS (30s) after a failure. Fresh
write() calls reset the backoff so new topology changes still attempt
reload immediately.
Fixes#1113.
sync_agent called ensure_user_email on every tick, which sent
PATCH /api/v1/admin/users/{name} without the required login_name
field. Forgejo's EditUserOption handler was resetting use_custom_avatar
to false on each call, clobbering the avatar uploaded by the
forge-avatar-sync one-shot service.
Two-part fix:
- add login_name to the PATCH body (Forgejo EditUserOption requires it)
- write EMAIL_ALIGNED_MARKER_PREFIX{name} on first success; skip the
PATCH on all subsequent sync_agent ticks
Marker follows the same one-shot pattern as CORE_AVATAR_MARKER and
CONFIG_ORG_AVATAR_MARKER. Delete the marker to force re-alignment.
hive-priv had no PATH, so the nixos-container it runs for every container
op (incl. lifecycle::list) failed ENOENT. build_all swallows that into an
empty list — the 'no managed containers' symptom. Give the helper the
minimal set nixos-container shells out to (nixos-container, nix, util-linux,
e2fsprogs) on top of the systemd/coreutils/findutils already in the unit
PATH, rather than the whole system profile.
Also route forge/matrix is_present() through priv_client::list_containers
instead of spawning nixos-container directly from unprivileged hive-c0re.
The privsep drop to the hive-core user left four journalctl -M <container>
call sites shelling out directly. -M enters the container namespace via the
machine bus, which needs root, so all container-journal reads failed with
Permission denied. Add a ReadContainerJournal verb to hive-priv and route
dashboard get_journal, manager get_logs, the rebuild-failure journal tail,
and the agent host-journal -M path through it. Host-journal reads (no -M)
stay direct via systemd-journal group membership.
Drop the 'h-sock :8421' meta span from the agent card head.
ContainerView.container is now #[serde(skip)] since no frontend
code reads it; the field stays in Rust for internal use.
c.port remains serialized — used for fallback agent UI URLs.
add QueueKind::PermChange — dashboard tool-group and capability
handlers no longer write the shared JSON files inline. instead they
enqueue a PermChange entry; the FIFO worker applies the file write
then calls rebuild_agent so the updated env var takes effect.
concurrent batch-apply actions for different agents previously raced
on tool-groups.json / capabilities.json (last write wins, earlier
change silently dropped). serialising through the queue prevents this.
dedup check extended with perm-type discriminant so tool-groups and
capabilities changes for the same agent are kept as distinct entries
and never collapse into one slot.
- hive-sh4re: ToolGroup::description() and Capability::description() return
short human-readable strings for each variant
- hive-c0re: ToolGroupsSnapshot and CapabilitiesSnapshot now include a
`descriptions` map (name → description); get_capabilities now iterates
Capability::ALL instead of hardcoding the list
- tabs.js: renderToolGroups + renderCapabilities use descriptions[name] as
the column header title attribute (native browser tooltip on hover)
The hardcoded vec was a maintenance hazard — any new capability added
to Capability::ALL would silently be omitted from the permissions UI
column list until get_capabilities was manually updated.
Now both the GET and POST handlers derive their known-capability lists
from the same Capability::ALL source of truth.
- rename cap-cap-col → cap-col; add CSS rules for .cap-col and .cap-save-col
- drop zero-width-space replace in capability header cells (nowrap makes it a no-op)
- add Capability::ALL to hive-sh4re; validate incoming cap strings in post_capabilities
Add a new P3RM1SS10NS tab to the dashboard that consolidates all
per-agent permission configuration:
Backend:
- GET /api/capabilities returns { caps: [...], assignments: {...} }
driven by Capability::ALL variants (manage_root_agent,
read_host_journal, query_agent_state)
- POST /api/capabilities/{agent} writes capabilities.json and queues
a rebuild so HIVE_CAPABILITIES takes effect
Frontend:
- New 'permissions' entry in TABS, placed after 'system'
- P3RM1SS10NS tab pane with two sections:
C4P4B1L1T13S — agents × capabilities checkbox matrix (.cap-*)
T00L GR0UPS — agents × tool-groups checkbox matrix (.tg-*) moved
from SYST3M tab
- activateTab('permissions') fetches both tables; neither has an SSE
channel so they re-fetch on each activation to stay fresh
- CSS for .cap-* mirrors the .tg-* layout (scrollable, Catppuccin)
Remove the custom htpasswdFile option and bind-mount. The htpasswd file
now lives at the fixed path /var/lib/hyperhive/gateway/gateway.htpasswd
on the host, which is already exposed inside the container at
/run/hive-state/gateway.htpasswd via the existing gateway state
bind-mount — no extra bind-mount needed.
A tmpfiles rule pre-creates the file so nginx can open it even before
any users exist (empty file → all requests return 401, which is correct).
hivectl gateway commands default --file to the standard path so
`hivectl gateway create-user alice` just works without any flags.
Per mara's direction: both nixpkgs and nixpkgs-unstable are now
top-level meta flake inputs with explicit store-path URLs. Hyperhive
follows them rather than the other way around:
inputs.nixpkgs.url = "path:${pkgs.path}";
inputs.nixpkgs-unstable.url = "path:${nixpkgs-unstable}";
inputs.hyperhive.url = "...";
inputs.hyperhive.inputs.nixpkgs.follows = "nixpkgs";
inputs.hyperhive.inputs.nixpkgs-unstable.follows = "nixpkgs-unstable";
New NixOS host options (auto-set at build time, overridable):
services.hyperhive.c0re.nixpkgsFlake
default: "path:${pkgs.path}" — host's evaluated nixpkgs.
services.hyperhive.c0re.nixpkgsUnstableFlake
default: "path:${nixpkgs-unstable}" from hyperhive's flake.nix —
the channel that carries claude-code. Operators can override to
track a different unstable snapshot.
Legacy fallback (both args empty) preserved for backward compat.
Two new Rust tests cover the full-URL and fallback paths.
meta flake was using `nixpkgs.follows = "hyperhive/nixpkgs"` but
`hyperhive` is a store-path input, so nix resolves hyperhive's own
pinned lock rather than the host's follows-substituted version.
When an operator sets `inputs.hyperhive.inputs.nixpkgs.follows =
"nixpkgs"` in their host flake, the meta flake was silently ignoring
it and using hyperhive's pinned nixpkgs instead.
Fix: hive-c0re.nix injects `--nixpkgs-flake path:${pkgs.path}` into
the daemon's ExecStart. `pkgs` IS the host's nixpkgs when follows is
set; otherwise it's hyperhive's own pin — so the meta flake gets the
right nixpkgs in both cases. render_flake emits `nixpkgs.url = "..."`
(explicit) when nixpkgs_flake is non-empty, falling back to the old
`follows` form when empty for backward compat.
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.
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>