Commit graph hyperhive/hive-c0re/src/lifecycle.rs
Author SHA1 Message Date
damocles
fb726197ea fix(#1375): clean up pedantic warnings and re-enable -D warnings without pedantic bypass 2026-06-05 16:55:09 +02:00
atlas
734fe88858 fix(ci): unblock nix flake check after clippy 0.1.95 bump (#1368)
The nixpkgs bump to clippy 0.1.95 / cargo 1.95.0 added + strengthened a
large batch of lints. CI denied ALL warnings (`-D warnings`) against the
`pedantic = warn` workspace lint, so the bump hard-failed `nix flake
check` workspace-wide with zero code changes — and would recur on every
future clippy bump.

Posture fix (the durable part): CI now runs
`-D warnings -A clippy::pedantic`, so the default/correctness/style lints
stay a hard gate while the "extra, opinionated" pedantic group is
advisory only (still `warn` for local `cargo clippy` via the workspace
lints table, just non-blocking in CI). `-A` rather than `-W` so the
group drop doesn't re-enable the specific pedantic lints the workspace
allows (e.g. `must_use_candidate`).

Also fixes the genuine DEFAULT/STYLE lints the bump surfaced across the
workspace (doc_lazy_continuation, collapsible_if, ptr_arg,
match_like_matches_macro, …) via `cargo clippy --fix` + manual stragglers
(`too_many_arguments` #[allow] on the host-config constructors), and
three tests that had rotted while the CI runner was offline (#1221):
- topology::top_level_agents_in_multi_root — hardcoded unsorted expected
- rebuild_queue::depends_on_evicted_dep_counts_as_resolved — needs
  MAX_HISTORY_PER_KIND newer terminals to evict, not one
- coordinator::agent_paths doctest — illustrative pseudo-code, now `ignore`

Validated: clippy + formatting + cargo-test checks all pass.
2026-06-05 15:32:07 +02:00
damocles
7118c5efdd feat(queue): link rebuild queue entries to build log rows for live streaming 2026-06-04 17:39:31 +02:00
atlas
7e229889a6 fix: address argus review on ensure_claude_dir — narrow to EPERM, fix doc comment 2026-06-04 15:03:12 +02:00
atlas
e553e40577 fix: soft-fail chmod in ensure_claude_dir when dir is agent-owned
hive-agent-user-migrate chowns the bind-mounted claude dir to the agent
user on every container boot. After that, hive-core (a different user)
cannot chmod it — set_permissions fails with EPERM, which was propagated
as an error and caused the rebuild to fail entirely.

Fix: make the chmod best-effort. Newly created dirs (owned by hive-core)
get the 0755 mode set immediately; after the agent-migration chown the
mode is preserved so claude_has_session works correctly. Subsequent calls
that hit the EPERM path just log at DEBUG and continue.
2026-06-04 14:48:08 +02:00
atlas
bc0087830f fix: chmod claude dir 0755 so hive-core can list session files
claude_has_session() calls read_dir() on the agent's claude/ dir to
detect a valid session. The dir was 0700 (create-only path), so
hive-core (a different unix user) got EACCES → read_dir returns Err
→ has_session returns false → every agent shows 'needs login' in the
dashboard even when working.

Fix: ensure_claude_dir now always sets 0755. The credential files
inside (e.g. .credentials.json) are 0600, so the secrets stay private
regardless of the containing directory's mode. Existing 0700 dirs are
corrected on the next spawn/rebuild cycle that calls ensure_claude_dir.
2026-06-04 12:15:32 +02:00
damocles
29c7f64bd3 refactor(#1202): introduce HiveEnv + AgentPaths to reduce arg repetition 2026-06-03 22:09:08 +02:00
damocles
cff3b6ff85 fix(#1201): prevent sync_agents from dropping all agents on list() failure 2026-06-03 21:46:38 +02:00
damocles
69a48f086c fix(#1190): consistent param order in set_resource_limits, drop stale blank line 2026-06-03 21:39:14 +02:00
damocles
d1fbb4aef8 feat(#1184): make agent CPU quota and memory limit configurable 2026-06-03 21:39:14 +02:00
damocles
41befe3839 feat(#1167): hive-wide knowledge repo — forge, local clone, bind-mount, webhook 2026-06-03 20:56:17 +02:00
damocles
a9ce8a945f fix(#1160): restart agent on failed rebuild if it was running 2026-06-03 18:32:38 +02:00
damocles
c44fa14f7c fix(#1145): clone logs/cmdline in priv_run match arms to avoid use-after-move 2026-06-03 15:22:23 +02:00
atlas
68e30b857c feat(#1137): rich unread summary in loose ends and wake signal
- 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
2026-06-03 13:56:00 +02:00
damocles
1386439591 fix(#1116): address review nits - dedup log callback, comment stderr truncation 2026-06-03 12:40:23 +02:00
damocles
68451eb205 feat(#1116): stream nixos-container create/update output live into build_logs 2026-06-03 12:34:16 +02:00
damocles
3c012efbea fix(#883): warn + bail on unparseable subnet instead of silently writing empty LOCAL_ADDRESS; doc /0 corner case 2026-06-03 12:31:54 +02:00
damocles
ec8fcb29ce test: add normalization test for bridge-IP subnet form (atlas note) 2026-06-03 12:31:54 +02:00
damocles
3bb07b1fde feat(#14): network isolation rust side — PRIVATE_NETWORK + veth wiring in set_nspawn_flags 2026-06-03 12:31:54 +02:00
müde
9e12012a95 fix(#702): route container journal reads through hive-priv
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.
2026-06-02 23:43:02 +02:00
damocles
0bec3af933 feat(#702): drop hive-c0re from root to hive-core user (privsep phase 2) 2026-06-02 22:45:12 +02:00
damocles
0c5760b0da feat(#1014): rename manager agent 'root' to 'ruth' in lifecycle + sh4re 2026-06-02 18:18:06 +02:00
atlas
db50da570a refactor(#1003): nixpkgs + nixpkgs-unstable as top-level meta inputs
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.
2026-06-01 23:30:04 +02:00
atlas
fe5a41288d feat(#1003): inject pkgs.path into meta flake as explicit nixpkgs.url
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.
2026-06-01 23:29:54 +02:00
atlas
4bff450343 feat(gateway): hivectl gateway user management + fix htpasswdFile assertion
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.
2026-06-01 23:25:28 +02:00
atlas
5c5ca38fe8 fix(#999): resolve all clippy warnings across the workspace
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>
2026-06-01 22:31:06 +02:00
atlas
fd87cf9924 fix(#962): top_level_agents = parentless agents, not children of manager
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.
2026-06-01 19:19:24 +02:00
atlas
a4e0628ba1 fix(#962): top_level_agents_in delegates to children_of(MANAGER_NAME)
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".
2026-06-01 19:19:24 +02:00
atlas
828be8e2c8 refactor(#962): replace children_of(MANAGER_NAME) with top_level_agents()
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.
2026-06-01 19:19:24 +02:00
atlas
6d2d0ed847 feat(#962): topology-driven child bind mounts + can_manage_top_level_agents role
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.
2026-06-01 19:19:24 +02:00
damocles
68cc433ac9 fix(#977): add MANAGER_CONTAINER=h-root, migrate root container name 2026-06-01 18:32:19 +02:00
atlas
286a7c8fd3 fix(#975): use hyperhive.local email domain to pass Forgejo validation
`@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.
2026-06-01 18:28:22 +02:00
damocles
37b5335c27 rename(162): MANAGER_NAME hm1nd -> root 2026-06-01 17:33:09 +02:00
damocles
eb108f9dd8 fix(947): trim prose in lifecycle.rs socket-dir block 2026-06-01 16:49:41 +02:00
damocles
7af29b3249 fix(#947): extend socket-dir bind to manager container 2026-06-01 16:49:41 +02:00
damocles
8b946a67c6 fix(#947): extend socket-dir bind to manager container 2026-06-01 16:49:41 +02:00
damocles
ae6d23594d feat(#2): split harness-internal state from agent-visible state 2026-06-01 13:27:35 +02:00
damocles
88cdab411e docs: add container lifecycle section to coordinator.md, trim lifecycle.rs prose 2026-06-01 11:38:42 +02:00
damocles
d14df8b9a0 lifecycle: thread on_step callback through rebuild for accurate queue step labels 2026-06-01 00:05:09 +02:00
müde
21213be1b6 lifecycle: chown per-agent socket dir to the agent user
Replace the 0777 fallback with a chown to the in-container agent's
uid/gid (resolvable via agent_uid_gid since nspawn shares uids with
the host). Keeps the dir at the default 0755 mode and avoids
world-writability. Falls back to 0777 only when the agent uid is
unavailable (first-spawn race before /etc/passwd is rendered).
2026-05-31 22:37:35 +02:00
müde
8b238bbfaf agent UDS: chmod per-agent socket dir 0777 + log web_ui::serve errors
The harness runs as the non-root agent user; the per-agent
/run/hive-agent/<name>/ dir lands at 0755 root:root after
create_dir_all, so bind(2) of web.sock failed with EACCES. The error
was invisible because the web_ui::serve future was tokio::spawn'd with
its JoinHandle dropped — no log, no socket, agent looks unreachable
through the gateway.
2026-05-31 21:38:34 +02:00
damocles
fdd5fa2977 fix(#636): stop before kill in cold-start fallback (argus) 2026-05-31 21:00:35 +02:00
damocles
b3fde40ac8 fix(#636): fall back to kill+start when container start fails after rebuild 2026-05-31 21:00:02 +02:00
iris
f1d2063a84 hive-c0re: full build-log capture to sqlite, drop 32-line stderr ring (#726 phase 1)
Backend half of #726. The 32-line stderr ring buffer in
`lifecycle::run` and `lifecycle::prebuild_toplevel` routinely
truncated the actual eval error — a single 'tried alternatives'
block out of a failing module ref is often 30+ lines on its own,
which pushed the real cause out of the bailout message. With this
patch the full stream lands in sqlite where the dashboard can
surface it; bail-outs now point at the build log id instead of an
arbitrary tail.

### New module: `hive-c0re::build_logs`

`BuildLogs::open(db_path)` creates a sqlite db at
`<db_path>/build_logs.sqlite`. Schema: id, agent, kind, cmdline,
started_at, finished_at, status, stdout, stderr — indexed for
both per-agent latest-N queries and the status-driven retention
sweep. API: `start / append_stdout / append_stderr / finish` for
the streaming writer side (best-effort — every append handles
sqlite errors via tracing::warn so a transient blip never tears
down a rebuild), plus `list_recent_for_agent / get_full` for the
read side (50-row cap clamped server-side).

### Process-singleton handle

`build_logs::install / global()` install the `Arc<BuildLogs>` at
`Coordinator::open` so `lifecycle::run` and
`lifecycle::prebuild_toplevel` can write without us threading
the handle through every `pub async fn` entry point in the
lifecycle surface — there are 10+ call sites and the handle is
the same Arc everywhere anyway. Reads via `global()` return None
in early-startup / standalone-test paths so callers no-op cleanly.

### Lifecycle integration

`run` derives the kind from `args[0]` (the nixos-container verb)
and the agent name from `args[1]` (stripped of the `h-` agent
prefix so dashboard grouping matches the bare agent name). It opens
a row before spawning, pipes stdout/stderr into both tracing AND
the row, then `finish`es with the terminal status. `prebuild_toplevel`
does the same with kind = "prebuild" and the agent name already
in scope from its caller. On failure both bail with "see build log
#<id>" instead of the ring-buffer tail.

### Retention

`spawn_vacuum` mirrors `stats_vacuum`/`events_vacuum` in shape —
hourly tick that calls `BuildLogs::vacuum()`. Rule: failures kept
30d (operators dig into them), successes 24h (mostly noise after
a day), in-flight rows never reaped regardless of age (running
builds shouldn't disappear from their own log viewer mid-stream).

### Out of scope (follow-ups)

- Dashboard endpoints (`GET /api/build-logs/{agent}`,
  `GET /api/build-logs/{id}`) — wire layer
- ContainerView.build_logs field — agent-card chip data source
- Side-panel viewer + SSE `build_log_appended` event — UX
- Download-as-text link — operator workflow polish

These all stack cleanly on top of the data layer + writer this
PR ships. Filing as phase 2 PRs.

### Validation

- 5 new unit tests pass (start/append/finish flow, list ordering
  + clamp, get_full miss, vacuum per-status rule, post-finish
  append fault tolerance)
- 157 hive-c0re lib tests pass overall
- cargo check workspace clean

Refs #726.
2026-05-31 20:28:43 +02:00
damocles
b0495c5167 hive-c0re: scrub lifecycle + meta cookies (#715 batch 9) 2026-05-31 18:06:15 +02:00
damocles
35a7ff03b7 hive-c0re: bind-mount /run/hive-agent/<name>/ per sub-agent (#784 phase 2 step 2b)
builds on step 2a (#809). lifecycle::set_nspawn_flags now adds a
--bind={socket_dir}:{socket_dir} flag per sub-agent so the harness's
HIVE_WEB_SOCKET bind (PR #800) lives in a dir both the agent
container and the host can see.

design (matches #809's a1a601d explanation):
- bind the SUBDIR, not the socket file. file bind-mounts drop on
  unlink; the harness's bind_unix unlinks any stale socket before
  binding, so a file bind would land the new socket in the agent's
  private namespace, invisible to the gateway. dir bind keeps both
  sides on the same dir inode.
- per-agent dir (one /run/hive-agent/<name>/ per agent, not a shared
  /run/hive-agent/ mount). The agent's container only sees its own
  subdir — never siblings' (mara on #800).
- manager skipped — the manager's UI serves at / via the c0re
  dashboard upstream, not via /agent/<name>/, so it never needs the
  per-agent socket dir.

mkdir source defensively before bind: nspawn refuses to start when
the bind source is missing, and /run/hive-agent/ doesn't exist on
fresh hosts.

remaining work in this phase:
- step 3 (atlas): gateway proxy_pass http://unix:/run/hive-agent/<name>/web.sock:/
- per-agent: flip HIVE_WEB_SOCKET in agent.nix to opt in (separate PRs)
- step 4 (later): drop TCP fallback once everyone's flipped
2026-05-31 15:44:26 +02:00
damocles
4526e40a49 lifecycle: drop manager port special case (#753) — manager hashes into 8100..8999 like every other agent 2026-05-31 13:25:13 +02:00
damocles
c3ec037105 lifecycle: prebuild attr path needs explicit nixosConfigurations.<name> (fix #721 regression) 2026-05-31 12:31:02 +02:00
damocles
0a98327685 lifecycle: only prebuild when there's downtime to shave (mara on #721) 2026-05-31 11:55:35 +02:00
damocles
f5372b6016 lifecycle: prebuild system toplevel before stop+update so container downtime shrinks (#706) 2026-05-31 11:55:35 +02:00