feat(#3549): refuse agent names the swarm already routes on

An agent name was checked for shape and never for meaning, so a name the
system already answers to could be taken. The list is derived, not invented:
operator/manager/root are broker recipients, and forge/reminder/schedule/
system/todo are message senders.

Senders are in it for a reason that is not cosmetic. The harness switches on
the sender name, so an agent called todo would have its messages read as
todo-wakes and one called system as helper events. That is a parse, not a
display quirk.

⚠️ The check is deliberately its own function rather than part of
validate_username, which runs over every user on every write — including
humans this bridge did not create. Folding it in would mean a store already
containing an operator named 'operator' could never be written again: a rule
about what an agent may be NAMED would have become a rule about what the file
may CONTAIN. A test pins that.

This is the static half only. The dynamic half — a name already taken by
another subject — waits on the heal-vs-refuse question open on #3550.
This commit is contained in:
atlas 2026-08-19 23:10:02 +02:00
commit dd1156d45c
2 changed files with 86 additions and 0 deletions

View file

@ -254,6 +254,10 @@ fn refuse(refusal: Refusal) -> Response {
async fn handle(state: &AppState, name: String) -> Result<BridgeResponse> {
store::validate_username(&name)?;
// After the charset check and before anything touches the store: a
// reserved name is refused on its own terms, not as a side effect of a
// collision with whatever happens to be in the file today.
store::reject_reserved_name(&name)?;
let cfg = &state.config;
// Held across the whole load → insert → publish sequence, not just the