diff --git a/crates/buzz-cli/src/commands/repos.rs b/crates/buzz-cli/src/commands/repos.rs index 608d495055..9ecb057e95 100644 --- a/crates/buzz-cli/src/commands/repos.rs +++ b/crates/buzz-cli/src/commands/repos.rs @@ -214,14 +214,14 @@ async fn submit_repo_update(client: &BuzzClient, builder: EventBuilder) -> Resul Ok(()) } -/// Build the kind:30617 announcement for `repos create`, including the -/// `buzz-channel` binding when requested. +/// Build the kind:30617 announcement for `repos create` with a required +/// `buzz-channel` binding. /// /// Pure (no I/O) so the emitted tags are unit-testable. Exactly one /// validated `buzz-channel` tag is appended — the tag is the git ACL -/// (issue #3527: without it the relay 404s every clone/fetch/push), so the -/// UUID is shape-validated here and its existence/membership is the relay's -/// authority at git-access time, same posture as `repos bind`. +/// (issue #3539 / #3527: without it the relay 404s every clone/fetch/push), +/// so the UUID is shape-validated here and its existence/membership is the +/// relay's authority at git-access time, same posture as `repos bind`. #[allow(clippy::too_many_arguments)] fn build_create_announcement( repo_id: &str, @@ -230,9 +230,10 @@ fn build_create_announcement( clone_urls: &[String], web_url: Option<&str>, relays: &[String], - channel: Option<&str>, + channel: &str, ) -> Result { validate_repo_id(repo_id)?; + crate::validate::validate_uuid(channel)?; let clone_refs: Vec<&str> = clone_urls.iter().map(|s| s.as_str()).collect(); let relay_refs: Vec<&str> = relays.iter().map(|s| s.as_str()).collect(); @@ -247,10 +248,7 @@ fn build_create_announcement( ) .map_err(|e| CliError::Other(format!("build_repo_announcement failed: {e}")))?; - if let Some(channel) = channel { - crate::validate::validate_uuid(channel)?; - builder = builder.tag(Tag::parse(["buzz-channel", channel]).map_err(tag_error)?); - } + builder = builder.tag(Tag::parse(["buzz-channel", channel]).map_err(tag_error)?); Ok(builder) } @@ -263,7 +261,7 @@ pub async fn cmd_create_repo( clone_urls: &[String], web_url: Option<&str>, relays: &[String], - channel: Option<&str>, + channel: &str, ) -> Result<(), CliError> { let builder = build_create_announcement( repo_id, @@ -437,7 +435,7 @@ pub async fn dispatch(cmd: crate::ReposCmd, client: &BuzzClient) -> Result<(), C &clone_urls, web.as_deref(), &relays, - channel.as_deref(), + &channel, ) .await } @@ -770,7 +768,7 @@ mod tests { assert!(matches!(error, crate::error::CliError::Usage(_))); } - /// Issue #3527: `repos create --channel` must emit exactly one + /// Issue #3539 / #3527: `repos create --channel` must emit exactly one /// `buzz-channel` tag so the primary create command stops producing /// repos the relay 404s forever. #[test] @@ -783,7 +781,7 @@ mod tests { &["https://relay.example/git/owner/demo".to_string()], None, &[], - Some(&channel), + &channel, ) .expect("build create announcement") .sign_with_keys(&Keys::generate()) @@ -805,25 +803,9 @@ mod tests { .any(|tag| tag.as_slice() == ["name", "Demo"])); } - #[test] - fn create_without_channel_emits_no_binding_tag() { - let event = build_create_announcement("demo", None, None, &[], None, &[], None) - .expect("build create announcement") - .sign_with_keys(&Keys::generate()) - .expect("sign create announcement"); - - assert!( - !event - .tags - .iter() - .any(|tag| tag.as_slice().first().map(String::as_str) == Some("buzz-channel")), - "no --channel means no binding tag (vanilla NIP-34 stays possible)" - ); - } - #[test] fn create_rejects_malformed_channel_uuid() { - let error = build_create_announcement("demo", None, None, &[], None, &[], Some("nope")) + let error = build_create_announcement("demo", None, None, &[], None, &[], "nope") .expect_err("malformed channel id must not build an announcement"); assert!(matches!(error, crate::error::CliError::Usage(_))); } diff --git a/crates/buzz-cli/src/lib.rs b/crates/buzz-cli/src/lib.rs index 0726406d29..aa81a953a7 100644 --- a/crates/buzz-cli/src/lib.rs +++ b/crates/buzz-cli/src/lib.rs @@ -1113,6 +1113,11 @@ pub enum NotesCmd { #[derive(Subcommand)] pub enum ReposCmd { /// Announce a git repository (NIP-34) + #[command( + after_help = "The relay git read gate requires a buzz-channel binding. Always pass --channel.\n\n\ +Examples:\n \ +buzz repos create --id demo --channel --clone https:///git//demo" + )] Create { /// Repository identifier: [a-zA-Z0-9._-]{1,64} #[arg(long)] @@ -1132,11 +1137,11 @@ pub enum ReposCmd { /// Preferred Nostr relay(s) for repo discovery — can be specified multiple times #[arg(long = "nostr-relay")] relays: Vec, - /// Channel UUID to bind the repo to. The `buzz-channel` tag is the - /// git ACL: without it the relay 404s every clone/fetch/push until - /// the author runs `buzz repos bind` (issue #3527). + /// Channel UUID that binds this repo for the relay git read gate + /// (`buzz-channel` tag). Required — without it every clone/fetch/push + /// returns an opaque 404 until `buzz repos bind` (issue #3539 / #3527). #[arg(long)] - channel: Option, + channel: String, }, /// Get a repository announcement Get { diff --git a/desktop/src-tauri/src/managed_agents/nest_skill.md b/desktop/src-tauri/src/managed_agents/nest_skill.md index 79a5ea301d..a5d29cf916 100644 --- a/desktop/src-tauri/src/managed_agents/nest_skill.md +++ b/desktop/src-tauri/src/managed_agents/nest_skill.md @@ -43,7 +43,7 @@ Run `buzz agents draft-update --help` for optional runtime, provider, model, ren ## Git Repositories -Buzz hosts real git repos, and **you can own one yourself** — no human key needed. `repos create` signs the announcement with *your* key, so the repo is owned by whoever runs it; the owner segment in the clone URL is your own pubkey (hex, not a username). Git auth is automatic: the harness configures the `git-credential-nostr` helper, so plain `git clone`/`push`/`pull` against `/git//` just work over NIP-98 — never put a private key on a git command line. Announce with `repos create --id --clone /git//`, then `git remote add origin ` and `git push -u origin main` (the relay seeds an empty repo on announce, so it's immediately pushable). Requires git 2.46+ for the credential protocol. +Buzz hosts real git repos, and **you can own one yourself** — no human key needed. `repos create` signs the announcement with *your* key, so the repo is owned by whoever runs it; the owner segment in the clone URL is your own pubkey (hex, not a username). Git auth is automatic: the harness configures the `git-credential-nostr` helper, so plain `git clone`/`push`/`pull` against `/git//` just work over NIP-98 — never put a private key on a git command line. Announce with `repos create --id --channel --clone /git//`, then `git remote add origin ` and `git push -u origin main` (the relay seeds an empty repo on announce, so it's immediately pushable). Requires git 2.46+ for the credential protocol. Manage your repository's enforced branch and tag rules with `repos protect list|set|remove`. Ref patterns must use full Git names such as `refs/heads/main` or `refs/tags/*`; supported rules are `--push owner|admin|member`, `--no-force-push`, `--no-delete`, and `--require-patch`. `protect set` replaces the complete rule for that exact pattern, so omitted constraints are removed. Protection updates preserve every unrelated metadata tag and return exit code 5 when a newer NIP-33 head wins a concurrent write.