feat(swarm-authelia-bridge): report a heal as its own outcome

`EnsureAgentIdentity` answered `AlreadyExists` whether it had added the
agent marker to an existing subject or done nothing at all, and the
controller discarded the answer outright. So the one case worth telling
a human about — a subject that was NOT an agent a moment ago — could not
survive the socket, let alone reach a log.

`Healed` is a variant rather than a field on `AlreadyExists` because a
field is ignorable: adding a variant makes every existing match fail to
compile until its author decides what a heal means. That is the property
the old shape lacked.

The store cannot tell a pre-marker agent identity from a human operator
account created without a group — both are `groups: []`. So this is
either the intended migration or an agent joining a person's live SSO
account, and only the caller has the context to tell them apart.

Gated by state/gate-3549-heal.sh (8 arms + mutation): the mutation
collapses Healed back and reddens the discriminating arm while leaving
the anti-noise arm green. The W' control asserts exactly one warn in the
whole run, so a build that warned on every routine ensure would fail.
This commit is contained in:
atlas 2026-08-23 19:00:41 +02:00
commit a248db1fa2
3 changed files with 60 additions and 12 deletions

View file

@ -98,14 +98,30 @@ pub enum BridgeResponse {
/// The agent had no identity yet; one was minted and `users.yml` was
/// rewritten.
Created,
/// The agent already had an identity, so no password was minted.
/// The agent already had an identity **and already carried the agent
/// marker**, so nothing was minted and nothing was written.
///
/// ⚠️ Says nothing about whether the file was written. An identity that
/// exists but is missing the marker group gains it here, because
/// re-running agent creation is the whole of the migration story for
/// identities that predate that marker — so "already exists" has to mean
/// *the subject was there*, not *nothing changed on disk*.
/// The uneventful case: re-running agent creation for an agent that is
/// already an agent.
AlreadyExists,
/// A subject with this name existed **without** the agent marker, and
/// this call added it.
///
/// ⚠️ **Its own variant, not a flag on [`Self::AlreadyExists`], because
/// the two cases carry different risk and a `bool` field is ignorable.**
/// Adding a variant makes every existing `match` fail to compile until
/// its author decides what to do about a heal; a field would let the
/// dangerous case keep travelling as the safe one, which is how it went
/// unreported before.
///
/// Why the risk differs: the store cannot tell a pre-marker *agent*
/// identity from a *human* operator account created without a group —
/// both are simply `groups: []`. So this outcome is either the intended
/// migration (re-running creation is the whole of that story) or an
/// agent quietly joining a person's live SSO account. The caller is the
/// only place with the context to tell those apart, so it has to be
/// told the write happened.
Healed,
/// The roster, in answer to [`BridgeRequest::ListAgentIdentities`].
Agents {
/// Sorted by name — the store is a `BTreeMap`, and a stable order
@ -141,6 +157,13 @@ mod tests {
let exists = serde_json::to_value(BridgeResponse::AlreadyExists).unwrap();
assert_eq!(exists, serde_json::json!({"status": "already_exists"}));
// Distinct on the wire from `already_exists`, which is the whole
// point of it being a separate variant: a reader tailing these has
// to be able to see a heal without knowing the Rust type.
let healed = serde_json::to_value(BridgeResponse::Healed).unwrap();
assert_eq!(healed, serde_json::json!({"status": "healed"}));
assert_ne!(healed, exists);
let agents = serde_json::to_value(BridgeResponse::Agents {
agents: vec![AgentIdentity {
name: Ident::parse("atlas").expect("valid ident"),