🤖 ## 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>
7.1 KiB
NIP-CL
Channel Identity and Discovery
draft optional relay
This NIP adds identity facts to the relay-signed NIP-29 channel-state events (kinds 39000–39002). With these facts, a client can find channels with ordinary NIP-01 filters. It needs no new filter key and no relay-specific API.
Motivation
Plugins and agents often create a channel for their own use and must find it again later, for example "the config channel that this agent created". With plain NIP-29, a client cannot:
- tell who created a channel;
- read the channel type from any event except kind:39000; or
- tell which of two channels is older. The
created_atof a kind:39000 event is the time of the last edit, not the time of creation.
A client also cannot mark its own channels so that it can find them again. Channel labels (below) do this.
Also, every channel that a person belongs to shows in their client as a chat. So a group that exists only to control who can read some data appears in sidebars, search results and unread counts. System channels (below) fix this.
Channel identity tags
On every channel-state event that the relay signs, the relay MUST put these
tags in this order, before any other t or P tag:
["t", <channel type>], for examplestream,forum,dm,workflow,system.["t", <label>]for each channel label, in stored order (see Channel labels).["P", <creator public key, hex>].
kind:39000 also carries ["created_at", <unix seconds>].
Example kind:39000 (signing fields and unrelated tags omitted):
{
"kind": 39000,
"pubkey": "<relay public key>",
"created_at": 1760025600,
"tags": [
["d", "9b353519-f4fe-4757-aef4-bec6cc0ae54c"],
["name", "agent-config"],
["t", "stream"],
["t", "agent-config"],
["P", "<creator public key>"],
["created_at", "1759939200"]
]
}
| Tag | Kinds | Meaning |
|---|---|---|
first t |
39000–39002 | Channel type. |
later t |
39000–39002 | Channel labels. |
P |
39000–39002 | Public key that signed the kind:9007 that created the channel. |
created_at |
39000 | Time the channel was created, in unix seconds, as a decimal string. |
Channel type. Clients MUST read the channel type from the first t tag.
Later t tags are labels.
Creator. The relay writes P from its own record of the kind:9007 that
created the channel. No client can set it. It does not change when ownership
moves to another member or when the channel is edited.
Uppercase P follows NIP-22, NIP-34 and NIP-72, where P names the author of
the root object. On these kinds, lowercase p already lists members, admins
and DM participants. A filter matches tag values, not their roles, so #p
would mix creators with members.
Creation time. The created_at tag does not change when the channel is
edited. Use it, not the event's created_at, to compare the age of channels.
Older events. A relay adds these tags only when it publishes a
channel-state event. An event that a relay version without this NIP published
can lack them, including one published during an upgrade. Such an event stays
until the channel next changes, for example on an edit or a membership
change. A client MUST treat a missing tag as unknown. A missing P does not
mean that a key did not create the channel.
Channel labels
A label groups channels across channel types, for example workspace or
agent-config. Labels have no meaning to the relay.
Set labels. A kind:9007 (create group) MAY carry up to 8 distinct
labels as ["t", <label>] tags. On kind:9002 (edit metadata):
ttags replace the whole label set;["t", ""]tags and no otherttag clear the set;- no
ttag leaves the labels as they are.
Only an owner or admin of the channel MAY change its labels.
Rules. A label is 1–64 characters from a-z, 0-9, ., : and -. A
label MUST NOT be stream, forum, dm, workflow or system. These are
channel type names, so this rule stops a label from making a channel look like
another type. The relay MUST reject the whole event if it has more than 8
distinct labels, an invalid label, a reserved name, or ["t", ""] together
with other labels. The relay stores repeated labels once, so a repeat does
not count toward the 8, and repeated ["t", ""] tags clear the set like one.
Finding channels
Use ordinary NIP-01 filters on the channel-state kinds.
| To find | Filter |
|---|---|
| Channels that a key created | {"kinds": [39000], "#P": ["<creator>"]} |
| Channels with a label | {"kinds": [39000], "#t": ["<label>"]} |
| One creator's channels with a label | {"kinds": [39000], "#P": ["<creator>"], "#t": ["<label>"]} |
| Channels of one type | {"kinds": [39000], "#t": ["<type>"]} |
A #P filter does not return a channel whose events lack P (see
Older events). So it can miss channels that a key created before the relay
supported this NIP.
Values in one tag filter combine with OR, as in NIP-01. So
"#t": ["workspace", "notes"] finds channels with either label. To require a
label and a creator, use #t and #P together.
The relay MUST match #t and #P before it applies limit, as NIP-01
requires. A relay that reads the newest channel events first and matches tags
after can return a short or empty page while matching channels exist.
Choosing one channel
The relay does not make labels unique. Many channels can share a label, and any owner or admin can set any label. So two creates for the same label can both succeed, for example when two processes of one agent start at the same time.
A client that needs one channel per creator and label:
- queries
{"kinds": [39000], "#P": ["<creator>"], "#t": ["<label>"]}; - creates the channel only if the query returns nothing; and
- when the query returns more than one channel, uses the oldest one.
The oldest channel has the lowest created_at tag value, compared as an
integer. If two channels have the same value, the lowest channel ID (d tag,
compared as a string) wins. A channel without a created_at tag ranks after
every channel that has one, and two channels without one compare by channel
ID.
Every reader that follows this rule picks the same channel, even after either
channel is edited.
System channels
A system channel exists only to control who can read some data, for example
config that only an agent and its owner can read. It has the channel type
system. In every other way it is an ordinary channel: it has members, roles,
messages and labels.
Create. A kind:9007 with ["channel_type", "system"] creates a system
channel. It is private unless the kind:9007 also has ["visibility", "open"].
Hidden. While a system channel is private, its kind:39000 carries the
NIP-29 hidden tag.
Trust
A client that relies on these tags MUST check that the relay's key signed the event, as for all NIP-29 group-state events.
Labels give no trust. Any owner or admin can set them. Only P says who
created a channel.
Relation to other NIPs
- NIP-29: the group-state events that this NIP adds tags to.
- NIP-01: the filters that find channels.
- NIP-22, NIP-34 and NIP-72: the convention for uppercase
P.