Skip to content

feat(acp): ACP Server with WebSocket transport#1260

Closed
chaodu-agent wants to merge 3 commits into
mainfrom
feat/acp-server
Closed

feat(acp): ACP Server with WebSocket transport#1260
chaodu-agent wants to merge 3 commits into
mainfrom
feat/acp-server

Conversation

@chaodu-agent

Copy link
Copy Markdown
Collaborator

Summary

Implements Phase 1 MVP of the ACP Server per ADR #1258.

Any ACP-compliant client (Zed, JetBrains, desktop apps, web apps, CLIs) can now connect to OAB via WebSocket and interact with the multi-agent platform using the standard Agent Client Protocol.

Architecture

ACP Client ──WSS GET /acp──> OpenAB
                              │
  initialize ────────────────>│──> capability negotiation
  session/new ───────────────>│──> create session + channel_id
  session/prompt ────────────>│──> GatewayEvent → Dispatcher → Agent
                              │
  <── session/notification ───│<── GatewayReply → AgentMessageChunk
  <── session/notification ───│<── (streaming deltas)
  <── prompt response ────────│<── {stopReason: "endTurn"}

What's implemented (Phase 1)

  • GET /acp WebSocket upgrade endpoint
  • Bearer token auth (OPENAB_ACP_AUTH_KEY env var)
  • initialize — protocol version + capability negotiation
  • session/new — creates OAB session with internal channel mapping
  • session/prompt — converts prompt to GatewayEvent, dispatches through existing pipeline
  • Reply streaming — GatewayReply translated to AgentMessageChunk notifications (delta-based)
  • session/cancel — placeholder
  • Reply registry (AcpReplyRegistry) for routing replies back to the correct WebSocket
  • Feature-gated behind acp feature flag

Files changed

File Change
Cargo.toml Add acp feature flag
crates/openab-gateway/Cargo.toml Add agent-client-protocol optional dep + acp feature
crates/openab-gateway/src/adapters/acp_server.rs New — ACP server adapter (561 lines)
crates/openab-gateway/src/adapters/mod.rs Register acp_server module
crates/openab-gateway/src/lib.rs Add ACP fields to AppState, wire route + reply dispatch

Configuration

export OPENAB_ACP_ENABLED=true
export OPENAB_ACP_AUTH_KEY="your-secret-key"  # optional but recommended

What's next (Phase 2+)

  • Tool call notifications (ToolCall, ToolCallUpdate)
  • requestPermission flow for dangerous operations
  • session/load — resume existing sessions
  • Multi-session per connection
  • Agent routing via model parameter
  • Streamable HTTP transport (POST + SSE)

Testing

# Build with ACP support
cargo build --features acp

# Connect with any WebSocket client
websocat ws://localhost:8080/acp?token=your-key

# Send initialize
{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"v1","clientInfo":{"name":"test","version":"1.0"}}}

Refs: ADR PR #1258
cc @pahud

Add an ACP-compliant server endpoint at GET /acp that accepts WebSocket
connections and speaks JSON-RPC 2.0 per the Agent Client Protocol spec.

Implements:
- WebSocket upgrade with Bearer token auth (OPENAB_ACP_AUTH_KEY)
- initialize: capability negotiation
- session/new: create OAB session mapped to internal channel
- session/prompt: dispatch to agent via GatewayEvent, stream back
  AgentMessageChunk notifications from GatewayReply
- session/cancel: placeholder for Phase 2

Architecture:
  ACP Client --WS JSON-RPC--> /acp endpoint --> GatewayEvent
  GatewayReply --> AcpReplyRegistry --> AgentMessageChunk notification --> Client

Feature-gated behind 'acp' feature flag. Enable with:
  OPENAB_ACP_ENABLED=true cargo run --features acp

Refs: ADR PR #1258
@chaodu-agent chaodu-agent requested a review from thepagent as a code owner June 30, 2026 03:51
@chaodu-agent

This comment has been minimized.

F1: Use subtle::ConstantTimeEq for auth token comparison (timing attack)
F2: Remove duplicate AcpConfig/registry construction in serve()
F3: Validate jsonrpc == "2.0" per spec (reject with -32600)
F4: Defer reply channel creation to session/prompt (no dead _reply_rx)
+  Add tracing::debug for reply send failure (race condition visibility)
@chaodu-agent

This comment has been minimized.

Critical fixes:
- 🔴 Reject concurrent prompts per session (-32001 'Session busy')
- 🔴 Abort prompt tasks on disconnect to prevent registry leaks
- 🔴 Clean up registry entries after prompt completes (not just on Done)

Important fixes:
- 🟡 Check event_tx.send() result — return JSON-RPC error if no agent connected
- 🟡 Remove unused agent-client-protocol dep (Phase 1 is manual JSON-RPC)
- 🟡 Add 'acp' to unified feature list in root Cargo.toml
- 🟡 Switch AcpReplyRegistry from tokio::sync::Mutex to std::sync::Mutex
     (operations are CPU-bound, never held across .await)

