feat(#3255): register the swarm-wide forge hooks against the controller

The endpoint landed inert: nothing pointed at it, so the only way to see
it work was to mint an HMAC by hand. Register the two swarm-wide hooks
at startup so a real forge event produces a journal line.

Registered ALONGSIDE the per-hive hooks, not instead of them. Every hive
keeps receiving and acting on its own deliveries; the controller gets a
copy and logs it. Moving the registration is a later step and has to be:
fan-out swarm->hive does not exist yet, so a hook moved now would point
at a receiver that forwards nowhere, silently on both sides.

Deliberately no stale-hook deletion arm, unlike the two per-hive
registrars this otherwise mirrors: theirs delete hooks matching their own
path with a foreign base, and the hives' hooks are not stale.

The route prefix is what keeps this safe. Both hive-side registrars
delete any hook ending in /webhook/knowledge or /webhook/config-pr with a
different base, so a swarm hook under those paths would be deleted by
every hive on every boot. Serving them under /webhook/forge/ avoids it,
and a test pins it -- there is nothing else that can.

SWARM_CONTROLLER_PUBLIC_URL is set only where the swarm vhost is served,
because a hook whose target_url nothing answers is worse than no hook.
This commit is contained in:
atlas 2026-08-18 10:07:49 +02:00 committed by mara
commit 4573865745
6 changed files with 325 additions and 13 deletions

View file

@ -47,6 +47,10 @@ hive-c0re maintains the local clone at
The endpoint is protected by an auto-generated HMAC secret that
hive-c0re verifies on every delivery.
On a swarm, a **second** hook on the same repo points at the
swarm controller — see `docs/swarm/README.md` § Swarm-wide forge
webhooks. Both are expected; neither should be deleted.
2. **Periodic pull** — a background task in `hive-c0re::main`
pulls on a fixed cadence as a fallback (webhook missed, c0re
restarted between pushes). The pull is best-effort — a failure

View file

@ -349,6 +349,30 @@ list, which would look like a silent swarm rather than a controller that
cannot see. The body says which. Status survives a controller restart:
it is stored in the queue, not in the daemon.
### Swarm-wide forge webhooks
At startup the controller registers two Forgejo hooks pointing at
itself — a `push` hook on `internal/knowledge` and a `pull_request` hook
on the `agent-configs` org, both under
`https://<swarm.ui.domain>/webhook/forge/`.
**You will see two hooks where there used to be one**, and that is the
intended state for now: each hive still registers and receives its own,
and the controller's is an additional copy that it currently logs and
nothing more. **Do not delete either.** Removing a hive's hook stops that
hive acting on knowledge pushes and config PRs; removing the
controller's just gets recreated on its next start.
Nothing to configure. The hooks are registered only when this host also
serves the swarm UI vhost — that is what publishes the endpoint, and a
hook the forge cannot reach would collect failed deliveries while
looking healthy. The HMAC secret is generated on first start and kept
(see [`docs/persistence.md`](../persistence.md)).
To check it is working, push to `internal/knowledge` and look for
`webhook: verified delivery` in `journalctl -u swarm-controller`. A
refused delivery logs `webhook: refused delivery` with the reason.
## Cross-references
- `docs/snapshot-store.md` — the swarm's `btrfs receive` endpoint, and