feat(relay): add the system channel type (#8180)

🤖
## Summary
Buzz uses channel membership to decide who can read what, and every
channel a person belongs to shows up in their client as a chat. So there
is no way to make a group that exists only for access control, for
example "only this agent and its owner can read the agent's config".
Such a channel would appear in both people's sidebars, search results
and unread counts.

This stack adds a **system channel**: an ordinary channel that queries
leave out unless they ask for it. This first PR adds the type itself.

- `kind:9007` (create channel) with `["channel_type", "system"]` creates
a system channel.
- It is private unless the creator sends `["visibility", "open"]`.
- While it is private, its channel metadata event (kind:39000) carries
the NIP-29 `hidden` marker, which means "hide this group's metadata from
non-members". Until now the relay set `hidden` only for DMs.

On its own this PR does not yet hide system channels from queries. Part
2 does that.

Part 1 of 3, stacked on #8176 (the channel labels and creator lookup
stack, which this feature uses).

### Related issue
None found.

### Details
- Migration 0062 adds `system` to the `channel_type` Postgres enum. No
new columns.
- `buzz_core::channel::ChannelType::System`, and `system` is now a
reserved label name (labels cannot equal a channel type name).
- `buzz-admin` reconcile writes `hidden` with the same rule as the
relay. Usage metrics count system channels.
- **Rollback.** A relay older than this change cannot read `channel_type
= 'system'`. Do not roll back past it while system channels exist.
- **Spec.** NIP-CL gets a "System channels" section. `ARCHITECTURE.md`
lists the `System` type.

### Testing
- Relay test: a system channel is private and `hidden` by default;
opening it removes `hidden`; its first `t` tag is `system`.
- Migration embed and usage-metrics tests.

### Stack
These are two stacks. Merge them in this order. Client support
(creating, labeling and finding these channels) will be in
block/buzz-app, not the buzz SDK and CLI.

**Stack 1: channel labels and creator lookup (#8174–#8176)**

1. #8174 channel type and creator tags, and the channel creation time
2. #8175 channel labels
3. #8176 database-side label and creator match, and the rule for
choosing one channel

**Stack 2: system channels (#8180–#8182)**, stacked on stack 1 because
it uses labels and the creator tag

4. #8180 the system channel type ← this PR
5. #8181 leave system channels out of reads that do not name them
6. #8182 channel-state filters opt in to system channels

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
This commit is contained in:
Logan Johnson
2026-10-14 19:03:34 -04:00
committed by GitHub
parent a3909acc67
commit 71d7aa0436
14 changed files with 251 additions and 37 deletions

View File

@@ -705,7 +705,7 @@ mod postgres_tests {
let mut migrations: Vec<_> = MIGRATOR.iter().collect();
migrations.sort_by_key(|migration| migration.version);
assert_eq!(migrations.len(), 61);
assert_eq!(migrations.len(), 62);
assert_eq!(migrations[58].version, 59);
assert!(migrations[58].sql.as_str().contains("buzz.push_enabled"));
assert_eq!(migrations[59].version, 60);
@@ -715,6 +715,11 @@ mod postgres_tests {
.contains("ALTER TABLE push_leases DROP COLUMN app_profile"));
assert_eq!(migrations[60].version, 61);
assert!(migrations[60].sql.as_str().contains("ADD COLUMN labels"));
assert_eq!(migrations[61].version, 62);
assert!(migrations[61]
.sql
.as_str()
.contains("ALTER TYPE channel_type ADD VALUE IF NOT EXISTS 'system'"));
assert_eq!(migrations[55].version, 56);
assert!(migrations[55]
.sql

View File

@@ -38,6 +38,8 @@ pub struct FleetStockSnapshot {
pub channels_dm: i64,
/// Non-deleted workflow channels.
pub channels_workflow: i64,
/// Non-deleted system channels.
pub channels_system: i64,
/// Relay owners.
pub members_owner: i64,
/// Relay administrators.
@@ -96,7 +98,8 @@ where
COUNT(*) FILTER (WHERE channel_type = 'stream') AS stream,
COUNT(*) FILTER (WHERE channel_type = 'forum') AS forum,
COUNT(*) FILTER (WHERE channel_type = 'dm') AS dm,
COUNT(*) FILTER (WHERE channel_type = 'workflow') AS workflow
COUNT(*) FILTER (WHERE channel_type = 'workflow') AS workflow,
COUNT(*) FILTER (WHERE channel_type = 'system') AS system
FROM channels
WHERE deleted_at IS NULL
),
@@ -122,6 +125,7 @@ where
channels.forum AS channels_forum,
channels.dm AS channels_dm,
channels.workflow AS channels_workflow,
channels.system AS channels_system,
members.owner AS members_owner,
members.admin AS members_admin,
members.member AS members_member,
@@ -1455,7 +1459,7 @@ mod postgres_tests {
.await
.expect("deactivate user");
for channel_type in ["stream", "forum", "dm", "workflow"] {
for channel_type in ["stream", "forum", "dm", "workflow", "system"] {
sqlx::query(
"INSERT INTO channels (id, community_id, name, channel_type, visibility, created_by) \
VALUES ($1, $2, $3, $4::channel_type, 'open', $5)",
@@ -1589,8 +1593,9 @@ mod postgres_tests {
stock.channels_forum,
stock.channels_dm,
stock.channels_workflow,
stock.channels_system,
),
(1, 1, 1, 1)
(1, 1, 1, 1, 1)
);
assert_eq!(
(