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

@ -274,14 +274,25 @@ async fn handle(state: &AppState, name: String) -> Result<BridgeResponse> {
let mut user_store = store::load_store(&cfg.users_file)?;
if user_store.users.contains_key(&name) {
// The subject is there, so no password is minted — but it may
// predate the marker that puts it in the roster, and running agent
// creation again is the whole of the migration for those. Reported
// as `AlreadyExists` either way: the wire answer is about the
// subject existing, not about whether bytes moved.
// The subject is there, so no password is minted. Whether it gained
// the marker here is the caller's business, not an implementation
// detail: an unmarked subject is either a pre-marker agent (the
// intended migration) or a human created without a group, and this
// process cannot tell them apart. So the two outcomes are reported
// as different variants rather than folded into one.
if store::mark_as_agent(&mut user_store, &name) {
store::publish(&cfg.users_file, &mut user_store)?;
tracing::info!(agent = %name, "marked an existing identity as an agent");
// `warn`, not `info`: the uneventful path is silent, so anything
// logged here is a subject that was *not* an agent a moment ago.
// Names the account because "which one" is the entire question
// an operator will have.
tracing::warn!(
subject = %name,
"added the agent marker to an EXISTING identity — intended if this \
predates the marker, but the store cannot distinguish that from a \
human account created without a group"
);
return Ok(BridgeResponse::Healed);
}
return Ok(BridgeResponse::AlreadyExists);
}