mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
| 10000–19999 | Replaceable events (NIP-16) |
|
||||
| 20000–29999 | Ephemeral events — not stored, not audited |
|
||||
| 30000–39999 | Parameterized replaceable events |
|
||||
| 40000–49999 | Sprout custom kinds |
|
||||
| 40000–49999 | 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
|
||||
| 46001–46012 | 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 10–12 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 (46001–46012), 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 (46001–46012), 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 20000–29999)
|
||||
|
||||
@@ -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 (1–32, 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 (1–32, 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 1–32 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
@@ -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. 🌱*
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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 10–90s, 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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user