Per mara's PR review:
- snapshot label is now mandatory (was optional w/ timestamp default)
and must start with "hive-" — hive-priv enforces this as an
allow-list on top of the existing credential-name charset check, so
only hivectl-issued labels can reach the btrfs shellout.
- nest under `subvol snapshot create`/`subvol snapshot delete`
instead of othering delete as a separate top-level `delete-snapshot`
verb.
Per argus's review:
- regenerate docs/tools/hivectl-cli.md (hivectl markdown-docs) to
include the new subcommands — CI's hivectl-docs-fresh check compares
this file against generated output.
Adds the first missing piece from #2391's migration-gaps list: a
read-only btrfs snapshot priv op so hivectl migrate can freeze a
consistent point-in-time copy of an agent's state subvolume for
btrfs send, without stopping the live agent.
- PrivRequest::SnapshotAgentSubvolume / DeleteAgentSnapshot (hive-sh4re)
- hive-priv handlers: btrfs subvolume snapshot -r / delete, sibling
dot-prefixed path (<AGENT_STATE_ROOT>/.<agent>.snapshot.<label>)
- hive-c0re::priv_client wrappers
- hivectl subvol snapshot / delete-snapshot verbs (no agent stop needed
— btrfs snapshots are atomic against a live subvolume)
Does not yet wire actual btrfs send/receive or the hivectl migrate
verb — those stay tracked on #2391 as separate follow-up pieces.
extra.rs (external-forge account minting) was removed in the
dashboard-provisioned redesign; TOKEN_SCOPES only applies to tokens
hive-c0re mints itself on the internal forge. External forge tokens
are pasted by the operator verbatim, so we never mint them and don't
need to know their scope.
Per mara's feedback on PR #2407 ("better: you can also provide url in
dashboard, same as with matrix, no host config"), drops
services.hyperhive.extraForges and the admin-API mint/revoke flow
entirely. The operator now creates a token on the external forge
themselves and pastes a label + base URL + access token into the
dashboard's FORGES tab, the same shape as the GitHub PAT flow plus the
base-URL field from the matrix extra-account flow. hive-c0re only ever
writes/deletes two local files per account (forge-<label>-token,
forge-<label>.json sidecar for the URL) via hive-priv — no remote
account creation, no admin token, no revoke-on-the-remote-side, no nix
config to enumerate.
- nix/host-modules/hive-forge/default.nix: removed the extraForges
option, its label-format assertion, and the HYPERHIVE_EXTRA_FORGES
env forwarding.
- hive-c0re/src/forge/extra.rs: deleted (REST admin-API provisioning,
no longer needed).
- hive-c0re/src/dashboard/extra_forges.rs: GET /api/extra-forges?
agent= lists an agent's stored forges by scanning its state dir
(mirrors matrix_accounts.rs's filename-scan listing), POST
/api/extra-forge-account (agent/label/base_url/token/
action=add|remove) stores or removes an account.
- hive-sh4re/priv_proto.rs + hive-priv/main.rs: new
WriteAgentExtraForgeAccount/DeleteAgentExtraForgeAccount priv
requests (adds base_url, writes/deletes a JSON sidecar alongside the
token).
- hive-c0re/src/priv_client.rs: matching wrapper functions.
- frontend/packages/dashboard/src/credentials.{html,js}: FORGES tab is
a per-agent list + add-account paste form (label/base_url/token), no
grant/revoke-from-catalog UI.
- docs/web-ui/dashboard.md: FORGES tab section rewritten.
Supersedes the design in PR #2407 (already approved+green on the old
admin-API model) — opening as a fresh PR against the same issues
rather than force-pushing over the approved one.
All agent containers now receive their bridge IP dynamically via DHCP
from the dnsmasq pool instead of a hash-derived static address:
- nix/templates/harness-base.nix: networking.useDHCP = true
- nix/modules/hive-gateway.nix: expand DHCP pool to full usable range
(.2 to .254 on /24) — was last-14-IPs-only
- hive-sh4re/src/priv_proto.rs: remove agent_ip from NetworkIsolation
- hive-c0re/src/lifecycle/mod.rs: drop agent_network_ip + DHCP_POOL_SIZE
- hive-c0re/src/lifecycle/host_config.rs: remove agent_network_ip call
- hive-priv/src/main.rs: LOCAL_ADDRESS= empty (DHCP assigns IP);
HOST_ADDRESS still set so nixos-container installs default route
before the DHCP lease arrives
- nix/dhcp-pool-size: deleted (no longer needed)
The nix/dhcp-pool-size single-source-of-truth file and all associated
Rust/Nix dual-constant plumbing are gone — there is no static map.
bridge_gateway_ip() is retained (still needed for HOST_ADDRESS).
Closes#2363
No need to preserve agent IPs across this deploy — nothing outside a
container depends on a specific agent IP. Simpler approach: subtract
DHCP_POOL_SIZE from the usable count before hashing so agents only ever
land in [2, usable - DHCP_POOL_SIZE + 1], never in the DHCP pool.
Removes the secondary-hash remap block (~10 lines). Returns None for
subnets too small to hold both agent slots and the pool (edge case;
practical subnets are /24).
Move the DHCP pool size constant out of the two separate definitions
(Nix literal + Rust const) into a shared data file: nix/dhcp-pool-size.
- nix/dhcp-pool-size: new file, contains '14'
- hive-gateway.nix: reads via builtins.readFile + toIntBase10
- lifecycle/mod.rs: parses via include_bytes! const block at compile time
Cargo automatically tracks include_bytes! as a file dependency so a
change to nix/dhcp-pool-size triggers recompilation without build.rs.
Add a DHCP pool to the gateway's dnsmasq so bridge-attached service
containers (hive-ci and future equivalents) get their addresses from
a proper DHCP server instead of a brittle static derivation.
gateway (hive-gateway.nix):
- Add IPv4 arithmetic helpers (ipToInt, intToIp, pow2) to compute the
DHCP pool range at nix eval time from bridgeIp + bridgePrefixLength.
- Reserve the last dhcpPoolSize (14) usable host addresses as the DHCP
pool (e.g. .241-.254 on a /24 with 10.42.0.0 network).
- Add dhcp-range and dhcp-leasefile to the dnsmasq settings block.
The pool is active whenever services.hyperhive.network.enable is true.
hive-ci (hive-ci.nix):
- Remove the ciBridgeIp / ciBridgeOctets static derivation and the
brittle top-of-/24 comment block.
- Switch networking.interfaces.eth0 to useDHCP = true so hive-ci gets
its address from the gateway DHCP pool.
lifecycle (mod.rs, tests.rs):
- Add DHCP_POOL_SIZE = 14 constant (must stay in sync with
dhcpPoolSize in hive-gateway.nix).
- Remap agents whose FNV-1a hash falls in the DHCP pool into the
agent-only window [2, dhcp_start - 1]. Only the rare agent whose
name hashes into the pool is affected; all others keep their IPs.
- Update and extend tests: agent range is now .2-.240 on /24;
add agent_network_ip_never_in_dhcp_pool covering 18 agent names.
New POST /api/infra-container/{name}/{action} dashboard route (start/
stop/restart on hive-ci/hive-forge/hive-gateway/hive-matrix), reusing
the existing priv_client::control_infra_container helper the
infra_admin agent path already uses, plus an audit_log entry per
attempt. Adds infra_containers to the /api/state StateSnapshot (name +
live running status via systemctl is-active). New 1NFR4 sub-tab on the
C0R3 dashboard page: one row per infra container with a running/
stopped badge and start/stop/restart buttons, polled every 5s while
the sub-tab is open.
Two issues flagged by argus in PR #2388 review:
1. Empty-key fallback: when load_or_generate() failed, webhook_secret was
String::new(). An attacker knowing this could forge deliveries with a
valid HMAC of the empty key. Fix: change to Option<String>; on None,
skip hook registration entirely and return 503 from /webhook/* handlers
(rather than 401 with a misleadingly-verifiable empty-key HMAC).
2. Stale hook cleanup: on upgrade from old code, old loopback hooks
(http://127.0.0.1:.../webhook/knowledge, .../webhook/config-pr) were
left alongside the new domain-URL hook. Fix: during ensure_webhook /
ensure_config_pr_webhook, after listing hooks, delete any that end with
our path suffix but point at a different base URL.
clippy + nix fmt clean.
Both webhook registrations (knowledge push + config-PR pull_request) now
use the public hive domain instead of loopback:
https://<HYPERHIVE_HIVE_DOMAIN>/webhook/{knowledge,config-pr}
This routes deliveries through the gateway, bypassing the Forgejo SSRF
guard that blocked loopback delivery and silently broke the config-PR
merge flow since launch.
Changes:
- webhook_secret: new module — auto-generate + persist a 32-byte HMAC
secret to STATE_ROOT/webhook-secret on first startup; verify
X-Hub-Signature-256 on every incoming webhook POST (HMAC-SHA256).
- forge/mod.rs: ensure_config_pr_webhook now takes hive_domain +
webhook_secret; sets secret in Forgejo hook config.
- workers/knowledge.rs: ensure_webhook same update.
- dashboard/webhook.rs: both handlers read raw Bytes first, verify HMAC,
then parse JSON. Returns 401 on signature mismatch.
- dashboard/mod.rs: AppState carries webhook_secret; serve() takes it.
- main.rs: load/generate secret at startup; pass to registration tasks
+ dashboard; add 5-minute config-PR polling fallback task.
- forge/config_pr_poll.rs: new — scan agent-configs/* for open PRs with
no pending MergeConfigPr approval; queue them. Idempotent.
- stores/approvals.rs: has_pending_merge_config_pr() for poll dedup.
- nix/modules/hive-gateway.nix: remove dashboardAuth from /webhook/
location (HMAC replaces basic auth for webhook endpoints; Forgejo
cannot send HTTP Basic credentials with webhook deliveries).
If the remote is ahead of our local mirror (non-fast-forward), the old
code used --force which silently destroyed remote history. Fix:
- Drop --force from the git push invocation.
- On non-ff exit, detect the condition and return Ok(()) instead of
bailing (intentional no-op; leaving remote history intact is correct).
- Raise a persistent dashboard warning banner via crate::warnings so the
operator sees it in the UI rather than having to grep the journal.
- Clear the banner on the next successful push.
Closes#2380.
dispatch was 101 lines (1 over limit) due to the AgentStatus arm.
Extract it to a dedicated handle_agent_status helper to bring dispatch
under the 100-line lint limit without the allow attribute.
Per mara review comment on PR #2379.
- Backtick-quote `pull_request` in doc comments (4x doc_markdown)
- Add #[allow(clippy::too_many_lines)] to server::dispatch (101/100;
+1 line from submit_kind fetched_sha param in 5dd0a36f)
org_list_hooks response type is Vec<Hook> (no pagination headers),
so .all() (which is impl'd for (H, Vec<T>) paginated responses) does
not compile. Switch to .send() — the non-paginated call path.
repo_list_hooks (used in workers/knowledge.rs) returns (H, Vec<T>)
and correctly uses .all(); the org variant is different.
Two hardening items from argus's review of #2374:
1. PR state check at submission:
- Add `pr_is_open(repo, pr)` to forge/pr_merge.rs using
`repo_get_pull_request` + `StateType` — early error if the PR is
already closed or merged instead of queuing a card that fails later
- Call it in `submit_merge_config_pr` before fetching the head sha
2. Atomic fetched_sha INSERT:
- Add `fetched_sha: Option<&str>` to `Approvals::submit_kind` so
the sha can be included in the INSERT rather than a follow-up UPDATE
- MergeConfigPr already knows the sha before inserting the row
(pr_head_sha runs first) → pass `Some(&sha)`, drop the separate
`set_fetched_sha` call → truly atomic
- ApplyCommit still needs two writes (sha resolved by git_fetch_to_tag
after the row exists) → pass `None`, `set_fetched_sha` unchanged
- All other callers (InitConfig, Spawn, UpdateMetaInputs,
SchedulePrompt) pass `None` — no behavioural change
- Add `fetched_sha_in_insert_is_readable_via_get` test covering the
MergeConfigPr path
MergeConfigPr approvals had a fully-implemented approve handler
(run_merge_config_pr, ff_push_to_main, mark_pr_merged) and dashboard
display, but no way to submit one. An agent with the `approvals` tool
group calling request_merge_config_pr(agent, pr_number) is the missing
piece.
What this adds:
- RequestMergeConfigPr variant in hive-sh4re AgentRequest + ToolGroup::Approvals
- submit_merge_config_pr: fetches PR head sha (the drift-gate reviewed sha),
queues a MergeConfigPr row, sets fetched_sha, emits approval_added with
pr_number so the dashboard card links to the forge PR
- handle_request_merge_config_pr: topology (require_descendant) +
tool-group (require_group(approvals)) guards before submit
- socket_server/mod.rs: dispatch arm for RequestMergeConfigPr
- hive-ag3nt MCP tool: request_merge_config_pr with full description
- docs/tools/lifecycle.md: documents the new tool + boundary table row
Unlike submit_apply_commit, no flake pre-flight at submission time (eval-
verify happens at approval time inside run_merge_config_pr, same as the
rest of the merge pipeline). Applied repo must already exist (guard added
with a clear error message pointing at request_apply_commit for first-spawn).
- Add `priv_proto::AGENT_RUNTIME_ROOT` to hive-sh4re as the shared
single source for the per-agent runtime root path. hive-priv now
imports it instead of carrying a local const with a stale comment
that still pointed at `coordinator::AGENT_RUNTIME_ROOT` (removed in
#2285/#2367 — moved to `paths::agent_runtime_root()`).
- Add 'must stay in sync' cross-ref comments on both sides of the
privsep boundary:
· priv_proto::META_DIR ↔ paths::meta_root()
· priv_proto::AGENT_STATE_ROOT ↔ paths::AGENTS_ROOT
· priv_proto::AGENT_RUNTIME_ROOT ↔ paths::RUNTIME_ROOT + agent_runtime_root()
· paths::AGENTS_ROOT ↔ priv_proto::AGENT_STATE_ROOT
· paths::RUNTIME_ROOT ↔ priv_proto::AGENT_RUNTIME_ROOT
The dep graph prevents a shared import (hive-sh4re is a leaf; both
hive-c0re and hive-priv depend on it but not each other), so the
lockstep comments are the enforced contract.
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
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.