Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/_ci-relay.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ jobs:
-p buzz-test-client \
--lib \
--test e2e_event_reminder \
--test e2e_author_only_deletion \
--archive-file target/ci/backend-integration-tests.tar.zst
- name: Build PostgreSQL test archive
if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' && matrix.artifact == 'postgres-tests'
Expand Down Expand Up @@ -580,10 +581,12 @@ jobs:
run: |
cargo nextest run \
--archive-file target/ci/backend-integration-tests.tar.zst \
-E 'binary(e2e_event_reminder)' \
-E 'binary(e2e_event_reminder) | binary(e2e_author_only_deletion)' \
--run-ignored ignored-only
env:
RELAY_URL: ws://localhost:3000
# schema/schema.sql (applied above) indexes kind:5 content.
BUZZ_TEST_SEARCH_INDEXES_DELETIONS: "1"
- name: Upload relay log
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
Expand Down
12 changes: 10 additions & 2 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ name: Docker image
# the relay image version tracks crates/buzz-relay/Cargo.toml, never desktop.
#
# Triggers:
# - push to main → :main + :sha-<7>
# + :debug-main + :debug-sha-<7>
# - push to main → :main + :sha-<7> + :main-<commit UTC>-<7>
# + the matching :debug-* tags
# - push tags relay-v*.*.* → :{version} + :{major}.{minor} + :{major}
# + matching :debug-* tags
# (+ :latest/:debug-latest for stable releases)
Expand All @@ -34,6 +34,12 @@ name: Docker image
# intact) and the bare version flows straight to the semver parser. On a
# real push event value= is empty and the match strips relay-v from the ref.
#
# :main-<YYYYMMDDHHmmss>-<7> names the same main build as :sha-<7>, prefixed
# with its UTC commit time so the tag sorts by build order. Kargo's Lexical
# strategy picks the newest from the tag list alone; :sha-<7> cannot be
# ordered without fetching every image's metadata. It uses the commit date,
# not the run date, so a rerun of the same commit re-pushes the same tag.
#
# The :latest tag tracks the latest STABLE relay release: metadata-action's
# `flavor.latest=auto` (its default) emits :latest only for non-prerelease
# semver, so relay-v0.3.0-rc.1 publishes :0.3.0-rc.1 without moving :latest,
Expand Down Expand Up @@ -165,6 +171,7 @@ jobs:
type=semver,pattern={{version}},match=^relay-v(.*)$,value=${{ inputs.version }}
type=semver,pattern={{major}}.{{minor}},match=^relay-v(.*)$,value=${{ inputs.version }}
type=semver,pattern={{major}},match=^relay-v(.*)$,value=${{ inputs.version }}
type=raw,value=main-{{commit_date 'YYYYMMDDHHmmss'}}-{{sha}},enable=${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
labels: |
org.opencontainers.image.title=Buzz
org.opencontainers.image.description=WebSocket relay server for the Buzz communications platform
Expand Down Expand Up @@ -376,6 +383,7 @@ jobs:
type=semver,pattern={{version}},match=^relay-v(.*)$,value=${{ inputs.version }}
type=semver,pattern={{major}}.{{minor}},match=^relay-v(.*)$,value=${{ inputs.version }}
type=semver,pattern={{major}},match=^relay-v(.*)$,value=${{ inputs.version }}
type=raw,value=main-{{commit_date 'YYYYMMDDHHmmss'}}-{{sha}},enable=${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}

- name: Create and push manifest list
id: manifest
Expand Down
3 changes: 3 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ The `kind` integer is the only dispatch switch. The relay routes, stores, and fa
| 45003 | KIND_FORUM_COMMENT | Forum thread reply |
| 46001–46012 | KIND_WORKFLOW_* | Workflow execution events |
| 20001 | KIND_PRESENCE_UPDATE | Ephemeral presence heartbeat |
| 20003 | KIND_AGENT_PRESENCE | Ephemeral presence heartbeat an owner sends for its agents |

`buzz-core` defines each event kind as a `pub const u32` and exports the full registry as `ALL_KINDS: &[u32]` (127 kinds at the time of writing); `crates/buzz-core/src/kind.rs` is the source of truth for the current list. Kinds are `u32` (NIP-01 specifies unsigned integer; `u32` covers the full range). Buzz uses both standard Nostr kinds (e.g., kind 7 for reactions) and custom ranges (40000+).

Expand Down Expand Up @@ -305,6 +306,8 @@ Ephemeral events bypass DB storage, audit, and search. Two sub-paths:
```
Presence events skip membership checks and use local-only fan-out. Multi-node presence fan-out would require Redis pub/sub (documented as future work).

**Agent presence (kind 20003):** an owner's one heartbeat for all its agents, so an app that runs agents without a socket per agent can still show them online. Content is the status (`online`, `away` or `offline`) and each `p` tag names an agent, at most 256. WebSocket only. The relay keeps the agents the signer owns (`users.agent_owner_pubkey`, through the owner cache), skips the rest, and sets or clears their presence exactly as their own kind 20001 would, so the presence snapshot reports them unchanged. It is not fanned out, since live presence is matched by author; viewers see it on their next snapshot read.

**Other ephemeral events (e.g., typing indicators):**
```
1. VERIFY — spawn_blocking(verify_event)
Expand Down
54 changes: 54 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ members = [
"crates/buzz-mesh-smoke",
"crates/buzz-dev-mcp",
"crates/buzz-voice",
"crates/buzz-audio-client",
"crates/buzz-backend-kubernetes",
"crates/buzz-datastore-tracing",
"examples/countdown-bot",
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ FROM chef AS builder
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
cmake \
pkg-config \
libssl-dev \
ca-certificates \
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.push-gateway
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
ARG BUZZ_PUSH_CARGO_FEATURES=""
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential pkg-config libssl-dev ca-certificates \
&& apt-get install -y --no-install-recommends build-essential cmake pkg-config libssl-dev ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=planner /build/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json --features "$BUZZ_PUSH_CARGO_FEATURES"
Expand Down
4 changes: 3 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,8 @@ test-unit:
#!/usr/bin/env bash
set -euo pipefail
./scripts/test-ensure-local-relay-key.sh
# Shared Huddle transport, codec, and loopback authentication regressions.
cargo test -p buzz-audio-client
if command -v cargo-nextest &>/dev/null; then
cargo nextest run -p buzz-core -p buzz-auth --lib
cargo nextest run -p buzz-audit --lib
Expand Down Expand Up @@ -528,7 +530,7 @@ test-unit:
# are infra-free; config parses env into a struct and never connects.
# `^config::` is anchored so it does not also select nip_fi_config.
cargo nextest run -p buzz-relay --lib --bin buzz-relay \
-E 'test(/^api::admin::/) + test(/^push_runtime::/) + test(/^handlers::channel_authz::/) + test(/^handlers::moderation_authz::/) + test(/^handlers::side_effects::tests::/) + test(/^storage_sweep::tests::/) + test(/^nip_fi_core::tests::/) + test(/^nip_fi_http::tests::/) + test(/^nip_fi_config::tests::/) + test(/^readiness::tests::/) + test(/^router::tests::/) + test(/^api::parse_query_tests::/) + test(/^api::git::transport::off_mode_precedence_tests::/) + test(/^audio::join::tests::/) + test(/^audio::handler::tests::/) + test(/^nip_fi_gate::tests::/) + test(/^nip_fi_session::tests::/) + test(/^nip_fi_shadow::tests::/) + test(/^nip_fi_shadow_session::tests::/) + test(/^startup_steps::tests::/) + test(/^telemetry::tests::/) + test(/^config::tests::/) + test(=state::tests::neither_a_confirmed_inactive_community_nor_a_failed_lookup_admits_the_socket) + test(=state::tests::periodic_revalidation_disconnects_inside_the_fenced_callback) + test(=state::tests::bare_community_disconnect_fails_closed_when_the_fence_is_unavailable) + test(=connection::tests::send_loop_sends_policy_close_when_community_is_archived) + test(=state::tests::community_disconnect_then_nip_fi_keeps_community_deleted_reason) + test(=state::tests::disconnect_community_wins_reason_losing_nip_fi_does_not_enqueue_frame) + test(=handlers::req::tests::timed_out_historical_read_deregisters_before_closed) + test(=handlers::req::tests::superseded_timeout_leaves_replacement_intact) + test(=handlers::req::tests::search_claim_retires_live_and_yields_to_replacement) + test(=handlers::req::tests::concurrent_claims_and_stale_teardowns_keep_the_last_owner) + test(=handlers::req::tests::timeout_closed_is_emitted_before_a_replacement_can_claim) + test(=handlers::req::tests::revoke_then_replacement_keeps_replacement_whole) + test(=handlers::req::tests::claims_after_connection_cleanup_are_refused) + test(=handlers::req::tests::dropped_terminal_frame_cancels_connection) + test(=handlers::req::tests::revoke_dropped_terminal_frame_cancels_connection) + (kind(bin) & (test(/^tests::/) + test(/^composition_tests::/) + test(/^env_filter_tests::/)) - test(/^tests::postgres_tests::/))'
-E 'test(/^api::admin::/) + test(/^push_runtime::/) + test(/^handlers::channel_authz::/) + test(/^handlers::moderation_authz::/) + test(/^handlers::side_effects::tests::/) + test(/^storage_sweep::tests::/) + test(/^nip_fi_core::tests::/) + test(/^nip_fi_http::tests::/) + test(/^nip_fi_config::tests::/) + test(/^readiness::tests::/) + test(/^router::tests::/) + test(/^api::parse_query_tests::/) + test(/^api::git::transport::off_mode_precedence_tests::/) + test(/^audio::join::tests::/) + test(/^audio::handler::tests::/) + test(/^nip_fi_gate::tests::/) + test(/^nip_fi_session::tests::/) + test(/^nip_fi_shadow::tests::/) + test(/^nip_fi_shadow_session::tests::/) + test(/^startup_steps::tests::/) + test(/^telemetry::tests::/) + test(/^config::tests::/) + test(=state::tests::neither_a_confirmed_inactive_community_nor_a_failed_lookup_admits_the_socket) + test(=state::tests::periodic_revalidation_disconnects_inside_the_fenced_callback) + test(=state::tests::bare_community_disconnect_fails_closed_when_the_fence_is_unavailable) + test(=connection::tests::send_loop_sends_policy_close_when_community_is_archived) + test(=state::tests::community_disconnect_then_nip_fi_keeps_community_deleted_reason) + test(=state::tests::disconnect_community_wins_reason_losing_nip_fi_does_not_enqueue_frame) + test(=handlers::req::tests::timed_out_historical_read_deregisters_before_closed) + test(=handlers::req::tests::superseded_timeout_leaves_replacement_intact) + test(=handlers::req::tests::search_claim_retires_live_and_yields_to_replacement) + test(=handlers::req::tests::concurrent_claims_and_stale_teardowns_keep_the_last_owner) + test(=handlers::req::tests::timeout_closed_is_emitted_before_a_replacement_can_claim) + test(=handlers::req::tests::revoke_then_replacement_keeps_replacement_whole) + test(=handlers::req::tests::claims_after_connection_cleanup_are_refused) + test(=handlers::req::tests::dropped_terminal_frame_cancels_connection) + test(=handlers::req::tests::revoke_dropped_terminal_frame_cancels_connection) + test(=handlers::req::tests::count_fallback_covers_filters_that_can_match_private_deletions) + (kind(bin) & (test(/^tests::/) + test(/^composition_tests::/) + test(/^env_filter_tests::/)) - test(/^tests::postgres_tests::/))'
# Note on audio::join::tests scope: the full suite is infra-free (no DB,
# no Redis). The infra-free audio/FI regression witnesses — bootstrap
# ordering barrier, CommitConfirmed arm, pending-close invisibility,
Expand Down
1 change: 1 addition & 0 deletions NOSTR.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ PGPASSWORD=buzz_dev psql -h localhost -U buzz -d buzz -c \
| **Group members (kind:39002)** | ✅ | Relay-signed; `d` tag + `p` tags for all members |
| **Membership notifications** | ✅ | kind:44100 (added) / kind:44101 (removed); relay-signed, community-global scope (`channel_id=None` inside the connected community) |
| **Presence (kind:20001)** | ✅ | Ephemeral; arbitrary status string (truncated to 128 chars); writes to Redis (`set_presence`/`clear_presence` on `"offline"`), then fan-out to local subscribers. In multi-community mode presence is scoped to the connected community. |
| **Agent presence (kind:20003)** | ✅ | Ephemeral; an owner's presence for the agents it owns, named by `p` tags (at most 256); `online`/`away` set and `offline` clears each agent's presence as its own kind:20001 would. Not fanned out. |
| **Typing indicators (kind:20002)** | ✅ | Ephemeral, not stored; published via Redis pub/sub (multi-node capable unlike presence fan-out) |
| **NIP-42 authentication** | ✅ | Proactive challenge; optional pubkey allowlist |
| **NIP-11 relay info** | ✅ | `GET /` with `Accept: application/nostr+json` |
Expand Down
3 changes: 3 additions & 0 deletions crates/buzz-acp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ buzz-persona = { path = "../buzz-persona" }
git-credential-nostr = { path = "../git-credential-nostr" }
git-sign-nostr = { path = "../git-sign-nostr" }
tempfile = "3"
# Holds the key folder's .lock (flock / LockFileEx). std's File::lock is 1.89+, past the
# 1.88 MSRV; buzz-agent uses fs2 for the same reason.
fs2 = "0.4"
zeroize = { workspace = true }

# Nostr
Expand Down
Loading
Loading