refactor(#3255): one knowledge subject, single writer and many readers

Review call: the event was addressed per hive — `$SWARM.events.<hive>.knowledge`,
published in a loop over the roster, granted through a wildcard. It does not
need to be. The payload is empty and the event means the same thing to every
hive, so one publish to one subject delivers exactly what N publishes to N
subjects did, and core NATS already fans out to whoever is subscribed. A hive
that was down misses it either way and reconciles on its next periodic pull.

That deletes rather than reshuffles: the roster loop, the wildcard, and the
shared subject-building function whose entire purpose was keeping the grant and
the publish from drifting apart. With one literal there is nothing to disagree
about.

The per-hive shape was justified by the callout policy's rule that an extra
subject must contain the hive name. That rule governs `extra_hive_subjects` —
what a HIVE may publish. This subject lives in the controller's reader grant,
which the rule does not constrain, so a real rule was carried across into a
decision it had no authority over.

Knowledge becomes its own category rather than a leaf under a general event
namespace, since a namespace shaped for events that do not exist yet is a
decision made before there is anything to decide from. The empty config-PR match
arm goes with it: an arm with no body claims this is where the deploy path is
handled, and it is not.

The deny test stays and matters more, not less: with one shared subject a forged
event would reach the whole swarm where a per-hive one reached a single hive.
This commit is contained in:
atlas 2026-08-19 19:49:37 +02:00 committed by mara
commit 9b939f4626
6 changed files with 104 additions and 156 deletions

View file

@ -156,14 +156,18 @@ pub fn chain(error: &dyn std::error::Error) -> String {
/// which is the disagreement this module exists to prevent.
pub mod status;
/// Swarm event subjects — the names the controller publishes on and hives
/// subscribe to.
/// The *knowledge repository changed* subject — one writer (the controller),
/// many readers (the hives).
///
/// Its own category rather than a leaf under a general event namespace: a
/// namespace built for events that do not exist yet is a shape decided before
/// there is anything to decide it from.
///
/// Unconditional and NATS-type-free for the same reason the bucket name above
/// is: three crates must agree on these strings, and the one that agrees
/// hardest — the auth-callout responder, which decides whether a publish is
/// even permitted — speaks neither `jetstream` nor `kv`.
pub mod events;
/// is: three crates must agree on this string, and the one that agrees hardest
/// — the auth-callout responder, which decides whether a publish is even
/// permitted — speaks neither `jetstream` nor `kv`.
pub mod knowledge;
/// Only the fields this needs; authelia returns several.
#[derive(serde::Deserialize)]