fix(#3344): type the roster entry's name as Ident, per review

A bare String in a crate whose whole suite types validated names as
hive_types::Ident, so a consumer had to re-derive at the boundary what every
sibling field gets checked for free.

Ident is strictly narrower than the store's charset — [a-z0-9-] against the
[A-Za-z0-9._-] the users database allows, because that file holds humans too.
Every agent name is a legal Ident by construction (creation parses one before
the job is queued), so the narrowing costs real agents nothing. What it does
mean is that a human hand-added to the agent group cannot be described as an
agent: the reader omits that row and logs it, rather than failing the whole
roster or quietly shrinking the answer.

The REQUEST keeps its String. That side carries what a caller asked for, and
validating it server-side is what lets a bad name be refused with a message
instead of failing to deserialise.
This commit is contained in:
atlas 2026-08-19 23:06:51 +02:00
commit d58be6834b
6 changed files with 51 additions and 7 deletions

View file

@ -312,7 +312,26 @@ fn list(state: &AppState) -> Result<BridgeResponse> {
Ok(BridgeResponse::Agents {
agents: store::agent_names(&user_store)
.into_iter()
.map(|name| AgentIdentity { name })
.filter_map(|name| match hive_types::Ident::parse(&name) {
Ok(ident) => Some(AgentIdentity { name: ident }),
// The store's charset is wider than an agent name's, because
// it holds humans too. Every agent this bridge creates is a
// legal `Ident` by construction, so reaching here means a
// subject was hand-added to the agent group that cannot be an
// agent. Skipped rather than failing the whole roster — one
// bad row must not make every other agent invisible — but
// logged, because silently shrinking an answer is how a
// roster starts lying about who is missing.
Err(reason) => {
tracing::warn!(
subject = %name,
%reason,
"subject carries the agent group but is not a legal agent name; \
omitting it from the roster"
);
None
}
})
.collect(),
})
}