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
- Have a member with role 'owner' publish the 9008 event (verify with get_members / relay_members).
- 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).
- Promote the intended actor to owner first if group deletion is the goal — admin is insufficient by design.
- 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
- Reserve 9008 in tooling for owner-role keys; surface a clear 'ask the owner' message for admins.
- After role changes or owner key rotation, verify get_members still lists the intended owner before destructive ops.
- When agents own channels, record the owning human at creation so the is_agent_owner path works.
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
- actor is not an active member
- must be event author or channel owner/admin
- target event belongs to a different channel
- an admin cannot ban or time out a community owner or fellow…
- cannot remove the last owner
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 produceView on GitHub (pinned to dad5a33865)