From 5ec4fbe3200f3c33f45c33669d4767ebe7c69ba5 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Tue, 25 Aug 2026 01:53:34 -0700 Subject: [PATCH] fix(relay-skills): enforce fetchable coordinate slugs Align persisted and ACP coordinate validation with the buzz notes command so lazy-fetch instructions are always executable and delimiter-safe. Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho --- crates/buzz-acp/src/relay_skills.rs | 22 ++++++++++++++++++- .../src/managed_agents/relay_skills.rs | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/buzz-acp/src/relay_skills.rs b/crates/buzz-acp/src/relay_skills.rs index 93536c0b79..0df4370b42 100644 --- a/crates/buzz-acp/src/relay_skills.rs +++ b/crates/buzz-acp/src/relay_skills.rs @@ -8,6 +8,7 @@ use crate::relay::RestClient; const SKILL_KIND: u16 = 30023; const MAX_SKILLS: usize = 64; +const MAX_SLUG_BYTES: usize = 80; const MAX_DESCRIPTION_BYTES: usize = 1024; #[derive(Debug, Clone, PartialEq, Eq)] @@ -24,7 +25,14 @@ fn parse_coordinate(raw: &str) -> Option { } let publisher = PublicKey::from_hex(parts.next()?).ok()?; let slug = parts.next()?.to_string(); - (!slug.is_empty()).then_some(Coordinate { + (!slug.is_empty() + && slug.len() <= MAX_SLUG_BYTES + && slug.chars().all(|character| { + character.is_ascii_lowercase() + || character.is_ascii_digit() + || matches!(character, '-' | '_' | '.') + })) + .then_some(Coordinate { raw: raw.to_string(), publisher, slug, @@ -154,6 +162,18 @@ mod tests { use super::*; use nostr::{EventBuilder, Keys, Tag, Timestamp}; + #[test] + fn rejects_slugs_the_lazy_fetch_command_cannot_accept() { + let publisher = Keys::generate().public_key().to_hex(); + assert!(parse_coordinate(&format!("30023:{publisher}:valid_slug-1.0")).is_some()); + assert!(parse_coordinate(&format!("30023:{publisher}:Uppercase")).is_none()); + assert!(parse_coordinate(&format!( + "30023:{publisher}:{}", + "x".repeat(MAX_SLUG_BYTES + 1) + )) + .is_none()); + } + #[test] fn exact_query_filters_do_not_admit_author_slug_cross_products() { let alpha = Keys::generate(); diff --git a/desktop/src-tauri/src/managed_agents/relay_skills.rs b/desktop/src-tauri/src/managed_agents/relay_skills.rs index 1bf20f3399..15ae5af873 100644 --- a/desktop/src-tauri/src/managed_agents/relay_skills.rs +++ b/desktop/src-tauri/src/managed_agents/relay_skills.rs @@ -17,7 +17,7 @@ use tauri::State; use crate::{app_state::AppState, native_relay_client::NativeRelayClient}; pub const RELAY_SKILL_KIND: u16 = 30023; -pub const MAX_RELAY_SKILL_SLUG_BYTES: usize = 255; +pub const MAX_RELAY_SKILL_SLUG_BYTES: usize = 80; pub const MAX_AGENT_SKILL_NAME_BYTES: usize = 64; pub const MAX_AGENT_SKILL_DESCRIPTION_BYTES: usize = 1024; pub const MAX_ASSIGNED_RELAY_SKILLS: usize = 64;