docs: finish Buzz rename cleanup (#974)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Wes
2026-06-11 09:28:18 -07:00
committed by GitHub
co-authored by Pinky
parent 6f3733d43b
commit 79bcee55cb
12 changed files with 302 additions and 302 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
name: Sprig
# Builds and publishes Sprig — one deploy-anywhere Linux multicall binary for:
# buzz-acp ACP harness that bridges Sprout events to the LLM agent
# buzz-acp ACP harness that bridges Buzz events to the LLM agent
# buzz-agent ACP-compliant agent (spawns MCP, calls LLMs)
# buzz-dev-mcp Developer MCP server (multicall: rg, tree, buzz,
# git-credential-nostr, git-sign-nostr)
+47 -47
View File
@@ -1,6 +1,6 @@
# AGENTS.md — AI Agent Contributor Guide
This guide is for AI agents contributing to the Sprout codebase. It covers
This guide is for AI agents contributing to the Buzz codebase. It covers
agent-specific context and conventions. For general contributor info (setup,
code style, PR process, architecture), see [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -8,7 +8,7 @@ code style, PR process, architecture), see [CONTRIBUTING.md](CONTRIBUTING.md).
## Ecosystem
Sprout spans five repos. This one (`block/sprout`) is the OSS source for the relay, desktop, mobile, and CLI. The others handle internal builds and deployment:
Buzz spans five repos. This one (`block/sprout`) is the OSS source for the relay, desktop, mobile, and CLI. The others handle internal builds and deployment:
| Repo | Purpose |
|------|---------|
@@ -37,32 +37,32 @@ access information.
```
crates/
# Relay + core
sprout-relay # WebSocket relay server — main entry point; also hosts git + huddle audio
sprout-core # Core types, event verification, filter matching, kind registry
sprout-db # Postgres event store and data access layer
sprout-auth # Authentication and authorization
sprout-pubsub # Redis pub/sub fan-out, presence, typing indicators
sprout-search # Typesense-backed full-text search
sprout-audit # Hash-chain audit log
sprout-media # Blossom/S3 media storage
buzz-relay # WebSocket relay server — main entry point; also hosts git + huddle audio
buzz-core # Core types, event verification, filter matching, kind registry
buzz-db # Postgres event store and data access layer
buzz-auth # Authentication and authorization
buzz-pubsub # Redis pub/sub fan-out, presence, typing indicators
buzz-search # Typesense-backed full-text search
buzz-audit # Hash-chain audit log
buzz-media # Blossom/S3 media storage
# Agent surface
sprout-acp # ACP harness bridging Sprout events to AI agents
sprout-agent # Minimal ACP-compliant agent (non-streaming, tool-calls-as-output)
sprout-dev-mcp # Developer MCP server — shell + file-edit tools
sprout-persona # Agent persona packs
sprout-workflow # YAML-as-code workflow engine (evalexpr conditions)
buzz-acp # ACP harness bridging Buzz events to AI agents
buzz-agent # Minimal ACP-compliant agent (non-streaming, tool-calls-as-output)
buzz-dev-mcp # Developer MCP server — shell + file-edit tools
buzz-persona # Agent persona packs
buzz-workflow # YAML-as-code workflow engine (evalexpr conditions)
# Clients + interop
sprout-proxy # Nostr client compatibility proxy (NIP-28)
sprout-pair-relay # Ephemeral sidecar relay for NIP-AB device pairing
sprout-pairing-cli # CLI for NIP-AB device pairing interop testing
buzz-proxy # Nostr client compatibility proxy (NIP-28)
buzz-pair-relay # Ephemeral sidecar relay for NIP-AB device pairing
buzz-pairing-cli # CLI for NIP-AB device pairing interop testing
git-sign-nostr # Sign git objects with a Nostr key
git-credential-nostr # Git credential helper for Nostr-authed push/fetch
# Tooling + shared
sprout-cli # Agent-first CLI
sprout-sdk # Typed Nostr event builders
sprout-admin # Operator CLI for relay administration
sprout-ws-client # Shared NIP-42 WebSocket client (connect, auth, publish)
sprout-test-client # Integration test client and E2E test suite
buzz-cli # Agent-first CLI
buzz-sdk # Typed Nostr event builders
buzz-admin # Operator CLI for relay administration
buzz-ws-client # Shared NIP-42 WebSocket client (connect, auth, publish)
buzz-test-client # Integration test client and E2E test suite
sprig # All-in-one harness bundling ACP, agent, and dev MCP
desktop/ # Tauri 2 + React 19 desktop app
@@ -94,8 +94,8 @@ See CONTRIBUTING.md for full setup details and dependency requirements.
Run `just ci` before every PR — it runs `fmt` + `clippy` + desktop lint +
unit tests + builds. Clippy passing does not mean fmt passes; run both.
Run `just test` for integration tests if you touched `sprout-relay`,
`sprout-db`, or `sprout-auth` — these require a running Postgres and Redis.
Run `just test` for integration tests if you touched `buzz-relay`,
`buzz-db`, or `buzz-auth` — these require a running Postgres and Redis.
**Pre-commit hooks** are installed automatically by `just setup` and auto-fix
formatting via `stage_fixed`. Pre-commit runs fix variants in parallel (Rust
@@ -116,13 +116,13 @@ Additional rules:
## Key Patterns
**Dual API surface**: Sprout exposes both a REST API and a NIP-29 WebSocket
relay. Both paths converge on shared DB functions in `sprout-db`. When adding
**Dual API surface**: Buzz exposes both a REST API and a NIP-29 WebSocket
relay. Both paths converge on shared DB functions in `buzz-db`. When adding
a feature, implement the shared DB logic first, then wire up both surfaces.
**Prefer Nostr events over new REST endpoints**: For new feature work, model
the operation as a Nostr event (new kind in `sprout-core/src/kind.rs`, handler
in `sprout-relay`) rather than adding a new REST endpoint. REST is reserved
the operation as a Nostr event (new kind in `buzz-core/src/kind.rs`, handler
in `buzz-relay`) rather than adding a new REST endpoint. REST is reserved
for things that genuinely need an HTTP-only surface: media upload/download
(Blossom), OAuth callbacks, health checks, and the existing read endpoints
that proxy DB queries. Two helpful endpoints already exist and rarely need
@@ -130,7 +130,7 @@ to be duplicated:
- `POST /events` — submit any signed event (same path the WebSocket uses).
- `POST /query` — Nostr REQ filters over HTTP. NIP-50 `search` filters
are routed to `sprout-search` (Typesense-backed) automatically.
are routed to `buzz-search` (Typesense-backed) automatically.
- `POST /count` — Nostr COUNT filters over HTTP.
If you find yourself reaching for a new REST endpoint, first check whether
@@ -140,15 +140,15 @@ fan-out, NIP-29 scoping, and the existing auth pipeline for free.
Reference https://github.com/nostr-protocol/nips
**Event kinds**: All event kind integers are defined in
`sprout-core/src/kind.rs`. New features get new kind integers — add them here
`buzz-core/src/kind.rs`. New features get new kind integers — add them here
first, then implement handling in the relay.
**Channel scoping**: Channels use `h` tags (NIP-29 group tag), not `e` tags.
Filters and queries must scope to `h` tags when operating within a channel.
**Agent-facing operations go in `sprout-cli`**: New agent-facing features belong in `sprout-cli` — add a subcommand there first, then wire the REST/WebSocket call in `client.rs`. `sprout-dev-mcp` (shell + file tools for `sprout-agent`) is separate.
**Agent-facing operations go in `buzz-cli`**: New agent-facing features belong in `buzz-cli` — add a subcommand there first, then wire the REST/WebSocket call in `client.rs`. `buzz-dev-mcp` (shell + file tools for `buzz-agent`) is separate.
**Workflow conditions**: `sprout-workflow` uses
**Workflow conditions**: `buzz-workflow` uses
[evalexpr](https://docs.rs/evalexpr) for condition evaluation. Keep expressions
simple and testable.
@@ -158,20 +158,20 @@ check existing reply handlers for the pattern.
---
## Agent CLI (`sprout-cli`)
## Agent CLI (`buzz-cli`)
`sprout` is the agent-first CLI. Auth env vars
(`SPROUT_RELAY_URL`, `SPROUT_PRIVATE_KEY`, `SPROUT_AUTH_TAG`) are auto-injected
`buzz` is the agent-first CLI. Auth env vars
(`BUZZ_RELAY_URL`, `BUZZ_PRIVATE_KEY`, `BUZZ_AUTH_TAG`) are auto-injected
by the ACP harness into managed agent subprocesses. In development, set
`SPROUT_PRIVATE_KEY` and `SPROUT_RELAY_URL` in your environment manually.
`BUZZ_PRIVATE_KEY` and `BUZZ_RELAY_URL` in your environment manually.
### Building the CLI
```bash
cargo build --release -p sprout-cli
cargo build --release -p buzz-cli
```
Binary location: `./target/release/sprout`. Add `./target/release` to `PATH`
Binary location: `./target/release/buzz`. Add `./target/release` to `PATH`
or invoke with the full path.
### Deep Links
@@ -180,7 +180,7 @@ or invoke with the full path.
thread. To read the linked thread:
```bash
sprout messages thread --channel <uuid> --event <hex> --format compact
buzz messages thread --channel <uuid> --event <hex> --format compact
```
Extract `channel` and `id` from the URL query parameters. The optional
@@ -192,9 +192,9 @@ All reads return sig-stripped JSON arrays; all writes return
0=ok, 1=input error, 2=network/relay, 3=auth, 4=other, 5=write conflict (NIP-33 LWW).
`--format compact` is a **global** flag — it goes before the subcommand:
`sprout --format compact channels list`, NOT `sprout channels list --format compact`.
`buzz --format compact channels list`, NOT `buzz channels list --format compact`.
See `crates/sprout-cli/TESTING.md` for the full live-testing runbook.
See `crates/buzz-cli/TESTING.md` for the full live-testing runbook.
---
@@ -205,7 +205,7 @@ just test-unit # unit tests, no infrastructure needed
just test # full integration suite (requires Postgres + Redis)
```
E2E tests live in `crates/sprout-test-client/tests/`:
E2E tests live in `crates/buzz-test-client/tests/`:
- `e2e_relay.rs` — WebSocket relay protocol
- `e2e_rest_api.rs` — REST endpoint coverage
- `e2e_tokens.rs` — auth token flows
@@ -220,7 +220,7 @@ See [TESTING.md](TESTING.md) for the full multi-agent E2E guide.
### Desktop Screenshots (Playwright)
> **Do NOT use `sprout upload`, the relay media endpoint, or any third-party
> **Do NOT use `buzz upload`, the relay media endpoint, or any third-party
> image host for PR screenshots.** Relay media URLs fail through GitHub's camo
> proxy. Always use `scripts/post-screenshots.sh` — see the `desktop-screenshot`
> skill for the full workflow.
@@ -246,7 +246,7 @@ Output is a PNG path on stdout.
Use `--messages` to inject content into a channel before capture. The JSON file
is an array of objects — `channelName` and `content` are required, all other
fields are optional and passed through to `__SPROUT_E2E_EMIT_MOCK_MESSAGE__`:
fields are optional and passed through to `__BUZZ_E2E_EMIT_MOCK_MESSAGE__`:
```json
[
@@ -350,7 +350,7 @@ must run BEFORE `installMockBridge(page)` — React reads state on mount, the
bridge triggers mount.
**Live messages:** Call `waitForMockLiveSubscription(page, channelName)` before
`__SPROUT_E2E_EMIT_MOCK_MESSAGE__` — messages are silently dropped without a
`__BUZZ_E2E_EMIT_MOCK_MESSAGE__` — messages are silently dropped without a
subscription. Navigate to the channel first (triggers subscription), then away
(so unread indicators appear), then inject.
@@ -393,7 +393,7 @@ description. See [PR #803](https://github.com/block/sprout/pull/803).
## Common Gotchas
1. **Kind `39000` for channel metadata, not `41`** — kind 41 is NIP-01 (unused). All kinds defined in `sprout-core/src/kind.rs`.
1. **Kind `39000` for channel metadata, not `41`** — kind 41 is NIP-01 (unused). All kinds defined in `buzz-core/src/kind.rs`.
2. **Relay queries must specify `kinds`** — omitting `kinds` triggers the p-gate (403). Always include explicit kind filters.
3. **`messages search` must include `--kinds`** — an open-ended search (no kinds) hits the relay p-gate and returns 403. Pass at least `--kinds 9,45001,45003` to scope the query.
4. **Worktrees: `cd` in the same command** — shell CWD doesn't persist between tool calls. Use `cd /path && cargo build` as one command.
+67 -67
View File
@@ -1,12 +1,12 @@
# Sprout Architecture
# Buzz Architecture
## 1. Executive Summary
Sprout is a self-hosted team communication platform built on the Nostr protocol (NIP-01 wire format), where AI agents and humans are first-class equals. Every action — a chat message, a reaction, a workflow step, a canvas update, a huddle event — is a cryptographically signed Nostr event identified by a `kind` integer. Adding a new feature means defining a new kind number; existing clients see nothing and break nothing.
Buzz is a self-hosted team communication platform built on the Nostr protocol (NIP-01 wire format), where AI agents and humans are first-class equals. Every action — a chat message, a reaction, a workflow step, a canvas update, a huddle event — is a cryptographically signed Nostr event identified by a `kind` integer. Adding a new feature means defining a new kind number; existing clients see nothing and break nothing.
The relay is the single source of truth. All reads and writes flow through it. There is no peer-to-peer event exchange, no gossip, no replication — just clients connecting to one relay over WebSocket, and the relay enforcing auth, verifying signatures, persisting events, fanning out to subscribers, indexing for search, and triggering automation.
Sprout is a Rust monorepo, licensed Apache 2.0 under Block, Inc.
Buzz is a Rust monorepo, licensed Apache 2.0 under Block, Inc.
---
@@ -16,14 +16,14 @@ Sprout is a Rust monorepo, licensed Apache 2.0 under Block, Inc.
┌─────────────────────────────────────────────────────────────────────┐
│ CLIENTS │
│ │
│ Human (Nostr app, web, mobile) Agent (CLI tools via sprout-cli) │
│ Human (Nostr app, web, mobile) Agent (CLI tools via buzz-cli)
│ │ │ │
│ └──────────── WebSocket ─────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
sprout-relay (Axum) │
buzz-relay (Axum) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────────────┐ │
│ │ NIP-42 │ │ EVENT │ │ REQ │ │ REST API │ │
@@ -56,7 +56,7 @@ Sprout is a Rust monorepo, licensed Apache 2.0 under Block, Inc.
(multi-node fan-out wired; local-echo dedup via AppState.local_event_ids).
┌──────────────┐
│ Typesense │ ← sprout-search (bounded worker queue)
│ Typesense │ ← buzz-search (bounded worker queue)
│ (full-text │
│ search) │
└──────────────┘
@@ -67,33 +67,33 @@ Sprout is a Rust monorepo, licensed Apache 2.0 under Block, Inc.
### Crate Dependency Hierarchy
```
sprout-core (zero I/O — types, verification, filter matching, kind registry)
buzz-core (zero I/O — types, verification, filter matching, kind registry)
├── sprout-db (Postgres: events, channels, tokens, workflows, audit)
├── sprout-auth (NIP-42, NIP-98, API tokens, scopes, rate limiting)
├── sprout-pubsub (Redis pub/sub, presence, typing indicators)
├── sprout-search (Typesense: index, query, delete)
├── sprout-audit (hash-chain tamper-evident log)
└── sprout-workflow (YAML-as-code automation engine)
├── buzz-db (Postgres: events, channels, tokens, workflows, audit)
├── buzz-auth (NIP-42, NIP-98, API tokens, scopes, rate limiting)
├── buzz-pubsub (Redis pub/sub, presence, typing indicators)
├── buzz-search (Typesense: index, query, delete)
├── buzz-audit (hash-chain tamper-evident log)
└── buzz-workflow (YAML-as-code automation engine)
└── sprout-relay (ties everything together — the server)
└── buzz-relay (ties everything together — the server)
sprout-acp (agent harness — bridges relay @mentions → AI agents via ACP/JSON-RPC)
sprout-proxy (NIP-28 compatibility proxy — translates standard Nostr clients ↔ Sprout relay)
sprout-sdk (typed Nostr event builders — used by sprout-acp and sprout-cli)
sprout-media (Blossom/S3 media storage)
sprout-cli (agent-first CLI)
sprout-admin (operator CLI: relay membership + key generation)
sprout-test-client (integration test harness + manual CLI)
buzz-acp (agent harness — bridges relay @mentions → AI agents via ACP/JSON-RPC)
buzz-proxy (NIP-28 compatibility proxy — translates standard Nostr clients ↔ Buzz relay)
buzz-sdk (typed Nostr event builders — used by buzz-acp and buzz-cli)
buzz-media (Blossom/S3 media storage)
buzz-cli (agent-first CLI)
buzz-admin (operator CLI: relay membership + key generation)
buzz-test-client (integration test harness + manual CLI)
```
**Key architectural principle:** The relay is the single source of truth. `sprout-relay` orchestrates all subsystems by calling them directly — it imports `sprout-db`, `sprout-auth`, `sprout-pubsub`, `sprout-search`, `sprout-audit`, and `sprout-workflow`. However, those subsystems are isolated from each other: `sprout-workflow` never calls `sprout-pubsub`, `sprout-search` never calls `sprout-db`, etc. Cross-subsystem coordination happens only through the relay. `sprout-proxy` connects to the relay as a WebSocket client and translates NIP-28 events between standard Nostr clients and the Sprout relay.
**Key architectural principle:** The relay is the single source of truth. `buzz-relay` orchestrates all subsystems by calling them directly — it imports `buzz-db`, `buzz-auth`, `buzz-pubsub`, `buzz-search`, `buzz-audit`, and `buzz-workflow`. However, those subsystems are isolated from each other: `buzz-workflow` never calls `buzz-pubsub`, `buzz-search` never calls `buzz-db`, etc. Cross-subsystem coordination happens only through the relay. `buzz-proxy` connects to the relay as a WebSocket client and translates NIP-28 events between standard Nostr clients and the Buzz relay.
---
## 2. The Protocol
Sprout uses Nostr NIP-01 on the wire. Every action is a JSON event with six fields:
Buzz uses Nostr NIP-01 on the wire. Every action is a JSON event with six fields:
```json
{
@@ -116,9 +116,9 @@ The `kind` integer is the only dispatch switch. The relay routes, stores, and fa
| 1000019999 | Replaceable events (NIP-16) |
| 2000029999 | Ephemeral events — not stored, not audited |
| 3000039999 | Parameterized replaceable events |
| 4000049999 | Sprout custom kinds |
| 4000049999 | Buzz custom kinds |
### Sprout Custom Kinds (selected)
### Buzz Custom Kinds (selected)
| Kind | Name | Description |
|------|------|-------------|
@@ -132,9 +132,9 @@ The `kind` integer is the only dispatch switch. The relay routes, stores, and fa
| 4600146012 | KIND_WORKFLOW_* | Workflow execution events |
| 20001 | KIND_PRESENCE_UPDATE | Ephemeral presence heartbeat |
`sprout-core` defines all 81 kinds as `pub const KIND_*: u32` and exports `ALL_KINDS: &[u32]`. Kinds are `u32` (NIP-01 specifies unsigned integer; `u32` covers the full range). Sprout uses both standard Nostr kinds (e.g., kind 7 for reactions) and custom ranges (40000+).
`buzz-core` defines all 81 kinds as `pub const KIND_*: u32` and exports `ALL_KINDS: &[u32]`. 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+).
Note: `KIND_AUTH` (22242) is `pub const KIND_AUTH: u32` in `sprout-core/src/kind.rs` and imported by `sprout-relay/src/handlers/event.rs`. `KIND_CANVAS` (40100) is likewise `pub const KIND_CANVAS: u32` in `sprout-core/src/kind.rs`.
Note: `KIND_AUTH` (22242) is `pub const KIND_AUTH: u32` in `buzz-core/src/kind.rs` and imported by `buzz-relay/src/handlers/event.rs`. `KIND_CANVAS` (40100) is likewise `pub const KIND_CANVAS: u32` in `buzz-core/src/kind.rs`.
### Wire Protocol (NIP-01 messages)
@@ -224,7 +224,7 @@ Steps 1012 are fire-and-forget. Search indexing is sent to a bounded worker q
Step 9 (fan-out) explicitly **excludes** global subscriptions (no `channel_id` constraint) from channel-scoped events — global subscriptions do NOT receive events from private channels, regardless of filter match. This is a deliberate security boundary: only subscriptions scoped to an accessible `channel_id` receive those events.
Workflow loop prevention: workflow execution kinds (4600146012), relay-signed messages with `sprout:workflow` tag, and `KIND_GIFT_WRAP` are excluded from triggering workflows. All other stored events (including kind 9 stream messages) trigger workflow evaluation.
Workflow loop prevention: workflow execution kinds (4600146012), relay-signed messages with `buzz:workflow` tag, and `KIND_GIFT_WRAP` are excluded from triggering workflows. All other stored events (including kind 9 stream messages) trigger workflow evaluation.
### Ephemeral Sub-Pipeline (kinds 2000029999)
@@ -312,7 +312,7 @@ After registering, the REQ handler queries Postgres for stored events matching t
## 6. Crate Reference
### sprout-core — Shared Types and Verification
### buzz-core — Shared Types and Verification
**Zero I/O.** The foundation every other crate builds on. Explicitly prohibits tokio, sqlx, redis, and axum in its `Cargo.toml`.
@@ -341,7 +341,7 @@ pub const ALL_KINDS: &[u32] // 80 entries (KIND_AUTH excluded — never stored)
---
### sprout-auth — Authentication and Authorization
### buzz-auth — Authentication and Authorization
Handles authentication paths, scope enforcement, and token operations.
@@ -368,13 +368,13 @@ pub trait RateLimiter: Send + Sync { ... }
**Security details:**
- NIP-98 auth: Schnorr-signed `kind:27235` events with URL + method tags.
- NIP-42 timestamp tolerance: ±60 seconds.
- Dev-only key derivation: `SHA-256("sprout-test-key:{username}")` — gated behind `#[cfg(any(test, feature = "dev"))]`. The `dev` feature must not be enabled in production relay deployments.
- Dev-only key derivation: `SHA-256("buzz-test-key:{username}")` — gated behind `#[cfg(any(test, feature = "dev"))]`. The `dev` feature must not be enabled in production relay deployments.
**Does NOT:** implement `RateLimiter` beyond a test stub (`AlwaysAllowRateLimiter`, gated behind `#[cfg(any(test, feature = "test-utils"))]`). No Redis-backed rate limiter exists anywhere in the codebase — rate limiting is not currently enforced. `RateLimitConfig` defines 4 tiers (human, agent-standard, agent-elevated, agent-platform) as a design target.
---
### sprout-db — Postgres Event Store
### buzz-db — Postgres Event Store
All database access. Uses `sqlx::query()` (runtime, not compile-time macros) — no `.sqlx/` offline cache required.
@@ -412,31 +412,31 @@ All database access. Uses `sqlx::query()` (runtime, not compile-time macros) —
---
### sprout-pubsub — Redis Pub/Sub, Presence, Typing
### buzz-pubsub — Redis Pub/Sub, Presence, Typing
Manages Redis pub/sub fan-out, presence tracking, and typing indicators.
**Architecture:**
```
Publisher → pool connection → PUBLISH sprout:channel:{uuid}
Subscriber → dedicated PubSub → PSUBSCRIBE sprout:channel:*
Publisher → pool connection → PUBLISH buzz:channel:{uuid}
Subscriber → dedicated PubSub → PSUBSCRIBE buzz:channel:*
→ broadcast::channel(4096)
```
The subscriber uses a **dedicated** `redis::aio::PubSub` connection — not from the pool. This is intentional: pool connections cannot hold `PSUBSCRIBE` state.
**Current state:** The subscriber loop is spawned in `sprout-relay/src/main.rs` and populates the broadcast channel. A consumer task subscribes via `pubsub.subscribe_local()`, calls `sub_registry.fan_out()` on each received event, and delivers matches to local WebSocket connections via `conn_manager.send_to()`. Multi-node fan-out is now wired end-to-end. Local-echo deduplication is implemented via `AppState.local_event_ids` — events published by the local relay instance are tracked and skipped when received via the Redis round-trip.
**Current state:** The subscriber loop is spawned in `buzz-relay/src/main.rs` and populates the broadcast channel. A consumer task subscribes via `pubsub.subscribe_local()`, calls `sub_registry.fan_out()` on each received event, and delivers matches to local WebSocket connections via `conn_manager.send_to()`. Multi-node fan-out is now wired end-to-end. Local-echo deduplication is implemented via `AppState.local_event_ids` — events published by the local relay instance are tracked and skipped when received via the Redis round-trip.
**Reconnection:** exponential backoff 1s → 30s (`backoff_secs * 2`). Backoff resets to 1s only after a clean stream end, not on each reconnect attempt.
**Presence:** `SET sprout:presence:{pubkey_hex} {status} EX 90` — 90-second TTL (3× the 30-second heartbeat interval). Single missed heartbeat does not cause presence flap.
**Presence:** `SET buzz:presence:{pubkey_hex} {status} EX 90` — 90-second TTL (3× the 30-second heartbeat interval). Single missed heartbeat does not cause presence flap.
**Typing indicators:**
```
ZADD sprout:typing:{channel_id} {now_unix} {pubkey_hex}
ZREMRANGEBYSCORE sprout:typing:{channel_id} -inf {now - 5.0}
EXPIRE sprout:typing:{channel_id} 60
ZADD buzz:typing:{channel_id} {now_unix} {pubkey_hex}
ZREMRANGEBYSCORE buzz:typing:{channel_id} -inf {now - 5.0}
EXPIRE buzz:typing:{channel_id} 60
```
5-second activity window. 60-second key TTL prevents orphaned empty sets.
@@ -444,7 +444,7 @@ EXPIRE sprout:typing:{channel_id} 60
---
### sprout-search — Typesense Integration
### buzz-search — Typesense Integration
Full-text search via Typesense. All HTTP calls use `reqwest` with `X-TYPESENSE-API-KEY`.
@@ -456,13 +456,13 @@ Full-text search via Typesense. All HTTP calls use `reqwest` with `X-TYPESENSE-A
- Upsert indexing: `POST /documents?action=upsert` (single), `POST /documents/import?action=upsert` (batch JSONL).
- `delete_event()` validates event ID (64-char hex) before constructing the URL — prevents path injection.
- `delete_event()` is idempotent: 404 treated as success.
- Permission filtering is **caller's responsibility**`sprout-search` provides the `filter_by` mechanism but does not enforce access policy.
- Permission filtering is **caller's responsibility**`buzz-search` provides the `filter_by` mechanism but does not enforce access policy.
**Does NOT:** enforce channel membership or access control. Does NOT store events in Postgres.
---
### sprout-audit — Hash-Chain Audit Log
### buzz-audit — Hash-Chain Audit Log
Tamper-evident append-only log with SHA-256 hash chaining.
@@ -478,7 +478,7 @@ Tamper-evident append-only log with SHA-256 hash chaining.
---
### sprout-workflow — YAML-as-Code Automation Engine
### buzz-workflow — YAML-as-Code Automation Engine
Parses, validates, and executes channel-scoped workflow definitions.
@@ -529,9 +529,9 @@ Note: Both `TriggerDef` and `ActionDef` use serde internally-tagged enums. Trigg
---
### sprout-proxy — NIP-28 Compatibility Proxy
### buzz-proxy — NIP-28 Compatibility Proxy
Lets standard Nostr clients (Coracle, nak, Amethyst, nostr-tools, nostr-sdk) read and write Sprout channels using the NIP-28 Public Chat Channels protocol. Connects to the relay as a WebSocket client; presents a standard NIP-01/NIP-11/NIP-28/NIP-42 interface to external clients.
Lets standard Nostr clients (Coracle, nak, Amethyst, nostr-tools, nostr-sdk) read and write Buzz channels using the NIP-28 Public Chat Channels protocol. Connects to the relay as a WebSocket client; presents a standard NIP-01/NIP-11/NIP-28/NIP-42 interface to external clients.
**Key modules:** `server.rs` (Axum WebSocket server, NIP-11, NIP-42 auth, filter splitting), `translate.rs` (bidirectional kind/tag translation), `upstream.rs` (persistent relay connection with auto-reconnect and subscription replay), `channel_map.rs` (bidirectional UUID ↔ kind:40 event ID mapping), `shadow_keys.rs` (deterministic keypair derivation), `guest_store.rs` (pubkey-based guest registry), `invite_store.rs` (token-based invite system).
@@ -539,11 +539,11 @@ Lets standard Nostr clients (Coracle, nak, Amethyst, nostr-tools, nostr-sdk) rea
**Kind translation (lossy):**
`KindTranslator` defines the full mapping between standard Nostr kinds and Sprout kinds. The proxy's event paths gate which kinds actually flow through — only a subset is accepted inbound or emitted outbound.
`KindTranslator` defines the full mapping between standard Nostr kinds and Buzz kinds. The proxy's event paths gate which kinds actually flow through — only a subset is accepted inbound or emitted outbound.
*Inbound (client → relay) — accepted kinds:*
| Standard Kind | Sprout Kind | Note |
| Standard Kind | Buzz Kind | Note |
|--------------|-------------|------|
| 1, 42 | KIND_STREAM_MESSAGE | Multiple → one (lossy) |
| 41 | KIND_STREAM_MESSAGE_EDIT | Channel message edit |
@@ -553,7 +553,7 @@ Kind 5 (deletion) is intentionally blocked inbound — the relay's deletion hand
*Outbound (relay → client) — emitted kinds:*
| Sprout Kind | Standard Kind | Note |
| Buzz Kind | Standard Kind | Note |
|-------------|--------------|------|
| KIND_STREAM_MESSAGE | 42 | NIP-28 channel message |
| KIND_STREAM_MESSAGE_V2 | 42 | Rich format collapses to plain kind:42 |
@@ -575,7 +575,7 @@ Kind 5 (deletion) is intentionally blocked inbound — the relay's deletion hand
### Huddle Audio — WebSocket Opus Relay
Real-time voice lives inside `sprout-relay` (`src/audio/`), not a separate crate. A WebSocket endpoint (`wss://.../huddle/{channel_id}/audio`) authenticates each participant with a NIP-42 challenge, checks channel membership, admits them to an in-memory room, and forwards opaque Opus frames between peers. No external SFU.
Real-time voice lives inside `buzz-relay` (`src/audio/`), not a separate crate. A WebSocket endpoint (`wss://.../huddle/{channel_id}/audio`) authenticates each participant with a NIP-42 challenge, checks channel membership, admits them to an in-memory room, and forwards opaque Opus frames between peers. No external SFU.
**Frame protocol (v2):** 8-byte big-endian header (sequence `u16`, 48 kHz timestamp `u32`, level dBov `i8`, flags `u8`) followed by an opaque Opus payload. Invalid `level_dbov` values are clamped rather than dropped — losing a metric beats losing audio.
@@ -587,7 +587,7 @@ Real-time voice lives inside `sprout-relay` (`src/audio/`), not a separate crate
---
### sprout-relay — The Server
### buzz-relay — The Server
Axum WebSocket server. Ties all other crates together. The only crate that imports and orchestrates all subsystems.
@@ -678,17 +678,17 @@ pub enum AuthState { Pending { challenge: String }, Authenticated(AuthContext),
---
### sprout-acp — Agent Communication Protocol Harness
### buzz-acp — Agent Communication Protocol Harness
Standalone binary that bridges Sprout relay events to AI agents via the [Agent Communication Protocol](https://agentclientprotocol.com/) (ACP).
Standalone binary that bridges Buzz relay events to AI agents via the [Agent Communication Protocol](https://agentclientprotocol.com/) (ACP).
**Architecture:**
```
Sprout Relay ──WS──→ sprout-acp ──stdio (ACP/JSON-RPC)──→ Agent (goose/codex/claude)
Buzz Relay ──WS──→ buzz-acp ──stdio (ACP/JSON-RPC)──→ Agent (goose/codex/claude)
```
`sprout-acp` spawns AI agent subprocesses (132, default 1), connects to the relay via WebSocket with NIP-42 auth, discovers channels via REST API, and queues `@mention` events per channel. At most one prompt is in-flight per channel. Queued events are batched into a single prompt sent via `session/prompt` over ACP.
`buzz-acp` spawns AI agent subprocesses (132, default 1), connects to the relay via WebSocket with NIP-42 auth, discovers channels via REST API, and queues `@mention` events per channel. At most one prompt is in-flight per channel. Queued events are batched into a single prompt sent via `session/prompt` over ACP.
**Key modules:**
@@ -706,13 +706,13 @@ Sprout Relay ──WS──→ sprout-acp ──stdio (ACP/JSON-RPC)──→ Ag
- Pool of 132 agent subprocesses with claim/return lifecycle.
- Per-channel queuing: at most one prompt in-flight per channel; subsequent @mentions queue until the agent responds.
- Crash recovery: agent subprocess crashes are detected and the agent is respawned.
- Depends on `sprout-core` (kind constants) and `sprout-sdk` (relay/REST utilities).
- Depends on `buzz-core` (kind constants) and `buzz-sdk` (relay/REST utilities).
**Does NOT:** persist state.
---
### sprout-admin — Operator CLI
### buzz-admin — Operator CLI
Subcommands:
@@ -725,9 +725,9 @@ Subcommands:
---
### sprout-test-client — Integration Test Harness
### buzz-test-client — Integration Test Harness
**`SproutTestClient`** wraps a WebSocket connection with a `VecDeque<RelayMessage>` buffer for message interleaving. Methods: `connect`, `connect_unauthenticated`, `authenticate`, `send_event`, `send_text_message`, `subscribe`, `close_subscription`, `recv_event`, `collect_until_eose`, `disconnect`.
**`BuzzTestClient`** wraps a WebSocket connection with a `VecDeque<RelayMessage>` buffer for message interleaving. Methods: `connect`, `connect_unauthenticated`, `authenticate`, `send_event`, `send_text_message`, `subscribe`, `close_subscription`, `recv_event`, `collect_until_eose`, `disconnect`.
**Test coverage:**
@@ -743,7 +743,7 @@ Subcommands:
All e2e tests are `#[ignore]` — require a running relay. Total: **134 e2e tests**.
`src/main.rs` is a manual testing CLI (`sprout-test-cli`) with `--send`, `--subscribe`, `--channel`, `--url`, `--kind` flags.
`src/main.rs` is a manual testing CLI (`buzz-test-cli`) with `--send`, `--subscribe`, `--channel`, `--url`, `--kind` flags.
Defines `parse_relay_message`, `OkResponse`, `RelayMessage` directly in `src/lib.rs`.
@@ -765,7 +765,7 @@ Every security-sensitive operation uses an explicit, verified pattern. No implic
| Concern | Mechanism |
|---------|-----------|
| Schnorr signatures | `verify_event()` in `sprout-core` — every event verified before storage |
| Schnorr signatures | `verify_event()` in `buzz-core` — every event verified before storage |
| Event ID | SHA-256 of canonical serialization verified independently of signature |
| Frame size | `MAX_FRAME_BYTES = 65,536` — oversized frames rejected, connection closed |
| Search event IDs | 64-char hex validation before URL construction — prevents path injection |
@@ -774,12 +774,12 @@ Every security-sensitive operation uses an explicit, verified pattern. No implic
### SSRF Protection
`is_private_ip()` in `sprout-core` covers:
`is_private_ip()` in `buzz-core` covers:
- IPv4: unspecified (0.0.0.0/8), loopback (127.0.0.0/8), private (10/8, 172.16/12, 192.168/16), link-local (169.254/16), CGNAT (100.64/10), benchmarking (198.18/15), broadcast (255.255.255.255)
- IPv6: loopback (::1), ULA (fc00::/7), link-local (fe80::/10), multicast (ff00::/8), documentation (2001:db8::/32)
- IPv4-mapped IPv6 (::ffff:0:0/96) — recursively checks the embedded IPv4 address
Applied in: `sprout-workflow` (CallWebhook action), `sprout-core` (shared utility).
Applied in: `buzz-workflow` (CallWebhook action), `buzz-core` (shared utility).
### Audit Integrity
@@ -834,9 +834,9 @@ Docker Compose provides the full local development stack. All services include h
| Pattern | Type | TTL | Purpose |
|---------|------|-----|---------|
| `sprout:channel:{uuid}` | Pub/Sub channel | — | Event fan-out |
| `sprout:presence:{pubkey_hex}` | String | 90s | Online/away status |
| `sprout:typing:{channel_uuid}` | Sorted Set | 60s | Active typers (5s window) |
| `buzz:channel:{uuid}` | Pub/Sub channel | — | Event fan-out |
| `buzz:presence:{pubkey_hex}` | String | 90s | Online/away status |
| `buzz:typing:{channel_uuid}` | Sorted Set | 60s | Active typers (5s window) |
### Typesense Collection
@@ -851,7 +851,7 @@ These are verified gaps in the current implementation — not design aspirations
| # | Limitation | Detail |
|---|-----------|--------|
| 1 | **No sqlx offline query cache** | Uses `sqlx::query()` (runtime) not `sqlx::query!()` (compile-time). No `.sqlx/` directory. Queries are not validated at compile time. |
| 2 | **No rate limiting implementation** | `RateLimiter` trait exists in `sprout-auth`. Only implementation is `AlwaysAllowRateLimiter` (test stub, gated behind `#[cfg(any(test, feature = "test-utils"))]`). `RateLimitConfig` defines 4 tiers (human, agent-standard, agent-elevated, agent-platform) but none are enforced. |
| 2 | **No rate limiting implementation** | `RateLimiter` trait exists in `buzz-auth`. Only implementation is `AlwaysAllowRateLimiter` (test stub, gated behind `#[cfg(any(test, feature = "test-utils"))]`). `RateLimitConfig` defines 4 tiers (human, agent-standard, agent-elevated, agent-platform) but none are enforced. |
| 3 | **No dedicated typing REST endpoint** | Typing indicators (kind 20002) are delivered via both local fan-out and Redis pub/sub (cross-node). There is no REST endpoint to query current typers — `/api/presence` returns online/away status only, not typing state. |
| 4 | **Huddle recording/tracks not built** | Voice, room lifecycle, and join/leave/end events are wired (see Huddle Audio above). Recording and per-track publishing have reserved kinds but no producer yet. |
| 5 | **Approval gates not wired end-to-end** | The executor returns `StepResult::Suspended` and the relay has grant/deny API endpoints with DB CRUD, but the engine intercepts before creating `WaitingApproval` rows — runs that hit an approval gate are marked as Failed (🚧 WF-08). |
+27 -27
View File
@@ -1,6 +1,6 @@
# Contributing to Sprout
# Contributing to Buzz
Welcome, and thank you for your interest in contributing! Sprout is an
Welcome, and thank you for your interest in contributing! Buzz is an
open-source project and we're glad you're here. This guide will help you
get from zero to a merged pull request.
@@ -29,7 +29,7 @@ reach out in the community channels.
This project follows the [Contributor Covenant v2.1](CODE_OF_CONDUCT.md).
By participating you agree to uphold these standards. Please report
unacceptable behavior to **conduct@sprout-relay.org**.
unacceptable behavior to **conduct@buzz-relay.org**.
---
@@ -87,7 +87,7 @@ OAuth/OIDC testing, MinIO on `:9000` for media storage, and Prometheus on
```bash
just relay
# or: cargo run -p sprout-relay
# or: cargo run -p buzz-relay
```
The relay listens on `ws://localhost:3000` by default. You should see log
@@ -126,7 +126,7 @@ already running.
### End-to-End Tests
End-to-end tests live in `crates/sprout-test-client/tests/`:
End-to-end tests live in `crates/buzz-test-client/tests/`:
- `e2e_rest_api.rs` — REST API tests
- `e2e_relay.rs` — WebSocket relay tests
@@ -140,7 +140,7 @@ End-to-end tests live in `crates/sprout-test-client/tests/`:
Run them with (requires running infrastructure):
```bash
cargo test -p sprout-test-client -- --ignored
cargo test -p buzz-test-client -- --ignored
```
See `TESTING.md` for the full multi-agent E2E testing guide.
@@ -287,7 +287,7 @@ design principles:
**The relay is the single source of truth.** All state flows through the
event store. Crates communicate through the database and Redis pub/sub — not
through direct function calls across crate boundaries (with the exception
of `sprout-core` types, which are shared everywhere).
of `buzz-core` types, which are shared everywhere).
**Event kinds are the only switch.** Every action in the system — a message,
a reaction, a workflow step, a canvas update — is a Nostr event with a kind
@@ -298,7 +298,7 @@ to existing clients.
## Ecosystem
Sprout is developed across multiple repositories. This repo (`block/sprout`)
Buzz is developed across multiple repositories. This repo (`block/sprout`)
is the open-source home for all application code — the relay, desktop app,
mobile app, CLI, and agent harness. Internal repositories handle
enterprise-signed builds and infrastructure deployment.
@@ -318,7 +318,7 @@ for team access setup, onboarding, and the full repo inventory. See
## How to Add a New Event Kind
1. **Define the kind constant** in `sprout-core/src/kind.rs`:
1. **Define the kind constant** in `buzz-core/src/kind.rs`:
```rust
/// My new event kind — description of what it represents.
@@ -329,7 +329,7 @@ for team access setup, onboarding, and the full repo inventory. See
Check the `ALL_KINDS` array for collisions. Each sub-range is documented
with comments in the file.
2. **Define the payload type** in the appropriate module in `sprout-core/src/`
2. **Define the payload type** in the appropriate module in `buzz-core/src/`
(e.g., alongside `event.rs`) if the content field is structured JSON:
```rust
@@ -341,7 +341,7 @@ for team access setup, onboarding, and the full repo inventory. See
```
3. **Register the kind's required scope** in
`crates/sprout-relay/src/handlers/ingest.rs` inside
`crates/buzz-relay/src/handlers/ingest.rs` inside
`required_scope_for_kind()`. This controls which auth scope a caller
needs to submit the event:
@@ -350,7 +350,7 @@ for team access setup, onboarding, and the full repo inventory. See
```
4. **Handle post-storage side effects** by adding a match arm in
`crates/sprout-relay/src/handlers/side_effects.rs` inside
`crates/buzz-relay/src/handlers/side_effects.rs` inside
`handle_side_effects()`:
```rust
@@ -360,21 +360,21 @@ for team access setup, onboarding, and the full repo inventory. See
`handle_side_effects()` runs after the event is stored — use it for
notifications, cache invalidation, or derived data. If the new kind
also needs a REST surface (e.g., a query endpoint for clients), add a
handler in `crates/sprout-relay/src/api/` and register it in
`crates/sprout-relay/src/router.rs`.
handler in `crates/buzz-relay/src/api/` and register it in
`crates/buzz-relay/src/router.rs`.
5. **Persist to the database** — if the event needs to be queryable, add a
handler in `sprout-db/src/` (e.g., `sprout-db/src/my_feature.rs`) with
handler in `buzz-db/src/` (e.g., `buzz-db/src/my_feature.rs`) with
the appropriate `INSERT` and `SELECT` queries.
6. **Index for search** (if applicable) — add the kind to the Typesense
indexing logic in `sprout-search/src/index.rs`.
indexing logic in `buzz-search/src/index.rs`.
7. **Audit** — the audit log captures all events automatically; no changes
needed unless you need custom audit metadata.
8. **Write tests** — add a unit test for payload serialization in
`sprout-core` and an integration test in `sprout-test-client` that sends
`buzz-core` and an integration test in `buzz-test-client` that sends
the new event kind and verifies the expected behavior.
9. **Document**`kind.rs` is the authoritative registry of all kind numbers.
@@ -384,9 +384,9 @@ for team access setup, onboarding, and the full repo inventory. See
## How to Add a New API Endpoint
REST endpoints live in `crates/sprout-relay/src/api/` — each resource has
REST endpoints live in `crates/buzz-relay/src/api/` — each resource has
its own submodule (e.g., `channels.rs`, `messages.rs`, `tokens.rs`). Routes
are registered in `crates/sprout-relay/src/router.rs`.
are registered in `crates/buzz-relay/src/router.rs`.
1. **Define the handler function:**
@@ -399,7 +399,7 @@ are registered in `crates/sprout-relay/src/router.rs`.
let channel_id = uuid::Uuid::parse_str(&channel_id_str)
.map_err(|_| api_error(StatusCode::BAD_REQUEST, "invalid channel_id"))?;
let ctx = extract_auth_context(&headers, &state).await?;
sprout_auth::require_scope(&ctx.scopes, sprout_auth::Scope::ChannelsRead)
buzz_auth::require_scope(&ctx.scopes, buzz_auth::Scope::ChannelsRead)
.map_err(scope_error)?;
let pubkey_bytes = ctx.pubkey_bytes.clone();
check_token_channel_access(&ctx, &channel_id)?;
@@ -411,20 +411,20 @@ are registered in `crates/sprout-relay/src/router.rs`.
}
```
2. **Register the route** in `crates/sprout-relay/src/router.rs`:
2. **Register the route** in `crates/buzz-relay/src/router.rs`:
```rust
.route("/api/channels/{channel_id}/my-resource", get(get_my_resource))
```
3. **Add the database query** in `sprout-db/src/` — follow the existing
3. **Add the database query** in `buzz-db/src/` — follow the existing
patterns in `channel.rs`, `event.rs`, etc.
4. **Handle errors** — use the `api_error()` and `internal_error()` helpers in
`sprout-relay/src/api/mod.rs`. Return `(StatusCode, Json<Value>)` tuples.
`buzz-relay/src/api/mod.rs`. Return `(StatusCode, Json<Value>)` tuples.
5. **Write tests** — add an integration test using the `sprout-test-client`
harness in `crates/sprout-test-client/tests/e2e_rest_api.rs`.
5. **Write tests** — add an integration test using the `buzz-test-client`
harness in `crates/buzz-test-client/tests/e2e_rest_api.rs`.
6. **Document** — if the endpoint is part of the public API surface, add it
to the API reference section of `README.md` or a dedicated `API.md`.
@@ -433,7 +433,7 @@ are registered in `crates/sprout-relay/src/router.rs`.
## License and CLA
Sprout is licensed under the **Apache License, Version 2.0**. See
Buzz is licensed under the **Apache License, Version 2.0**. See
[LICENSE](LICENSE) for the full text.
By submitting a pull request, you agree that your contribution is licensed
@@ -444,5 +444,5 @@ their sign-off. When in doubt, check with your legal team.
---
*Thank you for contributing to Sprout. Every bug report, documentation fix,
*Thank you for contributing to Buzz. Every bug report, documentation fix,
and code contribution makes the project better for everyone. 🌱*
+48 -48
View File
@@ -1,12 +1,12 @@
# Using Third-Party Nostr Clients with Sprout
# Using Third-Party Nostr Clients with Buzz
Sprout is a Nostr relay that speaks NIP-29 (relay-based groups) natively. There are two ways for
Buzz is a Nostr relay that speaks NIP-29 (relay-based groups) natively. There are two ways for
third-party Nostr clients to connect:
| Path | Protocol | Connects to | Expected clients (not all verified in-repo) |
|------|----------|-------------|----------------------------------------------|
| **Direct** | NIP-29 | `sprout-relay :3000` | NIP-29 clients (e.g. Chachi, 0xchat), nak |
| **Via proxy** | NIP-28 | `sprout-proxy :4869` | NIP-28 clients (e.g. Coracle, Amethyst), nostr-tools apps |
| **Direct** | NIP-29 | `buzz-relay :3000` | NIP-29 clients (e.g. Chachi, 0xchat), nak |
| **Via proxy** | NIP-28 | `buzz-proxy :4869` | NIP-28 clients (e.g. Coracle, Amethyst), nostr-tools apps |
**Direct** is simpler — no extra process, no translation layer. Use it when your client speaks
NIP-29. **Proxy** is for external guests (investors, press, partners, etc.) who use standard NIP-28
@@ -24,14 +24,14 @@ Connect any NIP-29 client straight to the relay.
```bash
# 1. (Optional) Enable pubkey allowlist — must be set BEFORE relay startup
export SPROUT_PUBKEY_ALLOWLIST=true
export BUZZ_PUBKEY_ALLOWLIST=true
# 2. Start the relay (auto-starts Docker services and runs migrations)
just relay & # relay on :3000
# 3. Add a pubkey to the allowlist (if enabled)
# Insert directly — there is no CLI command for this yet.
PGPASSWORD=sprout_dev psql -h localhost -U sprout -d sprout -c \
PGPASSWORD=buzz_dev psql -h localhost -U buzz -d buzz -c \
"INSERT INTO pubkey_allowlist (pubkey) VALUES (decode('<64-char-hex-pubkey>', 'hex'))"
# 5. Connect any NIP-29 + NIP-42 client to ws://localhost:3000
@@ -66,8 +66,8 @@ PGPASSWORD=sprout_dev psql -h localhost -U sprout -d sprout -c \
| **NIP-17 DMs (gift wrap)** | ✅ | kind:1059 accepted with ephemeral signing keys. Stored globally (channel_id=None). Delivered via `#p`-filtered subscriptions. Not indexed in search. |
| **DM discovery** | ✅ | DM creation emits kind:39000 (with `hidden` tag) + kind:44100 membership notifications. NIP-29 clients discover DMs via standard group discovery flow. |
| **Join request (kind:9021)** | ✅ | Open channels only. Adds member, emits system message + group discovery events + kind:44100 membership notification. Private channels rejected at ingest. |
| **Edits (kind:40003)** | ⚠️ | Works on the wire but Sprout-only — no standard NIP-29 client renders these |
| **Rich content (kind:40002)** | ⚠️ | Works on the wire but Sprout-only — no standard NIP-29 client renders these |
| **Edits (kind:40003)** | ⚠️ | Works on the wire but Buzz-only — no standard NIP-29 client renders these |
| **Rich content (kind:40002)** | ⚠️ | Works on the wire but Buzz-only — no standard NIP-29 client renders these |
### What Doesn't Work
@@ -79,7 +79,7 @@ PGPASSWORD=sprout_dev psql -h localhost -U sprout -d sprout -c \
### Pubkey Allowlist
When `SPROUT_PUBKEY_ALLOWLIST=true`, NIP-42 connections that authenticate with only a pubkey
When `BUZZ_PUBKEY_ALLOWLIST=true`, NIP-42 connections that authenticate with only a pubkey
(no API token) are checked against the `pubkey_allowlist` table. This lets you open the
relay to specific external Nostr identities without granting full access.
@@ -101,7 +101,7 @@ All discovery events include a `d` tag set to the channel UUID (NIP-29 addressab
| Kind | Tags | Content |
|------|------|---------|
| **39000** | `d=<uuid>`, `name`, `closed` (always); `about` (if description non-empty); `private` (if applicable); `hidden` (DM channels only) | Group metadata. **Note:** `closed` is always emitted per NIP-29 convention (Sprout channels require explicit membership), but open channels are still readable/writable by non-members at runtime. The tag reflects the membership model, not access enforcement. |
| **39000** | `d=<uuid>`, `name`, `closed` (always); `about` (if description non-empty); `private` (if applicable); `hidden` (DM channels only) | Group metadata. **Note:** `closed` is always emitted per NIP-29 convention (Buzz channels require explicit membership), but open channels are still readable/writable by non-members at runtime. The tag reflects the membership model, not access enforcement. |
| **39001** | `d=<uuid>`, `p` tags with role label (`owner`, `admin`) | Admin list |
| **39002** | `d=<uuid>`, `p` tags for all members | Member list |
@@ -184,7 +184,7 @@ nak req -k 1059 --tag "p=<your-hex-pubkey>" \
| Client | Platform | Evidence | Notes |
|--------|----------|:--------:|-------|
| **SproutTestClient** | Rust (repo) | Automated E2E | Full NIP-29 flow: discovery (39000/39001/39002), kind:9 send/receive, reactions, deletions, h-tag enforcement |
| **BuzzTestClient** | Rust (repo) | Automated E2E | Full NIP-29 flow: discovery (39000/39001/39002), kind:9 send/receive, reactions, deletions, h-tag enforcement |
| **E2E nostr interop** | Rust (repo) | Automated E2E | NIP-50 search (3 tests), NIP-10 threads (3 tests), NIP-17 gift wraps (3 tests), DM discovery (1 test) |
| **nak** | CLI | Manual (verified) | kind:9 send/recv, NIP-50 search, NIP-10 thread replies, group discovery |
@@ -194,9 +194,9 @@ nak req -k 1059 --tag "p=<your-hex-pubkey>" \
---
## Path 2: NIP-28 via sprout-proxy
## Path 2: NIP-28 via buzz-proxy
For clients that speak NIP-28 (kind:40/41/42) but not NIP-29, **sprout-proxy** translates between
For clients that speak NIP-28 (kind:40/41/42) but not NIP-29, **buzz-proxy** translates between
the two protocols in real time. Events are re-signed with deterministic shadow keys so each
external user maps to a consistent identity on the relay.
@@ -206,31 +206,31 @@ external user maps to a consistent identity on the relay.
# 1. Start infrastructure + relay (see Path 1)
# 2. Generate proxy server key and derive its pubkey
export SPROUT_PROXY_SERVER_KEY=$(openssl rand -hex 32)
PROXY_PUBKEY=$(echo $SPROUT_PROXY_SERVER_KEY | nak key public)
export BUZZ_PROXY_SERVER_KEY=$(openssl rand -hex 32)
PROXY_PUBKEY=$(echo $BUZZ_PROXY_SERVER_KEY | nak key public)
# 3. Mint a proxy API token (required until proxy is migrated to NIP-98 auth)
export SPROUT_PROXY_API_TOKEN=$(curl -s -X POST http://localhost:3000/api/tokens \
export BUZZ_PROXY_API_TOKEN=$(curl -s -X POST http://localhost:3000/api/tokens \
-H "Authorization: Nostr <base64-nip98-event>" \
-H "Content-Type: application/json" \
-d '{"name":"proxy"}' | jq -r .token)
# 4. Get the relay's public key (needed for attribution trust)
# This is the pubkey of the relay's signing keypair. If SPROUT_RELAY_PRIVATE_KEY
# is set, derive it: echo $SPROUT_RELAY_PRIVATE_KEY | nak key public
# This is the pubkey of the relay's signing keypair. If BUZZ_RELAY_PRIVATE_KEY
# is set, derive it: echo $BUZZ_RELAY_PRIVATE_KEY | nak key public
# If not set, the relay generates a random keypair at startup — check relay logs.
export SPROUT_RELAY_PUBKEY=<relay-hex-pubkey>
export BUZZ_RELAY_PUBKEY=<relay-hex-pubkey>
# 5. Start the proxy
export SPROUT_UPSTREAM_URL=ws://localhost:3000
export SPROUT_PROXY_SALT=$(openssl rand -hex 32)
export SPROUT_PROXY_ADMIN_SECRET=$(openssl rand -hex 16)
cargo run -p sprout-proxy # proxy on :4869
export BUZZ_UPSTREAM_URL=ws://localhost:3000
export BUZZ_PROXY_SALT=$(openssl rand -hex 32)
export BUZZ_PROXY_ADMIN_SECRET=$(openssl rand -hex 16)
cargo run -p buzz-proxy # proxy on :4869
# 6. Register a guest
curl -X POST http://localhost:4869/admin/guests \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SPROUT_PROXY_ADMIN_SECRET" \
-H "Authorization: Bearer $BUZZ_PROXY_ADMIN_SECRET" \
-d '{"pubkey": "<guest-hex-pubkey>", "channels": "<channel-uuid>"}'
# 7. Connect any NIP-28 + NIP-42 client to ws://localhost:4869
@@ -242,9 +242,9 @@ curl -X POST http://localhost:4869/admin/guests \
|---------|:------:|-------|
| **NIP-11 relay info** | ✅ | Standard relay info document at `GET /` |
| **NIP-42 authentication** | ✅ | Proactive challenge + reactive-auth compatible |
| **Channel discovery (kind:40)** | ✅ | Synthesized from Sprout REST API at startup; served locally. Content uses channel UUID as `name` for ID stability; human-readable name is in kind:41. **Snapshot — new channels created after proxy start require restart. Renames do NOT affect kind:40 (UUID-anchored).** |
| **Channel discovery (kind:40)** | ✅ | Synthesized from Buzz REST API at startup; served locally. Content uses channel UUID as `name` for ID stability; human-readable name is in kind:41. **Snapshot — new channels created after proxy start require restart. Renames do NOT affect kind:40 (UUID-anchored).** |
| **Channel metadata (kind:41)** | ✅ | Name, description (picture always empty — no channel-picture source in proxy path); synthesized at startup, served locally. **Snapshot — new channels AND renames require restart to update local kind:41.** |
| **Channel messages (kind:42)** | ✅ | Translated to/from Sprout kind:9 |
| **Channel messages (kind:42)** | ✅ | Translated to/from Buzz kind:9 |
| **Inbound kind:1** | ✅ | Text notes (kind:1) accepted and translated to kind:9, same as kind:42 |
| **Message editing (kind:41)** | ✅ | Bidirectional: inbound kind:41 → kind:40003; outbound kind:40003 → kind:41. **Note:** inbound kind:41 is always treated as a message edit, never as a channel metadata update. Standard NIP-28 channel-metadata writes are not supported. |
| **Reactions (kind:7)** | ✅ | Bidirectional; inbound channel scope verified against allowed channels. **Constraint:** target must already be known to the proxy's ID mapping cache (populated by prior fetch, outbound delivery, or inbound publish). Error if unknown: `reaction target is unknown to the proxy; fetch the message first`. |
@@ -273,7 +273,7 @@ curl -X POST http://localhost:4869/admin/guests \
### Channel UUIDs vs Event IDs
Sprout identifies channels by UUID. NIP-28 clients identify channels by the event ID of the
Buzz identifies channels by UUID. NIP-28 clients identify channels by the event ID of the
kind:40 creation event. The proxy translates automatically, but you need the event ID to subscribe.
The synthesized kind:40 uses the channel UUID as the `name` field in content (for deterministic
@@ -293,17 +293,17 @@ Two methods, both using NIP-42:
```bash
# Register
curl -X POST http://localhost:4869/admin/guests \
-H "Authorization: Bearer $SPROUT_PROXY_ADMIN_SECRET" \
-H "Authorization: Bearer $BUZZ_PROXY_ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"pubkey": "<hex>", "channels": "<uuid1>,<uuid2>"}'
# List
curl http://localhost:4869/admin/guests \
-H "Authorization: Bearer $SPROUT_PROXY_ADMIN_SECRET"
-H "Authorization: Bearer $BUZZ_PROXY_ADMIN_SECRET"
# Revoke
curl -X DELETE http://localhost:4869/admin/guests \
-H "Authorization: Bearer $SPROUT_PROXY_ADMIN_SECRET" \
-H "Authorization: Bearer $BUZZ_PROXY_ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"pubkey": "<hex>"}'
```
@@ -318,7 +318,7 @@ curl -X DELETE http://localhost:4869/admin/guests \
```bash
# Create
curl -X POST http://localhost:4869/admin/invite \
-H "Authorization: Bearer $SPROUT_PROXY_ADMIN_SECRET" \
-H "Authorization: Bearer $BUZZ_PROXY_ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"channels": "<uuid1>,<uuid2>", "max_uses": 5, "hours": 48}'
@@ -421,14 +421,14 @@ Test script: `scripts/test-proxy-nostr-sdk-python.py`.
```
NIP-29 (direct)
┌──────────────────┐ ◄──────────────────────────► ┌──────────────────┐
│ NIP-29 Client │ kind:9, kind:7, kind:5 │ Sprout Relay │
│ NIP-29 Client │ kind:9, kind:7, kind:5 │ Buzz Relay │
│ (Chachi, 0xchat,│ kind:9000/01/02/05/07/08 │ :3000 │
│ nak) │ #h(uuid), NIP-42 │ │
└──────────────────┘ │ kind:39000/1/2 │
│ kind:44100/44101│
│ Blossom media │
┌──────────────────┐ ┌────────────────┐ │ /media/upload │
│ NIP-28 Client │◄──────►│ sprout-proxy │◄───►│ │
│ NIP-28 Client │◄──────►│ buzz-proxy │◄───►│ │
│ (Coracle, nak, │ NIP-28 │ :4869 │ WS └──────────────────┘
│ nostr-tools) │ │ │ +REST
└──────────────────┘ │ kind:42↔kind:9 │ (/api/channels,
@@ -450,7 +450,7 @@ notifications (kind:44100/44101), NIP-29 admin commands (kind:9000, 9001, 9002,
(edits), kind:7 (reactions, bidirectional), and kind:5 (deletions, outbound only — standard kind:5
events only; admin/REST deletions do not surface as NIP-28 delete events). Re-signs events with
deterministic shadow keys (HMAC-SHA256 of salt + pubkey). Channel discovery (kind:40) is synthesized
locally from Sprout's REST API at startup and never forwarded upstream. Channel metadata (kind:41)
locally from Buzz's REST API at startup and never forwarded upstream. Channel metadata (kind:41)
is dual-sourced: local snapshot metadata plus upstream edit events (kind:40003 → kind:41).
---
@@ -459,15 +459,15 @@ is dual-sourced: local snapshot metadata plus upstream edit events (kind:40003
| Variable | Required | Default | Description |
|----------|:--------:|---------|-------------|
| `SPROUT_UPSTREAM_URL` | ✅ | — | WebSocket URL of the relay |
| `SPROUT_PROXY_API_TOKEN` | ✅ | — | Relay API token for REST calls (required until proxy is migrated to NIP-98 auth) |
| `SPROUT_PROXY_SERVER_KEY` | ✅ | — | Hex-encoded 32-byte secret key (raw hex, not bech32 `nsec`) |
| `SPROUT_PROXY_SALT` | ✅ | — | Hex 32-byte salt for shadow keys (keep stable and secret) |
| `SPROUT_RELAY_PUBKEY` | ✅ | — | Hex-encoded 64-char relay public key (for attribution trust) |
| `SPROUT_PROXY_BIND_ADDR` | ❌ | `0.0.0.0:4869` | Listen address |
| `SPROUT_PROXY_RELAY_URL` | ❌ | derived from bind addr | Public WebSocket URL for NIP-42 relay-tag validation. Set if behind a reverse proxy. |
| `SPROUT_PROXY_ADMIN_SECRET` | ❌ | — | Bearer secret for `/admin/*` (unset = no auth, dev mode) |
| `RUST_LOG` | ❌ | `sprout_proxy=info,tower_http=info` | Log level |
| `BUZZ_UPSTREAM_URL` | ✅ | — | WebSocket URL of the relay |
| `BUZZ_PROXY_API_TOKEN` | ✅ | — | Relay API token for REST calls (required until proxy is migrated to NIP-98 auth) |
| `BUZZ_PROXY_SERVER_KEY` | ✅ | — | Hex-encoded 32-byte secret key (raw hex, not bech32 `nsec`) |
| `BUZZ_PROXY_SALT` | ✅ | — | Hex 32-byte salt for shadow keys (keep stable and secret) |
| `BUZZ_RELAY_PUBKEY` | ✅ | — | Hex-encoded 64-char relay public key (for attribution trust) |
| `BUZZ_PROXY_BIND_ADDR` | ❌ | `0.0.0.0:4869` | Listen address |
| `BUZZ_PROXY_RELAY_URL` | ❌ | derived from bind addr | Public WebSocket URL for NIP-42 relay-tag validation. Set if behind a reverse proxy. |
| `BUZZ_PROXY_ADMIN_SECRET` | ❌ | — | Bearer secret for `/admin/*` (unset = no auth, dev mode) |
| `RUST_LOG` | ❌ | `buzz_proxy=info,tower_http=info` | Log level |
---
@@ -475,9 +475,9 @@ is dual-sourced: local snapshot metadata plus upstream edit events (kind:40003
| Variable | Required | Default | Description |
|----------|:--------:|---------|-------------|
| `SPROUT_PUBKEY_ALLOWLIST` | ❌ | `false` | Enable pubkey allowlist for NIP-42 pubkey-only auth |
| `SPROUT_RELAY_PRIVATE_KEY` | ❌ | random | Hex secret key for relay signing (discovery events, system messages) |
| `SPROUT_REQUIRE_AUTH_TOKEN` | ❌ | `false` | Require authenticated NIP-42 for all connections |
| `BUZZ_PUBKEY_ALLOWLIST` | ❌ | `false` | Enable pubkey allowlist for NIP-42 pubkey-only auth |
| `BUZZ_RELAY_PRIVATE_KEY` | ❌ | random | Hex secret key for relay signing (discovery events, system messages) |
| `BUZZ_REQUIRE_AUTH_TOKEN` | ❌ | `false` | Require authenticated NIP-42 for all connections |
---
@@ -524,10 +524,10 @@ is dual-sourced: local snapshot metadata plus upstream edit events (kind:40003
| `auth-required: authentication timeout` | Client didn't respond to NIP-42 within 30s | Use a NIP-42-capable client |
| No messages after auth | Unresolved `#e` filter silently returns zero events | Re-query `nak req -k 40` for correct kind:40 event ID |
| Guest still has access after revoke | Active sessions not terminated | Restart proxy to cut all sessions |
| Proxy startup fails | Can't reach relay REST API or missing env vars | Check relay is running; verify all required env vars (especially `SPROUT_RELAY_PUBKEY`) |
| Proxy startup fails | Can't reach relay REST API or missing env vars | Check relay is running; verify all required env vars (especially `BUZZ_RELAY_PUBKEY`) |
---
## Further Reading
- [`crates/sprout-proxy/README.md`](crates/sprout-proxy/README.md) — proxy crate internals, shadow key derivation, subscription namespacing. **Note:** some auth/buffering details in that README may be stale; this document is the authoritative reference for proxy behavior.
- [`crates/buzz-proxy/README.md`](crates/buzz-proxy/README.md) — proxy crate internals, shadow key derivation, subscription namespacing. **Note:** some auth/buffering details in that README may be stale; this document is the authoritative reference for proxy behavior.
+17 -17
View File
@@ -1,4 +1,4 @@
<h1 align="center">Sprout 🌱</h1>
<h1 align="center">Buzz 🐝</h1>
<p align="center">
<strong>A workspace where humans and agents build together, on a relay you own.</strong>
@@ -16,14 +16,14 @@
<!-- HERO MEDIA SLOT — 15s clip of a human↔agent exchange in a channel goes here -->
<p align="center">
<sub><em>Drafted in a Sprout channel by agents and a human with opinions.</em></sub>
<sub><em>Drafted in a Buzz channel by agents and a human with opinions.</em></sub>
</p>
---
## What is this, really?
Sprout is a self-hostable workspace where humans and AI agents share the same rooms.
Buzz is a self-hostable workspace where humans and AI agents share the same rooms.
It's a Nostr relay: every message, reaction, workflow step, review approval, and git event is a signed event in one log. Same shape, same identity model, same audit trail, whether the author is a person or a process.
@@ -33,7 +33,7 @@ Yes, it's another AI-adjacent developer tool. We're sorry. The difference is wha
---
## What Sprout is trying to make normal
## What Buzz is trying to make normal
- **Ask the project a question and get an answer with receipts.** Agents search six months of history and post the threads, not vibes.
- **Let an agent triage a bug without giving it the keys to the kingdom.** Agents have their own keys, their own channel memberships, and their own audit trail. Scoped by identity, not by permission flags — the same way you'd scope a teammate.
@@ -69,7 +69,7 @@ Agents are colleagues, not haunted cron jobs.
|---|---|---|
| Relay, channels, threads, DMs, canvases, media, search, audit log | Git hosting backend | Web-of-trust reputation across relays |
| Desktop app (Tauri + React) | Mobile clients (iOS + Android, Flutter) | Push notifications |
| `sprout-cli` (agent-first, JSON in / JSON out) + ACP harness (Goose, Codex, Claude Code) | Workflow approval gates (infra exists, glue still drying) | Culture features |
| `buzz-cli` (agent-first, JSON in / JSON out) + ACP harness (Goose, Codex, Claude Code) | Workflow approval gates (infra exists, glue still drying) | Culture features |
| YAML workflows: message / reaction / schedule / webhook triggers | Huddle lifecycle events | |
| Git events (NIP-34: patches, repo announcements, status) | | |
@@ -96,7 +96,7 @@ just dev # terminal 2 — desktop app opens automatically
Relay on `ws://localhost:3000`. Desktop app pops up. You're in.
For agents, set `SPROUT_PRIVATE_KEY` and use [`sprout-cli`](crates/sprout-cli) — JSON in, JSON out, designed for LLM tool calls.
For agents, set `BUZZ_PRIVATE_KEY` and use [`buzz-cli`](crates/buzz-cli) — JSON in, JSON out, designed for LLM tool calls.
---
@@ -106,9 +106,9 @@ For agents, set `SPROUT_PRIVATE_KEY` and use [`sprout-cli`](crates/sprout-cli)
┌─────────────────────────────────────────────────────────────────────────┐
│ Clients │
│ Human client AI agent CLI / scripts │
│ (Sprout desktop) (Goose, Codex, ...) (sprout-cli, agents) │
│ (Buzz desktop) (Goose, Codex, ...) (buzz-cli, agents) │
│ │ ┌──────────────┐ │ │
│ │ │ sprout-acp │ │ │
│ │ │ buzz-acp │ │ │
│ │ │ (ACP ↔ MCP) │ │ │
│ │ └──────┬───────┘ │ │
│ │ │ │ │
@@ -116,7 +116,7 @@ For agents, set `SPROUT_PRIVATE_KEY` and use [`sprout-cli`](crates/sprout-cli)
│ WebSocket │ WS + REST │ WS + REST
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────┐
sprout-relay │
buzz-relay │
│ NIP-01 · NIP-42 auth · channel/DM/media/workflow/git REST · audit log │
└───┬──────────────────┬──────────────────┬──────────────────┬────────────┘
│ │ │ │
@@ -131,17 +131,17 @@ A Rust workspace of focused crates. Single source of truth: the relay. See [ARCH
<details>
<summary><strong>Crate map</strong></summary>
**Core protocol** — `sprout-core` (zero-I/O types, NIP-01 filters, Schnorr verify) · `sprout-relay` (Axum WS + REST)
**Core protocol** — `buzz-core` (zero-I/O types, NIP-01 filters, Schnorr verify) · `buzz-relay` (Axum WS + REST)
**Services** — `sprout-db` (Postgres) · `sprout-auth` (NIP-42/98 Schnorr auth, rate limiting) · `sprout-pubsub` (Redis, presence, typing) · `sprout-search` (Typesense) · `sprout-audit` (hash-chain log)
**Services** — `buzz-db` (Postgres) · `buzz-auth` (NIP-42/98 Schnorr auth, rate limiting) · `buzz-pubsub` (Redis, presence, typing) · `buzz-search` (Typesense) · `buzz-audit` (hash-chain log)
**Agent surface** — `sprout-cli` (agent-first CLI, JSON in / JSON out) · `sprout-acp` (ACP harness for Goose/Codex/Claude Code) · `sprout-agent` (ACP agent — see [VISION_AGENT.md](VISION_AGENT.md)) · `sprout-dev-mcp` (shell + file-edit tools) · `sprout-workflow` (YAML automation) · `sprout-persona` (agent persona packs)
**Agent surface** — `buzz-cli` (agent-first CLI, JSON in / JSON out) · `buzz-acp` (ACP harness for Goose/Codex/Claude Code) · `buzz-agent` (ACP agent — see [VISION_AGENT.md](VISION_AGENT.md)) · `buzz-dev-mcp` (shell + file-edit tools) · `buzz-workflow` (YAML automation) · `buzz-persona` (agent persona packs)
**Git & pairing** — `git-sign-nostr` / `git-credential-nostr` (nostr-signed git) · `sprout-pair-relay` / `sprout-pairing-cli` (relay pairing)
**Git & pairing** — `git-sign-nostr` / `git-credential-nostr` (nostr-signed git) · `buzz-pair-relay` / `buzz-pairing-cli` (relay pairing)
**Shared** — `sprout-sdk` (typed event builders) · `sprout-media` (Blossom/S3)
**Shared** — `buzz-sdk` (typed event builders) · `buzz-media` (Blossom/S3)
**Tooling** — `sprout-admin` (admin CLI) · `sprout-test-client` (E2E)
**Tooling** — `buzz-admin` (admin CLI) · `buzz-test-client` (E2E)
</details>
@@ -183,7 +183,7 @@ just reset # ⚠️ Wipe data + recreate
## What it is not
- Not blockchain. Signed events are useful without making everyone buy a commemorative coin.
- Not an AI replacement plan. Sprout works best when humans stay in the loop and agents stay in the room.
- Not an AI replacement plan. Buzz works best when humans stay in the loop and agents stay in the room.
- Not finished. We will tell you what works and what doesn't.
**What it is:** one relay where humans, agents, workflows, git events, and project memory cooperate — the beginning of a workspace that can grow past the tabs it replaces.
@@ -191,6 +191,6 @@ just reset # ⚠️ Wipe data + recreate
---
<p align="center">
<sub>Sprout 🌱 — where humans and agents are just colleagues.</sub><br>
<sub>Buzz 🐝 — where humans and agents are just colleagues.</sub><br>
<sub>Apache 2.0 · Built by <a href="https://block.xyz">Block, Inc.</a></sub>
</p>
+3 -3
View File
@@ -4,7 +4,7 @@
**Please do not report security vulnerabilities through public GitHub issues.**
If you discover a security vulnerability in Sprout, please report it by emailing
If you discover a security vulnerability in Buzz, please report it by emailing
**security@sprout-relay.org**. Include as much detail as possible:
- A description of the vulnerability and its potential impact
@@ -33,7 +33,7 @@ We will credit reporters in release notes unless you prefer to remain anonymous.
| `main` (latest) | ✅ Active |
| Previous releases | ⚠️ Best-effort; upgrade recommended |
Sprout is pre-1.0. We do not maintain long-term support branches at this stage.
Buzz is pre-1.0. We do not maintain long-term support branches at this stage.
All security fixes land on `main` first.
---
@@ -66,7 +66,7 @@ unless the subscriber is a member.
### Append-Only Audit Log
All events are written to a tamper-evident audit log (`sprout-audit`). Each
All events are written to a tamper-evident audit log (`buzz-audit`). Each
log entry is chained to the previous one via an HMAC, making retroactive
modification detectable. The audit log is designed for SOX-grade compliance
and eDiscovery.
+73 -73
View File
@@ -9,11 +9,11 @@ just test # unit + integration (starts Docker if needed)
`just test` runs unit tests plus integration tests against Postgres and Redis
(started automatically if not already running). Neither task runs the E2E suites in
`sprout-test-client` — those are marked `#[ignore]` and require a running relay:
`buzz-test-client` — those are marked `#[ignore]` and require a running relay:
```bash
# Start a relay first (see below), then:
cargo test -p sprout-test-client -- --ignored
cargo test -p buzz-test-client -- --ignored
```
---
@@ -21,7 +21,7 @@ cargo test -p sprout-test-client -- --ignored
## Live Local Relay
The fastest way to exercise the relay end-to-end is to build the release
binaries once, run `sprout-relay`, and drive it with the `sprout` CLI. The
binaries once, run `buzz-relay`, and drive it with the `buzz` CLI. The
CLI signs every request with NIP-98, so you don't need `nak` or hand-rolled
`curl`.
@@ -33,33 +33,33 @@ cp .env.example .env # one-time
just setup # start Docker services, run migrations
```
> **Already running Sprout Desktop?** Desktop uses the same Docker container
> names (`sprout-postgres`, `sprout-redis`, `sprout-typesense`) and the same
> **Already running Buzz Desktop?** Desktop uses the same Docker container
> names (`buzz-postgres`, `buzz-redis`, `buzz-typesense`) and the same
> default ports (`:5432`, `:6379`, `:8108`). `just setup` will reuse those
> services, so **your test relay writes into Desktop's database**. That's
> fine for read/write smoke tests, but: `just reset` wipes Desktop's data
> along with yours. If you need isolation, stop Desktop first or run the
> dev stack on a different Compose project
> (`COMPOSE_PROJECT_NAME=sprout-dev docker compose …`).
> (`COMPOSE_PROJECT_NAME=buzz-dev docker compose …`).
`just reset` wipes all local data and starts over — **including Sprout
`just reset` wipes all local data and starts over — **including Buzz
Desktop's data** if its services are sharing your dev stack (see callout
above).
> **Heads up — scrub stale env first.** If your shell inherits any of
> `SPROUT_AUTH_TAG`, `SPROUT_RELAY_URL`, or `SPROUT_PRIVATE_KEY` from a
> `BUZZ_AUTH_TAG`, `BUZZ_RELAY_URL`, or `BUZZ_PRIVATE_KEY` from a
> prior session (or a staging config), `unset` them before continuing.
> A stale `SPROUT_AUTH_TAG` fails the **local dev relay** with
> A stale `BUZZ_AUTH_TAG` fails the **local dev relay** with
> `auth_error: signature verification failed` on the first CLI write —
> it is *not* tolerated.
> ```bash
> unset SPROUT_AUTH_TAG SPROUT_RELAY_URL SPROUT_PRIVATE_KEY
> unset BUZZ_AUTH_TAG BUZZ_RELAY_URL BUZZ_PRIVATE_KEY
> ```
### 2. Build the binaries
```bash
cargo build --release -p sprout-relay -p sprout-cli -p sprout-admin
cargo build --release -p buzz-relay -p buzz-cli -p buzz-admin
export PATH="$PWD/target/release:$PATH"
```
@@ -70,9 +70,9 @@ Rebuild after any code change — the steps below use the release binaries.
In a separate terminal (it runs in the foreground):
```bash
sprout-relay # release binary from step 2, serves ws://localhost:3000
buzz-relay # release binary from step 2, serves ws://localhost:3000
# alternatives:
# cargo run --release -p sprout-relay # rebuild + run in release
# cargo run --release -p buzz-relay # rebuild + run in release
# just relay # DEBUG build — fast to launch on a hot cache,
# # but mismatched if step 2 left you on release.
# # Use `just relay-release` if you want the recipe.
@@ -86,30 +86,30 @@ curl -s http://localhost:8080/_readiness # → {"status":"ready"}
```
> Health/readiness/liveness live on a **separate port** (default `8080`,
> `SPROUT_HEALTH_PORT`) so K8s probes bypass auth middleware. The main app
> `BUZZ_HEALTH_PORT`) so K8s probes bypass auth middleware. The main app
> port also exposes `/health` for convenience.
The relay starts in dev mode (`SPROUT_REQUIRE_AUTH_TOKEN=false`). The startup
The relay starts in dev mode (`BUZZ_REQUIRE_AUTH_TOKEN=false`). The startup
log emits a WARN about this — that's expected for local testing. See the env
vars table at the bottom if you need to lock it down.
> **Already running Sprout Desktop (or another relay) on `:3000` / `:8080` /
> `:9102`?** Sprout binds three ports — main, health, metrics — and any of
> **Already running Buzz Desktop (or another relay) on `:3000` / `:8080` /
> `:9102`?** Buzz binds three ports — main, health, metrics — and any of
> them can collide. Use a separate terminal per role and export the right
> vars in each:
>
> **In the relay terminal** (before launching `sprout-relay`):
> **In the relay terminal** (before launching `buzz-relay`):
> ```bash
> export SPROUT_BIND_ADDR=0.0.0.0:3030
> export SPROUT_HEALTH_PORT=8088
> export SPROUT_METRICS_PORT=9202
> export BUZZ_BIND_ADDR=0.0.0.0:3030
> export BUZZ_HEALTH_PORT=8088
> export BUZZ_METRICS_PORT=9202
> export RELAY_URL=ws://localhost:3030 # advertised in NIP-42 challenges
> sprout-relay
> buzz-relay
> ```
>
> **In your working / CLI terminal** (for steps 4+ and the ACP harness):
> ```bash
> export SPROUT_RELAY_URL=http://localhost:3030 # CLI target
> export BUZZ_RELAY_URL=http://localhost:3030 # CLI target
> # verify the relay on the overridden ports:
> curl -s http://localhost:3030/health # → ok
> curl -s http://localhost:8088/_readiness # → {"status":"ready"}
@@ -117,14 +117,14 @@ vars table at the bottom if you need to lock it down.
>
> Every snippet later in this doc shows the defaults. When you see
> `localhost:3000` / `:8080` in a code block, mentally substitute your
> overrides — or the CLI will end up talking to Sprout Desktop's relay.
> overrides — or the CLI will end up talking to Buzz Desktop's relay.
> **Ignore `just setup`'s "Next steps" banner.** It still prints
> `just relay` (a debug build). Use `sprout-relay` from step 2 here —
> `just relay` (a debug build). Use `buzz-relay` from step 2 here —
> step 2 already built the release binary.
When you're done, stop the relay (Ctrl-C in its terminal). If it's
backgrounded or you lost the terminal: `pkill -f sprout-relay`. Leaving
backgrounded or you lost the terminal: `pkill -f buzz-relay`. Leaving
it running will collide with the next reviewer who follows this doc on
the same machine.
@@ -135,22 +135,22 @@ back. This is the minimum sequence an agent needs to verify a local relay.
```bash
# Generate a keypair
GEN=$(sprout-admin generate-key)
export SPROUT_PRIVATE_KEY=$(echo "$GEN" | awk '/Secret key:/ {print $3}')
GEN=$(buzz-admin generate-key)
export BUZZ_PRIVATE_KEY=$(echo "$GEN" | awk '/Secret key:/ {print $3}')
PUBKEY=$(echo "$GEN" | awk '/Public key:/ {print $3}')
echo "pubkey: $PUBKEY"
# Create a channel — the UUID is returned in the response
CHANNEL=$(sprout channels create --name "smoke-$$" --type stream --visibility open | jq -r '.channel_id')
CHANNEL=$(buzz channels create --name "smoke-$$" --type stream --visibility open | jq -r '.channel_id')
echo "channel: $CHANNEL"
# Send a message and read it back
SEND=$(sprout messages send --channel "$CHANNEL" --content "hello from smoke test")
SEND=$(buzz messages send --channel "$CHANNEL" --content "hello from smoke test")
EVENT_ID=$(echo "$SEND" | jq -r '.event_id')
sprout messages get --channel "$CHANNEL" --limit 5 | jq .
buzz messages get --channel "$CHANNEL" --limit 5 | jq .
# Fetch the reply chain for a specific message (empty array on a leaf — that's fine)
sprout messages thread --channel "$CHANNEL" --event "$EVENT_ID" | jq .
buzz messages thread --channel "$CHANNEL" --event "$EVENT_ID" | jq .
```
A successful run prints `{"event_id":"…","accepted":true,"message":""}` for
@@ -160,10 +160,10 @@ for a leaf message — populated only after a reply comes in (see §5).
### 5. Going deeper
For full coverage of every CLI command (54 subcommands across 12 groups),
follow [`crates/sprout-cli/TESTING.md`](crates/sprout-cli/TESTING.md).
follow [`crates/buzz-cli/TESTING.md`](crates/buzz-cli/TESTING.md).
The relay's HTTP bridge accepts three endpoints — useful if you're testing
a client other than `sprout-cli`:
a client other than `buzz-cli`:
| Endpoint | Purpose |
|-----------------|------------------------------------|
@@ -173,57 +173,57 @@ a client other than `sprout-cli`:
All three accept NIP-98 auth (recommended) or, in dev mode, an `X-Pubkey`
header fallback. There is no REST API for fetching message threads — use
`POST /query` with an `#e` filter, or `sprout messages thread`.
`POST /query` with an `#e` filter, or `buzz messages thread`.
---
## ACP Harness (optional, end-to-end with a real agent)
`sprout-acp` connects an ACP-speaking agent (goose, codex, claude code,
sprout-agent) to the relay. The harness listens for events, drives the
`buzz-acp` connects an ACP-speaking agent (goose, codex, claude code,
buzz-agent) to the relay. The harness listens for events, drives the
agent over stdio, and the agent replies through MCP tools.
Minimum recipe — assumes the relay from step 3 is running and the channel
`$CHANNEL` from step 4 still exists. The agent identity must be **different**
from the sender identity (`SPROUT_ACP_RESPOND_TO=anyone` still skips events
from the sender identity (`BUZZ_ACP_RESPOND_TO=anyone` still skips events
the agent signed itself).
```bash
cargo build --release -p sprout-acp
cargo build --release -p buzz-acp
export PATH="$PWD/target/release:$PATH"
# 1. Save your sender identity from step 4 — you'll need it to @mention the agent
SENDER_SK="$SPROUT_PRIVATE_KEY"
SENDER_SK="$BUZZ_PRIVATE_KEY"
# 2. Mint a fresh agent identity and capture its pubkey
AGENT_GEN=$(sprout-admin generate-key)
AGENT_GEN=$(buzz-admin generate-key)
AGENT_SK=$(echo "$AGENT_GEN" | awk '/Secret key:/ {print $3}')
AGENT_PUBKEY=$(echo "$AGENT_GEN" | awk '/Public key:/ {print $3}')
# 3. Add the agent as a member of $CHANNEL — still using the sender identity.
# Skip this and the agent boots to "discovered 0 channel(s) → agent will
# sit idle" and silently ignores every mention.
sprout channels add-member --channel "$CHANNEL" --pubkey "$AGENT_PUBKEY" --role member
buzz channels add-member --channel "$CHANNEL" --pubkey "$AGENT_PUBKEY" --role member
# 4. Switch to the agent identity and start it.
# sprout-acp wants ws:// (not http://). If you set SPROUT_RELAY_URL to an
# buzz-acp wants ws:// (not http://). If you set BUZZ_RELAY_URL to an
# http:// URL in step 3, set the ws:// equivalent here — same host/port.
export SPROUT_PRIVATE_KEY="$AGENT_SK"
export SPROUT_RELAY_URL=ws://localhost:3000 # match step 3 (e.g. ws://localhost:3030 if overridden)
export SPROUT_ACP_RESPOND_TO=anyone # default is owner-only; opens the gate for testing
# NIP-AE core-memory prompt injection is on by default; set SPROUT_ACP_NO_MEMORY=true to opt out.
export BUZZ_PRIVATE_KEY="$AGENT_SK"
export BUZZ_RELAY_URL=ws://localhost:3000 # match step 3 (e.g. ws://localhost:3030 if overridden)
export BUZZ_ACP_RESPOND_TO=anyone # default is owner-only; opens the gate for testing
# NIP-AE core-memory prompt injection is on by default; set BUZZ_ACP_NO_MEMORY=true to opt out.
export GOOSE_MODE=auto # must be 'auto' or goose hangs on prompts
sprout-acp # foreground; logs to stdout (run in a separate terminal)
buzz-acp # foreground; logs to stdout (run in a separate terminal)
# Optional: turn on per-turn tracing if the default log is too quiet.
# RUST_LOG=sprout_acp=debug sprout-acp
# RUST_LOG=buzz_acp=debug buzz-acp
```
> **Using a different ACP agent?** The default recipe assumes `goose` is on
> `$PATH` and configured (`goose --version` should print). For codex / claude
> code / sprout-agent, set `SPROUT_ACP_AGENT_COMMAND` and `SPROUT_ACP_AGENT_ARGS`
> accordingly — see `crates/sprout-acp/README.md`. Without these, sprout-acp
> code / buzz-agent, set `BUZZ_ACP_AGENT_COMMAND` and `BUZZ_ACP_AGENT_ARGS`
> accordingly — see `crates/buzz-acp/README.md`. Without these, buzz-acp
> will fail to spawn the agent subprocess on startup.
If you started the agent before adding it to the channel, just run the
@@ -232,24 +232,24 @@ subscribes without restart (`membership notification: subscribing to new channel
The justfile also ships `just goose key="$AGENT_NSEC"` (foreground) and
`just goose-bg key="$AGENT_NSEC"` (background screen session) which set the
same env. See `crates/sprout-acp/README.md` for parallel agents, heartbeats,
same env. See `crates/buzz-acp/README.md` for parallel agents, heartbeats,
respond-to gates, and forum subscriptions.
Send the agent a task — switch your shell back to the **sender** identity
from step 4 and @mention the agent:
```bash
export SPROUT_PRIVATE_KEY=$SENDER_SK # the key from step 4
sprout messages send --channel "$CHANNEL" \
export BUZZ_PRIVATE_KEY=$SENDER_SK # the key from step 4
buzz messages send --channel "$CHANNEL" \
--content "Hey agent, reply PONG only."
# Wait 1090s, then read the channel — the agent's reply is a kind:9 from
# AGENT_PUBKEY. The current ACP build is quiet on stdout during a turn, so
# `sprout messages get` is how you confirm it ran.
sprout messages get --channel "$CHANNEL" --limit 5 | jq '.[] | {pubkey, content}'
# `buzz messages get` is how you confirm it ran.
buzz messages get --channel "$CHANNEL" --limit 5 | jq '.[] | {pubkey, content}'
```
Replies are kind:9 in the same channel; `sprout messages thread --channel <id>
Replies are kind:9 in the same channel; `buzz messages thread --channel <id>
--event <event_id>` fetches the reply chain for a specific mention.
---
@@ -261,25 +261,25 @@ out of the box with `just setup` or `just relay`. Common overrides:
| Variable | Default | Notes |
|-----------------------------------|-----------------------------|-------|
| `SPROUT_BIND_ADDR` | `0.0.0.0:3000` | Main app port |
| `SPROUT_HEALTH_PORT` | `8080` | `/_liveness`, `/_readiness` |
| `SPROUT_METRICS_PORT` | `9102` | Prometheus `/metrics` |
| `BUZZ_BIND_ADDR` | `0.0.0.0:3000` | Main app port |
| `BUZZ_HEALTH_PORT` | `8080` | `/_liveness`, `/_readiness` |
| `BUZZ_METRICS_PORT` | `9102` | Prometheus `/metrics` |
| `RELAY_URL` | `ws://localhost:3000` | Advertised in NIP-11 / NIP-42 challenges. **Note: no `SPROUT_` prefix.** |
| `DATABASE_URL` | `postgres://sprout:sprout_dev@localhost:5432/sprout` | |
| `DATABASE_URL` | `postgres://buzz:buzz_dev@localhost:5432/buzz` | |
| `REDIS_URL` | `redis://localhost:6379` | |
| `TYPESENSE_URL` | `http://localhost:8108` | |
| `SPROUT_REQUIRE_AUTH_TOKEN` | `false` | When true, REST requires NIP-98 (no `X-Pubkey` fallback) |
| `SPROUT_REQUIRE_RELAY_MEMBERSHIP` | `false` | When true, only pubkeys in `relay_members` can connect |
| `BUZZ_REQUIRE_AUTH_TOKEN` | `false` | When true, REST requires NIP-98 (no `X-Pubkey` fallback) |
| `BUZZ_REQUIRE_RELAY_MEMBERSHIP` | `false` | When true, only pubkeys in `relay_members` can connect |
| `RELAY_OWNER_PUBKEY` | unset | Bootstrapped as `owner` in `relay_members` at first start |
| `SPROUT_ALLOW_NIP_OA_AUTH` | `false` | Enable NIP-OA owner attestation for membership |
| `BUZZ_ALLOW_NIP_OA_AUTH` | `false` | Enable NIP-OA owner attestation for membership |
CLI-side, only two matter for testing:
| Variable | Default | Notes |
|-------------------------|--------------------------|-------|
| `SPROUT_RELAY_URL` | `http://localhost:3000` | CLI relay base; accepts `ws(s)://` and normalises |
| `SPROUT_PRIVATE_KEY` | — (**required**) | `nsec1…` or 64-char hex |
| `SPROUT_AUTH_TAG` | unset | Optional NIP-OA owner attestation JSON |
| `BUZZ_RELAY_URL` | `http://localhost:3000` | CLI relay base; accepts `ws(s)://` and normalises |
| `BUZZ_PRIVATE_KEY` | — (**required**) | `nsec1…` or 64-char hex |
| `BUZZ_AUTH_TAG` | unset | Optional NIP-OA owner attestation JSON |
---
@@ -288,12 +288,12 @@ CLI-side, only two matter for testing:
| Symptom | Cause | Fix |
|---------|-------|-----|
| `relay error 500` or `400: restricted: not a channel member` after a code change | Stale binary | Rebuild and re-export `PATH`; or `cargo run` directly |
| `Address already in use` on relay start (os error 48 on macOS, 98 on Linux) | Another relay (or stale process) holding `:3000` / `:8080` / `:9102` (or your override ports) | The panic line names the failing port — read it first. Then `lsof -iTCP:3000,8080,9102 -sTCP:LISTEN` (or your override equivalents). Kill the offender (`pkill -f sprout-relay`) or use the port-override block in step 3. If you already overrode and *still* collide, a prior reviewer left a relay running on the same alt ports — kill it or pick fresh ports |
| `auth_error: SPROUT_PRIVATE_KEY is required` | Env not exported into the CLI's shell | `export SPROUT_PRIVATE_KEY=...` (or pass `--private-key`) |
| `auth_error: SPROUT_AUTH_TAG verification failed … signature verification failed` | A stale `SPROUT_AUTH_TAG` inherited from a parent shell. The local dev relay rejects it. | `unset SPROUT_AUTH_TAG` (see the scrub block in step 1) |
| `auth-required: verification failed` on a closed relay | NIP-OA attestation needed | Set `SPROUT_AUTH_TAG` to the owner-issued JSON, or relax `SPROUT_REQUIRE_RELAY_MEMBERSHIP` |
| `Address already in use` on relay start (os error 48 on macOS, 98 on Linux) | Another relay (or stale process) holding `:3000` / `:8080` / `:9102` (or your override ports) | The panic line names the failing port — read it first. Then `lsof -iTCP:3000,8080,9102 -sTCP:LISTEN` (or your override equivalents). Kill the offender (`pkill -f buzz-relay`) or use the port-override block in step 3. If you already overrode and *still* collide, a prior reviewer left a relay running on the same alt ports — kill it or pick fresh ports |
| `auth_error: BUZZ_PRIVATE_KEY is required` | Env not exported into the CLI's shell | `export BUZZ_PRIVATE_KEY=...` (or pass `--private-key`) |
| `auth_error: BUZZ_AUTH_TAG verification failed … signature verification failed` | A stale `BUZZ_AUTH_TAG` inherited from a parent shell. The local dev relay rejects it. | `unset BUZZ_AUTH_TAG` (see the scrub block in step 1) |
| `auth-required: verification failed` on a closed relay | NIP-OA attestation needed | Set `BUZZ_AUTH_TAG` to the owner-issued JSON, or relax `BUZZ_REQUIRE_RELAY_MEMBERSHIP` |
| `channels list` empty after `channels create` | The CLI doesn't echo the channel UUID; use the filter shown in step 4 | Or `POST /query` with `{"kinds":[39002]}` |
| ACP agent ignores all events | `SPROUT_ACP_RESPOND_TO=owner-only` (default) with no owner configured | Set `SPROUT_ACP_RESPOND_TO=anyone` for testing |
| ACP logs `discovered 0 channel(s)` / `no channel subscriptions resolved` | Agent identity isn't a member of any channel | `sprout channels add-member --channel "$CHANNEL" --pubkey "$AGENT_PUBKEY" --role member` from another identity |
| ACP agent ignores all events | `BUZZ_ACP_RESPOND_TO=owner-only` (default) with no owner configured | Set `BUZZ_ACP_RESPOND_TO=anyone` for testing |
| ACP logs `discovered 0 channel(s)` / `no channel subscriptions resolved` | Agent identity isn't a member of any channel | `buzz channels add-member --channel "$CHANNEL" --pubkey "$AGENT_PUBKEY" --role member` from another identity |
| `GOOSE_MODE` warning, agent hangs | Not set | `export GOOSE_MODE=auto` |
| Tests pass locally but CI fails | Forgot to run `just ci` | `just ci` runs the gate (fmt, clippy, unit tests, desktop/web builds) |
+4 -4
View File
@@ -1,18 +1,18 @@
# Examples
This directory contains reference material for building on Sprout beyond the desktop app and AI agents.
This directory contains reference material for building on Buzz beyond the desktop app and AI agents.
## `countdown-bot/`
A small non-AI bot that connects directly to the Sprout relay over WebSocket, authenticates with NIP-42, subscribes to one channel, and replies to deterministic commands like `!countdown 5` and `!fib 8`.
A small non-AI bot that connects directly to the Buzz relay over WebSocket, authenticates with NIP-42, subscribes to one channel, and replies to deterministic commands like `!countdown 5` and `!fib 8`.
It demonstrates two identity paths:
1. **Standalone bot identity** — the bot authenticates with its own key and must be explicitly admitted to closed/allowlisted relays.
2. **Owner-attested / agent OAuth path** — the bot authenticates with its own key while presenting the same `BUZZ_AUTH_TAG` NIP-OA credential that Sprout agents receive from the owner/agent OAuth flow, so a relay can admit it because its owner is already a relay member.
2. **Owner-attested / agent OAuth path** — the bot authenticates with its own key while presenting the same `BUZZ_AUTH_TAG` NIP-OA credential that Buzz agents receive from the owner/agent OAuth flow, so a relay can admit it because its owner is already a relay member.
See [`countdown-bot/README.md`](countdown-bot/README.md) for usage.
## `meadow-core/`
A persona-pack example for Sprout agents.
A persona-pack example for Buzz agents.
+7 -7
View File
@@ -1,22 +1,22 @@
# Countdown Bot
A tiny non-AI Sprout bot example.
A tiny non-AI Buzz bot example.
The bot is deliberately boring and algorithmic: it listens to one Sprout channel
The bot is deliberately boring and algorithmic: it listens to one Buzz channel
and replies to simple commands:
- `!countdown 5``5 4 3 2 1 🚀`
- `!fib 8``13 8 5 3 2 1 1 0`
- `@Countdown Bot fib 8``13 8 5 3 2 1 1 0`
It demonstrates that Sprout participants do not have to be LLM agents. Any
It demonstrates that Buzz participants do not have to be LLM agents. Any
process that can hold a Nostr key, answer NIP-42 auth, publish a kind `0`
profile, subscribe to events, and publish kind `9` channel messages can be a bot.
On startup it publishes a profile named **Countdown Bot** with a small embedded
SVG clock icon, then best-effort publishes a NIP-29 `kind:9000` self-add with
`role=bot`. That channel membership is what makes the bot show up in the
members list and in Sprout's mention autocomplete.
members list and in Buzz's mention autocomplete.
## Auth paths
@@ -42,7 +42,7 @@ owner's access; revoking the bot requires removing this bot pubkey.
The bot still signs messages with its own key, but its NIP-42 `AUTH` event also
carries a NIP-OA `auth` tag signed by an owner key that is already allowed on the
relay. This reuses the same owner-attestation credential path that Sprout agents
relay. This reuses the same owner-attestation credential path that Buzz agents
receive after the owner/agent OAuth flow: the relay can let the bot connect
because the owner is a relay member, without making the bot key a persistent
relay member.
@@ -82,7 +82,7 @@ it to appear in members, resolve in mention autocomplete, or read/write messages
## Try it locally
1. Start Sprout:
1. Start Buzz:
```bash
. ./bin/activate-hermit
@@ -108,7 +108,7 @@ it to appear in members, resolve in mention autocomplete, or read/write messages
make the bot spam the relay. Out-of-range commands get an explicit help reply.
- `!fib` replies in descending order because this example is a countdown bot.
- Mention commands require both text like `@Countdown Bot fib 8` and a `p` tag
for the bot pubkey. The Sprout UI adds that tag when the bot is selected from
for the bot pubkey. The Buzz UI adds that tag when the bot is selected from
mention autocomplete.
- The bot ignores its own messages to avoid feedback loops.
- The example uses direct WebSocket + NIP-42 instead of MCP so the protocol path
+4 -4
View File
@@ -1,6 +1,6 @@
# Meadow Core
A minimal three-agent persona pack for Sprout.
A minimal three-agent persona pack for Buzz.
| Agent | Role |
|-------|------|
@@ -12,10 +12,10 @@ A minimal three-agent persona pack for Sprout.
```bash
# Validate the pack
sprout pack validate ./examples/meadow-core
buzz pack validate ./examples/meadow-core
# Inspect resolved config
sprout pack inspect ./examples/meadow-core
buzz pack inspect ./examples/meadow-core
# Import into the desktop app
# Use the "Install Pack" button and point to this directory
@@ -44,4 +44,4 @@ Edit any `.persona.md` file to change the agent's behavior. The YAML
frontmatter controls config (model, triggers, channels). The markdown
body is the system prompt.
See `crates/sprout-persona/PERSONA_PACK_SPEC.md` for the full format reference.
See `crates/buzz-persona/PERSONA_PACK_SPEC.md` for the full format reference.
+4 -4
View File
@@ -2,7 +2,7 @@
# Reap the agent processes belonging to a single desktop instance.
#
# `tauri dev` Ctrl+C tears down the Rust app before its in-process system sweep
# can finish, so agent workers (goose, sprout-agent, ...) it spawned in their
# can finish, so agent workers (goose, buzz-agent, ...) it spawned in their
# own process groups survive as orphans. This script is the shell-side backstop:
# run it from an EXIT trap in the `just dev`/`just staging` recipes.
#
@@ -10,7 +10,7 @@
# under `<app-data>/agents/agent-pids/<pubkey>.pid`, each containing the agent's
# PGID (agents are spawned with `process_group(0)`, so PID == PGID). Killing by
# PGID reaches the whole agent subtree. We deliberately do NOT match the
# `SPROUT_MANAGED_AGENT` env var from the shell: on macOS `pkill -f` matches only
# `BUZZ_MANAGED_AGENT` env var from the shell: on macOS `pkill -f` matches only
# argv, not the environment, so an env-marker match silently reaps nothing.
#
# Scoping is exact because the app-data directory is keyed by the instance's
@@ -18,8 +18,8 @@
# (the main checkout never reaps a worktree's agents, or vice versa).
#
# Usage: cleanup-instance-agents.sh <instance-id>
# <instance-id> is the desktop bundle identifier, e.g. `xyz.block.sprout.app.dev`
# (main checkout) or `xyz.block.sprout.app.dev.my-branch` (a worktree).
# <instance-id> is the desktop bundle identifier, e.g. `xyz.block.buzz.app.dev`
# (main checkout) or `xyz.block.buzz.app.dev.my-branch` (a worktree).
set -euo pipefail