Last of the swarm.* -> deploy.* moves for the enable-shaped options.
Whether this host also runs the CI runner is a per-machine decision, so
it was sitting in the namespace that has to be identical swarm-wide.
Renames all five options, not just enable: name, concurrency, labels and
package all describe the runner THIS host would run, and leaving them
behind would keep the original defect for four more options. One rename
entry each, because ci is a plain attrset rather than a submodule type,
so there is no parent path to rename in one go.
deploy.forgejo is the name deploy.nix's own header already specifies,
including this exact case -- "ci (does this host run the runner too) is
exactly that shape". It is the only entry with no enable: the forge is
not optional, it deploys with hyperhive itself, so running the runner is
the only deployment decision it has.
Also fixes hive-c0re/src/forge/ci_runner.rs, whose doc comment named
services.hyperhive.forge.ci.enable -- missing the swarm. segment, so it
had never resolved to a real option.
Six diagnostics and eight comments explained where an environment
variable comes from by naming the module file that sets it. The reader
of a panic is an operator, who does not edit that file; the reader of
the comment is already in the code. Neither can act on a filename, and
a filename in prose is checked by nothing.
Each one now names what the reader can act on: the option where there
is one (services.hyperhive.network.bridgeName, .bridgeIp,
.bridgePrefixLength, c0re.assets, c0re.orgAvatarPng), otherwise the
unit the process is expected to run under. Two of the six diagnostics
had a real option available; the other four have none, so they name
the unit rather than an invented path.
Four of them pointed at hive-c0re.nix, which is a directory. Two of
those spelled it nix/modules/hive-c0re.nix — a directory that does not
exist either, and one of the two was wrapped across a line break, so
no grep for the whole path could have found it. That is the argument
for the change: prose naming a file rots silently because nothing
compiles it.
Scope here is the four files around those diagnostics. The same
pattern still matches elsewhere in the tree; that sweep follows.
act_runner records the --instance URL it was registered with and reads the
forge address from .runner and nowhere else: the nix option reaches only
`register`, and upstream re-registers on a changed token or labels, never on
a changed URL. Our own precond short-circuits on .runner existing, and
runner_valid asks whether the runner id still exists -- which after a rename
it does. So changing swarm.forge.domain left the runner dialling a name its
container's derived extraHosts no longer resolves, with every check green.
The symptom is CI going quiet rather than anything failing.
Compare the persisted address against the configured base as part of the
same early return. A disagreement mints a fresh registration token, which
changes the token hash upstream already keys on, so upstream removes .runner
and re-registers against the current --instance. This module never writes or
deletes that file; the deletion stays with the script that owns registration.
The comparison is host-only and lenient on purpose. Both sides render from
one nix expression -- http://${swarm.forge.domain}, reaching the runner as
instances.hive.url and this daemon as HIVE_FORGE_URL -- so they cannot drift,
while scheme, port and trailing slash are exactly the cosmetic differences
that would otherwise re-register on every boot. Unreadable, missing or
unparseable input keeps the existing credentials, matching runner_valid's
treatment of a transport error: only a positive disagreement counts.
The test's doc named knowledge::ensure_webhook as one of two hive-side
reapers. That function is gone; its replacement matches the full URL and
cannot touch another hive's hook.
The assertion arm stays. The hazard is not what this repository's source
says, it is what is deployed — a hive still running the previous version
reaps by suffix until it is upgraded, so the arm guards the transition
rather than a current code path. Recorded when to drop it.
Takes the crate from 26 rustdoc warnings to 1, on top of the ten in the
previous commit.
argus's review findings:
- agent_sockets.rs: [`write`] was still ambiguous (function vs macro).
The previous change narrowed the qualifier and left the ambiguity;
[`write()`] is what resolves it.
- forge/users.rs <hex> and stats/container_stats.rs <name>: unclosed
HTML tags in prose, now backticked.
The rest of the crate, so the count actually reaches zero:
- job_queue/mod.rs: Queue::graph_snapshot -> JobQueue::graph_snapshot
(there is no Queue type), and super::scheduler -> scheduler (mod.rs
*is* job_queue, so super:: pointed outside it)
- job_queue/resource.rs: NodeKind -> super::model::NodeKind
- matrix.rs: password_path(name) -> password_path; and
forge::provision_user_token -> crate::forge::provision_user_token.
Note the path has no `users` segment: forge/mod.rs declares `mod
users` private and re-exports it, so the canonical path comes from the
re-export rather than the directory tree.
- socket_server/lifecycle_handlers.rs: InfraContainer ->
hive_priv_sock::InfraContainer
- stats/otel_metrics.rs: crate::meta::otel_config is a private fn no
path can name from another module, so it becomes prose
- main.rs: redundant explicit link target dropped
coordinator.rs:405 (CrashWatchGuard) is deliberately untouched: #3244
deletes that doc block, so fixing it here would conflict with an open PR
and repair a symbol that is about to stop existing.
Remove or fix broken documentation links that accumulate silently:
- container_view.rs: HiveEnv reference
- forge/mod.rs: READY_TIMEOUT and webhook handler links
- workers/knowledge.rs: webhook handler link
- job_queue/model.rs: Claim::deps and WireNode::data references
- stats/hive_stats.rs: read_skill_breakdown reference
- stores/audit_log.rs: global() reference
- workers/agent_sockets.rs: ambiguous agent_sockets::write reference
- coordinator.rs: systemd.services.<harness> formatting
- resource_limits.rs: ambiguous write/read references
Some broken links were to deleted functions/types; these are replaced
with prose descriptions. Others referenced items outside this crate or
were private; these are replaced with plain text references or qualified
paths as appropriate.
Fixes: #3245
hive_sh4re::assets::branding_svg() resolved a server-side default
icon at runtime from HIVE_ASSETS_DIR — the only consumer was
serve_icon(), which fell back to it whenever the agent had no
`hyperhive.icon` override. Removed both the fallback and the
function: serve_icon() now 404s when /etc/hyperhive/icon.svg is
absent, and the per-agent web UI (app.js) picks up the existing
dashboard swarm.js pattern — swap the <img> src to the
frontend-bundled /favicon.svg on load failure, guarded against
looping if the fallback itself 404s.
Updated the doc/comment claims that said the server always returns
an image (docs/web-ui/agent.md, nix/agent-modules/default.nix, the
hive-c0re/forge/users.rs comment referencing the old shared-asset
set). forge-avatar-sync and the matrix avatar sync are unaffected —
both are gated on hyperhive.icon != null and never depended on the
removed fallback.
core_avatar_png() and config_org_avatar_png() were hive-sh4re
functions used only by hive-c0re::forge::users. Move them there as
private path-resolution helpers (core_avatar_png_path,
config_org_avatar_png_path), following the existing
std::env::var(...).expect(...) required-env-var style used elsewhere
in hive-c0re.
The org avatar is now independently configurable:
services.hyperhive.c0re.orgAvatarPng (nullable path, default null)
lets an operator override just the agent-configs org avatar PNG
without replacing the whole assets package. Wired via a new
HIVE_ORG_AVATAR_PNG env var that falls back to the bundled PNG when
the option is unset. The core avatar stays under HIVE_ASSETS_DIR,
unchanged.
`forge_git_url` spliced `core:<token>@` between scheme and authority, and
that URL is a process argument. `/proc/<pid>/cmdline` is mode 0444 —
world-readable — so the core admin token, which provisions every agent's
forge account, was published to any local user for the lifetime of each
git child. Seven call sites built such a URL.
The credential now travels in the environment instead:
`git_command_authed` sets `http.extraHeader` via `GIT_CONFIG_*`, which
git reads exactly like a config file, and `/proc/<pid>/environ` is 0400 —
owner-only. Same credential, materially smaller audience. The remote is a
plain `http://forge/<org>/<repo>.git`, and `forge_git_url` no longer takes
a token, so the old shape cannot be rebuilt by accident.
`knowledge`'s clone was the one place a credentialed URL was stored as a
named remote — git persists the clone URL into `.git/config`, so the
token sat on disk and every later `pull` authenticated from there. That
is the case `forge::repos::push_config` documents as forbidden ("the
tokenised URL ... deliberately never stored as a named remote"). `pull`
now rewrites `origin` to the plain URL first, which also scrubs the
persisted token from existing deployments, and authenticates from the
environment when a token is available. The repo is public, so the pull
still works without one.
Three call sites also stopped spawning `Command::new("git")` directly,
so they honour the `HYPERHIVE_GIT` path the NixOS module bakes in and
the `kill_on_drop` every other git spawn gets.
The two URL-shape tests now assert the *absence* of a credential, and a
new one decodes the header back to `core:<token>` — without that, a
malformed header would leave every forge operation silently anonymous
with the other assertions still green.
forge_admin interpolated its whole argument vector into the error
context, and two callers pass a live operator password in that vector
(user create --password, user change-password). Any failure of those
commands wrote the password to hive-c0re's log in cleartext -- and the
likeliest trigger is forgejo rejecting a weak password, so the secret
got logged precisely because forgejo refused it.
Redacting the value after --password would repeat the bug the issue is
about: redact_password_line matched one keyword and a differently named
secret walked past it. A denylist fails open, silently, and the next
secret-bearing flag would leak until someone extended the list.
describe_forge_admin keeps the leading verb path and stops at the first
flag, so "user create --username iris --password ..." is reported as
"forgejo admin user create". The verbs are a closed set this crate
chooses itself; argument values never are, so a new flag is excluded by
construction. Nothing useful is lost -- the context says which operation
failed, and the underlying error already carries forgejo's own message
about why.
The same pattern in hive-priv is deliberately untouched: that crate runs
as root and the redactor's shape is still an open question on the issue.
This change holds under either answer.
mint_token interpolated forgejo's raw stdout into its anyhow context on
the parse-failure path, and on that call stdout carries the access token
that was just created. The happy path below it is careful to log only
the user and token names; the error path handed the secret over whole.
It fires exactly when forgejo's output format drifts, which is the same
drift that breaks extract_token in the first place -- so the "help me
debug this" context printed the secret it had failed to find.
Report the shape of the output (bytes, lines) instead of its contents.
That is what diagnoses a version drift anyway: you want to know forgejo
printed something with no token-shaped word in it, not the bytes.
Redaction at a logging call site does not cover the error path.
with_context and bail! are output channels too.
Two sites, same class, different blast radius:
`forge_http_base()` fell back to `http://localhost:3000` when
`HIVE_FORGE_URL` was unset. The NixOS module sets that variable
unconditionally, so the fallback could only ever fire for a process
started outside the module — where guessing produces a confusing
"connection refused" far from its cause. It now panics saying exactly
that.
`forge_git_url()` had a second, nastier fallback: a base URL with no
`://` produced `http://core:<token>@localhost:3000/...`, sending a
*credentialed* git push at whatever happened to answer on the local
port. Split the credential-insertion half out as `git_url_with_base`,
which panics on a malformed base.
That split also lets the tests cover the shape without setting a
process-wide env var, which would race every other test in the binary.
Adds a case pinning that the scheme is carried through rather than
assumed — the old hardcoded `http://` would have silently downgraded a
TLS-fronted forge.
Refs #2860
Rewrite the approval flow's step 4 as the three phases, with the reason
the rollback state is a git ref, and refresh the coordinator's node
inventory + dispatch table. Fix four doc comments that still pointed at
the deleted `run_merge_config_pr`.
The boot provisioning pass (orgs, repos, teams, CI-runner token) all hits
the Forgejo API, but ensure_all only checked the container is *present*,
not that its HTTP is *listening*. A nixos-rebuild that restarts hive-forge
and hive-c0re together races: every ensure_* fired at a refused socket and
left a stale 'provisioning failed' banner that never cleared, since the
pass is one-shot. Poll GET /api/v1/version (unauthenticated) until it
answers, bounded at 1 minute, before provisioning; on timeout proceed
anyway so a genuinely-down forge still surfaces its real errors. Mirrors
the readiness-retry the gateway-nginx path already has.
sync_agent() now returns bool (false if any step fails). ensure_all()
collects the names of agents whose sync failed and raises a single
set_boot_warning with the aggregated list:
forge: per-agent sync failed for: alice, bob (see journal for per-step
detail)
The static_kind() leak is already used for per-org boot warnings in the
same file — the leak is bounded (one per hive-c0re boot, not per request)
so reusing it here is appropriate.
The rebuild call site in job_queue/exec.rs discards the bool return and
keeps its existing tracing::warn! lines, which is the right separation:
rebuilds are their own retry loop and don't need to post a persistent boot
warning.
Both warn!-and-forget sites in ensure_ci_runner_registered() now also
call set_boot_warning():
- fetch_registration_token failure → crit banner (forge unreachable or
API error; runner stays with stale/absent creds)
- hive-priv register_ci_runner failure → crit banner (EROFS or priv
socket error; runner token not written)
Both are only ever invoked from ensure_all() at hive-c0re startup (no
periodic retry), so set_boot_warning() is the right API: the banner
persists until the next c0re restart that re-runs the step, which is
exactly when a config/environment fix (e.g. the ReadWritePaths EROFS
fix from 64075107) would take effect.
Journal warn! lines are kept alongside the banner (belt-and-suspenders).
Remaining in scope for #2289: sync_agent() warn! sites (called from
both startup and rebuild paths — needs set_warning RAII or explicit
return value to enable later success to clear the banner; left for a
follow-up).
Forgejo returns team-already-exists as HTTP 422 ValidationFailed, not
409 Conflict, so the 409-only guard in ensure_operators_team missed it
and logged a spurious warning every boot (and skipped the settings
reconcile). Add a lenient discriminator that also treats a 422 whose
message says already-exists as benign.
Adds set_boot_warning() to stats/warnings.rs: a one-shot variant of
set_warning() for steps that run once at hive-c0re boot (no periodic
retry to observe a later success and clear the banner via WarningGuard
drop). It forgets the guard, leaking it for the life of the process —
the banner clears on the next hive-c0re restart, which is also when a
config/environment fix would take effect.
Wires every remaining tracing::warn!()-and-forget site in
forge::ensure_all() to also raise a boot warning: core user/token
provisioning, per-org seeding, the operators-team grant per org, the
core/meta repo, shared docs repo, knowledge repo (+ its local clone),
both avatar uploads, and the final nixos-container list failure.
Split the core_token.is_some() body of ensure_all() into a new
ensure_all_orgs_and_repos() helper to stay under clippy's
function-length limit.
Deliberately out of scope: sync_agent() (shared with rebuild_agent(),
different blast radius) and ci_runner::ensure_ci_runner_registered()
(its own retry logic) — matrix::ensure_all() got the same slice-by-
slice treatment.
Ran nix fmt on the whole change (not just the .nix): rustfmt wraps the long
ci_enabled() is_ok_and line that tripped the treefmt CI gate. Adds the # Errors
doc section on the pub priv_client::register_ci_runner per the HOW-TO rule.
Part B (c0re half). New forge/ci_runner.rs: validate the hive-ci runner's
persisted .runner against the forge (GET /admin/runners/{id}); if absent or
stale, mint a fresh registration token (GET /admin/runners/registration-token,
raw request against the local http forge — forgejo-api 0.11 doesn't wrap it)
and hand it to hive-priv's RegisterCiRunner to write the host env-file +
restart the runner. Gated on HYPERHIVE_FORGE_CI_ENABLED; best-effort (never
aborts the startup sweep). Called from ensure_all after the org/repo seeding.
The nix boot-path change (drop prefetch gating, add runner precond, set the
env var) lands next on this branch.
Extends the SweepHealth/warnings registry (already landed for
knowledge_pull) to two more background sweeps:
- gateway_nginx::reload_gateway_nginx: raises a warn-level banner
immediately on the first failed reload (routing changes silently
not taking effect is user-visible right now, so no debounce).
- forge::repos::ensure_config_repo: raises a crit-level banner
listing every agent whose config-repo branch protection is
currently unapplied (security-relevant — bypasses the deploy
pipeline), clearing agents out of the message as they recover.
Journal warn!/error! logging is left in place; the registry adds a
dashboard-visible signal on top. forge::ensure_all() and
matrix::ensure_all() sweeps are deliberately left for a fast-follow.
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.
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.
- 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
On 409 (team already exists), list the org teams to find the operators
team id, then unconditionally PATCH to the desired settings via
org_edit_team. This self-heals a team that was created with the wrong
shape by an older code path (missing units, wrong permission) without
touching membership (separate endpoint, operator-managed).
Addresses mara's review: 'shouldnt we get, then change, then update'.
Unconditional PATCH is simpler than GET→diff→conditional PATCH and safe
here since we own units/permission/description fully.