mirror of
https://github.com/block/buzz.git
synced 2026-11-09 10:51:42 +00:00
🤖 ## 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>
182 lines
7.1 KiB
Markdown
182 lines
7.1 KiB
Markdown
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_at` of 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:
|
||
|
||
1. `["t", <channel type>]`, for example `stream`, `forum`, `dm`, `workflow`, `system`.
|
||
2. `["t", <label>]` for each channel label, in stored order (see
|
||
[Channel labels](#channel-labels)).
|
||
3. `["P", <creator public key, hex>]`.
|
||
|
||
kind:39000 also carries `["created_at", <unix seconds>]`.
|
||
|
||
Example kind:39000 (signing fields and unrelated tags omitted):
|
||
|
||
```json
|
||
{
|
||
"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):
|
||
|
||
- `t` tags replace the whole label set;
|
||
- `["t", ""]` tags and no other `t` tag clear the set;
|
||
- no `t` tag 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:
|
||
|
||
1. queries `{"kinds": [39000], "#P": ["<creator>"], "#t": ["<label>"]}`;
|
||
2. creates the channel only if the query returns nothing; and
|
||
3. 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`.
|