docs: update all documentation to reflect current codebase (#40)

This commit is contained in:
tlongwell-block
2026-03-12 14:21:25 -04:00
committed by GitHub
parent 272708cb14
commit 80356e8cb5
6 changed files with 194 additions and 769 deletions
+100 -591
View File
@@ -1,620 +1,129 @@
# Sprout Agent Integration Guide
# AGENTS.md — AI Agent Contributor Guide
Agents connect to Sprout via MCP (Model Context Protocol) over stdio. Each agent authenticates
with a Nostr keypair using NIP-42 challenge/response, optionally presenting an API token for
elevated scopes. Once connected, agents interact through standard MCP tools: send messages,
read history, create channels, and manage canvases.
This guide is for AI agents contributing to the Sprout codebase. It covers
agent-specific context and conventions. For general contributor info (setup,
code style, PR process, architecture), see [CONTRIBUTING.md](CONTRIBUTING.md).
---
## Prerequisites
## Repo Structure
- Built `sprout-mcp-server` binary (`cargo build -p sprout-mcp` or from release)
- Running Sprout relay (default: `ws://localhost:3000`)
- MySQL database running with `DATABASE_URL` set (for token minting)
- A minted API token (or a Nostr keypair for open-relay dev mode)
---
## Minting a Token
Use `sprout-admin mint-token` to create an API token bound to a Nostr pubkey.
**Generate a new keypair + token in one step:**
```bash
DATABASE_URL="mysql://sprout:sprout_dev@localhost:3306/sprout" \
sprout-admin mint-token \
--name "my-agent" \
--scopes "messages:read,messages:write,channels:read"
```
crates/
sprout-relay # WebSocket relay server — main entry point
sprout-core # Core types, event verification, filter matching
sprout-db # MySQL event store and data access layer
sprout-auth # Authentication and authorization
sprout-pubsub # Redis pub/sub fan-out, presence, typing indicators
sprout-mcp # MCP server providing AI agent tools
sprout-acp # ACP harness bridging Sprout events to AI agents
sprout-workflow # YAML-as-code workflow engine (evalexpr conditions)
sprout-search # Typesense-backed full-text search
sprout-audit # Hash-chain audit log
sprout-huddle # LiveKit audio/video integration
sprout-proxy # Nostr client compatibility proxy
sprout-admin # Operator CLI for relay administration
sprout-test-client # Integration test client and E2E test suite
Output includes a one-time-shown private key (`nsec...`) and API token. Save both immediately.
**Bind token to an existing pubkey:**
```bash
DATABASE_URL="mysql://sprout:sprout_dev@localhost:3306/sprout" \
sprout-admin mint-token \
--name "my-agent" \
--scopes "messages:read,messages:write,channels:read,channels:write" \
--pubkey <hex-pubkey>
```
**List active tokens:**
```bash
DATABASE_URL="mysql://sprout:sprout_dev@localhost:3306/sprout" \
sprout-admin list-tokens
desktop/ # Tauri 2 + React 19 desktop app
migrations/ # SQL migrations (auto-applied on relay startup)
scripts/ # Dev tooling
.env.example # Config template — copy to .env before running
```
---
## Connecting an Agent
### Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
| `SPROUT_RELAY_URL` | No | `ws://localhost:3000` | WebSocket URL of the relay |
| `SPROUT_PRIVATE_KEY` | No | ephemeral (generated) | Nostr private key (`nsec...` or hex) |
| `SPROUT_API_TOKEN` | No | none | API token for elevated scopes |
If `SPROUT_PRIVATE_KEY` is omitted, a random keypair is generated each run (ephemeral identity).
If `SPROUT_API_TOKEN` is omitted on an open relay (`SPROUT_REQUIRE_AUTH_TOKEN=false`), the agent gets
baseline `messages:read` + `messages:write` scopes only.
### Goose (stdio MCP)
## Getting Started
```bash
goose --with-extension "SPROUT_RELAY_URL=ws://localhost:3000 SPROUT_PRIVATE_KEY=nsec1... SPROUT_API_TOKEN=<token> sprout-mcp-server"
. ./bin/activate-hermit # activate hermit toolchain (Rust, Node, etc.)
cp .env.example .env # configure local environment
just setup # install deps, run migrations
just relay # start relay at ws://localhost:3000
just ci # run before any PR
```
Or in a goose profile / config:
```yaml
extensions:
- name: sprout
cmd: sprout-mcp-server
env:
SPROUT_RELAY_URL: ws://localhost:3000
SPROUT_PRIVATE_KEY: nsec1abc...
SPROUT_API_TOKEN: spr_tok_...
```
See CONTRIBUTING.md for full setup details and dependency requirements.
---
## Quality Gates
Run `just ci` before every PR. It runs: `fmt`, `clippy`, unit tests, desktop
build, and Tauri check. All must pass.
Run `just test` for integration tests if you touched `sprout-relay`,
`sprout-db`, or `sprout-auth` — these require a running MySQL and Redis.
Additional rules:
- No `unsafe` code
- No `unwrap()` or `expect()` in production paths — use `?` and proper error types
- New public API must have doc comments
---
## 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
a feature, implement the shared DB logic first, then wire up both surfaces.
**Event kinds**: All event kind integers are defined in
`sprout-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.
**MCP tools proxy REST**: The MCP server in `sprout-mcp` wraps REST endpoints.
Add the REST endpoint first, then add the MCP tool that calls it. Do not
implement logic directly in MCP handlers.
**Workflow conditions**: `sprout-workflow` uses
[evalexpr](https://docs.rs/evalexpr) for condition evaluation. Keep expressions
simple and testable.
**Thread counters**: `reply_count` and `descendant_count` are materialized on
thread root events. Any code that inserts replies must update these counters —
check existing reply handlers for the pattern.
---
## Testing
### Direct stdio test
```bash
SPROUT_RELAY_URL=ws://localhost:3000 \
SPROUT_PRIVATE_KEY=nsec1abc... \
SPROUT_API_TOKEN=spr_tok_... \
sprout-mcp-server
just test-unit # unit tests, no infrastructure needed
just test # full integration suite (requires MySQL + Redis)
```
Logs go to stderr; MCP JSON-RPC runs on stdout.
E2E tests live in `crates/sprout-test-client/tests/`:
- `e2e_rest_api.rs` — REST endpoint coverage
- `e2e_relay.rs` — WebSocket relay protocol
- `e2e_mcp.rs` — MCP tool surface
- `e2e_tokens.rs` — auth token flows
- `e2e_workflows.rs` — workflow engine
Desktop E2E: `cd desktop && pnpm exec playwright test`
See [TESTING.md](TESTING.md) for the full multi-agent E2E guide.
---
## MCP Tools Reference
## Desktop App
Sprout exposes **16 MCP tools** across three groups: messaging & channels,
workflow management, and home feed.
The desktop app is Tauri 2 + React 19 + Vite + Tailwind CSS. Features are
organized under `desktop/src/features/`. Biome handles linting and formatting.
---
### Messaging & Channels
### `send_message`
Send a message to a channel.
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
| `channel_id` | string (UUID) | ✅ | — | Must be a valid UUID |
| `content` | string | ✅ | — | Message body |
| `kind` | integer | No | `40001` | Nostr event kind |
```json
{
"tool": "send_message",
"arguments": {
"channel_id": "550e8400-e29b-41d4-a716-446655440000",
"content": "Hello from the agent"
}
}
```
Returns: `"Message sent. Event ID: <hex>"` or error string.
---
### `get_channel_history`
Fetch recent messages from a channel.
| Parameter | Type | Required | Default |
|---|---|---|---|
| `channel_id` | string (UUID) | ✅ | — |
| `limit` | integer | No | `50` |
```json
{
"tool": "get_channel_history",
"arguments": {
"channel_id": "550e8400-e29b-41d4-a716-446655440000",
"limit": 20
}
}
```
Returns: JSON array of `{ id, pubkey, content, kind, created_at }` objects.
---
### `list_channels`
List channels accessible to this agent.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| `visibility` | string | No | Filter by `"open"` or `"private"`**not yet implemented**; parameter is accepted but ignored |
```json
{ "tool": "list_channels", "arguments": {} }
```
Returns: JSON array of channel metadata events (kind 40/41).
---
### `create_channel`
Create a new channel.
| Parameter | Type | Required | Values |
|---|---|---|---|
| `name` | string | ✅ | — |
| `channel_type` | string | ✅ | `"stream"`, `"forum"`, `"dm"` |
| `visibility` | string | ✅ | `"open"`, `"private"` |
| `description` | string | No | — |
```json
{
"tool": "create_channel",
"arguments": {
"name": "agent-coordination",
"channel_type": "stream",
"visibility": "open",
"description": "Multi-agent task coordination"
}
}
```
Returns: `"Channel created. Event ID: <hex>"` or error string.
---
### `get_canvas`
Read the shared document (canvas) for a channel.
| Parameter | Type | Required |
|---|---|---|
| `channel_id` | string (UUID) | ✅ |
```json
{
"tool": "get_canvas",
"arguments": { "channel_id": "550e8400-e29b-41d4-a716-446655440000" }
}
```
Returns: Canvas content string, or `"No canvas set for this channel."`.
---
### `set_canvas`
Write or replace the canvas for a channel. Full replace — not a patch.
| Parameter | Type | Required |
|---|---|---|
| `channel_id` | string (UUID) | ✅ |
| `content` | string | ✅ |
```json
{
"tool": "set_canvas",
"arguments": {
"channel_id": "550e8400-e29b-41d4-a716-446655440000",
"content": "# Task Board\n\n## In Progress\n- Agent A: research\n"
}
}
```
Returns: `"Canvas updated."` or error string.
---
### Workflow Management
### `list_workflows`
List workflows defined in a channel.
| Parameter | Type | Required |
|---|---|---|
| `channel_id` | string (UUID) | ✅ |
```json
{
"tool": "list_workflows",
"arguments": { "channel_id": "550e8400-e29b-41d4-a716-446655440000" }
}
```
Returns: JSON array of workflow objects, or error string.
---
### `create_workflow`
Create a new workflow in a channel from a YAML definition.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| `channel_id` | string (UUID) | ✅ | Channel that owns the workflow |
| `yaml_definition` | string | ✅ | Full workflow YAML |
```json
{
"tool": "create_workflow",
"arguments": {
"channel_id": "550e8400-e29b-41d4-a716-446655440000",
"yaml_definition": "name: daily-standup\ntrigger:\n type: schedule\n cron: \"0 9 * * MON-FRI\"\nsteps:\n - action: send_message\n content: \"Good morning! Time for standup.\"\n"
}
}
```
Returns: JSON object with the created workflow ID, or error string.
---
### `update_workflow`
Replace a workflow's YAML definition. Full replace — not a patch.
| Parameter | Type | Required |
|---|---|---|
| `workflow_id` | string (UUID) | ✅ |
| `yaml_definition` | string | ✅ |
```json
{
"tool": "update_workflow",
"arguments": {
"workflow_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"yaml_definition": "name: daily-standup\ntrigger:\n type: schedule\n cron: \"0 10 * * MON-FRI\"\nsteps:\n - action: send_message\n content: \"Good morning! Standup in 10 minutes.\"\n"
}
}
```
Returns: JSON object with the updated workflow, or error string.
---
### `delete_workflow`
Delete a workflow by ID. This also cancels any pending runs.
| Parameter | Type | Required |
|---|---|---|
| `workflow_id` | string (UUID) | ✅ |
```json
{
"tool": "delete_workflow",
"arguments": { "workflow_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }
}
```
Returns: `"Workflow deleted."` or error string.
---
### `trigger_workflow`
Manually trigger a workflow with optional input variables. Useful for
webhook-triggered workflows or testing.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| `workflow_id` | string (UUID) | ✅ | — |
| `inputs` | object | No | JSON object of input variables passed to the workflow |
```json
{
"tool": "trigger_workflow",
"arguments": {
"workflow_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"inputs": { "incident_id": "INC-1234", "severity": "high" }
}
}
```
Returns: JSON object with the new run ID, or error string.
---
### `get_workflow_runs`
Get execution history for a workflow.
| Parameter | Type | Required | Default |
|---|---|---|---|
| `workflow_id` | string (UUID) | ✅ | — |
| `limit` | integer | No | `20` (max `100`) |
```json
{
"tool": "get_workflow_runs",
"arguments": {
"workflow_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"limit": 10
}
}
```
Returns: JSON array of run objects with status, start time, steps, and any
error messages.
---
### `approve_workflow_step`
Approve or deny a pending workflow approval step. The `approval_token` comes
from a `kind:46010` event posted to the channel when the workflow reaches a
`request_approval` step.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| `approval_token` | string | ✅ | Opaque token from the kind:46010 event |
| `approved` | boolean | ✅ | `true` = approve, `false` = deny |
| `note` | string | No | Human-readable note attached to the decision |
```json
{
"tool": "approve_workflow_step",
"arguments": {
"approval_token": "tok_appr_abc123xyz",
"approved": true,
"note": "Looks good — deploying to production."
}
}
```
Returns: Confirmation string, or error string.
**Pattern: agent as approver**
```
1. Agent subscribes to the channel (or polls get_feed_actions)
2. Sees a kind:46010 approval request event
3. Extracts the approval_token from the event tags
4. Calls approve_workflow_step with its decision
5. Workflow resumes (or is denied and halted)
```
---
### Feed
### `get_feed`
Get the agent's personalized home feed. Returns mentions, needs-action items,
channel activity, and agent activity — equivalent to what a human sees on the
Home tab in the desktop app.
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
| `since` | integer | No | now 7 days | Unix timestamp; only return items newer than this |
| `limit` | integer | No | `50` (max `50`) | Max items per category |
| `types` | string | No | all categories | Comma-separated filter: `"mentions,needs_action,activity,agent_activity"` |
```json
{
"tool": "get_feed",
"arguments": {
"since": 1700000000,
"limit": 20,
"types": "mentions,needs_action"
}
}
```
Returns: JSON object with categorized feed items.
---
### `get_feed_mentions`
Get only @mentions for this agent — events where the agent's pubkey appears
in a `p` tag. Equivalent to the @Mentions tab on the Home feed.
| Parameter | Type | Required | Default |
|---|---|---|---|
| `since` | integer | No | now 7 days |
| `limit` | integer | No | `50` (max `50`) |
```json
{
"tool": "get_feed_mentions",
"arguments": { "limit": 25 }
}
```
Returns: JSON array of mention events.
---
### `get_feed_actions`
Get items that require action from this agent: approval requests (`kind:46010`)
and reminders (`kind:40007`) addressed to the agent's pubkey. Equivalent to
the "Needs Action" section on the Home feed.
| Parameter | Type | Required | Default |
|---|---|---|---|
| `since` | integer | No | now 7 days |
| `limit` | integer | No | `50` (max `50`) |
```json
{
"tool": "get_feed_actions",
"arguments": {}
}
```
Returns: JSON array of action items. Each item includes the event kind, the
approval token (for `kind:46010`), and the channel context.
---
## Authentication Flow
1. Agent connects via WebSocket to the relay.
2. Relay sends `["AUTH", "<challenge>"]` (NIP-42).
3. Agent signs a `kind:22242` event containing the challenge and relay URL.
4. If `SPROUT_API_TOKEN` is set, the signed event also includes an `auth_token` tag with the token value.
5. Agent sends `["AUTH", <signed-event>]`.
6. Relay responds `["OK", <event-id>, true, ""]` on success.
```
Client Relay
| |
|------- WebSocket connect ---->|
|<------ ["AUTH", challenge] ---|
| |
| (sign kind:22242 + auth_token)|
|------- ["AUTH", event] ------>|
|<------ ["OK", id, true, ""] --|
| |
| (MCP tools now available) |
```
**Auth methods:**
| Method | When | Scopes |
|---|---|---|
| Keypair only (NIP-42) | No token, open relay | `messages:read`, `messages:write` |
| API token | `SPROUT_API_TOKEN` set | As minted |
| Okta JWT | JWT in `auth_token` tag | From JWT `scp`/`scope` claim |
AUTH events are never stored or logged by the relay.
---
## Scopes
| Scope | Allows |
|---|---|
| `messages:read` | Read channel messages and history |
| `messages:write` | Send messages to channels |
| `channels:read` | List and inspect channels |
| `channels:write` | Create channels |
| `admin:channels` | Modify/archive any channel |
| `users:read` | Read user profiles |
| `users:write` | Update user profiles |
| `admin:users` | Manage users (ban, role changes) |
| `jobs:read` | Read background job status |
| `jobs:write` | Submit background jobs |
| `subscriptions:read` | Read subscription records |
| `subscriptions:write` | Manage subscriptions |
| `files:read` | Read uploaded files |
| `files:write` | Upload files |
**Typical agent token:** `messages:read,messages:write,channels:read`
**Coordinator agent:** add `channels:write`
**Admin agent:** add `admin:channels,admin:users`
---
## Channel Model
### Types
| Type | Use Case |
|---|---|
| `stream` | Linear message feed (like a chat channel) |
| `forum` | Threaded discussion |
| `dm` | Direct message between two parties |
### Visibility
| Visibility | Behavior |
|---|---|
| `open` | Searchable; any authenticated agent can join and read |
| `private` | Hidden; invite-only; requires an owner/admin to add members |
### Roles
| Role | Capabilities |
|---|---|
| `owner` | Full control; can grant any role |
| `admin` | Manage members and content; can grant up to `admin` |
| `member` | Read and write messages |
| `guest` | Read-only access |
| `bot` | Programmatic access; same as `member` by default |
Agents joining open channels are assigned `member` role. Elevated roles (`owner`, `admin`)
require an existing owner/admin to grant them explicitly.
---
## Canvas
Each channel has one canvas — a shared mutable document stored as a string. Agents use it for
structured coordination: task boards, shared state, handoff notes.
- **One canvas per channel.** `set_canvas` is a full replace, not a patch.
- **Nostr kind 40100.** Canvas events are tagged with the channel ID (`e` tag).
- **Last write wins.** No merge — agents must read before write to avoid clobbering.
**Pattern: read-modify-write**
```
1. get_canvas(channel_id) → read current state
2. Modify content in memory
3. set_canvas(channel_id, content) → write full updated document
```
**Pattern: structured canvas (markdown)**
```markdown
# Agent Coordination — Channel: agent-coordination
## Status
- Agent A: researching auth patterns
- Agent B: idle
## Findings
- NIP-42 challenge timeout: 5s
- Token format: 32-byte random, hex-encoded
```
---
## Multi-Agent Setup
Each agent needs its own Nostr keypair. Tokens can share a keypair if scopes differ,
but separate keypairs give independent audit trails.
**Mint tokens for each agent:**
```bash
# Coordinator agent — can create channels
sprout-admin mint-token --name "coordinator" \
--scopes "messages:read,messages:write,channels:read,channels:write"
# Worker agent — messages only
sprout-admin mint-token --name "worker-1" \
--scopes "messages:read,messages:write,channels:read"
# Observer agent — read only
sprout-admin mint-token --name "observer" \
--scopes "messages:read,channels:read"
just desktop-dev # web-only dev server (faster iteration)
just desktop-app # full Tauri app with native shell
```
**Run agents with distinct identities:**
```bash
# Agent 1
SPROUT_PRIVATE_KEY=nsec1coordinator... SPROUT_API_TOKEN=tok_coord... sprout-mcp-server
---
# Agent 2
SPROUT_PRIVATE_KEY=nsec1worker1... SPROUT_API_TOKEN=tok_w1... sprout-mcp-server
## See Also
# Agent 3
SPROUT_PRIVATE_KEY=nsec1observer... SPROUT_API_TOKEN=tok_obs... sprout-mcp-server
```
**Coordination pattern using canvas + messages:**
- Coordinator creates a channel and sets the canvas with the task plan.
- Workers read the canvas to understand their assignments.
- Workers post progress updates as messages (`send_message`).
- Coordinator reads history (`get_channel_history`) and updates the canvas.
- All agents see the same channel state via the relay.
- [CONTRIBUTING.md](CONTRIBUTING.md) — setup, code style, PR process, how to add event kinds / MCP tools / API endpoints
- [TESTING.md](TESTING.md) — multi-agent E2E test guide
- [ARCHITECTURE.md](ARCHITECTURE.md) — system design and component relationships
- [README.md](README.md) — project overview and quick start
-25
View File
@@ -1,25 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Core relay with WebSocket event ingestion and subscription matching
- MySQL event store with monthly partitioning
- Redis pub/sub fan-out with presence and typing indicators
- Okta SSO authentication with NIP-42 challenge-response
- API token management with SHA-256 hashing
- Typesense-backed full-text search with permission-aware filtering
- Hash-chain audit log for compliance
- YAML-as-code workflow engine with 4 trigger types and 7 action types
- Approval gates with cryptographic tokens
- MCP server with 16 tools for AI agent integration
- Nostr client compatibility proxy for guest access
- LiveKit integration for audio/video huddles
- Home feed with @mentions, needs-action, and activity streams
- Operator CLI for relay administration
- E2E test suite with 13 integration tests
+45 -34
View File
@@ -77,8 +77,8 @@ lefthook install
```
`just setup` starts Docker services (MySQL on `:3306`, Redis on `:6379`,
Typesense on `:8108`, Adminer on `:8082`) and runs all pending database
migrations.
Typesense on `:8108`, Adminer on `:8082`, Keycloak on `:8180` for local
OAuth/OIDC testing) and runs all pending database migrations.
### Running the Relay
@@ -105,7 +105,6 @@ just reset # ⚠️ Wipe all data and recreate the environment
```bash
just test-unit
# or: cargo test --lib
```
Unit tests are self-contained and run without Docker. They cover event
@@ -115,7 +114,6 @@ parsing, filter matching, auth logic, workflow YAML parsing, and more.
```bash
just test
# or: cargo test
```
Integration tests spin up the relay and exercise the full stack — WebSocket
@@ -125,14 +123,21 @@ already running.
### End-to-End Tests
The `sprout-test-client` crate contains a WebSocket harness for scenario-level
tests:
End-to-end tests live in `crates/sprout-test-client/tests/`:
- `e2e_rest_api.rs` — REST API tests
- `e2e_relay.rs` — WebSocket relay tests
- `e2e_mcp.rs` — MCP tool tests
- `e2e_tokens.rs` — token management tests
- `e2e_workflows.rs` — workflow tests
Run them with (requires running infrastructure):
```bash
cargo run -p sprout-test-client -- --scenario basic-pubsub
cargo test -p sprout-test-client
```
Run `cargo run -p sprout-test-client -- --help` for available scenarios.
See `TESTING.md` for the full multi-agent E2E testing guide.
### CI Gate
@@ -152,8 +157,7 @@ merged.
### Formatting
We use `rustfmt` with the project's `rustfmt.toml`. Format your code before
committing:
We use `rustfmt` with default settings. Format your code before committing:
```bash
cargo fmt --all
@@ -255,7 +259,6 @@ required. The scope (in parentheses) is optional but encouraged.
- [ ] `just ci` passes (fmt + clippy + unit tests)
- [ ] Integration tests pass (`just test`)
- [ ] New public APIs / tools / endpoints are documented
- [ ] CHANGELOG entry added (if user-facing change)
- [ ] No new `unwrap()` in production code paths
- [ ] No new `unsafe` blocks
```
@@ -284,10 +287,12 @@ sprout-search ← Typesense full-text search
sprout-audit ← Tamper-evident hash-chain audit log
sprout-workflow ← YAML-as-code workflow engine
sprout-mcp ← stdio MCP server (agent API surface)
sprout-acp ← ACP harness (bridges Sprout relay events to AI agents via stdio)
sprout-proxy ← Nostr client compatibility layer
sprout-huddle ← LiveKit integration
sprout-admin ← Operator CLI
sprout-test-client← Integration test harness
desktop/ ← Desktop app (Tauri 2 + React 19 + Vite + Tailwind)
```
**Key design principle:** The relay is the single source of truth. All state
@@ -304,18 +309,19 @@ to existing clients.
## How to Add a New Event Kind
1. **Define the kind constant** in `sprout-core/src/kinds.rs`:
1. **Define the kind constant** in `sprout-core/src/kind.rs`:
```rust
/// My new event kind — description of what it represents.
pub const KIND_MY_FEATURE: u16 = 4XXXX;
pub const KIND_MY_FEATURE: u32 = 4XXXX;
```
Pick a kind number in the `4000049999` range (Sprout's reserved range
for enterprise extensions). Check `kinds.rs` to avoid collisions.
Pick a kind number in the appropriate sub-range defined in `kind.rs`.
Check the `ALL_KINDS` array for collisions. Each sub-range is documented
with comments in the file.
2. **Define the payload type** in `sprout-core/src/types/` (if the content
field is structured JSON):
2. **Define the payload type** in the appropriate module in `sprout-core/src/`
(e.g., alongside `event.rs`) if the content field is structured JSON:
```rust
#[derive(Debug, Serialize, Deserialize)]
@@ -325,19 +331,26 @@ to existing clients.
}
```
3. **Handle the kind in the relay** in `sprout-relay/src/api.rs` (or the
appropriate handler module). Add a match arm for your kind:
3. **Handle the kind in the relay** by adding a match arm in
`crates/sprout-relay/src/handlers/side_effects.rs` inside the
`handle_side_effects()` function:
```rust
KIND_MY_FEATURE => handle_my_feature(&state, &event).await?,
```
This is the central dispatch point for event side-effects. 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` — that's separate from event
dispatch.
4. **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
the appropriate `INSERT` and `SELECT` queries.
5. **Index for search** (if applicable) — add the kind to the Typesense
indexing logic in `sprout-search/src/indexer.rs`.
indexing logic in `sprout-search/src/index.rs`.
6. **Audit** — the audit log captures all events automatically; no changes
needed unless you need custom audit metadata.
@@ -346,8 +359,8 @@ to existing clients.
`sprout-core` and an integration test in `sprout-test-client` that sends
the new event kind and verifies the expected behavior.
8. **Document** — add the kind to the kind reference table in `VISION.md`
and update `README.md` if it's a user-facing feature.
8. **Document** — `kind.rs` is the authoritative registry of all kind numbers.
Update `README.md` if it's a user-facing feature.
---
@@ -394,20 +407,19 @@ provides the `#[tool]` and `#[tool_router]` macros.
3. **The `#[tool_router]` macro** on the `impl SproutMcpServer` block
automatically discovers all `#[tool]`-annotated methods and registers
them. No manual registration needed.
them. The MCP server auto-discovers `#[tool]`-annotated methods — no
manual registration or doc updates needed.
4. **Update the tool count** in `README.md` and add a full parameter table
and example to `AGENTS.md`.
5. **Write a test** — add an integration test in
`crates/sprout-mcp/tests/` that exercises the new tool end-to-end.
4. **Write a test** — add an integration test in
`crates/sprout-test-client/tests/e2e_mcp.rs` that exercises the new tool end-to-end.
---
## How to Add a New API Endpoint
REST endpoints live in `crates/sprout-relay/src/api.rs` (or the module
it delegates to after the planned split into `src/api/`).
REST endpoints live in `crates/sprout-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`.
1. **Define the handler function:**
@@ -425,11 +437,10 @@ it delegates to after the planned split into `src/api/`).
}
```
2. **Register the route** in the router (look for `Router::new().route(...)`
in `api.rs` or `router.rs`):
2. **Register the route** in `crates/sprout-relay/src/router.rs`:
```rust
.route("/api/channels/:channel_id/my-resource", get(get_my_resource))
.route("/api/channels/{channel_id}/my-resource", get(get_my_resource))
```
3. **Add the database query** in `sprout-db/src/` — follow the existing
@@ -439,7 +450,7 @@ it delegates to after the planned split into `src/api/`).
Map database errors and not-found cases to appropriate HTTP status codes.
5. **Write tests** — add an integration test using the `sprout-test-client`
harness or `axum::test` utilities.
harness in `crates/sprout-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`.
+29 -40
View File
@@ -18,6 +18,7 @@ append-only and audited.
| ✅ | **Nostr wire protocol** — any Nostr client works out of the box |
| ✅ | **YAML-as-code workflows** — automation with approval gates and execution traces |
| ✅ | **Agent-native MCP server** — LLMs are first-class participants |
| ✅ | **ACP agent harness** — AI agents connect out of the box via `sprout-acp` |
| ✅ | **Tamper-evident audit log** — hash-chain, SOX-grade compliance |
| ✅ | **Permission-aware full-text search** — Typesense, respects channel membership |
| ✅ | **Enterprise SSO bridge** — NIP-42 authentication with OIDC |
@@ -29,6 +30,8 @@ append-only and audited.
|-----|-------|--------|
| [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) | Basic protocol flow — events, filters, subscriptions | ✅ Implemented |
| [NIP-11](https://github.com/nostr-protocol/nips/blob/master/11.md) | Relay information document | ✅ Implemented |
| [NIP-25](https://github.com/nostr-protocol/nips/blob/master/25.md) | Reactions | ✅ Implemented |
| [NIP-29](https://github.com/nostr-protocol/nips/blob/master/29.md) | Relay-based groups | ✅ Partial (kinds 90009008 implemented; 9009, 9021 deferred) |
| [NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md) | Authentication of clients to relays | ✅ Implemented |
## Architecture
@@ -39,10 +42,15 @@ append-only and audited.
│ │
│ Human client AI agent (goose, etc.) │
│ (any Nostr app) ┌──────────────────┐ │
│ │ │ sprout-acp │ ← ACP harness │
│ │ │ (ACP ↔ MCP) │ (event listener │
│ │ └────────┬─────────┘ + agent bridge) │
│ │ │ │
│ │ ┌────────┴─────────┐ │
│ │ │ sprout-mcp │ │
│ │ │ (stdio MCP srv) │ │
│ │ └────────┬─────────┘ │
│ │ │ WebSocket
│ │ │ WebSocket + REST
└────────┼───────────────────────┼─────────────────────────────-─┘
│ WebSocket │
▼ ▼
@@ -81,12 +89,13 @@ append-only and audited.
| `sprout-auth` | NIP-42 challenge/response + Okta OIDC JWT validation + token scopes |
| `sprout-pubsub` | Redis pub/sub bridge — fan-out events across relay instances |
| `sprout-search` | Typesense indexing and query — full-text search over event content |
| `sprout-audit` | Append-only audit log with HMAC chain for tamper detection |
| `sprout-audit` | Append-only audit log with hash chain for tamper detection |
**Agent interface**
| Crate | Role |
|-------|------|
| `sprout-mcp` | stdio MCP server — 16 tools for messages, channels, workflows, and feed |
| `sprout-mcp` | stdio MCP server — 43 tools for messages, channels, workflows, and feed |
| `sprout-acp` | ACP harness — bridges Sprout relay events to AI agents over stdio (goose, codex, claude code) |
| `sprout-workflow` | YAML-as-code workflow engine — triggers, actions, approval gates, execution traces |
| `sprout-proxy` | Protocol translation layer — shadow keypairs, kind remapping for legacy clients |
| `sprout-huddle` | LiveKit integration — voice/video session tokens for channel participants |
@@ -149,6 +158,8 @@ cargo run -p sprout-admin -- mint-token \
Save the `nsec...` private key and API token from the output. They are shown only once.
> **Note:** Requires infrastructure from Step 2 to be running.
**6. Launch an agent with the MCP extension**
```bash
@@ -160,8 +171,7 @@ goose run --no-profile \
--instructions "List available Sprout channels."
```
`sprout-mcp-server` is a stdio MCP extension, so start it through a host such as Goose rather than
as a standalone user-facing process. See [TESTING.md](TESTING.md) for the full multi-agent flow.
`sprout-mcp-server` is a stdio MCP server — Goose manages its lifecycle. Do not run it directly in a terminal. See [TESTING.md](TESTING.md) for the full multi-agent flow.
**7. Run the desktop app (optional)**
@@ -170,6 +180,10 @@ just desktop-app
# or: just desktop-dev
```
The desktop app includes a home feed, Cmd+K search, settings page, profile management, presence
indicators, unread badges, diff message rendering, custom window chrome (macOS overlay titlebar),
and a full channel management UI.
## Configuration
Copy `.env.example` to `.env`. All defaults work with `docker compose up` out of the box.
@@ -184,46 +198,16 @@ Copy `.env.example` to `.env`. All defaults work with `docker compose up` out of
| `SPROUT_BIND_ADDR` | `0.0.0.0:3000` | Relay bind address (host:port) |
| `RELAY_URL` | `ws://localhost:3000` | Public URL (used in NIP-42 challenges) |
| `SPROUT_REQUIRE_AUTH_TOKEN` | `false` | Require bearer token for auth (set `true` in production) |
| `SPROUT_RELAY_PRIVATE_KEY` | auto-generated | Relay keypair for signing system messages |
| `OKTA_ISSUER` | — | Okta OIDC issuer URL (optional) |
| `OKTA_AUDIENCE` | — | Expected JWT audience (optional) |
| `RUST_LOG` | `sprout_relay=debug,...` | Log filter (tracing env-filter syntax) |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | — | OTLP endpoint for distributed tracing (optional) |
| `RUST_LOG` | `sprout_relay=info` | Log filter (tracing env-filter syntax) |
## MCP Tools
The `sprout-mcp` binary exposes 16 tools over stdio. See [AGENTS.md](AGENTS.md) for full parameter
reference and usage examples.
**Messaging & Channels**
| Tool | Description |
|------|-------------|
| `send_message` | Send a message to a channel (Nostr kind 40001 by default) |
| `get_channel_history` | Fetch recent messages from a channel (default: last 50) |
| `list_channels` | List channels visible to this agent |
| `create_channel` | Create a new channel with name, type, and visibility |
| `get_canvas` | Read the shared canvas document for a channel (kind 40100) |
| `set_canvas` | Write or update the canvas for a channel |
**Workflows**
| Tool | Description |
|------|-------------|
| `list_workflows` | List workflows defined in a channel |
| `create_workflow` | Create a new workflow from a YAML definition |
| `update_workflow` | Replace a workflow's YAML definition |
| `delete_workflow` | Delete a workflow by ID |
| `trigger_workflow` | Manually trigger a workflow with optional input variables |
| `get_workflow_runs` | Get execution history for a workflow (default: last 20) |
| `approve_workflow_step` | Approve or deny a pending workflow approval step |
**Feed**
| Tool | Description |
|------|-------------|
| `get_feed` | Get the agent's personalized home feed (mentions, activity, actions) |
| `get_feed_mentions` | Get only @mentions for this agent |
| `get_feed_actions` | Get items requiring action (approvals, reminders) |
The MCP server generates an ephemeral Nostr keypair on first run if `SPROUT_PRIVATE_KEY` is not set.
Set `SPROUT_PRIVATE_KEY` (nsec format) to use a persistent identity.
The `sprout-mcp` server exposes 43 tools over stdio, covering messaging, channels, threads,
reactions, DMs, workflows, search, profiles, presence, and more. Agents discover tools
automatically via the MCP protocol — see [AGENTS.md](AGENTS.md) for integration details.
## Development
@@ -272,6 +256,11 @@ cargo run -p sprout-mcp --bin sprout-mcp-server
`sprout-mcp-server` is normally launched by Goose or another MCP host.
**Tests**
Run `just test-unit` for unit tests (no infra required) or `just test` for the full suite.
See [TESTING.md](TESTING.md) for the multi-agent E2E suite (Alice/Bob/Charlie via `sprout-acp`).
**Database migrations** live in `migrations/`. The relay applies them automatically on startup.
To run manually: `just migrate` (uses `sqlx` CLI if available, falls back to `docker exec`).
+18 -79
View File
@@ -20,6 +20,8 @@ The platform made it possible. The agent made it happen. Sprout is the pipe —
| ⚡ **Workflows** | YAML-as-code automation. Traces. | Approvals only |
| 🔍 **Search** | Cmd+K. Instant. Full-text. | — |
*Desktop app ships Home, Stream, and Search today. Forum, DMs, Agents directory, and Workflows UI are next.*
- **Stream** — Slack-like, fast. Mandatory topics → sub-replies. Zero-notification default.
- **Forum** — Discourse-like, slow. Post → flat replies. Zero-notification default.
- **Workflow** — Structured, traceable. Steps → approval gates. Approvals only.
@@ -64,22 +66,7 @@ New message type? New kind integer. Zero breaking changes.
## Architecture
All Rust. Crates in a Cargo workspace:
| Crate | Role |
|-------|------|
| `sprout-relay` | WebSocket server, event ingestion, subscription matching |
| `sprout-core` | Shared types, event verification, filter matching |
| `sprout-db` | MySQL event store, migrations, partition manager |
| `sprout-pubsub` | Redis fan-out, presence, typing indicators |
| `sprout-auth` | Okta bridge, NIP-42, API tokens, rate limiting |
| `sprout-search` | Typesense integration, permission-aware indexing |
| `sprout-audit` | Hash-chain audit log, compliance, retention |
| `sprout-mcp` | MCP server (the agent API surface) |
| `sprout-proxy` | Nostr client compatibility layer (optional, for guests) |
| `sprout-huddle` | LiveKit integration (audio/video/screen share) |
**Tooling:** `sprout-admin` (operator CLI), `sprout-test-client` (integration testing harness).
All Rust. A Cargo workspace of focused crates — relay, auth, pub/sub, search, audit, workflow engine, MCP agent interface, and more. See [README.md](README.md) for the full crate map.
---
@@ -92,7 +79,7 @@ Humans and agents get the same thing:
- Okta SSO → keypair bridge (humans) or API token (agents)
- Bot badge on agent messages. Operator shown. That's it.
No trust levels. No capability taxonomy. Auth is binary. Channel membership controls access.
Auth is simple — authenticated or not. Channel membership gates content visibility. Agent tokens support optional scope restrictions for least-privilege deployments.
---
@@ -110,32 +97,13 @@ LiveKit SFU handles all media routing. Sprout provides rooms and tokens.
- Huddle state flows as Nostr events (started, joined, left, ended, recording available)
- Workflows can trigger on huddle events
*(LiveKit token minting and kind definitions exist; relay-side lifecycle event emission is planned)*
LiveKit token minting and kind definitions are in place. Relay-side lifecycle event emission is planned.
---
## Workflows
Slack Workflow Builder, done better. Channel-scoped YAML-as-code automation with conditional logic — the feature Slack paywalled for 5 years.
| Trigger | Description |
|---------|-------------|
| `message_posted` | Fires on new messages, with optional `filter` expression |
| `reaction_added` | Fires on emoji reactions, with optional `emoji` filter |
| `schedule` | Cron or interval-based (`cron: "0 9 * * MON"` or `interval: "30m"`) |
| `webhook` | External HTTP POST with secret-authenticated URL |
| Action | Description |
|--------|-------------|
| `send_message` | Post to the workflow's channel (or override) |
| `request_approval` | Suspend execution until a human/agent approves |
| `add_reaction` | React to the trigger message |
| `call_webhook` | HTTP POST to an external URL (SSRF-protected) |
| `set_channel_topic` | Update the channel topic |
| `delay` | Pause execution (max 5 minutes, capped for reliability) |
| `update_canvas` | Modify the channel's shared document |
Every step supports `if:` conditions (powered by evalexpr) and `timeout_secs`. Full execution traces are stored per-run. Approval gates suspend the workflow and resume on grant/deny. Agents manage workflows via MCP tools (`create_workflow`, `trigger_workflow`, `get_workflow_runs`, etc.).
Channel-scoped YAML-as-code automation with conditional logic — the feature Slack paywalled for 5 years. Message triggers, scheduled runs, webhooks, approval gates. Every step traced. Agents manage workflows through MCP tools.
---
@@ -143,20 +111,11 @@ Every step supports `if:` conditions (powered by evalexpr) and `timeout_secs`. F
Zero is the default. You opt in to noise, not out.
The Home Feed (`/api/feed`) is the personalized entry point — what matters to you, organized by urgency:
| Category | Content | Notification Tier |
|----------|---------|-------------------|
| **@Mentions** | Messages where your pubkey appears in a p-tag | URGENT |
| **Needs Action** | Approval requests, reminders addressed to you | URGENT |
| **Channel Activity** | Recent messages in channels you're a member of | WATCHING |
| **Agent Activity** | Job posts, results, status updates from agents | AMBIENT |
Fan-out-on-read: the feed is assembled at query time from the event store, not pre-computed. Sufficient at 10K-user scale. Agents read the same feed via MCP (`get_feed`, `get_feed_mentions`, `get_feed_actions`).
The Home Feed is the personalized entry point — @mentions, items needing action, channel activity, agent updates. Fan-out-on-read, assembled at query time. Agents read the same feed via MCP.
---
## Culture
## Culture Features
*(Planned design — not yet implemented)*
@@ -189,19 +148,7 @@ Not afterthoughts — ship blockers:
## Build Model
7 parallel workstreams. Greenfield. Agent swarms build simultaneously. Integration at the event store boundary.
| Workstream | Scope |
|------------|-------|
| WS1 Core Relay & Event Store | Foundation |
| WS2 API Layer | REST + WebSocket surface |
| WS3 Web Client | Stream + Forum + DM + Search |
| WS4 Subscription Engine | Persistent filters + delivery |
| WS5 Workflow Engine | YAML-as-code automation |
| WS6 Mobile Clients | iOS + Android |
| WS7 Developer Portal | Schema browser, playground, SDK gen |
Sprout is designed as a complete platform, not a collection of independent microservices.
Greenfield. Agent swarms build in parallel, integrating at the event store boundary. Sprout is being built with AI-assisted development — agents write code, crossfire reviews across multiple models catch blind spots before merge. A complete platform, not a collection of independent microservices.
---
@@ -209,23 +156,15 @@ Sprout is designed as a complete platform, not a collection of independent micro
| | Area |
|-|------|
| ✅ | Core relay (`sprout-relay`) |
| ✅ | Auth (`sprout-auth`) — Okta SSO, NIP-42, API tokens |
| ✅ | Pub/sub (`sprout-pubsub`) — Redis fan-out, presence |
| ✅ | Search (`sprout-search`) — Typesense, permission-aware |
| ✅ | Audit (`sprout-audit`) — hash-chain, SOX retention |
| ✅ | MCP server (`sprout-mcp`) — agent API surface |
| ✅ | Nostr proxy (`sprout-proxy`) — guest client compatibility |
| | Huddle (`sprout-huddle`) — LiveKit integration |
| | Admin CLI (`sprout-admin`) |
| ✅ | Channel features — messaging, threads, DMs, reactions, NIP-29 group management, soft-delete |
| 🚧 | Web client (Tauri) — Stream, Forum, DM, Search |
| ✅ | Workflow engine (`sprout-workflow`) — YAML-as-code, 4 trigger types, 7 action types, approval gates, execution traces |
| ✅ | Home Feed (`/api/feed`) — @mentions, needs-action, channel activity, agent activity |
| 📋 | Mobile clients — iOS + Android |
| 📋 | Developer portal — schema browser, playground, SDK gen |
| 📋 | Notifications — tiered delivery, digest |
| 📋 | Culture features — polls, kudos, coffee roulette, knowledge crystallization |
| ✅ | Core relay, auth, pub/sub, search, audit |
| ✅ | MCP server — 43 tools, full feature surface |
| ✅ | ACP agent harness — goose, codex, claude code |
| ✅ | Desktop client (Tauri) — Stream, Home, Search, Settings, Profiles, Presence |
| ✅ | Channel features — messaging, threads, DMs, reactions, NIP-29, soft-delete |
| ✅ | Workflow engine — YAML-as-code, approval gates, execution traces |
| ✅ | Identity — NIP-05, public profiles, self-service token minting, agent protection |
| 🚧 | Desktop client — Forum view, DM UI |
| 📋 | Mobile clients, developer portal, notifications, culture features |
---
+2
View File
@@ -1,3 +1,5 @@
#![deny(unsafe_code)]
mod acp;
mod config;
mod queue;