feat(relay): channel labels (#8175)

🤖
## Summary
A plugin or agent often needs to tag its own channels and then find them
again, for example "my attention-config channel" or "all channels of
this workspace". Today a channel has no labels: the relay ignores any
`t` tags in create or edit requests. This PR adds channel labels.

- A create request can carry up to 8 labels as `["t", "<label>"]` tags.
- An edit request with `t` tags replaces the label set. `["t", ""]`
alone clears it, the same convention as `["ttl", ""]`. Only owners and
admins can change labels, as for the name.
- The relay publishes each label as a `t` tag on the channel's state
events, after the channel type tag.

Part 2 of 3, stacked on part 1.

### Related issue
None found.

### Details
- **Rules.** A label uses 1–64 characters from `a-z`, `0-9`, `.`, `:`
and `-`. The relay refuses a label equal to a channel type name
(`stream`, `forum`, `dm`, `workflow`, `system`), so a label cannot make
a channel look like another type.
- **No trust from labels.** Any admin can set a label. Only the
relay-written creator tag (part 1) says who made a channel.
- **Storage.** New migration `0061_channel_labels.sql` adds
`channels.labels TEXT[] NOT NULL DEFAULT '{}'`. Channels without labels
look and behave as before.
- **Readers.** The CLI and `buzz-acp` read the channel type from the
first `t` tag. Before this PR they used the last one, which would now be
a label.
- **Spec.** NIP-CL gets a "Channel labels" section with these rules.

### Testing
- Relay tests: labels at creation appear after the type tag, edit
replaces and clears them, invalid labels are refused, and members who
are not admins cannot change them.
- Unit tests for the label rules: allowed characters, length, reserved
type names, the 8-label cap and the clear marker.
- `buzz-acp` test: the channel type comes from the first `t` tag, not a
label.

### 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 ← this PR
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
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 17:25:46 -04:00
committed by GitHub
parent b3795236cc
commit 2f79374f48
21 changed files with 769 additions and 57 deletions

View File

@@ -21,13 +21,18 @@ plain NIP-29, a client cannot:
- 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.
## 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`.
2. `["P", <creator public key, hex>]`.
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>]`.
@@ -42,6 +47,7 @@ Example kind:39000 (signing fields and unrelated tags omitted):
["d", "9b353519-f4fe-4757-aef4-bec6cc0ae54c"],
["name", "agent-config"],
["t", "stream"],
["t", "agent-config"],
["P", "<creator public key>"],
["created_at", "1759939200"]
]
@@ -51,11 +57,12 @@ Example kind:39000 (signing fields and unrelated tags omitted):
| 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.
A relay can add other `t` tags after it.
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
@@ -76,6 +83,28 @@ 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.
@@ -83,17 +112,26 @@ 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.
## 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.