block/buzz · error · IngestError::Rejected

only owner can delete group

Error message

only owner can delete group

What it means

Thrown by the relay's side-effect authorization for kind:9008 (DELETE_GROUP) events. The actor's pubkey must appear in the channel's active member list with role == "owner", or the actor must be the owning human of an active owner-role agent in that channel (an intentional divergence from kind:9001, per the comment in the source). If neither holds, the delete is rejected with this message.

Source

Thrown at crates/buzz-relay/src/handlers/side_effects.rs:732

                }
            }
        }
        9008 => {
            // DELETE_GROUP: owner only, or the owning human of the channel's agent-owner.
            let members = state.db.get_members(tenant.community(), channel_id).await?;
            let actor_member = members.iter().find(|m| m.pubkey == actor_bytes);
            match actor_member {
                Some(m) if m.role == "owner" => Ok(()),
                _ => {
                    // Allow the owning human of any active owner-role agent in the
                    // channel, even when the human is not a channel member —
                    // diverges from kind:9001 intentionally.
                    if actor_owns_any_owner_agent(state, tenant.community(), &members, &actor_bytes)
                        .await?
                    {
                        return Ok(());
                    }
                    Err(anyhow::anyhow!("only owner can delete group"))
                }
            }
        }
        9022 => {
            // LEAVE_REQUEST: must be an active member, and cannot be the last owner.
            let members = state.db.get_members(tenant.community(), channel_id).await?;
            let actor_member = members.iter().find(|m| m.pubkey == actor_bytes);
            match actor_member {
                None => {
                    return Err(anyhow::anyhow!("actor is not an active member"));
                }
                Some(m) if m.role == "owner" => {
                    let owner_count = members.iter().filter(|m| m.role == "owner").count();
                    if owner_count <= 1 {
                        return Err(anyhow::anyhow!("cannot remove the last owner"));
                    }
                }
                _ => {}

View on GitHub (pinned to f956e6fe06)

Solutions

  1. Re-publish the kind:9008 event signed by a pubkey whose channel membership role is owner
  2. If acting through an agent, promote that agent to owner role and confirm it is active, then retry as the agent's owning human
  3. Promote the current actor to owner first (membership/role change from an existing owner), then delete
  4. Verify the signing key and community host match the owner membership row (check the channel's kind:39002 membership events)

Example fix

// before: signed by an admin key — rejected with "only owner can delete group"
let ev = EventBuilder::new(Kind::Custom(9008), "").tags([tag_h(channel_id)]).to_event(&admin_keys)?;

// after: sign with an owner-role member's key
let members = client.get_members(channel_id).await?;
let owner = members.iter().find(|m| m.role == "owner").ok_or(NoOwner)?;
let ev = EventBuilder::new(Kind::Custom(9008), "").tags([tag_h(channel_id)]).to_event(&owner_keys)?;
Defensive patterns

Strategy: validation

Validate before calling

// Before publishing kind:9008, confirm the signer is an active owner of the channel
// (membership resolves from kind:39002 events whose d-tag is the channel id).
let members = client.get_members(channel_id).await?;
let is_owner = members.iter().any(|m| m.pubkey == actor_pubkey && m.role == "owner");
if !is_owner {
    return Err(DeleteGroupError::NotOwner);
}

Type guard

fn is_owner(m: &ChannelMember) -> bool {
    m.role == "owner"
}

Try / catch

match publish_delete_group(&owner_keys, channel_id).await {
    Ok(_) => {}
    Err(e) if e.to_string().contains("only owner can delete group") => {
        // terminal authorization failure — do not retry; surface to the user
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Publishing a signed kind:9008 event where the signer's membership role is "member" or "admin" (not "owner"), or the signer is not a channel member at all, AND actor_owns_any_owner_agent() finds no active owner-role agent in the channel owned by the actor.

Common situations: An admin assumes delete rights (admin suffices for kind:9001 edits, not 9008 deletes); a human tries to delete via their agent after the agent was demoted or deactivated; signing with a different key than the one holding the owner membership; the actor's membership lives under a different community host (tenant scoping hides it).

Related errors


AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16). Data as JSON: /api/errors/485987abc4317860. Report an issue: GitHub.