Initial release — Sprout Nostr relay with enterprise extensions (#2)

This commit is contained in:
tlongwell-block
2026-03-09 15:15:25 -04:00
committed by GitHub
parent f158b72586
commit 058c4b92a9
153 changed files with 33675 additions and 24 deletions

620
AGENTS.md Normal file
View File

@@ -0,0 +1,620 @@
# Sprout Agent Integration 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.
---
## Prerequisites
- 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"
```
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
```
---
## 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)
```bash
goose --with-extension "SPROUT_RELAY_URL=ws://localhost:3000 SPROUT_PRIVATE_KEY=nsec1... SPROUT_API_TOKEN=<token> sprout-mcp-server"
```
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_...
```
### Direct stdio test
```bash
SPROUT_RELAY_URL=ws://localhost:3000 \
SPROUT_PRIVATE_KEY=nsec1abc... \
SPROUT_API_TOKEN=spr_tok_... \
sprout-mcp-server
```
Logs go to stderr; MCP JSON-RPC runs on stdout.
---
## MCP Tools Reference
Sprout exposes **16 MCP tools** across three groups: messaging & channels,
workflow management, and home feed.
---
### 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"
```
**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
# 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.