Git push policy depends on a buzz-channel repo binding that no client can write
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
The git push policy resolves a non-owner pusher's permissions through a `buzz-channel` tag on the repo's kind:30617 announcement. **No shipped client writes that tag**, so the binding is never present, so every non-owner push is rejected with `no channel binding` before role resolution is ever attempted.
The channel-role branch of the push policy is unreachable in practice. Repos are effectively owner-push-only.
## The read side
`crates/buzz-relay/src/api/git/policy.rs:300-305` resolves the channel from the announcement tags:
```rust
let channel_id = tags
.iter()
.find(|t| t.first().map(|s| s.as_str()) == Some("buzz-channel"))
.and_then(|t| t.get(1))
.and_then(|id| Uuid::parse_str(id).ok());
```
`policy.rs:347-356` then makes that tag load-bearing for authorization:
```rust
let role = if is_repo_owner || is_managed_agent_owner {
MemberRole::Owner
} else {
match channel_id {
None => {
warn!(repo = %req.repo_id, "hook callback: no buzz-channel binding");
return (StatusCode::FORBIDDEN, "no channel binding").into_response();
}
Some(ch_id) => {
match state.db.get_member_role(community, ch_id, &pusher_bytes).await {
```
`get_member_role`, and with it the entire Owner/Admin/Member/Bot ladder for git, is only reachable when `channel_id` is `Some`. It also gates the archived-channel check at `policy.rs:307-320`, which silently no-ops for every repo.
`web/src/features/repos/use-repos.ts:34` also reads the tag and surfaces it as `Repo.channelId` — always `null` today.
## The missing write side
I grepped the tree for `"buzz-channel"` at `afe4aaf530a36ee7837bb0efc8674b7503156cef`. Outside the two readers above, every remaining hit is a test fixture (`crates/buzz-cli/src/commands/repos.rs:428` and `crates/buzz-sdk/src/builders.rs:2842`, both under `#[cfg(test)]`). Neither client can emit it:
- **CLI** — `buzz repos create` accepts `--id`, `--name`, `--description`, `--clone`, `--web`, `--nostr-relay`. There is no channel flag.
- **Desktop** — `desktop/src/features/projects/useCreateProject.ts:53-67` builds the tag list as `d`, `name`, and conditionally `description`, `clone`, `web`. No channel tag, and no channel input in the create-project form.
Scope of that negative: rust/ts/tsx/mjs sources excluding `node_modules`. `build_repo_announcement_with_tags` (`crates/buzz-sdk/src/builders.rs:952`) takes arbitrary tags, so a direct SDK consumer or a raw Nostr client *can* set it — but nothing in Buzz itself does, and there is no command to add the tag to an existing announcement.
## Impact
1. Non-owner pushes are rejected regardless of channel role. Adding someone to a channel does not grant them git access, which appears to be the intended model given how `policy.rs` is written.
2. The archived-channel read-only guard never fires.
3. `Repo.channelId` is dead in the web client.
4. Separately from permissions: there is no way to express "this project belongs to this channel" anywhere in the product, so a project and its discussion channel have no navigable link in either direction.
## Suggested fix
Smallest version that makes the existing policy code reachable:
- `buzz repos create --channel ` emitting `["buzz-channel", ""]`.
- A channel selector in Desktop's create-project flow, writing the same tag.
- Something to set the binding on an already-announced repo, since kind:30617 is addressable and can be re-published.
Validating that the channel exists in the community at announce time would also give a better error than a push-time rejection.
Happy to open a PR for the CLI flag and the Desktop tag if the approach is agreeable — I have a working local build and test setup for this repo.
## Environment
Observed at `afe4aaf530a36ee7837bb0efc8674b7503156cef` (fork of `main`, two unrelated files changed). Static read of the policy path plus a full-tree grep; I have not reproduced a live rejected push against a running relay, so the runtime claim is inferred from the control flow above rather than observed.
Contributor guide
Assessment
This issue has not been assessed yet.