Repository navigation
feat(relay): per-channel write policy for announcement / read-only channels #2497
Description
Use case
We run a membership-gated Buzz community. We want an #announcements
channel that every member can read and react to, but where only the
channel owner and admins can post. This is the standard announcements
or broadcast pattern from Slack and Discord: one low-noise, high-signal
feed that members trust precisely because replies do not dilute it.
Today the only ways to keep such a channel clean are social convention,
or a moderation agent that deletes non-owner posts after the fact. The
latter is reactive and visibly flashes the post before removing it.
Current gap
Per-channel access is membership-binary: if a principal is a member of a
channel they can both read and write it, so there is no concept of a
member who can only read. channel_members.member_role
(Owner / Admin / Member / Guest / Bot) exists but is not consulted in the
message-write path. PR #896 states this directly: guest was rejected
because "message writes are membership-binary today and guest would
silently fall through to write behavior." Issue #2475 confirms the same
invariant.
So there is currently no way to express "everyone may read, only Owner
or Admin may post" for a channel whose members are ordinary relay members.
Relation to existing work
This is the complementary axis to the current public-access cluster, not
a duplicate of it:
- Add explicit public-readable channel reads #920 (public-readable channel reads) and feat(relay): channel-scoped guest read/write access for public support #2475 (channel-scoped guest
read/write) expand access outward to non-members and the public. - This request restricts write access inward, by role, within a single
channel.
Both ride the same enforcement seams, so the mechanism should feel familiar.
Requested model
A per-channel write policy, for example a channel setting
post_policy: everyone | admins (default everyone, behaviour unchanged).
When a channel is set to admins:
- Message-write EVENTs bearing that channel's
#hare accepted only from
principals whose channel role is Owner or Admin. - All members keep read, search, live subscription and reactions for the
channel. - Non-admin writes fail closed with a clear relay message.
- Every other channel, and open relays, are unaffected.
Suggested mechanism, reusing the seams PR #896 already proved out: gate the
message-write scope for that channel's #h on channel role at ingest
(required_scope_for_kind and the *:write scope check in ingest.rs),
rather than introducing a new membership model. #896 showed read-only can
be expressed as scopes fixed at auth time; this applies the same idea
per-channel, per-role, for writes.
Design question
Would upstream accept a per-channel write policy (an announcement or
read-only channel) that gates message-write on channel role, reusing the
scope-based enforcement from #896 and per-channel policy storage along the
lines of #920? Happy to align the config surface (channel setting vs relay
config vs a signed channel-policy event) with whatever fits the roadmap.
Activity
vbalko-claimate commentedon Aug 25, 2026
Another use case for this, a bit different from the announcements one: a
#helpchannel holding the onboarding doc.Small self-hosted team relay. New people need one place that tells them what the channels are, which agents exist and what they do, how to get their tooling set up. The canvas is the obvious home for that, but it's only reachable through the management sheet (#4642), and a member without edit rights may not even see the row, so a newcomer will never find it. That leaves posting the doc as an ordinary message in the channel.
Which works, right up until somebody talks in that channel. One conversation and the doc is off screen, and the people it's written for are the least likely to scroll around looking for it.
post_policy: adminswould make the channel behave the way I actually need it to.One note on the workarounds you list: the moderation-agent route needs that agent to be an admin, since deleting someone else's message is a moderation action. If your bot is a plain member (mine is, on purpose) that option isn't there at all. So in practice today it really is just social convention.
For what it's worth #4642 would also solve my case, from the other end. If the canvas were visible in the chat view I wouldn't need the doc to be a message at all, and wouldn't care who posts in the channel.
Gforce10-design commentedon Aug 29, 2026
Another shape of the same gap, from a self-hosted multi-department relay: the principal that
needs read-only is not a channel property but a participant property.
Use case. An audit/compliance seat reads three department channels and posts its findings to
one audit channel.
post_policy: adminscannot express this — those three channels are thedepartments' own working channels, so everyone in them must keep posting. What we need is
"principal P may read A, B, C and write only D".
Confirming the membership-binary invariant, measured rather than inferred. The issue cites
PR #896 for it; we probed it directly on a self-hosted relay (private stream channel, one probe
identity, add/remove between rounds):
403 relay_membership_required403 relay_membership_required[](empty array)400 restricted: not a channel memberguest/member/bot/adminSo
member_roleaccepts all four values and grants the same access for each. Two side notes thatmay be worth their own treatment:
from "no messages" to a client, which makes a silently-unauthorised reader look like a quiet
channel.
key measures the outer gate and never reaches the role code. Worth stating in any test plan here.
Why a client-side workaround is not enough. We built one: our agent seats post through a
narrow MCP server that holds a per-seat allow-list of channel ids and refuses anything else. It
works, and it is still the wrong place for the control, for two reasons we hit in practice:
buzz messages sendfrom the samecontainer bypasses it entirely. Today our seats advertise no shell tool, so the path is closed
by a second control — but that is a property of the current tool set, not of the boundary. One
more advertised tool and the wall is gone.
reply_tois not bound to the destination channel.channel=<allowed>plusreply_to=<event in another channel>passes a naive allow-list check. We now resolve theparent event's channel before sending and fail closed when it cannot be resolved, but the same
reasoning applies upstream: if per-principal write scope lands, threading should be scoped with
it.
Requested shape, complementary to
post_policy: a per-member write scope — e.g.can_poston channel membership, or a
readerrole that the message-write path actually consults — so that"reads many, writes one" is expressible without every participant's client agreeing to behave.
Happy to re-run the probe table against any candidate branch.