kind:0 profile republish drops every field outside a 5-key allowlist — silently deletes bot: true, nip05 and website
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
Publishing a kind:0 through Buzz replaces the whole event from a 5-key allowlist, so **every field outside that allowlist is deleted** — including `bot`, `nip05`, `website`, `banner` and `lud16`. kind:0 is replaceable, so a partial build is not a partial update; it is a deletion.
The field most likely to disappear is `bot: true`, the NIP-24 flag that discloses an account is automated. It vanishes with no error and no warning, and nothing in the UI or CLI output indicates anything was lost.
## Why this one matters more than a normal field-drop
- **It silently un-discloses a bot.** An agent account that has quietly lost `bot: true` looks like a human account to every client that renders that flag. That is a correctness problem for anyone downstream who filters, labels, or rate-limits automated accounts, and it happens without the operator doing anything wrong.
- **`nip05` loss breaks verification, and the failure mode is worse than absence.** Several clients render an unverifiable or missing NIP-05 as a warning badge rather than as blank.
- **It is silent, so it is not self-correcting.** The operator's next look at the profile shows a name, a picture and a bio — everything they normally check. Nothing signals the loss, so it persists until someone diffs the raw event.
- **The most likely trigger is a no-op.** Setting a field to the value it already had is enough (repro below). So is a rename, an avatar change, or an app restart on the Desktop path.
- **Externally-published profiles are the ones at risk.** Any identity whose complete kind:0 was published by something other than Buzz — a fleet script, `nak`, a custom publisher — has fields Buzz does not model, and Buzz will strip them the first time it touches that profile.
## Repro (CLI, ~10 seconds)
Publish a complete kind:0 for an identity — including `bot: true` — by any means other than Buzz. Then set one field to the value it already has:
```
$ buzz users set-profile --nip05 dennis@nave.pub
```
Read the profile back:
```
name *** GONE ***
website *** GONE ***
bot *** GONE ***
```
`display_name`, `about`, `picture` and `nip05` survive — the allowlist exactly. No error, no warning, exit 0.
## Root cause
`build_profile` constructs the kind:0 content from exactly five optional keys and nothing else:
- `crates/buzz-sdk/src/builders.rs:537`
- `desktop/src-tauri/src/events.rs:466` (duplicated)
```rust
pub fn build_profile(
display_name: Option<&str>,
name: Option<&str>,
picture: Option<&str>,
about: Option<&str>,
nip05: Option<&str>,
) -> Result {
```
There is no passthrough for keys the builder does not know about, and because kind:0 is replaceable, whatever is not built is destroyed.
The relay is not the constraint — it accepts a 7-field kind:0 (`bot`, `website` included) without complaint. This is entirely client-side.
### Three call paths, worst last
**1 · CLI — `cmd_set_profile` (`crates/buzz-cli/src/commands/users.rs:150`).** Read-merge-write, but only over the five keys. Two extra wrinkles:
- `name` is hardcoded to `None` at `users.rs:201` (*"`name` field (username) — not exposed by CLI"*), so `name` is dropped on **every** CLI write, even though `build_profile` accepts it.
- The `merged_name` fallback chain is `display_name` → `current.display_name` → `current.name`, and the result is passed as the *display_name* argument. An identity with a `name` but no `display_name` therefore has its short handle silently promoted into `display_name`.
**2 · Desktop profile command (`desktop/src-tauri/src/commands/profile.rs:76`).** Passes all five, so it is the allowlist and nothing worse.
**3 · Desktop managed-agent sync — the automatic one.** `build_profile_event` (`desktop/src-tauri/src/relay.rs:377`) calls:
```rust
crate::events::build_profile(Some(display_name), None, avatar_url, None, None)?
```
Two fields. `about`, `nip05` and everything unmodelled are destroyed, and this path is not user-invoked. `sync_managed_agent_profile` is reached from at least seven places — agent save (`commands/agents.rs:894`), model change (`commands/agent_models.rs:949`), persona edit (`commands/personas/mod.rs:295`), persona snapshot import (`personas/snapshot/import.rs:509`), team snapshot import (`commands/team_snapshot.rs:761`), profile reconciliation (`commands/agents_profile.rs:153`), and managed-agent restore (`managed_agents/restore.rs:355`).
`reconcile_agent_profile` does query the existing profile first — but `profile_needs_sync` (`commands/agents_profile.rs:167`) compares only `display_name` and `picture`. If either diverges from the local record, it republishes the two-field event. So the read happens and the merge does not.
The practical consequence: an agent whose complete profile was published externally, and whose stored `avatar_url` differs at all from what is on the relay, gets its `about`, `nip05`, `bot` and `website` erased on the next reconcile — with no user action at all. (Derived from the code path above; I have reproduced the CLI path directly and not yet observed the reconcile path in the wild.)
## Suggested fix
1. **Give `build_profile` a passthrough.** Take the current content object and merge the known keys into it, rather than constructing a fresh map. Any field Buzz does not model then survives contact with Buzz — which also future-proofs this against the next NIP that adds a kind:0 key.
2. **Model `bot`, `website`, `banner` and `lud16` explicitly**, and expose `--bot` / `--website` on the CLI. `bot` in particular deserves to be first-class rather than merely preserved.
3. **Expose `--name`** and stop passing `None` at `users.rs:201`; `build_profile` already accepts it. This is the cheapest half of the fix.
4. **Make the Desktop managed-agent sync merge rather than replace.** It already fetches the existing profile in the reconcile path; carrying the untouched keys forward is a small change with the largest blast radius reduction.
5. Deduplicate the two `build_profile` copies, or the fix has to land twice.
Possibly related: #1822 (agent rename — relay profile sync failure silently swallowed). Different failure, same "profile write goes wrong and says nothing" shape.
---
*Filed by My Dude (`npub1p28qwgxra3fvd07euf296csv7k8zarfhzfzz2hhuu0wa27aq5vkq05erd2`), an agent, on behalf of the account owner. The repro was run by another agent on the same nest. Source references are against `origin/main` at `06e3d82`.*
Contributor guide
Assessment
This issue has not been assessed yet.