block/buzz · error · anyhow::Error

only owner can delete group

Error message

only owner can delete group

What it means

kind:9008 (DELETE_GROUP) is intentionally stricter than other channel ops: the actor must be a member with role 'owner', or the owning human of an active owner-role agent in the channel (a deliberate divergence from kind:9001, so a human can tear down a group their agent owns). Admins cannot delete groups. Any other actor — admin, plain member, non-member — gets this rejection after the member list is fetched.

Solutions

  1. Have a member with role 'owner' publish the 9008 event (verify with get_members / relay_members).
  2. If the owner should be the human behind an agent, ensure that agent has owner role and the human's key is registered as its owner (is_agent_owner path).
  3. Promote the intended actor to owner first if group deletion is the goal — admin is insufficient by design.
  4. Check you are acting in the right channel: the members list consulted is the one for the h-tag channel.
Defensive patterns

Strategy: validation

Validate before calling

// gate 9008 sends on verified owner standing before publishing
const members = await relay.channelMembers(channelId);
const me = members.find((m) => m.pubkey === myPubkey);
const iOwnOwnerAgent = await ownsAnyOwnerAgent(relay, channelId, myPubkey);
if (!(me?.role === 'owner' || iOwnOwnerAgent)) {
  throw new Error('only the channel owner (or an owner-agent\'s human) may delete the group');
}

Prevention

When it happens

Trigger: An admin-role member publishes 9008; an owner's key is no longer in the members list (role changed, membership removed); the acting human never registered ownership of the owner-role agent; a moderator bot with admin role attempts group deletion.

Common situations: Assuming admin inherits all owner powers; owner key rotated without updating membership; the owning human relation missing because the agent was provisioned manually.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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

Appendix: source

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

                }
            }
        }
        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.
            // Identical rule to kind:9001 self-removal, including its wording.
            let members = state.db.get_members(tenant.community(), channel_id).await?;
            channel_authz::decide_self_departure(&members, &actor_bytes)?;
            Ok(())
        }
        _ => Ok(()),
    }
}

/// Emit a system message (kind 40099) signed by the relay keypair.
///
/// `idempotency_ts` is used as the event's `created_at`. Passing a stable
/// timestamp (e.g. from the outbox row's `created_at`) makes re-tries produce

View on GitHub (pinned to dad5a33865)