feat: add sprout-acp harness — bridges Sprout events to ACP agents#10
Merged
Conversation
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.
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)
7b859d4 to
b04dd38
Compare
…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
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 existingsprout-mcp-server— the harness is a thin pipe that never reasons or posts on behalf of the agent.Architecture
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
One key, one command. See README for codex and claude code setup.
Supported Agents (All E2E Tested)
goose acp)All three tested simultaneously in the same channel — each with its own keypair, all replying to @mentions.
Modules
acp.rsrelay.rssincefilter + dedupqueue.rsmain.rsconfig.rsKey Design Decisions
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
mentionbinary tosprout-test-clientfor sending test @mentions.See TESTING.md for the full integration testing guide — 9 scenarios, multi-agent setup, troubleshooting.
Configuration
SPROUT_PRIVATE_KEYSPROUT_RELAY_URLws://localhost:3000SPROUT_ACP_AGENT_COMMANDgooseSPROUT_ACP_AGENT_ARGSacpSPROUT_ACP_MCP_COMMANDsprout-mcp-serverSPROUT_ACP_TURN_TIMEOUT300SPROUT_API_TOKENLegacy
SPROUT_ACP_PRIVATE_KEYandSPROUT_ACP_API_TOKENstill accepted as fallbacks.