diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 281fee0a1..c13d806a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -520,6 +520,35 @@ jobs: path: /tmp/buzz-relay.log if-no-files-found: ignore + relay-e2e: + name: Relay E2E + runs-on: ubuntu-latest + timeout-minutes: 20 + needs: [changes] + if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + save-if: ${{ github.event_name != 'pull_request' }} + - name: Start relay + run: ./scripts/start-relay-for-tests.sh + - name: Relay E2E tests + run: cargo test -p buzz-test-client --test '*' -- --ignored --nocapture + env: + RELAY_URL: ws://localhost:3000 + GIT_CREDENTIAL_NOSTR_BIN: ${{ github.workspace }}/target/ci/git-credential-nostr + - name: Upload relay logs + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: relay-e2e-artifacts + path: /tmp/buzz-relay.log + if-no-files-found: ignore + web: name: Web runs-on: ubuntu-latest diff --git a/crates/buzz-core/src/kind.rs b/crates/buzz-core/src/kind.rs index f3f1fca03..27a504e7b 100644 --- a/crates/buzz-core/src/kind.rs +++ b/crates/buzz-core/src/kind.rs @@ -110,6 +110,15 @@ pub const KIND_EVENT_REMINDER: u32 = 30300; /// a compile-time bitset or sorted array with binary search for hot-path use. pub const AUTHOR_ONLY_KINDS: &[u32] = &[KIND_EVENT_REMINDER]; +/// NIP-AP: Agent Persona (parameterized replaceable, owner-authored). +/// +/// Persona definition event published by the workspace owner. Addressed by +/// `(pubkey, kind, d_tag)` where `d_tag` is the plaintext persona slug. +/// Content is a JSON body containing persona fields (system_prompt, +/// display_name, avatar_url, runtime, model, provider, name_pool, env_vars). +/// Designed for discoverability and sharing — d-tag is not blinded. +pub const KIND_PERSONA: u32 = 30175; + // NIP-29 group admin events /// NIP-29: Add a user to a group. pub const KIND_NIP29_PUT_USER: u32 = 9000; @@ -391,6 +400,7 @@ pub const ALL_KINDS: &[u32] = &[ KIND_AGENT_PROFILE, KIND_AGENT_ENGRAM, KIND_EVENT_REMINDER, + KIND_PERSONA, KIND_NIP29_PUT_USER, KIND_NIP29_REMOVE_USER, KIND_NIP29_EDIT_METADATA, @@ -573,6 +583,7 @@ pub fn event_kind_i32(event: &nostr::Event) -> i32 { // Compile-time: new kinds are in the expected ranges. const _: () = assert!(is_replaceable(KIND_AGENT_PROFILE)); // 10100 ∈ 10000–19999 +const _: () = assert!(is_parameterized_replaceable(KIND_PERSONA)); // 30175 ∈ 30000–39999 const _: () = assert!(is_parameterized_replaceable(KIND_WORKFLOW_DEF)); // 30620 ∈ 30000–39999 const _: () = assert!(is_parameterized_replaceable(KIND_EVENT_REMINDER)); // 30300 ∈ 30000–39999 const _: () = assert!(is_parameterized_replaceable(KIND_MESH_LLM_RELAY_STATUS)); // 30621 ∈ 30000–39999 diff --git a/crates/buzz-relay/src/handlers/ingest.rs b/crates/buzz-relay/src/handlers/ingest.rs index 3b3123e64..64d1919ec 100644 --- a/crates/buzz-relay/src/handlers/ingest.rs +++ b/crates/buzz-relay/src/handlers/ingest.rs @@ -26,12 +26,13 @@ use buzz_core::kind::{ KIND_NIP29_CREATE_GROUP, KIND_NIP29_DELETE_EVENT, KIND_NIP29_DELETE_GROUP, KIND_NIP29_EDIT_METADATA, KIND_NIP29_JOIN_REQUEST, KIND_NIP29_LEAVE_REQUEST, KIND_NIP29_PUT_USER, KIND_NIP29_REMOVE_USER, KIND_NIP43_LEAVE_REQUEST, - KIND_NIP65_RELAY_LIST_METADATA, KIND_PIN_LIST, KIND_PRESENCE_UPDATE, KIND_PROFILE, - KIND_REACTION, KIND_READ_STATE, KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_BOOKMARKED, - KIND_STREAM_MESSAGE_DIFF, KIND_STREAM_MESSAGE_EDIT, KIND_STREAM_MESSAGE_PINNED, - KIND_STREAM_MESSAGE_SCHEDULED, KIND_STREAM_MESSAGE_V2, KIND_STREAM_REMINDER, KIND_TEXT_NOTE, - KIND_USER_STATUS, KIND_WORKFLOW_DEF, KIND_WORKFLOW_TRIGGER, RELAY_ADMIN_ADD_MEMBER, - RELAY_ADMIN_CHANGE_ROLE, RELAY_ADMIN_REMOVE_MEMBER, + KIND_NIP65_RELAY_LIST_METADATA, KIND_PERSONA, KIND_PIN_LIST, KIND_PRESENCE_UPDATE, + KIND_PROFILE, KIND_REACTION, KIND_READ_STATE, KIND_STREAM_MESSAGE, + KIND_STREAM_MESSAGE_BOOKMARKED, KIND_STREAM_MESSAGE_DIFF, KIND_STREAM_MESSAGE_EDIT, + KIND_STREAM_MESSAGE_PINNED, KIND_STREAM_MESSAGE_SCHEDULED, KIND_STREAM_MESSAGE_V2, + KIND_STREAM_REMINDER, KIND_TEXT_NOTE, KIND_USER_STATUS, KIND_WORKFLOW_DEF, + KIND_WORKFLOW_TRIGGER, RELAY_ADMIN_ADD_MEMBER, RELAY_ADMIN_CHANGE_ROLE, + RELAY_ADMIN_REMOVE_MEMBER, }; use buzz_core::verification::verify_event; use nostr::Event; @@ -154,7 +155,7 @@ fn required_scope_for_kind(kind: u32, event: &Event) -> Result Ok(Scope::UsersWrite), KIND_TEXT_NOTE | KIND_LONG_FORM => Ok(Scope::MessagesWrite), KIND_CONTACT_LIST | KIND_READ_STATE | KIND_USER_STATUS | KIND_AGENT_ENGRAM - | KIND_EVENT_REMINDER => Ok(Scope::UsersWrite), + | KIND_EVENT_REMINDER | KIND_PERSONA => Ok(Scope::UsersWrite), // NIP-51 standard lists and NIP-65 relay list — user-owned global state, // same ownership shape as kind:3 (contacts) and kind:0 (profile). KIND_MUTE_LIST @@ -344,6 +345,8 @@ pub(crate) fn is_global_only_kind(kind: u32) -> bool { | KIND_EVENT_REMINDER // Agent profile (10100): user-owned replaceable, keyed by pubkey. | KIND_AGENT_PROFILE + // NIP-AP: persona definitions (30175): owner-authored, keyed by (pubkey, kind, d_tag). + | KIND_PERSONA // NIP-34: git events use `a` tags (repo reference), not `h` tags (channel scope). // Parameterized replaceable kinds are keyed by (pubkey, kind, d_tag). | KIND_GIT_REPO_ANNOUNCEMENT @@ -885,6 +888,56 @@ fn validate_engram_envelope(event: &Event) -> Result<(), String> { Ok(()) } +/// Validate the envelope of a kind:30175 persona event. +/// +/// Enforces: +/// * exactly one `d` tag with a non-empty value matching the slug grammar +/// `^[a-z0-9][a-z0-9_-]{0,63}$`. +/// +/// Without this, an empty d-tag collapses every persona into the +/// `(pubkey, 30175, "")` slot — last-write-wins data loss. +fn validate_persona_envelope(event: &Event) -> Result<(), String> { + let mut d_tags: Vec<&str> = Vec::new(); + for tag in event.tags.iter() { + let parts = tag.as_slice(); + if parts.len() >= 2 && parts[0].as_str() == "d" { + d_tags.push(&parts[1]); + } + } + if d_tags.len() != 1 { + return Err(format!( + "persona event must have exactly one `d` tag (got {})", + d_tags.len() + )); + } + let d = d_tags[0]; + if d.is_empty() { + return Err("persona event `d` tag must not be empty".to_string()); + } + // Slug grammar: ^[a-z0-9][a-z0-9_-]{0,63}$ + if d.len() > 64 { + return Err(format!( + "persona event `d` tag too long ({} chars, max 64)", + d.len() + )); + } + let bytes = d.as_bytes(); + if !bytes[0].is_ascii_lowercase() && !bytes[0].is_ascii_digit() { + return Err( + "persona event `d` tag must start with a lowercase letter or digit".to_string(), + ); + } + if !bytes[1..] + .iter() + .all(|&b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'-') + { + return Err( + "persona event `d` tag must match [a-z0-9_-] after the first character".to_string(), + ); + } + Ok(()) +} + /// Validate that `content` is a syntactically plausible NIP-44 v2 ciphertext. /// /// Checks: @@ -1483,6 +1536,12 @@ pub async fn ingest_event( .map_err(|e| IngestError::Rejected(format!("invalid: {e}")))?; } + // ── 15c. Persona envelope (kind:30175) ────────────────────────────── + if kind_u32 == KIND_PERSONA { + validate_persona_envelope(&event) + .map_err(|e| IngestError::Rejected(format!("invalid: {e}")))?; + } + // Track pre-created channel UUID for compensation on insert failure. let mut pre_created_channel: Option = None; @@ -1867,7 +1926,8 @@ mod tests { use super::*; use buzz_core::kind::{ KIND_CANVAS, KIND_FORUM_COMMENT, KIND_FORUM_POST, KIND_FORUM_VOTE, KIND_LONG_FORM, - KIND_PRESENCE_UPDATE, KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_DIFF, KIND_USER_STATUS, + KIND_PERSONA, KIND_PRESENCE_UPDATE, KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_DIFF, + KIND_USER_STATUS, }; #[test] @@ -2036,6 +2096,7 @@ mod tests { KIND_EMOJI_LIST, KIND_AGENT_ENGRAM, KIND_AGENT_PROFILE, + KIND_PERSONA, ]; for kind in migrated { assert!( @@ -2094,6 +2155,21 @@ mod tests { } } + #[test] + fn persona_is_in_scope_allowlist() { + let dummy = make_dummy_event(); + assert_eq!( + required_scope_for_kind(KIND_PERSONA, &dummy).unwrap(), + Scope::UsersWrite, + ); + } + + #[test] + fn persona_is_global_only() { + assert!(is_global_only_kind(KIND_PERSONA)); + assert!(!requires_h_channel_scope(KIND_PERSONA)); + } + #[test] fn unknown_kind_rejected() { let dummy = make_dummy_event(); @@ -2582,4 +2658,97 @@ mod tests { ); assert_eq!(validate_event_reminder(&ev), Err("duplicate d tag")); } + + // ── NIP-AP persona envelope validation ─────────────────────────────── + + fn make_persona(tags: &[&[&str]]) -> Event { + make_event_with_tags( + KIND_PERSONA, + r#"{"display_name":"x","system_prompt":"y"}"#, + tags, + ) + } + + #[test] + fn persona_envelope_accepts_valid_slug() { + let ev = make_persona(&[&["d", "my-persona-1"]]); + assert!(validate_persona_envelope(&ev).is_ok()); + } + + #[test] + fn persona_envelope_accepts_single_char() { + let ev = make_persona(&[&["d", "a"]]); + assert!(validate_persona_envelope(&ev).is_ok()); + } + + #[test] + fn persona_envelope_accepts_max_length() { + let slug = "a".repeat(64); + let ev = make_persona(&[&["d", &slug]]); + assert!(validate_persona_envelope(&ev).is_ok()); + } + + #[test] + fn persona_envelope_rejects_missing_d_tag() { + let ev = make_persona(&[]); + let err = validate_persona_envelope(&ev).unwrap_err(); + assert!(err.contains("`d` tag"), "got: {err}"); + } + + #[test] + fn persona_envelope_rejects_empty_d_tag() { + let ev = make_persona(&[&["d", ""]]); + let err = validate_persona_envelope(&ev).unwrap_err(); + assert!(err.contains("must not be empty"), "got: {err}"); + } + + #[test] + fn persona_envelope_rejects_duplicate_d_tags() { + let ev = make_persona(&[&["d", "slug-a"], &["d", "slug-b"]]); + let err = validate_persona_envelope(&ev).unwrap_err(); + assert!(err.contains("`d` tag"), "got: {err}"); + } + + #[test] + fn persona_envelope_rejects_too_long() { + let slug = "a".repeat(65); + let ev = make_persona(&[&["d", &slug]]); + let err = validate_persona_envelope(&ev).unwrap_err(); + assert!(err.contains("too long"), "got: {err}"); + } + + #[test] + fn persona_envelope_rejects_uppercase() { + let ev = make_persona(&[&["d", "My-Persona"]]); + let err = validate_persona_envelope(&ev).unwrap_err(); + assert!(err.contains("`d` tag"), "got: {err}"); + } + + #[test] + fn persona_envelope_rejects_leading_underscore() { + let ev = make_persona(&[&["d", "_invalid"]]); + let err = validate_persona_envelope(&ev).unwrap_err(); + assert!(err.contains("start with"), "got: {err}"); + } + + #[test] + fn persona_envelope_rejects_leading_hyphen() { + let ev = make_persona(&[&["d", "-invalid"]]); + let err = validate_persona_envelope(&ev).unwrap_err(); + assert!(err.contains("start with"), "got: {err}"); + } + + #[test] + fn persona_envelope_rejects_spaces() { + let ev = make_persona(&[&["d", "has space"]]); + let err = validate_persona_envelope(&ev).unwrap_err(); + assert!(err.contains("`d` tag"), "got: {err}"); + } + + #[test] + fn persona_envelope_rejects_dots() { + let ev = make_persona(&[&["d", "has.dot"]]); + let err = validate_persona_envelope(&ev).unwrap_err(); + assert!(err.contains("`d` tag"), "got: {err}"); + } } diff --git a/crates/buzz-test-client/tests/e2e_persona.rs b/crates/buzz-test-client/tests/e2e_persona.rs new file mode 100644 index 000000000..49c3480ed --- /dev/null +++ b/crates/buzz-test-client/tests/e2e_persona.rs @@ -0,0 +1,451 @@ +//! End-to-end tests for kind:30175 persona events (NIP-AP). +//! +//! These tests verify the relay correctly handles persona events: +//! - Accepts valid persona events with proper d-tag slugs +//! - Enforces NIP-33 replacement semantics (same d-tag, newer timestamp wins) +//! - Rejects invalid d-tag values (empty, too long, invalid characters) +//! +//! # Running +//! +//! Start the relay, then run: +//! +//! ```text +//! RELAY_URL=ws://localhost:3000 cargo test --test e2e_persona -- --ignored +//! ``` + +use std::time::Duration; + +use nostr::{Alphabet, EventBuilder, Filter, Keys, Kind, SingleLetterTag, Tag, Timestamp}; +use buzz_test_client::BuzzTestClient; + +const PERSONA_KIND: u16 = 30175; + +fn relay_url() -> String { + std::env::var("RELAY_URL").unwrap_or_else(|_| "ws://localhost:3000".to_string()) +} + +fn sub_id(name: &str) -> String { + format!("e2e-persona-{name}-{}", uuid::Uuid::new_v4()) +} + +/// Build a minimal persona event with the given d-tag and content. +fn persona_event(keys: &Keys, d_tag: &str, content: &str) -> nostr::Event { + EventBuilder::new(Kind::Custom(PERSONA_KIND), content) + .tags(vec![Tag::parse(["d", d_tag]).unwrap()]) + .sign_with_keys(keys) + .unwrap() +} + +/// Build a persona event with an explicit created_at timestamp. +fn persona_event_at(keys: &Keys, d_tag: &str, content: &str, created_at: u64) -> nostr::Event { + EventBuilder::new(Kind::Custom(PERSONA_KIND), content) + .tags(vec![Tag::parse(["d", d_tag]).unwrap()]) + .custom_created_at(Timestamp::from(created_at)) + .sign_with_keys(keys) + .unwrap() +} + +// ── Publish and query back ─────────────────────────────────────────────────── + +#[tokio::test] +#[ignore] +async fn test_persona_publish_and_query() { + let url = relay_url(); + let keys = Keys::generate(); + let d_tag = format!("test-persona-{}", &uuid::Uuid::new_v4().to_string()[..8]); + + let content = serde_json::json!({ + "name": &d_tag, + "display_name": "Test Persona", + "description": "A test persona for E2E validation" + }) + .to_string(); + + let mut client = BuzzTestClient::connect(&url, &keys) + .await + .expect("connect"); + + // Publish persona event + let event = persona_event(&keys, &d_tag, &content); + let ok = client + .send_event(event.clone()) + .await + .expect("send persona"); + assert!(ok.accepted, "relay rejected persona event: {}", ok.message); + + // Query it back using NIP-33 filter (kind + author + d-tag) + let sid = sub_id("query"); + let filter = Filter::new() + .kind(Kind::Custom(PERSONA_KIND)) + .author(keys.public_key()) + .custom_tags(SingleLetterTag::lowercase(Alphabet::D), [d_tag.as_str()]); + + client + .subscribe(&sid, vec![filter]) + .await + .expect("subscribe"); + + let events = client + .collect_until_eose(&sid, Duration::from_secs(5)) + .await + .expect("collect events"); + + assert_eq!(events.len(), 1, "expected exactly one persona event"); + let ev = &events[0]; + assert_eq!(ev.content, content); + assert_eq!(ev.pubkey, keys.public_key()); + assert_eq!(ev.kind, Kind::Custom(PERSONA_KIND)); + + client.disconnect().await.expect("disconnect"); +} + +// ── NIP-33 replacement semantics ───────────────────────────────────────────── + +#[tokio::test] +#[ignore] +async fn test_persona_nip33_replacement_newer_wins() { + let url = relay_url(); + let keys = Keys::generate(); + let d_tag = format!("replace-{}", &uuid::Uuid::new_v4().to_string()[..8]); + + let mut client = BuzzTestClient::connect(&url, &keys) + .await + .expect("connect"); + + // Publish older version + let old_content = r#"{"name":"old","display_name":"Old","description":"Old version"}"#; + let old_event = persona_event_at(&keys, &d_tag, old_content, 1_700_000_000); + let ok = client.send_event(old_event).await.expect("send old"); + assert!(ok.accepted, "relay rejected old event: {}", ok.message); + + // Publish newer version with same d-tag + let new_content = r#"{"name":"new","display_name":"New","description":"New version"}"#; + let new_event = persona_event_at(&keys, &d_tag, new_content, 1_700_000_100); + let ok = client.send_event(new_event).await.expect("send new"); + assert!(ok.accepted, "relay rejected new event: {}", ok.message); + + // Query — should return only the newer event + let sid = sub_id("replace"); + let filter = Filter::new() + .kind(Kind::Custom(PERSONA_KIND)) + .author(keys.public_key()) + .custom_tags(SingleLetterTag::lowercase(Alphabet::D), [d_tag.as_str()]); + + client + .subscribe(&sid, vec![filter]) + .await + .expect("subscribe"); + + let events = client + .collect_until_eose(&sid, Duration::from_secs(5)) + .await + .expect("collect"); + + assert_eq!(events.len(), 1, "NIP-33: only newest event should remain"); + let ev = &events[0]; + assert_eq!(ev.content, new_content, "should be the newer version"); + + client.disconnect().await.expect("disconnect"); +} + +#[tokio::test] +#[ignore] +async fn test_persona_nip33_older_does_not_replace_newer() { + let url = relay_url(); + let keys = Keys::generate(); + let d_tag = format!("no-replace-{}", &uuid::Uuid::new_v4().to_string()[..8]); + + let mut client = BuzzTestClient::connect(&url, &keys) + .await + .expect("connect"); + + // Publish newer version first + let new_content = r#"{"name":"new","display_name":"New","description":"Newer"}"#; + let new_event = persona_event_at(&keys, &d_tag, new_content, 1_700_000_200); + let ok = client.send_event(new_event).await.expect("send new"); + assert!(ok.accepted, "relay rejected new event: {}", ok.message); + + // Publish older version — relay should accept but not replace + let old_content = r#"{"name":"old","display_name":"Old","description":"Older"}"#; + let old_event = persona_event_at(&keys, &d_tag, old_content, 1_700_000_100); + let _ok = client.send_event(old_event).await.expect("send old"); + // Note: relay may accept or reject the older event depending on implementation. + // The key assertion is that querying returns the newer one. + + // Query — should still return the newer event + let sid = sub_id("no-replace"); + let filter = Filter::new() + .kind(Kind::Custom(PERSONA_KIND)) + .author(keys.public_key()) + .custom_tags(SingleLetterTag::lowercase(Alphabet::D), [d_tag.as_str()]); + + client + .subscribe(&sid, vec![filter]) + .await + .expect("subscribe"); + + let events = client + .collect_until_eose(&sid, Duration::from_secs(5)) + .await + .expect("collect"); + + assert_eq!(events.len(), 1, "should have exactly one event"); + let ev = &events[0]; + assert_eq!(ev.content, new_content, "newer event should persist"); + + client.disconnect().await.expect("disconnect"); +} + +// ── D-tag validation ───────────────────────────────────────────────────────── + +#[tokio::test] +#[ignore] +async fn test_persona_rejects_empty_d_tag() { + let url = relay_url(); + let keys = Keys::generate(); + + let mut client = BuzzTestClient::connect(&url, &keys) + .await + .expect("connect"); + + let event = EventBuilder::new( + Kind::Custom(PERSONA_KIND), + r#"{"name":"x","display_name":"X","description":"X"}"#, + ) + .tags(vec![Tag::parse(["d", ""]).unwrap()]) + .sign_with_keys(&keys) + .unwrap(); + + let ok = client.send_event(event).await.expect("send"); + assert!(!ok.accepted, "relay should reject persona with empty d-tag"); + assert!( + ok.message.contains("empty") || ok.message.contains("d") || ok.message.contains("tag"), + "rejection message should mention d-tag issue, got: {}", + ok.message + ); + + client.disconnect().await.expect("disconnect"); +} + +#[tokio::test] +#[ignore] +async fn test_persona_rejects_missing_d_tag() { + let url = relay_url(); + let keys = Keys::generate(); + + let mut client = BuzzTestClient::connect(&url, &keys) + .await + .expect("connect"); + + // No d-tag at all + let event = EventBuilder::new( + Kind::Custom(PERSONA_KIND), + r#"{"name":"x","display_name":"X","description":"X"}"#, + ) + .sign_with_keys(&keys) + .unwrap(); + + let ok = client.send_event(event).await.expect("send"); + assert!(!ok.accepted, "relay should reject persona without d-tag"); + + client.disconnect().await.expect("disconnect"); +} + +#[tokio::test] +#[ignore] +async fn test_persona_rejects_d_tag_too_long() { + let url = relay_url(); + let keys = Keys::generate(); + + let mut client = BuzzTestClient::connect(&url, &keys) + .await + .expect("connect"); + + // 65 characters — exceeds the 64-char limit + let long_slug = "a".repeat(65); + let event = persona_event( + &keys, + &long_slug, + r#"{"name":"x","display_name":"X","description":"X"}"#, + ); + let ok = client.send_event(event).await.expect("send"); + assert!( + !ok.accepted, + "relay should reject persona with d-tag > 64 chars" + ); + assert!( + ok.message.contains("long") || ok.message.contains("64"), + "rejection should mention length, got: {}", + ok.message + ); + + client.disconnect().await.expect("disconnect"); +} + +#[tokio::test] +#[ignore] +async fn test_persona_rejects_d_tag_uppercase() { + let url = relay_url(); + let keys = Keys::generate(); + + let mut client = BuzzTestClient::connect(&url, &keys) + .await + .expect("connect"); + + let event = persona_event( + &keys, + "My-Persona", + r#"{"name":"x","display_name":"X","description":"X"}"#, + ); + let ok = client.send_event(event).await.expect("send"); + assert!( + !ok.accepted, + "relay should reject persona with uppercase d-tag" + ); + + client.disconnect().await.expect("disconnect"); +} + +#[tokio::test] +#[ignore] +async fn test_persona_rejects_d_tag_special_chars() { + let url = relay_url(); + let keys = Keys::generate(); + + let mut client = BuzzTestClient::connect(&url, &keys) + .await + .expect("connect"); + + let event = persona_event( + &keys, + "my.persona!", + r#"{"name":"x","display_name":"X","description":"X"}"#, + ); + let ok = client.send_event(event).await.expect("send"); + assert!( + !ok.accepted, + "relay should reject persona with special chars in d-tag" + ); + + client.disconnect().await.expect("disconnect"); +} + +#[tokio::test] +#[ignore] +async fn test_persona_rejects_d_tag_starting_with_underscore() { + let url = relay_url(); + let keys = Keys::generate(); + + let mut client = BuzzTestClient::connect(&url, &keys) + .await + .expect("connect"); + + // Slug must start with [a-z0-9], not underscore + let event = persona_event( + &keys, + "_invalid", + r#"{"name":"x","display_name":"X","description":"X"}"#, + ); + let ok = client.send_event(event).await.expect("send"); + assert!( + !ok.accepted, + "relay should reject persona with d-tag starting with underscore" + ); + + client.disconnect().await.expect("disconnect"); +} + +#[tokio::test] +#[ignore] +async fn test_persona_accepts_valid_slugs() { + let url = relay_url(); + let keys = Keys::generate(); + + let mut client = BuzzTestClient::connect(&url, &keys) + .await + .expect("connect"); + + // Various valid slug patterns + let valid_slugs = [ + "a", + "my-persona", + "persona_v2", + "0-starts-with-digit", + "a-b-c-d-e", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // exactly 64 chars + ]; + + for slug in valid_slugs { + let content = format!( + r#"{{"name":"{}","display_name":"Test","description":"Valid slug test"}}"#, + slug + ); + let event = persona_event(&keys, slug, &content); + let ok = client.send_event(event).await.expect("send"); + assert!( + ok.accepted, + "relay should accept valid slug '{}', got rejection: {}", + slug, ok.message + ); + } + + client.disconnect().await.expect("disconnect"); +} + +// ── Multiple personas per author ───────────────────────────────────────────── + +#[tokio::test] +#[ignore] +async fn test_persona_multiple_per_author() { + let url = relay_url(); + let keys = Keys::generate(); + + let mut client = BuzzTestClient::connect(&url, &keys) + .await + .expect("connect"); + + // Publish two different personas (different d-tags) + let slug_a = format!("persona-a-{}", &uuid::Uuid::new_v4().to_string()[..8]); + let slug_b = format!("persona-b-{}", &uuid::Uuid::new_v4().to_string()[..8]); + + let event_a = persona_event( + &keys, + &slug_a, + r#"{"name":"a","display_name":"Persona A","description":"First"}"#, + ); + let event_b = persona_event( + &keys, + &slug_b, + r#"{"name":"b","display_name":"Persona B","description":"Second"}"#, + ); + + let ok_a = client.send_event(event_a).await.expect("send A"); + assert!(ok_a.accepted, "persona A rejected: {}", ok_a.message); + + let ok_b = client.send_event(event_b).await.expect("send B"); + assert!(ok_b.accepted, "persona B rejected: {}", ok_b.message); + + // Query all personas by this author + let sid = sub_id("multi"); + let filter = Filter::new() + .kind(Kind::Custom(PERSONA_KIND)) + .author(keys.public_key()); + + client + .subscribe(&sid, vec![filter]) + .await + .expect("subscribe"); + + let events = client + .collect_until_eose(&sid, Duration::from_secs(5)) + .await + .expect("collect"); + + assert!( + events.len() >= 2, + "expected at least 2 persona events, got {}", + events.len() + ); + + client.disconnect().await.expect("disconnect"); +} diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 009b77ba3..304ea4ad9 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -71,9 +71,9 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ "alloc-no-stdlib", ] @@ -91,7 +91,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" dependencies = [ "alsa-sys", - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "libc", ] @@ -315,7 +315,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -350,7 +350,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -672,9 +672,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -713,9 +713,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", "zeroize", @@ -754,9 +754,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.9.1" +version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" +checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561" dependencies = [ "bon-macros", "rustversion", @@ -764,9 +764,9 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.9.1" +version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" +checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" dependencies = [ "darling 0.23.0", "ident_case", @@ -774,14 +774,14 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "brotli" -version = "8.0.2" +version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -790,9 +790,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -986,7 +986,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cairo-sys-rs", "glib", "libc", @@ -1058,9 +1058,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.62" +version = "1.2.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" dependencies = [ "find-msvc-tools", "jobserver", @@ -1150,9 +1150,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -1214,7 +1214,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1234,9 +1234,9 @@ dependencies = [ [[package]] name = "cmov" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "cobs" @@ -1401,7 +1401,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.10.1", "core-graphics-types", "foreign-types 0.5.0", @@ -1414,7 +1414,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.10.1", "libc", ] @@ -1425,7 +1425,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "libc", "objc2-audio-toolbox", "objc2-core-audio", @@ -1469,7 +1469,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.61.3", + "windows 0.62.2", ] [[package]] @@ -1541,7 +1541,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "crossterm_winapi", "mio", "parking_lot", @@ -1635,7 +1635,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1755,7 +1755,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1789,7 +1789,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1802,7 +1802,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1813,7 +1813,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1824,7 +1824,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1856,7 +1856,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1912,7 +1912,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -1924,7 +1923,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1945,7 +1944,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1955,7 +1954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1977,7 +1976,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", "unicode-xid", ] @@ -2004,7 +2003,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid", "crypto-common 0.2.2", "ctutils", @@ -2038,7 +2037,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2", "libc", "objc2", @@ -2052,7 +2051,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2075,7 +2074,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2256,7 +2255,7 @@ checksum = "3ed8956bd5c1f0415200516e78ff07ec9e16415ade83c056c230d7b7ea0d55b7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2277,7 +2276,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2472,7 +2471,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2584,7 +2583,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2726,9 +2725,9 @@ dependencies = [ [[package]] name = "generator" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" dependencies = [ "cc", "cfg-if 1.0.4", @@ -2862,7 +2861,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2871,7 +2870,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "futures-channel", "futures-core", "futures-executor", @@ -2899,7 +2898,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2920,9 +2919,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "global-hotkey" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7" +checksum = "8c386b0a4a70cb2d39fffd74480f985b6f0bfbcb934b6a6b6b7e630e448f242e" dependencies = [ "crossbeam-channel", "keyboard-types", @@ -3021,14 +3020,14 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -3287,9 +3286,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -3347,9 +3346,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -3448,7 +3447,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -3603,9 +3602,9 @@ dependencies = [ [[package]] name = "igd-next" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac9a3c8278f43b4cd8463380f4a25653ac843e5b177e1d3eaf849cc9ba10d4d" +checksum = "de7238d487a9aff61f81b5ab41c0a841532a115a398b5fa92a2fadd0885e2581" dependencies = [ "attohttpc", "bytes", @@ -3822,14 +3821,14 @@ dependencies = [ [[package]] name = "iroh-metrics-derive" -version = "1.0.0-rc.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c8e0c97f1dc787107f388433c349397c565572fe6406d600ff7bb7b7fe3b30" +checksum = "1ae5f0c4405d1fbc9fb16ff422ca40620e93dc36c30ecaba0c2aee3992b7bd48" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3984,7 +3983,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4012,7 +4011,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4027,13 +4026,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" dependencies = [ "cfg-if 1.0.4", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -4075,7 +4073,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "serde", "unicode-segmentation", ] @@ -4247,9 +4245,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "loom" @@ -4296,9 +4294,9 @@ dependencies = [ [[package]] name = "lzma-rust2" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e9ceaec84b54518262de7cf06b8b43e83c808349960f1610b21b0bfc9640f20" +checksum = "ce716bf1a316f47a280fc76295f6495b5bea4752bca01c3b3885e101b1c23c02" dependencies = [ "sha2 0.11.0", ] @@ -4311,14 +4309,16 @@ checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" [[package]] name = "mac-notification-sys" -version = "0.6.12" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29a16783dd1a47849b8c8133c9cd3eb2112cfbc6901670af3dba47c8bbfb07d3" +checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" dependencies = [ "cc", + "log", "objc2", "objc2-foundation", "time", + "uuid", ] [[package]] @@ -4373,9 +4373,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memoffset" @@ -4831,9 +4831,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -4972,13 +4972,13 @@ dependencies = [ [[package]] name = "n0-error-macros" -version = "1.0.0-rc.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "565305a21e6b3bf26640ad98f05a0fda12d3ab4315394566b52a7bddb8b34828" +checksum = "e2acd8b070213b0299282f884b4beba4e7b52d624fdcd504a3ad3665390c11e1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5036,7 +5036,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "jni-sys 0.3.1", "log", "ndk-sys", @@ -5118,7 +5118,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df9854ea6ad14e3f4698a7f03b65bce0833dd2d81d594a0e4a984170537146b6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "libc", "log", "netlink-packet-core", @@ -5130,7 +5130,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be8919612f6028ab4eacbbfe1234a9a43e3722c6e0915e7ff519066991905092" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "libc", "log", "netlink-packet-core", @@ -5211,7 +5211,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "cfg_aliases", "libc", @@ -5224,7 +5224,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "cfg_aliases", "libc", @@ -5236,7 +5236,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "cfg_aliases", "libc", @@ -5392,7 +5392,7 @@ dependencies = [ "mac-notification-sys", "serde", "tauri-winrt-notification", - "zbus 5.15.0", + "zbus 5.16.0", ] [[package]] @@ -5460,7 +5460,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5520,10 +5520,10 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 2.0.2", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5551,7 +5551,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2", "objc2", "objc2-core-foundation", @@ -5564,7 +5564,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "libc", "objc2", "objc2-core-audio", @@ -5589,7 +5589,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2", "objc2-foundation", ] @@ -5613,7 +5613,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2", ] @@ -5633,7 +5633,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2", "dispatch2", "libc", @@ -5646,7 +5646,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "dispatch2", "objc2", "objc2-core-foundation", @@ -5679,7 +5679,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -5691,7 +5691,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -5720,7 +5720,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2", "libc", "objc2", @@ -5743,7 +5743,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", ] @@ -5754,7 +5754,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2", "objc2-app-kit", "objc2-foundation", @@ -5766,7 +5766,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -5778,7 +5778,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", ] @@ -5799,7 +5799,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "dispatch2", "libc", "objc2", @@ -5813,7 +5813,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2", "objc2", "objc2-cloud-kit", @@ -5844,7 +5844,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2", "objc2", "objc2-app-kit", @@ -5911,11 +5911,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.80" +version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "foreign-types 0.3.2", "libc", @@ -5931,7 +5931,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5942,18 +5942,18 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.6.0+3.6.2" +version = "300.6.1+3.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.116" +version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ "cc", "libc", @@ -6285,7 +6285,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6314,7 +6314,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6382,7 +6382,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "crc32fast", "fdeflate", "flate2", @@ -6485,7 +6485,7 @@ checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6542,7 +6542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6636,7 +6636,7 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "num-traits", "rand 0.9.4", "rand_chacha 0.9.0", @@ -6647,9 +6647,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -6657,11 +6657,11 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "itertools", "log", "multimap", @@ -6670,28 +6670,28 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.117", + "syn 2.0.118", "tempfile", ] [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "prost-types" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ "prost", ] @@ -6989,7 +6989,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -7020,14 +7020,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -7054,9 +7054,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -7257,7 +7257,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7314,7 +7314,7 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -7367,7 +7367,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -7380,7 +7380,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.12.1", @@ -7405,9 +7405,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -7559,7 +7559,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7571,7 +7571,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7643,7 +7643,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -7656,7 +7656,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -7689,7 +7689,7 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cssparser", "derive_more", "log", @@ -7767,7 +7767,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7778,7 +7778,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7813,7 +7813,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7848,9 +7848,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64 0.22.1", "bs58", @@ -7868,14 +7868,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7920,7 +7920,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -8014,9 +8014,9 @@ dependencies = [ [[package]] name = "sherpa-onnx" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f70620e4fa58e4cb1acf4e0a9c2cbc7496ea8284f80e55be23d443b92e563e49" +checksum = "98baa63be165cc1bccd418c210269c68bcdd0c279195ecce0140645bc6e520f4" dependencies = [ "serde", "serde_json", @@ -8025,9 +8025,9 @@ dependencies = [ [[package]] name = "sherpa-onnx-sys" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f3fe4987367b162336027b5d1ffca6dcd627bee6a324e46f80e82dfcb4365b" +checksum = "21ac2c3342f826069bbf0be3578f9601f3802a1bff50bbced8230a15ec9eed8f" dependencies = [ "bzip2 0.4.4", "tar", @@ -8036,9 +8036,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" @@ -8105,7 +8105,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a75cbde1bf934313596a004973e462f9a82caa814dcf1a5f507bdf51597eeb4" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -8215,9 +8215,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket-pktinfo" @@ -8232,9 +8232,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -8302,7 +8302,7 @@ checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -8428,7 +8428,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -8613,9 +8613,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -8639,7 +8639,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -8662,7 +8662,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -8711,7 +8711,7 @@ version = "0.35.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2", "core-foundation 0.10.1", "core-graphics", @@ -8753,7 +8753,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -8863,7 +8863,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "syn 2.0.117", + "syn 2.0.118", "tauri-utils", "thiserror 2.0.18", "time", @@ -8881,7 +8881,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "tauri-codegen", "tauri-utils", ] @@ -8967,9 +8967,9 @@ dependencies = [ [[package]] name = "tauri-plugin-global-shortcut" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "424af23c7e88d05e4a1a6fc2c7be077912f8c76bd7900fd50aa2b7cbf5a2c405" +checksum = "b4dd9f4c5136c09cd962da0c86dc4accd4666db2ea591cf16e6597435843bd2b" dependencies = [ "global-hotkey", "log", @@ -9018,7 +9018,7 @@ dependencies = [ "thiserror 2.0.18", "url", "windows 0.61.3", - "zbus 5.15.0", + "zbus 5.16.0", ] [[package]] @@ -9044,7 +9044,7 @@ dependencies = [ "thiserror 2.0.18", "tracing", "windows-sys 0.60.2", - "zbus 5.15.0", + "zbus 5.16.0", ] [[package]] @@ -9106,7 +9106,7 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "log", "serde", "serde_json", @@ -9276,7 +9276,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -9287,7 +9287,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -9301,12 +9301,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" dependencies = [ "deranged", - "itoa", "js-sys", "libc", "num-conv", @@ -9319,15 +9318,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" dependencies = [ "num-conv", "time-core", @@ -9392,7 +9391,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -9407,9 +9406,9 @@ dependencies = [ [[package]] name = "tokio-retry" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40f644c762e9d396831ae2f8935c954b0d758c4532e924bead0f666d0c1c8640" +checksum = "4a129d95275ebf4c493ec53bf0f8cd95f5ac161bc4f381700809a54f595d4470" dependencies = [ "pin-project-lite", "rand 0.10.1", @@ -9721,7 +9720,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "bytes", "futures-util", "http", @@ -9778,7 +9777,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -9947,9 +9946,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "typewit" @@ -10044,9 +10043,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -10149,9 +10148,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -10228,7 +10227,7 @@ checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -10296,9 +10295,9 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen 0.57.1", ] @@ -10323,9 +10322,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" dependencies = [ "cfg-if 1.0.4", "once_cell", @@ -10336,9 +10335,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.72" +version = "0.4.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" dependencies = [ "js-sys", "wasm-bindgen", @@ -10346,9 +10345,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -10356,22 +10355,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" dependencies = [ "unicode-ident", ] @@ -10430,7 +10429,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "hashbrown 0.15.5", "indexmap 2.14.0", "semver", @@ -10438,9 +10437,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.99" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" dependencies = [ "js-sys", "wasm-bindgen", @@ -10561,7 +10560,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -10748,7 +10747,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -10759,7 +10758,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -11180,7 +11179,7 @@ dependencies = [ "heck 0.5.0", "indexmap 2.14.0", "prettyplease", - "syn 2.0.117", + "syn 2.0.118", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -11196,7 +11195,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -11208,7 +11207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags 2.13.0", "indexmap 2.14.0", "log", "serde", @@ -11249,8 +11248,8 @@ dependencies = [ "log", "serde", "thiserror 2.0.18", - "windows 0.61.3", - "windows-core 0.61.2", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] @@ -11550,9 +11549,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -11567,7 +11566,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] @@ -11605,9 +11604,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.15.0" +version = "5.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" dependencies = [ "async-broadcast", "async-executor", @@ -11633,9 +11632,9 @@ dependencies = [ "uuid", "windows-sys 0.61.2", "winnow 1.0.3", - "zbus_macros 5.15.0", + "zbus_macros 5.16.0", "zbus_names 4.3.2", - "zvariant 5.11.0", + "zvariant 5.12.0", ] [[package]] @@ -11647,23 +11646,23 @@ dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "zvariant_utils 2.1.0", ] [[package]] name = "zbus_macros" -version = "5.15.0" +version = "5.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "zbus_names 4.3.2", - "zvariant 5.11.0", - "zvariant_utils 3.3.1", + "zvariant 5.12.0", + "zvariant_utils 3.4.0", ] [[package]] @@ -11685,27 +11684,27 @@ checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", "winnow 1.0.3", - "zvariant 5.11.0", + "zvariant 5.12.0", ] [[package]] name = "zerocopy" -version = "0.8.49" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.49" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -11725,28 +11724,28 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -11779,7 +11778,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -11905,16 +11904,16 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.11.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" dependencies = [ "endi", "enumflags2", "serde", "winnow 1.0.3", - "zvariant_derive 5.11.0", - "zvariant_utils 3.3.1", + "zvariant_derive 5.12.0", + "zvariant_utils 3.4.0", ] [[package]] @@ -11926,21 +11925,21 @@ dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "zvariant_utils 2.1.0", ] [[package]] name = "zvariant_derive" -version = "5.11.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", - "zvariant_utils 3.3.1", + "syn 2.0.118", + "zvariant_utils 3.4.0", ] [[package]] @@ -11951,18 +11950,18 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "zvariant_utils" -version = "3.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.117", + "syn 2.0.118", "winnow 1.0.3", ] diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 266733abe..bd72dc3f7 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -534,15 +534,27 @@ pub fn run() { migration::reconcile_legacy_command_names(&app_handle); migration::reconcile_provider_mcp_commands(&app_handle); - if let Err(e) = managed_agents::sync_team_personas(&app_handle) { - eprintln!("buzz-desktop: sync-team-personas: {e}"); - } - // Resolve persisted identity key (env var → file → generate+save). // This is fatal — the app should not start with an ephemeral identity // that will be lost on restart, as that silently breaks channel // memberships, DMs, and relay identity. let state = app_handle.state::(); + resolve_persisted_identity(&app_handle, &state) + .map_err(|e| -> Box { e.into() })?; + + // Persona-event migration signs every retained event with the + // owner's keys, so it must run after the persisted identity is + // resolved (not before — at that point keys may still be ephemeral). + let owner_keys = state + .keys + .lock() + .map(|k| k.clone()) + .map_err(|e| -> Box { e.to_string().into() })?; + migration::migrate_personas_to_events(&app_handle, &owner_keys); + + if let Err(e) = managed_agents::sync_team_personas(&app_handle) { + eprintln!("buzz-desktop: sync-team-personas: {e}"); + } // Store the AppHandle so huddle commands can emit `huddle-state-changed` // events via `huddle::emit_huddle_state` without threading the handle @@ -551,9 +563,6 @@ pub fn run() { *guard = Some(app_handle.clone()); } - resolve_persisted_identity(&app_handle, &state) - .map_err(|e| -> Box { e.into() })?; - // Bring up the runtime-owned relay-mesh call-me-now listener now, // before any saved agent restore can request a connection. Its // lifetime is tied to the runtime, not a UI mount — this is what diff --git a/desktop/src-tauri/src/managed_agents/mod.rs b/desktop/src-tauri/src/managed_agents/mod.rs index d22598ed8..7c038adb8 100644 --- a/desktop/src-tauri/src/managed_agents/mod.rs +++ b/desktop/src-tauri/src/managed_agents/mod.rs @@ -4,12 +4,16 @@ mod env_vars; mod nest; mod persona_avatars; mod persona_card; +#[allow(dead_code)] +pub(crate) mod persona_events; mod personas; #[cfg(windows)] mod process_lifecycle; #[cfg(feature = "mesh-llm")] mod relay_mesh; mod restore; +#[allow(dead_code)] +pub mod retention; mod runtime; mod storage; mod team_repair; diff --git a/desktop/src-tauri/src/managed_agents/persona_events.rs b/desktop/src-tauri/src/managed_agents/persona_events.rs new file mode 100644 index 000000000..31e1e93b5 --- /dev/null +++ b/desktop/src-tauri/src/managed_agents/persona_events.rs @@ -0,0 +1,245 @@ +//! Serialize `PersonaRecord` ↔ kind:30175 persona events and publish/fetch via relay. +//! +//! Persona events are NIP-33 parameterized replaceable events keyed by +//! `(pubkey, kind, d_tag)` where `d_tag` is the plaintext persona slug. + +use std::collections::BTreeMap; + +use nostr::{EventBuilder, Kind, Tag}; +use serde::{Deserialize, Serialize}; +use sprout_core::kind::KIND_PERSONA; + +use super::PersonaRecord; +use crate::app_state::AppState; + +/// The JSON body stored in a persona event's content field. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PersonaEventContent { + pub display_name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, + pub system_prompt: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub runtime: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub name_pool: Vec, +} + +/// Derive the d-tag (persona slug) from a `PersonaRecord`. +/// +/// Uses `source_team_persona_slug` if available, otherwise falls back to `id`. +pub fn persona_d_tag(record: &PersonaRecord) -> String { + record + .source_team_persona_slug + .as_deref() + .unwrap_or(&record.id) + .to_string() +} + +/// Build a kind:30175 event from a `PersonaRecord`. +/// +/// Returns an unsigned `EventBuilder` — the caller signs and submits. +pub fn build_persona_event(record: &PersonaRecord) -> Result { + let content = PersonaEventContent { + display_name: record.display_name.clone(), + avatar_url: record.avatar_url.clone(), + system_prompt: record.system_prompt.clone(), + runtime: record.runtime.clone(), + model: record.model.clone(), + provider: record.provider.clone(), + name_pool: record.name_pool.clone(), + }; + + let content_json = serde_json::to_string(&content) + .map_err(|e| format!("failed to serialize persona content: {e}"))?; + + let d_tag = persona_d_tag(record); + let tags = vec![Tag::parse(["d", d_tag.as_str()]).map_err(|e| format!("invalid d-tag: {e}"))?]; + + Ok(EventBuilder::new(Kind::Custom(KIND_PERSONA as u16), content_json).tags(tags)) +} + +/// Parse a kind:30175 event back into a `PersonaRecord`. +/// +/// The event's d-tag becomes the persona ID and slug. +pub fn persona_from_event(event: &nostr::Event) -> Result { + let d_tag = event + .tags + .iter() + .find_map(|tag| { + let values: Vec<&str> = tag.as_slice().iter().map(|s| s.as_str()).collect(); + if values.first() == Some(&"d") { + values.get(1).map(|s| s.to_string()) + } else { + None + } + }) + .ok_or("persona event missing d-tag")?; + + let content: PersonaEventContent = serde_json::from_str(event.content.as_ref()) + .map_err(|e| format!("failed to parse persona event content: {e}"))?; + + let created_at = event.created_at.to_human_datetime(); + + Ok(PersonaRecord { + id: d_tag.clone(), + display_name: content.display_name, + avatar_url: content.avatar_url, + system_prompt: content.system_prompt, + runtime: content.runtime, + model: content.model, + provider: content.provider, + name_pool: content.name_pool, + is_builtin: false, + is_active: true, + source_team: None, + source_team_persona_slug: Some(d_tag), + env_vars: BTreeMap::new(), + created_at: created_at.clone(), + updated_at: created_at, + }) +} + +/// Publish a persona event to the relay. +pub async fn publish_persona_event( + record: &PersonaRecord, + state: &AppState, +) -> Result { + let builder = build_persona_event(record)?; + let response = crate::relay::submit_event(builder, state).await?; + Ok(response.event_id) +} + +/// Fetch all persona events authored by the current user from the relay. +pub async fn fetch_persona_events(state: &AppState) -> Result, String> { + let pubkey = { + let keys = state.keys.lock().map_err(|e| e.to_string())?; + keys.public_key().to_hex() + }; + + let filter = serde_json::json!({ + "kinds": [KIND_PERSONA], + "authors": [pubkey] + }); + + crate::relay::query_relay(state, &[filter]).await +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_persona() -> PersonaRecord { + PersonaRecord { + id: "test-persona".to_string(), + display_name: "Test Persona".to_string(), + avatar_url: Some("https://example.com/avatar.png".to_string()), + system_prompt: "You are a test assistant.".to_string(), + runtime: Some("goose".to_string()), + model: Some("claude-opus-4".to_string()), + provider: Some("anthropic".to_string()), + name_pool: vec!["Alpha".to_string(), "Beta".to_string()], + is_builtin: false, + is_active: true, + source_team: None, + source_team_persona_slug: Some("test-slug".to_string()), + env_vars: BTreeMap::from([("KEY".to_string(), "value".to_string())]), + created_at: "2025-01-01T00:00:00Z".to_string(), + updated_at: "2025-01-01T00:00:00Z".to_string(), + } + } + + #[test] + fn d_tag_uses_slug_when_available() { + let record = sample_persona(); + assert_eq!(persona_d_tag(&record), "test-slug"); + } + + #[test] + fn d_tag_falls_back_to_id() { + let mut record = sample_persona(); + record.source_team_persona_slug = None; + assert_eq!(persona_d_tag(&record), "test-persona"); + } + + #[test] + fn build_persona_event_produces_correct_kind() { + let record = sample_persona(); + let builder = build_persona_event(&record).unwrap(); + let keys = nostr::Keys::generate(); + let event = builder.sign_with_keys(&keys).unwrap(); + assert_eq!(event.kind.as_u16() as u32, KIND_PERSONA); + } + + #[test] + fn round_trip_serialization() { + let record = sample_persona(); + let builder = build_persona_event(&record).unwrap(); + let keys = nostr::Keys::generate(); + let event = builder.sign_with_keys(&keys).unwrap(); + + let restored = persona_from_event(&event).unwrap(); + assert_eq!(restored.id, "test-slug"); + assert_eq!(restored.display_name, "Test Persona"); + assert_eq!( + restored.avatar_url, + Some("https://example.com/avatar.png".to_string()) + ); + assert_eq!(restored.system_prompt, "You are a test assistant."); + assert_eq!(restored.runtime, Some("goose".to_string())); + assert_eq!(restored.model, Some("claude-opus-4".to_string())); + assert_eq!(restored.provider, Some("anthropic".to_string())); + assert_eq!(restored.name_pool, vec!["Alpha", "Beta"]); + // env_vars are not included in public persona events (secrets travel + // via NIP-44-encrypted engrams only). + assert!(restored.env_vars.is_empty()); + assert_eq!( + restored.source_team_persona_slug, + Some("test-slug".to_string()) + ); + assert!(!restored.is_builtin); + assert!(restored.is_active); + } + + #[test] + fn round_trip_minimal_persona() { + let record = PersonaRecord { + id: "minimal".to_string(), + display_name: "Minimal".to_string(), + avatar_url: None, + system_prompt: "Hello".to_string(), + runtime: None, + model: None, + provider: None, + name_pool: vec![], + is_builtin: true, + is_active: false, + source_team: Some("team-1".to_string()), + source_team_persona_slug: None, + env_vars: BTreeMap::new(), + created_at: "2025-01-01T00:00:00Z".to_string(), + updated_at: "2025-01-01T00:00:00Z".to_string(), + }; + + let builder = build_persona_event(&record).unwrap(); + let keys = nostr::Keys::generate(); + let event = builder.sign_with_keys(&keys).unwrap(); + + let restored = persona_from_event(&event).unwrap(); + assert_eq!(restored.id, "minimal"); + assert_eq!(restored.display_name, "Minimal"); + assert_eq!(restored.avatar_url, None); + assert_eq!(restored.runtime, None); + assert_eq!(restored.model, None); + assert_eq!(restored.provider, None); + assert!(restored.name_pool.is_empty()); + assert!(restored.env_vars.is_empty()); + // Deserialized persona is always non-builtin and active + assert!(!restored.is_builtin); + assert!(restored.is_active); + } +} diff --git a/desktop/src-tauri/src/managed_agents/personas.rs b/desktop/src-tauri/src/managed_agents/personas.rs index 95f58878b..63fcdf376 100644 --- a/desktop/src-tauri/src/managed_agents/personas.rs +++ b/desktop/src-tauri/src/managed_agents/personas.rs @@ -3,7 +3,12 @@ use std::{fs, path::PathBuf}; use tauri::AppHandle; use crate::{ - managed_agents::{managed_agents_base_dir, PersonaRecord}, + managed_agents::{ + managed_agents_base_dir, + persona_events::persona_from_event, + retention::{get_retained_personas, has_retained_personas, open_retention_db}, + PersonaRecord, + }, util::now_iso, }; @@ -327,6 +332,54 @@ pub fn validate_persona_activation_change( Ok(()) } +/// Path to the retention SQLite database. +#[allow(dead_code)] +fn retention_db_path(app: &AppHandle) -> Result { + Ok(managed_agents_base_dir(app)?.join("retention.db")) +} + +/// Load personas from the retention store, returning them as PersonaRecords. +/// +/// Returns `Ok(None)` if the retention DB doesn't exist or has no personas +/// for the current user pubkey. +#[allow(dead_code)] +fn load_from_retention( + app: &AppHandle, + pubkey: &str, +) -> Result>, String> { + let db_path = retention_db_path(app)?; + if !db_path.exists() { + return Ok(None); + } + + let conn = open_retention_db(&db_path)?; + if !has_retained_personas(&conn, pubkey)? { + return Ok(None); + } + + let retained = get_retained_personas(&conn, pubkey)?; + let mut records = Vec::with_capacity(retained.len()); + for row in &retained { + let event: nostr::Event = serde_json::from_str(&row.raw_event) + .map_err(|e| format!("failed to parse retained event: {e}"))?; + match persona_from_event(&event) { + Ok(record) => records.push(record), + Err(e) => { + eprintln!( + "sprout-desktop: retention: skipping malformed persona event (d_tag={}): {e}", + row.d_tag + ); + } + } + } + + if records.is_empty() { + Ok(None) + } else { + Ok(Some(records)) + } +} + pub fn load_personas(app: &AppHandle) -> Result, String> { let path = personas_store_path(app)?; let now = now_iso(); diff --git a/desktop/src-tauri/src/managed_agents/retention.rs b/desktop/src-tauri/src/managed_agents/retention.rs new file mode 100644 index 000000000..051c4d917 --- /dev/null +++ b/desktop/src-tauri/src/managed_agents/retention.rs @@ -0,0 +1,317 @@ +//! Local SQLite retention store for persona events. +//! +//! Provides durable client-side storage for persona events, enabling offline +//! boot when the relay is unreachable. Uses `INSERT OR REPLACE` keyed on +//! `(kind, pubkey, d_tag)` for NIP-33 latest-wins semantics. + +use std::path::Path; + +use rusqlite::{params, Connection, OptionalExtension}; + +/// A retained persona event row. +#[derive(Debug, Clone)] +pub struct RetainedEvent { + pub kind: u32, + pub pubkey: String, + pub d_tag: String, + pub content: String, + pub created_at: i64, + pub raw_event: String, + pub pending_sync: bool, +} + +/// Open (or create) the retention database at the given path. +pub fn open_retention_db(path: &Path) -> Result { + let conn = Connection::open(path).map_err(|e| format!("failed to open retention db: {e}"))?; + + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS persona_events ( + kind INTEGER NOT NULL, + pubkey TEXT NOT NULL, + d_tag TEXT NOT NULL, + content TEXT NOT NULL, + created_at INTEGER NOT NULL, + raw_event TEXT NOT NULL, + pending_sync INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (kind, pubkey, d_tag) + );", + ) + .map_err(|e| format!("failed to create retention table: {e}"))?; + + Ok(conn) +} + +/// Upsert a persona event into the retention store. +/// +/// Only replaces if the new event has a newer or equal `created_at` (NIP-33 semantics). +pub fn retain_event(conn: &Connection, event: &RetainedEvent) -> Result<(), String> { + conn.execute( + "INSERT INTO persona_events (kind, pubkey, d_tag, content, created_at, raw_event, pending_sync) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + ON CONFLICT (kind, pubkey, d_tag) DO UPDATE SET + content = excluded.content, + created_at = excluded.created_at, + raw_event = excluded.raw_event, + pending_sync = excluded.pending_sync + WHERE excluded.created_at >= persona_events.created_at", + params![ + event.kind, + event.pubkey, + event.d_tag, + event.content, + event.created_at, + event.raw_event, + event.pending_sync as i32, + ], + ) + .map_err(|e| format!("failed to retain event: {e}"))?; + + Ok(()) +} + +/// Load all retained persona events for a given pubkey. +pub fn get_retained_personas( + conn: &Connection, + pubkey: &str, +) -> Result, String> { + let mut stmt = conn + .prepare( + "SELECT kind, pubkey, d_tag, content, created_at, raw_event, pending_sync + FROM persona_events + WHERE pubkey = ?1 + ORDER BY d_tag", + ) + .map_err(|e| format!("failed to prepare query: {e}"))?; + + let rows = stmt + .query_map(params![pubkey], |row| { + Ok(RetainedEvent { + kind: row.get(0)?, + pubkey: row.get(1)?, + d_tag: row.get(2)?, + content: row.get(3)?, + created_at: row.get(4)?, + raw_event: row.get(5)?, + pending_sync: row.get::<_, i32>(6)? != 0, + }) + }) + .map_err(|e| format!("failed to query retained events: {e}"))?; + + rows.collect::, _>>() + .map_err(|e| format!("failed to read retained event row: {e}")) +} + +/// Get all events marked as pending sync (not yet confirmed on relay). +pub fn get_pending_sync(conn: &Connection) -> Result, String> { + let mut stmt = conn + .prepare( + "SELECT kind, pubkey, d_tag, content, created_at, raw_event, pending_sync + FROM persona_events + WHERE pending_sync = 1", + ) + .map_err(|e| format!("failed to prepare pending sync query: {e}"))?; + + let rows = stmt + .query_map([], |row| { + Ok(RetainedEvent { + kind: row.get(0)?, + pubkey: row.get(1)?, + d_tag: row.get(2)?, + content: row.get(3)?, + created_at: row.get(4)?, + raw_event: row.get(5)?, + pending_sync: row.get::<_, i32>(6)? != 0, + }) + }) + .map_err(|e| format!("failed to query pending sync events: {e}"))?; + + rows.collect::, _>>() + .map_err(|e| format!("failed to read pending sync row: {e}")) +} + +/// Clear the pending_sync flag for a specific event (after relay confirms). +pub fn mark_synced(conn: &Connection, kind: u32, pubkey: &str, d_tag: &str) -> Result<(), String> { + conn.execute( + "UPDATE persona_events SET pending_sync = 0 + WHERE kind = ?1 AND pubkey = ?2 AND d_tag = ?3", + params![kind, pubkey, d_tag], + ) + .map_err(|e| format!("failed to mark event synced: {e}"))?; + + Ok(()) +} + +/// Check if the retention store has any persona events for the given pubkey. +pub fn has_retained_personas(conn: &Connection, pubkey: &str) -> Result { + conn.query_row( + "SELECT EXISTS(SELECT 1 FROM persona_events WHERE pubkey = ?1)", + params![pubkey], + |row| row.get(0), + ) + .map_err(|e| format!("failed to check retained personas: {e}")) +} + +/// Look up a single retained event by its coordinate. +pub fn get_retained_event( + conn: &Connection, + kind: u32, + pubkey: &str, + d_tag: &str, +) -> Result, String> { + conn.query_row( + "SELECT kind, pubkey, d_tag, content, created_at, raw_event, pending_sync + FROM persona_events + WHERE kind = ?1 AND pubkey = ?2 AND d_tag = ?3", + params![kind, pubkey, d_tag], + |row| { + Ok(RetainedEvent { + kind: row.get(0)?, + pubkey: row.get(1)?, + d_tag: row.get(2)?, + content: row.get(3)?, + created_at: row.get(4)?, + raw_event: row.get(5)?, + pending_sync: row.get::<_, i32>(6)? != 0, + }) + }, + ) + .optional() + .map_err(|e| format!("failed to get retained event: {e}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_db() -> Connection { + open_retention_db(Path::new(":memory:")).unwrap() + } + + fn sample_event() -> RetainedEvent { + RetainedEvent { + kind: 30175, + pubkey: "abc123".to_string(), + d_tag: "test-persona".to_string(), + content: r#"{"display_name":"Test"}"#.to_string(), + created_at: 1000, + raw_event: r#"{"id":"..."}"#.to_string(), + pending_sync: true, + } + } + + #[test] + fn retain_and_retrieve() { + let conn = test_db(); + let event = sample_event(); + retain_event(&conn, &event).unwrap(); + + let results = get_retained_personas(&conn, "abc123").unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].d_tag, "test-persona"); + assert_eq!(results[0].created_at, 1000); + assert!(results[0].pending_sync); + } + + #[test] + fn upsert_replaces_newer() { + let conn = test_db(); + let mut event = sample_event(); + retain_event(&conn, &event).unwrap(); + + event.content = r#"{"display_name":"Updated"}"#.to_string(); + event.created_at = 2000; + retain_event(&conn, &event).unwrap(); + + let results = get_retained_personas(&conn, "abc123").unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].created_at, 2000); + assert!(results[0].content.contains("Updated")); + } + + #[test] + fn upsert_ignores_older() { + let conn = test_db(); + let mut event = sample_event(); + event.created_at = 2000; + retain_event(&conn, &event).unwrap(); + + event.content = r#"{"display_name":"Old"}"#.to_string(); + event.created_at = 1000; + retain_event(&conn, &event).unwrap(); + + let results = get_retained_personas(&conn, "abc123").unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].created_at, 2000); + assert!(!results[0].content.contains("Old")); + } + + #[test] + fn pending_sync_query() { + let conn = test_db(); + let mut event = sample_event(); + event.pending_sync = true; + retain_event(&conn, &event).unwrap(); + + let mut event2 = sample_event(); + event2.d_tag = "other".to_string(); + event2.pending_sync = false; + retain_event(&conn, &event2).unwrap(); + + let pending = get_pending_sync(&conn).unwrap(); + assert_eq!(pending.len(), 1); + assert_eq!(pending[0].d_tag, "test-persona"); + } + + #[test] + fn mark_synced_clears_flag() { + let conn = test_db(); + let event = sample_event(); + retain_event(&conn, &event).unwrap(); + + mark_synced(&conn, 30175, "abc123", "test-persona").unwrap(); + + let pending = get_pending_sync(&conn).unwrap(); + assert!(pending.is_empty()); + + let results = get_retained_personas(&conn, "abc123").unwrap(); + assert_eq!(results.len(), 1); + assert!(!results[0].pending_sync); + } + + #[test] + fn has_retained_personas_works() { + let conn = test_db(); + assert!(!has_retained_personas(&conn, "abc123").unwrap()); + + let event = sample_event(); + retain_event(&conn, &event).unwrap(); + + assert!(has_retained_personas(&conn, "abc123").unwrap()); + assert!(!has_retained_personas(&conn, "other").unwrap()); + } + + #[test] + fn get_retained_event_by_coordinate() { + let conn = test_db(); + let event = sample_event(); + retain_event(&conn, &event).unwrap(); + + let found = get_retained_event(&conn, 30175, "abc123", "test-persona").unwrap(); + assert!(found.is_some()); + assert_eq!(found.unwrap().d_tag, "test-persona"); + + let not_found = get_retained_event(&conn, 30175, "abc123", "nonexistent").unwrap(); + assert!(not_found.is_none()); + } + + #[test] + fn idempotent_retain_same_timestamp() { + let conn = test_db(); + let event = sample_event(); + retain_event(&conn, &event).unwrap(); + retain_event(&conn, &event).unwrap(); + + let results = get_retained_personas(&conn, "abc123").unwrap(); + assert_eq!(results.len(), 1); + } +} diff --git a/desktop/src-tauri/src/migration.rs b/desktop/src-tauri/src/migration.rs index 0387b31a4..62281266f 100644 --- a/desktop/src-tauri/src/migration.rs +++ b/desktop/src-tauri/src/migration.rs @@ -917,6 +917,123 @@ pub fn migrate_persona_provider_to_runtime(app: &tauri::AppHandle) { rename_provider_to_runtime_in_personas(&path); } +/// Migrate existing `personas.json` entries to persona events in the local +/// retention store. +/// +/// Must run AFTER `migrate_packs_to_teams` (depends on field renames being +/// complete) and AFTER the persisted identity is resolved (it signs every +/// retained event with the owner's keys). +/// +/// Idempotent: skips when the retention store already holds events for the +/// owner pubkey — the data is the sentinel, so no separate sentinel file is +/// needed. This avoids re-running (and resetting `pending_sync`) on every +/// launch when a sentinel write silently fails. +/// +/// Strategy: write to local SQLite retention first (durable copy), mark as +/// `pending_sync = 1` for later relay publish. Migration succeeds on local +/// write, not relay acknowledgment. Every retained row is a real signed +/// event — there is no placeholder path. +pub fn migrate_personas_to_events(app: &tauri::AppHandle, keys: &nostr::Keys) { + use crate::managed_agents::managed_agents_base_dir; + + let Ok(base_dir) = managed_agents_base_dir(app) else { + return; + }; + + match migrate_personas_in_dir(&base_dir, keys) { + Ok(0) => {} + Ok(migrated) => { + eprintln!( + "sprout-desktop: persona-event-migration: {migrated} personas migrated to retention" + ); + } + Err(e) => { + eprintln!("sprout-desktop: persona-event-migration: {e}"); + } + } +} + +/// Core migration logic, decoupled from the Tauri `AppHandle` for testing. +/// +/// Returns the number of personas written to the retention store. Returns +/// `Ok(0)` when migration has already run (retention store has rows for the +/// owner pubkey) or when there are no non-builtin personas to migrate. +fn migrate_personas_in_dir(base_dir: &Path, keys: &nostr::Keys) -> Result { + use crate::managed_agents::{ + persona_events::{build_persona_event, persona_d_tag}, + retention::{has_retained_personas, open_retention_db, retain_event, RetainedEvent}, + PersonaRecord, + }; + use nostr::JsonUtil; + use sprout_core::kind::KIND_PERSONA; + + let pubkey = keys.public_key().to_hex(); + + // Read personas.json fresh at migration time. Nothing to migrate if the + // file is absent. + let personas_path = base_dir.join("personas.json"); + if !personas_path.exists() { + return Ok(0); + } + + let content = std::fs::read_to_string(&personas_path) + .map_err(|e| format!("failed to read personas.json: {e}"))?; + + let records: Vec = serde_json::from_str(&content) + .map_err(|e| format!("failed to parse personas.json: {e}"))?; + + if records.is_empty() { + return Ok(0); + } + + // Open (or create) the retention database. + let db_path = base_dir.join("retention.db"); + let conn = + open_retention_db(&db_path).map_err(|e| format!("failed to open retention db: {e}"))?; + + // Idempotency: the retention rows themselves are the sentinel. If the + // owner already has retained personas, migration ran on a prior launch. + if has_retained_personas(&conn, &pubkey)? { + return Ok(0); + } + + let mut migrated = 0u32; + + for record in &records { + // Skip built-in personas — they're always available from code. + if record.is_builtin { + continue; + } + + let d_tag = persona_d_tag(record); + + let builder = build_persona_event(record) + .map_err(|e| format!("failed to build event for '{}': {e}", record.display_name))?; + + let event = builder + .sign_with_keys(keys) + .map_err(|e| format!("failed to sign event for '{}': {e}", record.display_name))?; + + let retained = RetainedEvent { + kind: KIND_PERSONA, + pubkey: pubkey.clone(), + d_tag, + content: event.content.to_string(), + // Safety: nostr timestamps are seconds and stay below i64::MAX + // until year 2262. + created_at: event.created_at.as_secs() as i64, + raw_event: event.as_json(), + pending_sync: true, + }; + + retain_event(&conn, &retained) + .map_err(|e| format!("failed to retain '{}': {e}", record.display_name))?; + migrated += 1; + } + + Ok(migrated) +} + #[cfg(test)] #[path = "migration_test_support.rs"] mod test_support; diff --git a/desktop/src-tauri/src/migration_tests.rs b/desktop/src-tauri/src/migration_tests.rs index b414383c1..241fdec50 100644 --- a/desktop/src-tauri/src/migration_tests.rs +++ b/desktop/src-tauri/src/migration_tests.rs @@ -716,3 +716,98 @@ fn reconcile_mcp_commands_skips_record_without_agent_command() { reconcile_mcp_commands_in_file(&path); assert_eq!(before, std::fs::read_to_string(&path).unwrap()); } + +/// Helper: write a `personas.json` directly in `base_dir` (the migration +/// reads `base_dir/personas.json`, where `base_dir` is the `agents` dir). +fn write_base_personas(base_dir: &Path, records: &serde_json::Value) { + std::fs::write( + base_dir.join("personas.json"), + serde_json::to_string_pretty(records).unwrap(), + ) + .unwrap(); +} + +fn one_persona() -> serde_json::Value { + serde_json::json!([{ + "id": "code-reviewer", + "display_name": "Code Reviewer", + "system_prompt": "You review code.", + "is_builtin": false, + "is_active": true, + "name_pool": [], + "env_vars": {}, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }]) +} + +#[test] +fn migrate_personas_writes_signed_retention_rows() { + use crate::managed_agents::retention::{get_retained_personas, open_retention_db}; + + let base = tempfile::tempdir().unwrap(); + write_base_personas(base.path(), &one_persona()); + let keys = nostr::Keys::generate(); + let pubkey = keys.public_key().to_hex(); + + let migrated = migrate_personas_in_dir(base.path(), &keys).unwrap(); + assert_eq!(migrated, 1); + + let conn = open_retention_db(&base.path().join("retention.db")).unwrap(); + let rows = get_retained_personas(&conn, &pubkey).unwrap(); + assert_eq!(rows.len(), 1); + // Row holds a real signed event for the owner — not a placeholder. + assert_eq!(rows[0].pubkey, pubkey); + let event: nostr::Event = nostr::JsonUtil::from_json(&rows[0].raw_event).unwrap(); + assert!(event.verify().is_ok()); + assert!(rows[0].pending_sync); +} + +#[test] +fn migrate_personas_skips_builtins() { + use crate::managed_agents::retention::{get_retained_personas, open_retention_db}; + + let base = tempfile::tempdir().unwrap(); + write_base_personas( + base.path(), + &serde_json::json!([{ + "id": "builtin:solo", + "display_name": "Solo", + "system_prompt": "x", + "is_builtin": true, + "is_active": true, + "name_pool": [], + "env_vars": {}, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }]), + ); + let keys = nostr::Keys::generate(); + + let migrated = migrate_personas_in_dir(base.path(), &keys).unwrap(); + assert_eq!(migrated, 0); + + let conn = open_retention_db(&base.path().join("retention.db")).unwrap(); + let rows = get_retained_personas(&conn, &keys.public_key().to_hex()).unwrap(); + assert!(rows.is_empty()); +} + +#[test] +fn migrate_personas_skips_when_retention_already_populated() { + let base = tempfile::tempdir().unwrap(); + write_base_personas(base.path(), &one_persona()); + let keys = nostr::Keys::generate(); + + // First run migrates; second run is a no-op (retention rows are the + // sentinel — no separate sentinel file). + assert_eq!(migrate_personas_in_dir(base.path(), &keys).unwrap(), 1); + assert_eq!(migrate_personas_in_dir(base.path(), &keys).unwrap(), 0); + assert!(!base.path().join("migration_state.json").exists()); +} + +#[test] +fn migrate_personas_no_file_is_noop() { + let base = tempfile::tempdir().unwrap(); + let keys = nostr::Keys::generate(); + assert_eq!(migrate_personas_in_dir(base.path(), &keys).unwrap(), 0); +} diff --git a/docs/nips/NIP-AP.md b/docs/nips/NIP-AP.md new file mode 100644 index 000000000..50ca53ba2 --- /dev/null +++ b/docs/nips/NIP-AP.md @@ -0,0 +1,258 @@ +NIP-AP +====== + +Agent Personas +-------------- + +`draft` `optional` + +This NIP defines `kind:30175` persona events — public, addressable definitions that describe how to instantiate an AI agent. A persona carries identity (display name, avatar), behavioral configuration (system prompt, model, runtime), and an optional name pool. It is the "blueprint" from which agents are spawned. + +## Kind + +This NIP claims `kind:30175` for agent persona definitions. It is in the NIP-33 parameterized replaceable range (30000–39999) per [NIP-01](01.md): addressed by `(pubkey, kind, d_tag)`, with only the latest event per address retained. + +A dedicated kind (rather than encoding personas as NIP-78 `kind:30078` "Application-specific Data") is taken for the same reasons as [NIP-AE](NIP-AE.md): (1) it isolates this NIP's address space from any other application using the same pubkey — persona slugs cannot collide with another app's `d` tag choices; (2) it lets observers, indexers, and unknown-kind viewers identify persona events from the kind alone, without parsing content as a namespace demultiplexer. + +## Roles + +- **owner** — a Nostr identity (`pubkey_o`) that publishes and manages persona definitions. Typically the workspace operator. +- **agent** — a Nostr identity instantiated from a persona. Agents do NOT author persona events; they consume them. An agent MAY store a private snapshot of its originating persona in a [NIP-AE](NIP-AE.md) engram at `mem/persona` (encrypted, owner-readable). + +## Slugs + +The `d` tag of a persona event is the **plaintext persona slug**. A valid slug matches: + +``` +^[a-z0-9][a-z0-9_-]{0,63}$ +``` + +Total length: 1–64 bytes. Slugs are flat identifiers (no path separators), unlike [NIP-AE](NIP-AE.md) memory slugs which are hierarchical (`mem/…`). + +### Plaintext rationale + +The d-tag is deliberately NOT blinded (contrast with [NIP-AE](NIP-AE.md) which HMAC-blinds d-tags to protect memory slug confidentiality). Personas are public definitions meant for discovery: + +- Direct filter queries: `{kinds: [30175], authors: [pubkey], "#d": ["my-persona"]}` +- Human-readable addressing in UIs +- Cross-workspace sharing without a shared secret + +## Event envelope + +```jsonc +{ + "kind": 30175, + "pubkey": "", + "created_at": , + "tags": [ + ["d", ""] + ], + "content": "" +} +``` + +There MUST be exactly one `d` tag and it MUST contain a valid slug per the grammar above. The relay enforces this constraint on ingest. There is no `p` tag — persona events are owner-to-self definitions, not directed at a counterparty. + +Implementations MAY include a [NIP-31](31.md) `["alt", "agent persona definition"]` tag to give unknown-kind viewers a non-leaking summary. Additional tags beyond `d` and `alt` are not defined by this NIP and have no effect on validity. + +## Content body + +The `content` field is a **plaintext** (unencrypted) JSON object: + +```jsonc +{ + "display_name": "", + "system_prompt": "", + "avatar_url": "", + "runtime": "", + "model": "", + "provider": "", + "name_pool": ["", ...] +} +``` + +### Required fields + +| Field | Type | Description | +|-------|------|-------------| +| `display_name` | string | Human-readable name for the persona. | +| `system_prompt` | string | The system prompt injected into agent sessions. | + +### Optional fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `avatar_url` | string \| null | `null` | URL to an avatar image. | +| `runtime` | string \| null | `null` | ACP runtime identifier (e.g. `"goose"`, `"claude-code"`). | +| `model` | string \| null | `null` | Model identifier (e.g. `"claude-opus-4"`). | +| `provider` | string \| null | `null` | Model provider (e.g. `"anthropic"`). | +| `name_pool` | string[] | `[]` | Pool of display names for agent instances spawned from this persona. When non-empty, the spawning system picks a name from this pool for each new agent instance, enabling multiple concurrent agents from the same persona to have distinct identities. | + +Unknown fields MUST be ignored by readers (forward compatibility). + +### Prohibited: secrets in content + +The content body is **public and unencrypted**. It MUST NOT contain secrets (API keys, tokens, credentials, or any sensitive environment variables). In particular, an `env_vars` field MUST NOT appear in the content body. + +Secrets required by agents spawned from a persona MUST be conveyed through a separate encrypted channel — specifically, the [NIP-AE](NIP-AE.md) engram at `mem/persona` (which is NIP-44 encrypted to the agent↔owner conversation key) or through out-of-band injection at spawn time. + +## Encryption rationale + +Persona events carry no encryption. This is deliberate: + +- Personas are *configuration*, not *state*. They describe what an agent should be, not what it has learned. +- Encryption would prevent relay-side indexing, search, and third-party client rendering — all desirable for definitions that workspace members should browse. +- Operators who need confidentiality should use relay-level access control ([NIP-42](42.md) authentication + [NIP-29](29.md) group membership) rather than event-level encryption. + +## Replacement semantics + +Standard NIP-33: for a given `(pubkey, kind:30175, d_tag)`, only the event with the greatest `created_at` is the **head**. Ties are broken by lowest event `id` per [NIP-01](01.md). Relays SHOULD return only the head; clients MUST select the head from any multi-event response. + +## Writing + +To write or update a persona with slug `s` and body `b`: + +1. Validate `s` against the slug grammar. Reject if invalid. +2. Serialize `b` to JSON. Reject if the serialized body exceeds 65,535 bytes. +3. Compute the head of `s` per NIP-33 and let `T` be its `created_at` (or 0 if no head exists). Set `created_at := max(now, T + 1)`. Monotonicity ensures fresh writes always supersede prior heads regardless of clock skew. +4. Tags: `[["d", s]]`. +5. Sign with `seckey_o` and publish to configured relays. + +## Reading + +To read a single persona by slug `s`: + +``` +Filter: {kinds: [30175], authors: [pubkey_o], "#d": [s]} +``` + +Select the head per NIP-33 rules. Parse `content` as JSON. Validate required fields. + +To list all personas for an owner: + +``` +Filter: {kinds: [30175], authors: [pubkey_o]} +``` + +Returns all heads. Clients scope by author pubkey — two different owners MAY publish personas with the same slug; these are independent events. + +## Deletion + +Owners MAY publish [NIP-09](09.md) deletion requests targeting persona events. A deletion request MUST be authored by the same key (`pubkey_o`). Such requests SHOULD include `["k", "30175"]` and use an `a`-tag identifier `30175::`. + +A subsequent write with a later timestamp resurrects the slug under NIP-33 replacement semantics. + +## Relationships to other NIPs + +### NIP-AE (Agent Engrams) + +Agents spawned from a persona MAY store a private snapshot at the reserved engram slug `mem/persona`. This engram: + +- Is NIP-44 encrypted (confidential to agent + owner) +- MAY contain secrets (env vars, API keys) that the public persona event must not carry +- Serves as the agent's private, mutable copy of its originating configuration +- References back to the persona event by slug convention, not by event ID + +The `mem/persona` slug conforms to [NIP-AE](NIP-AE.md)'s slug grammar and requires no amendment to that spec. + +### NIP-OA (Owner Attestation) + +Agents spawned from a persona carry [NIP-OA](NIP-OA.md) owner attestation — an `auth` tag proving that `pubkey_o` authorized the agent's key. The persona event itself does not contain attestation; it is the *definition* from which attestation is issued at spawn time. + +## Relay behavior + +- The relay MUST accept `kind:30175` events that pass standard NIP-33 validation (valid signature, exactly one `d` tag with a non-empty value). +- The relay stores persona events globally (`channel_id = NULL`); they are not channel-scoped. +- The relay is NOT required to validate that `content` parses as valid `PersonaEventContent` JSON. Relays are dumb stores per Nostr convention; content validation is a client responsibility. +- The relay MUST enforce that the `d` tag is non-empty (standard NIP-33 requirement for parameterized replaceable events). + +## Security considerations + +- **No encryption.** System prompts, model names, runtime identifiers, and all configuration are visible to anyone with relay read access. Operators MUST NOT store secrets in persona event content. +- **System prompt sensitivity.** System prompts may contain security-relevant behavioral instructions. Publishing them unencrypted enables adversarial prompt extraction. Operators who consider system prompts confidential SHOULD NOT publish them in persona events, or SHOULD use a relay with appropriate access controls. +- **Write authority.** Only the holder of `seckey_o` can publish or replace persona events. NIP-33 replacement is scoped by pubkey — no spoofing risk from other relay members. +- **Slug collision across pubkeys.** Two different owners can publish personas with the same slug. Clients MUST always scope queries by author pubkey, not just slug. +- **Metadata exposure.** The `(pubkey, kind:30175, slug)` triple reveals persona existence. Event timestamps reveal edit history. +- **No owner write authority over agents.** Persona events define *what* an agent should be; they do not grant runtime control over a running agent. The agent consumes the persona at spawn time. Updates to the persona event do not automatically propagate to running agents. + +## Reference test vectors + +> **TEST KEYS — DO NOT USE IN PRODUCTION.** The keys below are pinned for reproducibility. Production code MUST source randomness from a CSPRNG. + +### Inputs + +``` +seckey_o = 0000000000000000000000000000000000000000000000000000000000000001 +schnorr_aux = 0000000000000000000000000000000000000000000000000000000000000000 +``` + +### Derived + +``` +pubkey_o = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +``` + +### Event 1 — create persona with all fields + +```jsonc +// Body (exact UTF-8, no trailing whitespace): +{"display_name":"Test Agent","system_prompt":"You are a test assistant.","avatar_url":"https://example.com/avatar.png","runtime":"goose","model":"claude-opus-4","provider":"anthropic","name_pool":["Alpha","Beta"]} +``` + +``` +kind = 30175 +pubkey = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +created_at = 1700000000 +tags = [["d", "test-agent"]] +content = {"display_name":"Test Agent","system_prompt":"You are a test assistant.","avatar_url":"https://example.com/avatar.png","runtime":"goose","model":"claude-opus-4","provider":"anthropic","name_pool":["Alpha","Beta"]} +id = +sig = +``` + +### Event 2 — minimal persona (required fields only) + +```jsonc +// Body: +{"display_name":"Minimal","system_prompt":"Hello."} +``` + +``` +kind = 30175 +pubkey = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +created_at = 1700000001 +tags = [["d", "minimal"]] +content = {"display_name":"Minimal","system_prompt":"Hello."} +id = +sig = +``` + +### Event 3 — replacement (same slug, higher `created_at`) + +```jsonc +// Updated body (system_prompt changed): +{"display_name":"Test Agent","system_prompt":"You are an updated test assistant.","avatar_url":"https://example.com/avatar.png","runtime":"goose","model":"claude-opus-4","provider":"anthropic","name_pool":["Alpha","Beta","Gamma"]} +``` + +``` +kind = 30175 +pubkey = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +created_at = 1700000002 +tags = [["d", "test-agent"]] +content = {"display_name":"Test Agent","system_prompt":"You are an updated test assistant.","avatar_url":"https://example.com/avatar.png","runtime":"goose","model":"claude-opus-4","provider":"anthropic","name_pool":["Alpha","Beta","Gamma"]} +id = +sig = +``` + +After Event 3, the head for slug `test-agent` is Event 3 (greatest `created_at`). Event 1 is superseded. + +### Head selection with tiebreak + +If two events share `created_at = 1700000000` and slug `test-agent`, the head is the event with the lexicographically lowest `id` (hex comparison per NIP-01). + +### Implementation notes + +Unlike [NIP-AE](NIP-AE.md), persona events involve no encryption, no HMAC derivation, and no conversation key. The test vectors are standard NIP-33 events with JSON content — implementations need only: + +1. Correct NIP-01 event-id serialization: `json.dumps([0, pubkey, created_at, kind, tags, content], separators=(",", ":"), ensure_ascii=False)` over UTF-8 bytes. +2. BIP-340 Schnorr signing with the pinned aux value. +3. JSON serialization of the content body with no trailing whitespace or BOM. diff --git a/scripts/start-relay-for-tests.sh b/scripts/start-relay-for-tests.sh new file mode 100755 index 000000000..46f1a0fd0 --- /dev/null +++ b/scripts/start-relay-for-tests.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# ============================================================================= +# start-relay-for-tests.sh — Start the Buzz relay and its backing services +# ============================================================================= +# Shared script for CI jobs that need a running relay. Starts docker compose +# services, waits for health, applies the schema, builds the relay, starts it, +# and polls readiness. +# +# Usage: +# ./scripts/start-relay-for-tests.sh [--profile ] +# +# Options: +# --profile Cargo build profile (default: ci) +# +# Exports: +# RELAY_URL=ws://localhost:3000 +# ============================================================================= +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +# ── Defaults ────────────────────────────────────────────────────────────────── + +CARGO_PROFILE="${CARGO_PROFILE:-ci}" + +# ── Parse args ──────────────────────────────────────────────────────────────── + +while [[ $# -gt 0 ]]; do + case "$1" in + --profile) + CARGO_PROFILE="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +# ── Colors ──────────────────────────────────────────────────────────────────── + +BLUE='\033[0;34m' +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +log() { echo -e "${BLUE}[relay-test]${NC} $*"; } +ok() { echo -e "${GREEN}[relay-test]${NC} $*"; } +err() { echo -e "${RED}[relay-test]${NC} $*" >&2; } + +# ── Start docker compose services ──────────────────────────────────────────── + +cd "${REPO_ROOT}" + +log "Starting docker compose services..." +docker compose up -d postgres redis typesense minio minio-init + +# ── Wait for services to be healthy ────────────────────────────────────────── + +wait_healthy() { + local service="$1" + local container="$2" + log "Waiting for ${service}..." + for attempt in $(seq 1 60); do + status=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "not_found") + if [ "${status}" = "healthy" ]; then + ok "${service} is healthy" + return 0 + fi + sleep 2 + done + err "${service} did not become healthy within 120s" + docker logs "${container}" || true + return 1 +} + +wait_healthy "Postgres" "buzz-postgres" +wait_healthy "Redis" "buzz-redis" +wait_healthy "Typesense" "buzz-typesense" +wait_healthy "MinIO" "buzz-minio" + +# ── Apply database schema ──────────────────────────────────────────────────── + +log "Applying database schema..." +export PGHOST=localhost +export PGPORT=5432 +export PGUSER=buzz +export PGPASSWORD=buzz_dev +export PGDATABASE=buzz + +./bin/pgschema apply --file schema/schema.sql --auto-approve +ok "Schema applied" + +# ── Build relay ────────────────────────────────────────────────────────────── + +log "Building relay (profile: ${CARGO_PROFILE})..." +cargo build --profile "${CARGO_PROFILE}" -p buzz-relay +ok "Relay built" + +# ── Start relay ────────────────────────────────────────────────────────────── + +log "Starting relay..." +nohup env \ + DATABASE_URL=postgres://buzz:buzz_dev@localhost:5432/buzz \ + REDIS_URL=redis://localhost:6379 \ + TYPESENSE_URL=http://localhost:8108 \ + TYPESENSE_API_KEY=buzz_dev_key \ + RELAY_URL=ws://localhost:3000 \ + BUZZ_BIND_ADDR=0.0.0.0:3000 \ + BUZZ_REQUIRE_AUTH_TOKEN=false \ + BUZZ_RECONCILE_CHANNELS=true \ + BUZZ_GIT_PROBE_WRITERS=8 \ + "./target/${CARGO_PROFILE}/buzz-relay" > /tmp/buzz-relay.log 2>&1 & +echo $! > /tmp/buzz-relay.pid + +# ── Poll readiness ─────────────────────────────────────────────────────────── + +log "Waiting for relay readiness..." +for attempt in $(seq 1 60); do + if ! kill -0 "$(cat /tmp/buzz-relay.pid)" 2>/dev/null; then + err "Relay process died" + cat /tmp/buzz-relay.log + exit 1 + fi + status_code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/_readiness || true) + if [ "${status_code}" = "200" ]; then + ok "Relay is ready at ws://localhost:3000" + export RELAY_URL=ws://localhost:3000 + exit 0 + fi + sleep 1 +done + +err "Relay did not become ready within 60s" +cat /tmp/buzz-relay.log +exit 1