Replace the try-and-ignore-duplicate-column approach in apply_migrations
with proper schema versioning using a shared schema_versions table.
## mechanism
New function: db::apply_versioned_migrations(conn, subsystem,
legacy_column, migrations). Tracks the applied-migration count in a
schema_versions table (one row per subsystem key). Only migrations past
the stored version run.
Legacy detection: pre-versioning databases have no schema_versions row.
The legacy_column tuple (table, column) identifies a column that exists
only in a fully-migrated legacy database. If present, all known
migrations are skipped. If absent, migrations start from 0.
## stores migrated
- broker: removes bespoke ensure_message_columns / ensure_reminder_columns.
Unified into BROKER_MIGRATIONS (v1-v5). Legacy detector: messages.priority
(added in the last pre-versioning migration).
- approvals: 4 historical migrations (v1-v4). Legacy detector:
approvals.submitter.
- operator_questions: 3 historical migrations (v1-v3). Legacy detector:
operator_questions.target.
- scheduled_prompts: 1 historical migration (v1). Legacy detector:
scheduled_prompts.paused_at_unix.
apply_migrations removed (no callers).
## tests (db.rs)
- fresh_install_runs_all_migrations
- legacy_install_skips_all_migrations
- partial_migration_resumes_from_version
- already_at_latest_is_noop
- multiple_stores_in_same_db
Each binary now has its own derivation (daemonBins for hive-c0re/hive-ag3nt/MCP
servers, hivectlPkg, hiveForgePkg), all sharing cargoArtifacts for a single
rustc compilation. packages.default = pkgs.symlinkJoin assembles them without
any additional compilation — no rustc is invoked more than once regardless of
which packages the operator builds together.
NixOS module consumers (pkgs.hyperhive = packages.default) are unaffected:
symlinkJoin exposes all binaries at the same /bin/ paths.
Operators who only want hivectl or hive-forge can now add:
inputs.hyperhive.packages.${system}.hivectl
inputs.hyperhive.packages.${system}.hive-forge
to their environment.systemPackages without pulling the full
workspace (hive-c0re daemon, hive-ag3nt harness, etc.) into PATH.
Both reuse the shared cargoArtifacts so there is no extra compile
cost when both packages and the default workspace are built together.
packages.hivectl carries the shell completions and the wireguard-tools
wrapper identical to what packages.default ships for hivectl.
packages.hive-forge is a lean CLI-only build.
On a fresh deploy Persistent=true causes the weekly timer to fire
immediately. Without this ordering hive-tls-resign can race
hive-tls-ca before the CA key is written, failing the resign.
CA is always valid when this runs; resign is a no-op if the leaf
is still within its half-life margin.
Suggested by argus (review) + requested by mara.
The parked PR (#1934) triggered re-sign by restarting hive-tls-ca
(a RemainAfterExit oneshot), which mara flagged as a hack. This
replaces that with a dedicated hive-tls-resign.{service,timer}:
- hive-tls-ca unchanged — still handles boot-time CA generation +
initial leaf signing; RemainAfterExit, runs before the gateway.
- hive-tls-resign.service — runs the leaf re-sign directly (openssl
in its own PATH); does NOT touch hive-tls-ca. Re-signs only when
the leaf is within half its validity of expiry, so the weekly timer
catches short-lived (30d default) leaves well before they lapse.
Checks sha256 before/after; if the leaf rotated it propagates into
the running gateway via systemctl -M hive-gateway (mirroring how
hive-c0re reloads the gateway after agents.conf writes — the
documented correct approach; an inotify path unit inside the
container does not work across the nspawn mount-namespace boundary).
|| true on the propagation calls so a stopped gateway never fails
the unit (its next boot imports the already-rotated leaf).
- hive-tls-resign.timer — OnCalendar=weekly, Persistent=true (fires a
missed run on next boot so a host that was off on the scheduled day
catches up rather than letting the leaf lapse silently).
- leafValidityDays doc updated to reflect automatic renewal.
Closes#1933.
Security fixes landed without updating the security-model docs; bring them
current as present-tense invariants:
- security.md: matrix identities are public / cross-agent-readable via
GetAgentMeta while the token stays private; name validation extends to
GetAgentMeta; the nix-build blast radius has a network dimension (hive-ci
netns) alongside the 0600 token policy.
- boundary.md: the core dashboard /api has no app-layer auth — protection is
the gateway plus network isolation; stated as a standing invariant.
- ci.md: hive-ci runs in its own private netns behind the bridge, reaches the
forge via the gateway, and cannot reach host-loopback (dashboard/forge port).
- knowledge.md: /knowledge excludes .git via tmpfs so the core token in
.git/config isn't readable by agents.
Refs #2308, #2310, #2313.
claude emits a started tick then a completed tick per plugin install;
previously each got its own row (loading... then done) instead of one
line updating in place. Same coalescing pattern already used for
thinking_tokens and the generic status tick.
hive-ci is the one container on the hive bridge that used DHCP
(networking.interfaces.eth0.useDHCP = true, from #2310 / #2336). But the
bridge has no DHCP server: dnsmasq on the bridge is DNS-only and agent
containers get deterministic static IPs (lifecycle::agent_network_ip),
so the DHCP client never gets a lease and the container hangs at boot:
A start job is running for DHCP Client (5s / 1min 31s)
Assign a static address on eth0 (top host address of the subnet) plus a
default route via the bridge gateway, and drop useDHCP. Preserves the
#2310 netns isolation (no host-loopback reach) while letting the
container boot and reach the forge through the gateway.
Refs #2310, #2336.
The is_descendant_of and apply_set_parent cycle detection both used
hand-rolled 32-hop bounded ancestor walks. Correct in practice (no
real hive exceeds 32 levels) but carried an arbitrary ceiling and were
harder to reason about than proven graph primitives.
Changes:
- Add build_graph(): converts BTreeMap<name, parent|null> → DiGraph
with parent→child edges + BTreeMap<name, NodeIndex> index
- Add is_descendant_of_in(): pure (no disk I/O), uses
petgraph::algo::has_path_connecting from ancestor to candidate
- Rewrite is_descendant_of(): delegates to is_descendant_of_in(&read())
- Rewrite apply_set_parent() cycle detection: build_graph() + speculative
edge + is_cyclic_directed(); no depth limit
- Add tests for is_descendant_of_in (self, direct child, grandchild,
parent-is-not-child, sibling, unknown)
petgraph was already a workspace dep (used elsewhere). On-disk format
unchanged (flat JSON map). Public API surface unchanged.
nixos-containers.nix sets networking.useDHCP = false for every
container (to suppress the deprecated global DHCP warning). Setting
networking.useDHCP = true in the hive-ci inner config conflicts with
no priority differential → eval error on hosts running current main.
Fix: drop the global useDHCP = true and configure DHCP on eth0
(the inner veth interface name assigned by systemd-nspawn when
hostBridge is used). This is both conflict-free and the correct
modern NixOS practice for per-interface DHCP.
Claude's generic `system/status` subtype carries no detail beyond the
bare label, and previously each tick got its own note row. During a
compaction pass (which emits a burst of these with no other signal
before the completed compact_boundary) this looked like a wall of
identical noise followed by silence, making a routine compaction look
stuck.
Collapse consecutive status ticks into one updating row, same pattern
already used for the thinking_tokens counter.
The container-level nix.settings.fallback = lib.mkForce true (added as an
interim safety net so a down remote builder degrades gracefully rather than
hard-failing CI) is rendered redundant by the host-daemon routing fix: with
NIX_REMOTE=daemon set for all systemd units in the container, every nix
build invocation routes through the host daemon, which already has its own
fallback policy. The container setting would only apply to nix invocations
that bypass the daemon entirely — a case that should not exist post-deploy.
NOTE: draft — gated on the daemon-routing change (PR that adds
NIX_REMOTE=daemon to systemd.globalEnvironment + hive-priv service env)
being deployed and confirmed working in production before merging.