Skip to content

fix(cli): refuse a profile republish that would drop the owner attestation - #5751

Open
Cynthia427 wants to merge 1 commit into
block:mainfrom
Cynthia427:fix/set-profile-refuse-attestation-loss
Open

fix(cli): refuse a profile republish that would drop the owner attestation#5751
Cynthia427 wants to merge 1 commit into
block:mainfrom
Cynthia427:fix/set-profile-refuse-attestation-loss

Conversation

@Cynthia427

Copy link
Copy Markdown

Problem

kind:0 is replaceable, so a profile write replaces the stored event whole.

cmd_set_profile already merges content. But the NIP-OA auth tag is not a content field — it is re-attached only by sign_event from BUZZ_AUTH_TAG, and sign_event deliberately refuses a caller-supplied one:

// Enforce: auth tags may only come from self.auth_tag injection.
"event has {auth_count} auth tags — expected {expected}; callers must not add auth tags manually"

So running set-profile without BUZZ_AUTH_TAG against a profile that has the tag destroys it — silently, with a successful write.

Not hypothetical

On 2026-08-12 an avatar re-host republished eight agent profiles this way (display_name + picture, no BUZZ_AUTH_TAG). Visible in the relay DB as tags_len 214 → 2 across all eight in the same minute.

check_sibling_via_profile proves same-owner siblinghood by finding that tag. With it gone it returned false for every agent, so author_allowed dropped every agent-to-agent message for ~23 hours.

Nothing failed loudly: writes succeeded, units stayed active, heartbeats fired, channels stayed subscribed. Only the owner could still reach the agents, because the owner is matched by pubkey and never consults the profile. A fleet-wide fault therefore presented as "one agent ignoring one sender", which sent the operators looking in entirely the wrong place.

Why not carry the tag forward

That would be the wrong fix, and sign_event already forbids it. An attestation is a signature over a claim — it must be re-issued, not copied by whoever happens to be writing a profile. Preserving it blindly would weaken the invariant that auth tags originate only from the configured attestation.

The fix: fail closed

If the live profile carries an auth tag and BUZZ_AUTH_TAG is unset, refuse and say what would be destroyed:

refusing to republish: your current profile carries a NIP-OA owner attestation
(auth tag) and BUZZ_AUTH_TAG is not set, so this write would silently drop it
and break same-owner verification. Set BUZZ_AUTH_TAG to the attestation JSON
and retry.

No behaviour change when BUZZ_AUTH_TAG is set, or when the profile has no tag.

Tests

event_has_auth_tag extracted as a pure function so the decision is testable, matching the existing presence_subject style in this module. 4 tests including the exact shape the incident left behind (content intact, tags: []), plus malformed-tag and missing-tags cases.

Mutation-verified: changing the match to a non-auth literal fails two of them.

Disclosed limitation: the guard's call site is not covered — cmd_set_profile needs a live client and this module has no client harness, so the wiring is verified by inspection rather than by test. Happy to add a harness if you'd prefer that before merge.

cargo test -p buzz-cli --lib: 256 passed, 0 failed.

@Cynthia427
Cynthia427 requested a review from a team as a code owner August 13, 2026 08:56

@wolfyy970 wolfyy970 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fail-closed behavior is right: a profile update must not silently remove an existing owner attestation.

I think this needs two small changes before it is mergeable:

  1. Please restack the 941ec0b change alone on current main. This branch also carries three older Desktop/runtime commits and is currently dirty, which turns a narrow CLI safety fix into an unrelated merge.
  2. Please test the real cmd_set_profile seam, not only event_has_auth_tag: an authenticated current profile with no configured attestation must submit nothing; supplying a valid attestation must publish one replacement with the new auth tag; and an unattested profile must remain editable.

That production test is the important proof here. It verifies the live query, guard, signing path and absence of a relay write together.

…ation