Also:
- Rewrite acp_server.rs for clarity after multiple incremental fixes
- Add busy flag per session to track in-flight prompts
- Prompt handler now properly cleans up registry on all exit paths
@chaodu-agent

Copy link
Copy Markdown
Collaborator Author

CHANGES REQUESTED ⚠️ — All critical issues from prior rounds resolved; one remaining duplicate-state construction in serve().

What This PR Does

Adds ACP (Agent Client Protocol) server support to OpenAB, enabling any ACP-compliant client (Zed, JetBrains, desktop apps, CLIs) to connect via WebSocket at GET /acp and interact with the multi-agent platform using standard JSON-RPC 2.0.

How It Works

A new acp_server adapter behind an acp feature flag implements JSON-RPC over WebSocket: initializesession/newsession/prompt. Prompts are converted to GatewayEvent and dispatched through the existing pipeline. Replies stream back as delta-based AgentMessageChunk notifications via an AcpReplyRegistry (std::sync::Mutex). Feature-gated at workspace and crate level. Concurrent prompts on the same session are rejected. All cleanup paths handle registry removal and busy-flag release.

Findings

# Severity Finding Location
1 🟡 Duplicate AcpConfig::from_env() in serve() — called twice, creates two independent registries lib.rs:443-445
2 🟢 Timing-safe token comparison using subtle::ConstantTimeEq acp_server.rs:131
3 🟢 JSON-RPC 2.0 version validation with error code -32600 acp_server.rs:175
4 🟢 Concurrent prompt rejection with -32001 busy guard acp_server.rs:282
5 🟢 Prompt task abort on disconnect + full registry cleanup acp_server.rs:218
6 🟢 event_tx.send() error handled — returns JSON-RPC error if no agent connected acp_server.rs:315
Finding Details

🟡 F1: Duplicate AcpConfig::from_env() in serve()

In the serve() function, the AppState struct literal calls AcpConfig::from_env() twice:

acp: adapters::acp_server::AcpConfig::from_env(),
acp_reply_registry: adapters::acp_server::AcpConfig::from_env()
    .map(|_| adapters::acp_server::new_reply_registry()),

Issues:

  1. Two separate from_env() calls — the second could theoretically see different env state
  2. Creates a registry even if the first call returned None (they are independent)
  3. AppState::from_env() (line 164-167) already does this correctly with a single extraction

Fix: Extract once before the struct literal, matching the pattern in from_env():

#[cfg(feature = "acp")]
let acp = adapters::acp_server::AcpConfig::from_env();
#[cfg(feature = "acp")]
let acp_reply_registry = acp.as_ref().map(|_| adapters::acp_server::new_reply_registry());

Then reference the variables in the struct literal. This ensures the registry is only created when ACP is enabled and both fields share the same from_env() result.

🟢 F2–F6: All prior findings addressed

  • Timing-safe authsubtle::ConstantTimeEq eliminates the timing attack vector
  • JSON-RPC validation — rejects non-"2.0" requests per spec
  • Concurrent prompt guard — busy flag prevents dual-dispatch on same session
  • Disconnect cleanup — aborts in-flight tasks, removes all registry entries
  • Error propagationevent_tx send failure returns -32603 to client instead of hanging
Baseline Check
  • PR opened: 2026-06-30
  • Main already has: ACP client infrastructure (openab-core/src/acp/) for connecting to agent CLIs; multiple gateway adapters (telegram, line, feishu, googlechat, wecom, teams)
  • Net-new value: ACP server adapter — makes OAB itself an ACP endpoint. Entirely new direction, no overlap with existing adapters.
What's Good (🟢)
  • All critical and important findings from Rounds 1 and 2 are fixed
  • Clean feature-flag gating — zero impact on non-ACP builds
  • Proper streaming delta computation (full_text[sent_len..])
  • Graceful multi-path cleanup: disconnect, timeout, error, and normal completion
  • Follows existing adapter patterns (consistent with wecom, teams, etc.)
  • std::sync::Mutex for CPU-bound registry ops — avoids unnecessary async overhead
  • Good observability via tracing (info on connect/disconnect/dispatch, warn on failures, debug on race conditions)
  • agent-client-protocol dependency correctly removed (manual JSON-RPC for Phase 1)
  • acp added to unified feature list for complete builds
CI Status
  • check — passed
  • changes — passed
  • 🔄 Smoke tests — in progress
  • 🔄 build-builder — in progress

@github-actions github-actions Bot added the closing-soon PR missing Discord Discussion URL — will auto-close in 24 hours. label Jul 2, 2026
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

🔒 Auto-closing: this PR has had the closing-soon label for more than 1 days without activity from the author.

If you'd like to continue working on this, please submit a new PR and link to this one if necessary.

@github-actions github-actions Bot closed this Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

closing-soon PR missing Discord Discussion URL — will auto-close in 24 hours. pending-contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant