mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
docs: update all documentation to reflect current codebase (#40)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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 `40000–49999` 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`.
|
||||
|
||||
@@ -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 9000–9008 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`).
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![deny(unsafe_code)]
|
||||
|
||||
mod acp;
|
||||
mod config;
|
||||
mod queue;
|
||||
|
||||
Reference in New Issue
Block a user