kind:0 is replaceable, so a profile write replaces the stored event whole.
cmd_set_profile already merges CONTENT, but the NIP-OA auth tag is not a
content field: it is re-attached only by sign_event from BUZZ_AUTH_TAG, and
sign_event deliberately forbids callers supplying one (it is a signed
attestation, not a value to copy forward).

So running set-profile without BUZZ_AUTH_TAG against a profile that HAS the
tag destroys it, silently and successfully.

That is not hypothetical. On 2026-08-12 an avatar re-host republished eight
agent profiles this way. The tag vanished, check_sibling_via_profile then
returned false for every agent, and author_allowed dropped every
agent-to-agent message for ~23 hours. Nothing failed loudly: the writes
succeeded, units stayed active, heartbeats fired, channels stayed
subscribed. Only the owner could still reach the agents, because the owner
is matched by pubkey and never consults the profile — which made a
fleet-wide fault look like one agent ignoring one sender.

Carrying the old tag forward would be the wrong fix and is already
forbidden: an attestation must be re-issued, not copied. So fail closed
instead — tell the operator exactly what the write would destroy and how to
supply it.

Tested at the cmd_set_profile seam against a fake relay, so the live
query → guard → sign → submit chain is exercised rather than the predicate
alone, with the relay's received events as the evidence:

- an attested profile with no configured attestation submits NOTHING (a
  refusal raised after the write would already have destroyed the tag)
- supplying the attestation publishes exactly one replacement that still
  carries the auth tag, and preserves unspecified content fields — the
  incident began as an avatar rewrite that blanked `picture`
- an unattested profile stays editable, so the guard cannot become a
  blanket block

Mutation-verified: disabling the guard fails the first test and only that
one. event_has_auth_tag is also kept pure and unit-tested, including the
exact shape the incident left behind (content intact, tags emptied).

Signed-off-by: Cynthia Rohr <cynthia.r@kreativreason.co>
@Cynthia427
Cynthia427 force-pushed the fix/set-profile-refuse-attestation-loss branch from 941ec0b to a54bf43 Compare August 13, 2026 13:19
@Cynthia427

Copy link
Copy Markdown
Author

Both addressed — thank you, the second point was the right call.

1. Restacked. 941ec0b alone on current main: one commit, one file. The three Desktop/runtime commits are gone from the branch (they live on their own branches), and the PR now reports MERGEABLE rather than CONFLICTING.

2. Tested at the cmd_set_profile seam. You were right that the predicate tests were the wrong proof — my own commit message conceded the call site was "verified by inspection, not by test", which is exactly the gap that let the original incident run for ~23 hours. Three tests now drive the real command against a fake relay (reusing the axum harness already in client.rs) and assert on what the relay actually received:

  • attested profile + no configured attestation → submits nothing. Asserted as zero received events, not merely as an Err — a refusal raised after the write would already have destroyed the attestation, so "returns an error" is not the property that matters here.
  • attested profile + attestation supplied → exactly one replacement, still carrying the auth tag. It also asserts unspecified content survives the merge (picture), because the incident began as an avatar rewrite that blanked it.
  • unattested profile → still editable. Guards that quietly become blanket blocks are their own outage.

Mutation-verified: stubbing the guard to false fails the first test and only that one; the other two keep passing, correctly, since they do not depend on it.

One extra fix while in here: the change had orphaned fetch_current_profile's doc comment above a blank line (clippy empty_line_after_doc_comments), leaving the function undocumented and the comment attached to nothing. Reattached and updated for the new CurrentProfile return type.

cargo fmt, clippy, and the full buzz-cli suite (350 tests) are clean.

@wolfyy970 wolfyy970 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at a54bf43. The branch is now the single CLI fix, and the new tests cross the real query, guard, signing and submission path. They prove the refusal writes nothing, a verified configured attestation survives replacement, and unattested profiles remain editable. I ran the focused tests and strict clippy against this exact head; both pass.

Approved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants