Skip to content

feat: add sprout-acp harness — bridges Sprout events to ACP agents#10

Merged
tlongwell-block merged 10 commits into
mainfrom
acp
Mar 11, 2026
Merged

feat: add sprout-acp harness — bridges Sprout events to ACP agents#10
tlongwell-block merged 10 commits into
mainfrom
acp

Conversation

@tlongwell-block
Copy link
Copy Markdown
Collaborator

@tlongwell-block tlongwell-block commented Mar 10, 2026

Summary

Introduces sprout-acp, a new binary crate that connects AI agents to Sprout via the Agent Client Protocol (ACP). The harness listens for @mentions over WebSocket and prompts agents over stdio. Agents reply through the existing sprout-mcp-server — the harness is a thin pipe that never reasons or posts on behalf of the agent.

Architecture

Sprout Relay ──WS (NIP-01)──→ sprout-acp ──stdio (JSON-RPC)──→ Agent
                                                                  │
                                                             sprout-mcp
                                                            (16 tools)

Sprout MCP (existing) = the agent's toolbox — tools to act on Sprout.
Sprout ACP (this PR) = the agent's ears — listens for events and wakes the agent.

Quick Start

export SPROUT_PRIVATE_KEY="nsec1..."
export SPROUT_RELAY_URL="ws://localhost:3000"
export GOOSE_MODE=auto
sprout-acp

One key, one command. See README for codex and claude code setup.

Supported Agents (All E2E Tested)

Agent Adapter Test Result
goose Native (goose acp) ✅ "Capital of France?" → "Paris"
codex codex-acp ✅ "7 × 8?" → "56"
claude code claude-agent-acp ✅ "√144?" → "12"

All three tested simultaneously in the same channel — each with its own keypair, all replying to @mentions.

Modules

Module Lines Purpose
acp.rs ~850 ACP client — initialize, session/new, session/prompt, cancel, permission auto-approve
relay.rs ~1380 NIP-01 WebSocket + NIP-42 auth, background reader for ping/pong, reconnect with since filter + dedup
queue.rs ~490 Per-channel event queues, global one-in-flight, FIFO fairness, batch drain, requeue on failure
main.rs ~300 Orchestration — agent respawn, relay reconnect, turn timeout, graceful shutdown
config.rs ~100 Env-var config with legacy fallbacks

Key Design Decisions

  • One prompt in flight globally — ACP agents have no concurrency protection
  • Session per channel — conversation context accumulates per channel
  • Background WebSocket reader — responds to Pings during long turns (up to 300s)
  • Batch flush — drains all pending events for a channel into one prompt
  • Requeue on failure — events not lost if session creation or prompting fails
  • Standard ACP + standard MCP only — no agent-specific APIs

Testing

61 unit tests covering ACP wire format, relay message parsing, queue state machine, event dedup.

E2E verified with all 3 agents. Also adds a mention binary to sprout-test-client for sending test @mentions.

See TESTING.md for the full integration testing guide — 9 scenarios, multi-agent setup, troubleshooting.

Configuration

Env Var Required Default Description
SPROUT_PRIVATE_KEY Yes Agent's Nostr private key (relay auth + identity)
SPROUT_RELAY_URL No ws://localhost:3000 Relay WebSocket URL
SPROUT_ACP_AGENT_COMMAND No goose Agent binary to spawn
SPROUT_ACP_AGENT_ARGS No acp Agent args (comma-separated)
SPROUT_ACP_MCP_COMMAND No sprout-mcp-server MCP server binary
SPROUT_ACP_TURN_TIMEOUT No 300 Max turn duration (seconds)
SPROUT_API_TOKEN No API token (if relay enforces auth)

Legacy SPROUT_ACP_PRIVATE_KEY and SPROUT_ACP_API_TOKEN still accepted as fallbacks.

Introduces sprout-acp, a new binary crate that connects AI agents to
Sprout via the Agent Client Protocol (ACP). The harness listens for
Sprout events over WebSocket and wakes agents with session/prompt
requests over stdio. Agents act on Sprout through the existing
sprout-mcp-server toolbox — the harness itself is a thin pipe that
never reasons or posts on behalf of the agent.

Architecture:

  Sprout Relay ──WS──→ Harness ──stdio──→ Agent (goose, codex, etc.)
                           │                  │
                           │  session/prompt   │  sprout-mcp-server
                           │  session/update ←─│  (send_message, etc.)
                           └── logs to stdout ─┘

Modules:
- acp.rs — ACP client over stdio JSON-RPC 2.0 (initialize, session/new,
  session/prompt, session/cancel, permission auto-approve)
- relay.rs — NIP-01 WebSocket + NIP-42 auth with background reader task
  for ping/pong during long agent turns, reconnect with since filter and
  event deduplication
- queue.rs — Per-channel event queues with global one-in-flight
  enforcement, FIFO fairness, batch drain, and requeue on failure
- config.rs — Env-var configuration with separate harness/agent keypairs
- main.rs — Orchestration loop with agent respawn, relay reconnect, turn
  timeout, and graceful shutdown

Also adds a small mention binary to sprout-test-client for sending
@mention events with proper #p tags during manual testing.

Tested end-to-end: @mention → harness → goose → MCP send_message → reply
appears in channel.

61 unit tests.
Tyler Longwell and others added 8 commits March 9, 2026 21:36
Comprehensive TESTING.md covering:
- Prerequisites (Docker, relay, binaries, test keys)
- Quick start (5-minute goose smoke test)
- Agent-specific setup for all three supported agents:
  - goose (native ACP)
  - codex (via codex-acp adapter from Zed)
  - claude code (via claude-agent-acp adapter from Zed)
- 8 test scenarios with exact commands and verification steps:
  A. Basic @mention → reply
  B. Multi-event batching
  C. Agent crash recovery
  D. Relay disconnect recovery
  E. Turn timeout + cancel
  F. Permission auto-approve
  G. Channel discovery
  H. Concurrent channel FIFO fairness
- Verification commands (DB queries, log patterns, process checks)
- Troubleshooting guide (10 common failure modes)
- CI integration sketch (aspirational)
- Actual test results: all 3 agents passed E2E on 2026-03-10
Concise getting-started guide covering:
- Quick start with goose (4 env vars + one command)
- Running with codex via codex-acp adapter
- Running with claude code via claude-agent-acp adapter
- Full configuration reference table
- How the harness works (lifecycle, recovery, batching)
- Using any ACP-compatible agent
Gaps reported by a cold-start tester running goose, codex, and claude
simultaneously in the same channel:

README.md:
- Add 'Generating Keys' section (sprout-admin mint-token)
- Add 'Channel Membership' section (open channels vs explicit SQL)
- Clarify Quick Start uses test keys, link to key generation
- Add codex OPENAI_API_KEY note (avoid ChatGPT WebSocket fallback)

TESTING.md:
- Add Scenario I: Multi-Agent (3 agents, 1 channel) with full setup
- Add sprout-admin mint-token instructions to Test Keys section
- Add channel membership note (dev mode vs production)
- Run cargo fmt --all (line length, argument grouping)
- Box RelayError::WebSocket variant to fix clippy::result_large_err
- Add #[allow(dead_code)] for public API methods not yet called from main
- Replace or_insert_with(VecDeque::new) with or_default()
The harness and agent share one identity — there was never a reason
for separate keypairs. This simplifies configuration from 2 required
env vars to 1:

  SPROUT_PRIVATE_KEY=nsec1...  # that's it

Code changes:
- config.rs: Single 'keys' field, reads SPROUT_PRIVATE_KEY (falls back
  to legacy SPROUT_ACP_PRIVATE_KEY for compat)
- main.rs: Use config.keys everywhere (was config.harness_keys +
  config.agent_keys)

Doc changes:
- README: Rewrite to single key, replace SQL membership instructions
  with explanation that open channels just work, note private channel
  membership is a relay API gap
- TESTING: Update all env var references, simplify multi-agent setup
- Fix DB queries using nonexistent nostr_keys table (use channel_members.pubkey directly)
- Clarify codex 426 WebSocket error is expected/non-fatal
- Note hermit node path for claude-agent-acp
- Add stale event replay warning on startup
* origin/main:
  feat: soft-delete for events/channels, enriched API responses, NIP-29 group management (#17)
  feat: Channel management, messaging, threads, DMs, reactions, and NIP-29 support (#16)
  Improve chat scrolling and multiline composer (#14)
  chore: remove redundant inline comments across all crates (#13)
  Initial backend revisions, workflow expansion (#5)
  Add desktop Home feed (#12)
  Add desktop Playwright e2e harness (#11)
  Update desktop icon and persist window state (#9)
  feat: add channel creation flow (#8)
* origin/main:
  Migrate channel scoping to h tags (#22)
  Remove IF NOT EXISTS from migrations and drop mysql CLI fallback (#19)
  fix: resolve 10 E2E bugs + add desktop Playwright tests (#21)
  Add desktop search and result anchors (#15)
  fix: mcp and desktop communication (#18)
@tlongwell-block tlongwell-block force-pushed the acp branch 4 times, most recently from 7b859d4 to b04dd38 Compare March 11, 2026 02:37
…NG.md

Canvas REST API:
- GET/PUT /api/channels/{id}/canvas endpoints (canvas.rs)
- Relay-signed events with p-tag author attribution
- Archived channel rejection (403)
- MCP tools migrated from WebSocket to REST with clean string responses

MCP tool description fixes:
- create_channel: document stream/forum and open/private enums
- create_workflow: correct YAML schema (id field, direct properties)
- Removed invalid 'schedule' trigger from docs
- Fixed list_channels terminology (public → open)

E2E test overhaul:
- All 3 test suites (REST, relay, MCP) now self-contained
- Dynamic channel creation per test (no seed data dependency)
- h-tag migration for NIP-29 compliance
- Known keypairs for MCP tests
- Tightened canvas assertions (exact match)

ACP harness fixes:
- Subscription filter: #e → #h (was silently dropping all events)
- mention binary: e tag → h tag, multi-word message support

TESTING.md:
- Rewritten to use sprout-acp harness (not ad-hoc goose scripts)
- Consolidated ACP testing guide into root (deleted crate-level copy)
- 9 advanced ACP scenarios, 36 MCP tool reference, troubleshooting

All tests pass: unit ✅, REST 20/20 ✅, relay 13/13 ✅, MCP 7/7 ✅
Crossfire reviewed: opus 9/10 APPROVE, codex 9/10 APPROVE
@tlongwell-block tlongwell-block merged commit 4ecb5d3 into main Mar 11, 2026
8 checks passed
@tlongwell-block tlongwell-block deleted the acp branch March 11, 2026 02:53
wpfleger96 added a commit that referenced this pull request May 22, 2026
…iew findings

The original implementation created a second parallel Tauri command
(discover_all_acp_providers) alongside the existing one to avoid
changing the return type. This produced two commands, two hooks, two
query keys, and two raw type converters. Consolidates into a single
command returning the full catalog, with a useAvailableAcpProviders
hook that type-narrows for callers needing non-null command/binaryPath.

Also fixes: pipe deadlock in install command (#1), UTF-8 truncation
panic (#2/#4), adds install concurrency guard (#11), exact provider ID
match (#15), error display stdout fallback (#5), success banner
suppression when already available (#12), misleading re-run text (#13),
IIFE refactor in PersonaDialog (#14), hidden internal query lift (#7),
configurable e2e mocks (#9), shared raw type exports (#8), and
classify_provider unit tests (#10).
wpfleger96 added a commit that referenced this pull request May 22, 2026
The zizmor scanner runs as an org-level GHAS integration, not from a
repo checkout — it never reads .github/zizmor.yml. Dismissed the
cache-poisoning alerts (#10-13, #32, #33) as false positives via the
code-scanning API instead, with references to the upstream bug
(zizmorcore/zizmor#2051).
wpfleger96 added a commit that referenced this pull request May 22, 2026
…iew findings

The original implementation created a second parallel Tauri command
(discover_all_acp_providers) alongside the existing one to avoid
changing the return type. This produced two commands, two hooks, two
query keys, and two raw type converters. Consolidates into a single
command returning the full catalog, with a useAvailableAcpProviders
hook that type-narrows for callers needing non-null command/binaryPath.

Also fixes: pipe deadlock in install command (#1), UTF-8 truncation
panic (#2/#4), adds install concurrency guard (#11), exact provider ID
match (#15), error display stdout fallback (#5), success banner
suppression when already available (#12), misleading re-run text (#13),
IIFE refactor in PersonaDialog (#14), hidden internal query lift (#7),
configurable e2e mocks (#9), shared raw type exports (#8), and
classify_provider unit tests (#10).
wpfleger96 added a commit that referenced this pull request May 22, 2026
The zizmor scanner runs as an org-level GHAS integration, not from a
repo checkout — it never reads .github/zizmor.yml. Dismissed the
cache-poisoning alerts (#10-13, #32, #33) as false positives via the
code-scanning API instead, with references to the upstream bug
(zizmorcore/zizmor#2051).
wpfleger96 added a commit that referenced this pull request May 22, 2026
…iew findings

The original implementation created a second parallel Tauri command
(discover_all_acp_providers) alongside the existing one to avoid
changing the return type. This produced two commands, two hooks, two
query keys, and two raw type converters. Consolidates into a single
command returning the full catalog, with a useAvailableAcpProviders
hook that type-narrows for callers needing non-null command/binaryPath.

Also fixes: pipe deadlock in install command (#1), UTF-8 truncation
panic (#2/#4), adds install concurrency guard (#11), exact provider ID
match (#15), error display stdout fallback (#5), success banner
suppression when already available (#12), misleading re-run text (#13),
IIFE refactor in PersonaDialog (#14), hidden internal query lift (#7),
configurable e2e mocks (#9), shared raw type exports (#8), and
classify_provider unit tests (#10).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant