Skip to content

feat(relay): per-channel write policy for announcement / read-only channels #2497

Open

Description

@imikerussell

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:

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 #h are 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

  1. vbalko-claimate commented on Aug 25, 2026

    @vbalko-claimate

    Another use case for this, a bit different from the announcements one: a #help channel 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: admins would 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.

  2. Gforce10-design commented on Aug 29, 2026

    @Gforce10-design

    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: admins cannot express this — those three channels are the
    departments' 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):

    identity read write
    not a relay member 403 relay_membership_required 403 relay_membership_required
    relay member, not a channel member [] (empty array) 400 restricted: not a channel member
    channel member — guest / member / bot / admin allowed allowed (all four)

    So member_role accepts all four values and grants the same access for each. Two side notes that
    may be worth their own treatment:

    • A non-member's read returns an empty array, not a 403. "No permission" is indistinguishable
      from "no messages" to a client, which makes a silently-unauthorised reader look like a quiet
      channel.
    • Relay membership is checked per community before channel membership, so a probe with a fresh
      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:

    1. The allow-list lives in the server the agent itself invokes. buzz messages send from the same
      container 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.
    2. reply_to is not bound to the destination channel. channel=<allowed> plus
      reply_to=<event in another channel> passes a naive allow-list check. We now resolve the
      parent 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_post
    on channel membership, or a reader role 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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      Sponsor
      SponsoredKunjungi sekarang
      Promo