RFC-0030: ACP Streamable HTTP WebSocket Transport¶
Overview¶
ACP agents in AgentPool currently communicate over stdio or a bare WebSocket server. Neither transport aligns with the ACP RFC's Streamable HTTP WebSocket Transport profile, which defines a network-accessible endpoint at /acp with connection identification, lifecycle enforcement, and HTTP/SSE extensibility. This RFC proposes adding an ACPWebSocketTransport built on Starlette and uvicorn that serves the ACP protocol over WebSocket at /acp, returns Acp-Connection-Id headers, enforces the initialize handshake, and integrates with the existing CLI and YAML configuration pipeline. Phase 1 covers WebSocket only; Streamable HTTP (POST/GET/DELETE with SSE) is deferred to Phase 2.
Naming Note: The ACP RFC calls the full profile "Streamable HTTP WebSocket Transport". Phase 1 implements only the WebSocket subset. The dataclass is named
ACPWebSocketTransportto reflect this, while the CLI/YAML literal"streamable-http"refers to the RFC profile name. Phase 2 will extend the same class to support the full profile.
Table of Contents¶
- Background & Context
- Problem Statement
- Goals & Non-Goals
- Evaluation Criteria
- Options Analysis
- Recommendation
- Technical Design
- Security Considerations
- Implementation Plan
- Open Questions
- Decision Record
- References
Background & Context¶
The AgentPool ACP implementation (src/acp/) supports three transport modes today:
- Stdio: subprocess communication via stdin/stdout. The default and most widely used mode, suitable for IDEs that spawn AgentPool as a child process.
- WebSocket (legacy, non-compliant): a bare server using the
websocketslibrary, defined in_serve_websocket(). It accepts connections, wraps them inAgentSideConnection, and processes JSON-RPC messages. This transport is deprecated and will be removed in v0.6.0 (target: 2026-Q3). Migration: switch to--transport streamable-http. - Custom streams: direct
ByteReceiveStream/ByteSendStreaminjection.
The existing WebSocket transport has significant gaps when measured against the ACP RFC's Streamable HTTP WebSocket Transport profile (streamable-http-websocket-transport.mdx):
| Feature | RFC Requirement | Current Status |
|---|---|---|
/acp endpoint path |
Required | Not enforced; binds to any host:port |
Acp-Connection-Id header |
Required on upgrade | Not returned |
initialize lifecycle guard |
Required | Not enforced |
| ASGI compatibility | Needed for HTTP routes | Uses raw websockets library |
| Extensible to SSE routes | Phase 2 requirement | No HTTP routing possible |
The ACP RFC defines a unified /acp endpoint supporting both WebSocket upgrade and Streamable HTTP (POST/GET/DELETE with SSE). Per the RFC, clients MUST support both transports, but servers MAY support only WebSocket. This justifies a phased approach: Phase 1 implements the WebSocket profile, Phase 2 adds Streamable HTTP.
Key existing components that this work builds on:
AgentSideConnectiontakesByteSendStream+ByteReceiveStreamand handles the full JSON-RPC line protocol._WebSocketReadStream/_WebSocketWriteStreamadapters already bridge WebSocket frames to anyio byte streams.ACPBridgeis an independent code path (stdio subprocess + HTTP proxy) that requires no changes.uvicornis already a project dependency.websocketsis already a project dependency.starlettewould be a new dependency.
Problem Statement¶
The current WebSocket transport implementation cannot serve as a compliant ACP server for remote clients. It lacks connection identification, lifecycle enforcement, and the ability to extend to HTTP routes for Streamable HTTP. This blocks several use cases:
- IDE integration over the network: Tools like Zed and VS Code need to connect to an ACP agent running on a remote host or in a container, not just as a subprocess.
- Multi-agent orchestration across processes: Agents running in separate processes or machines need a network transport to communicate.
- Protocol compliance: The ACP RFC's Streamable HTTP WebSocket Transport profile is the standard. The current implementation deviates in multiple ways, making it incompatible with clients that follow the spec.
Without this change, AgentPool can only serve ACP agents via stdio or a non-compliant WebSocket server, limiting its integration surface to in-process or local-subprocess scenarios.
Goals & Non-Goals¶
Goals¶
- Implement a WebSocket server transport compliant with the ACP RFC's WebSocket profile.
- Return
Acp-Connection-Idheader (UUID v4) during WebSocket upgrade at/acp. - Enforce
initializelifecycle: client must sendinitializebefore other requests. - Reuse existing
AgentSideConnectionfor ACP protocol handling over WebSocket. - Integrate with uvicorn/Starlette for production-grade ASGI serving.
- Extend
Transporttype union,ACPServer, CLI, and YAML config to support the new transport. - Document HTTP/1.1 deviation from RFC's HTTP/2 requirement and provide migration path.
- Deprecate the legacy non-compliant
WebSocketTransportwith a migration path.
Non-Goals¶
- Streamable HTTP transport (POST/GET/DELETE with SSE). This is deferred to Phase 2.
- Multi-session support over a single WebSocket connection. Phase 1 is one connection equals one session.
- HTTP/2 support. Migration to Hypercorn is planned later; zero app-code change expected.
- Changes to
ACPBridgeor stdio transport. - Client-side implementation. This RFC covers server-side transport only.
- Authentication or authorization at the transport layer. Phase 1 assumes trusted network.
Success Criteria¶
- ACP server can be started with
agentpool serve-acp config.yml --transport streamable-http --port 8080and accepts WebSocket connections at/acp - WebSocket upgrade response includes
Acp-Connection-Idheader with UUID v4 - Pre-initialize requests receive JSON-RPC error code
-32002 - Post-initialize requests process normally through
AgentSideConnection - Connection cleanup calls
AgentSideConnection.close()on graceful and abnormal disconnect - YAML config
transport: streamable-httpresolves toACPWebSocketTransportinstance - Legacy
--transport websocketoutputs deprecation warning - All integration tests pass
Evaluation Criteria¶
The following criteria will be used to objectively evaluate each option:
| Criterion | Weight | Description | Minimum Threshold |
|---|---|---|---|
| RFC Compliance | High | Alignment with the ACP RFC's Streamable HTTP WebSocket Transport profile: /acp endpoint, Acp-Connection-Id header, initialize enforcement |
Must support all three features |
| Extensibility | High | Ability to add Streamable HTTP transport (POST/GET/DELETE with SSE) in Phase 2 without rewriting the server framework | Must support shared routing, middleware, and connection state |
| Implementation Cost | Medium | Development effort: lines of code, new concepts to learn, testing surface area | Must be achievable in a single sprint |
| Production Readiness | Medium | Middleware support (CORS, logging), monitoring hooks, graceful shutdown, connection lifecycle management | Must support graceful shutdown and at least one middleware |
| Dependency Impact | Low | New dependencies introduced and their cost: bundle size, supply-chain risk, version compatibility | Must not add dependencies with known CVEs |
Options Analysis¶
Option 1: Starlette + uvicorn (WebSocket-only, Phase 1)¶
Description: Build an ASGI application with a Starlette WebSocket endpoint at /acp. Serve via uvicorn. Generate Acp-Connection-Id header during upgrade. Enforce initialize lifecycle via a guard integrated into AgentSideConnection (protocol layer, not transport layer). Reuse existing stream adapter patterns adapted for Starlette's WebSocket API.
Advantages:
- Standard ASGI app enables adding HTTP routes (SSE, health checks) in Phase 2 without framework changes. Starlette's Router composes WebSocket and HTTP routes naturally.
- Middleware ecosystem is available immediately: CORS, logging, error handling, rate limiting. Any ASGI middleware works.
- uvicorn is already a project dependency, so the HTTP serving layer adds no new runtime requirement.
- Starlette's WebSocket API provides control over upgrade response headers via
websocket.accept(headers=...), makingAcp-Connection-Idreturn straightforward. - Production-grade: uvicorn handles process management, signal handling, and graceful shutdown out of the box.
Disadvantages:
- Introduces
starletteas a new dependency. While lightweight and well-maintained, it adds to the dependency tree and supply-chain surface. - Starlette's WebSocket API differs from the
websocketslibrary API, requiring new stream adapter implementations rather than reusing the existing ones directly. - The ASGI abstraction adds a thin layer of indirection compared to the raw
websocketslibrary. For a single WebSocket endpoint, this indirection is arguably unnecessary in Phase 1. - HTTP/1.1 only with uvicorn. The RFC may expect HTTP/2. Migration to Hypercorn is possible but introduces another ASGI server later.
Evaluation Against Criteria:
| Criterion | Score | Notes |
|---|---|---|
| RFC Compliance | 4 (Good) | Full control over upgrade headers, path routing, and lifecycle. Can implement all RFC requirements. |
| Extensibility | 5 (Excellent) | ASGI app can add HTTP routes and middleware. Phase 2 Streamable HTTP is a route addition, not a rewrite. |
| Implementation Cost | 3 (Adequate) | New stream adapters needed for Starlette API. Moderate effort for ASGI app scaffolding. |
| Production Readiness | 4 (Good) | Middleware, graceful shutdown, and monitoring all available through ASGI/uvicorn ecosystem. |
| Dependency Impact | 3 (Adequate) | One new dependency (starlette). Lightweight, no Pydantic requirement, widely used. |
Effort Estimate: Medium. Approximately 200-300 lines of new code for the ASGI app, stream adapters, initialize guard, and transport config. Integration with existing ACPServer and CLI is mechanical.
Risk Assessment:
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Starlette API drift between versions | Low | Low | Pin starlette version in pyproject.toml; Starlette follows semantic versioning. |
| HTTP/1.1 deviation causes client issues | Low | Medium | Document deviation; WebSocket upgrade works over HTTP/1.1; migration path to Hypercorn. |
| Stream adapter bugs in Starlette-WebSocket-to-anyio bridge | Medium | Medium | Write integration tests covering connect, disconnect, and abnormal-close scenarios. |
| Starlette adds transitive dependency weight | Low | Low | Starlette has minimal dependencies (no Pydantic). Audit dependency tree before pinning. |
Option 2: Raw websockets library (enhance existing)¶
Description: Extend the current _serve_websocket() implementation to add Acp-Connection-Id header return and initialize enforcement. No ASGI framework. The websockets library provides hooks for custom headers during upgrade via its process_request callback.
Advantages:
- No new dependencies. The
websocketslibrary is already in the project. - Minimal code changes to the existing
_serve_websocket(). Incremental enhancement of working code. - Familiar codebase. The team already understands the
websocketslibrary patterns used in the current implementation. - Lower risk of regression since the existing implementation continues to work; changes are additive.
Disadvantages:
- No path to Streamable HTTP. The
websocketslibrary handles only WebSocket connections. Adding HTTP routes for SSE in Phase 2 would require a separate HTTP server framework, resulting in two parallel server implementations. - No ASGI middleware. CORS, logging, rate limiting, and other cross-cutting concerns must be implemented manually as custom code within the
websocketshandler. - The
websocketslibrary'sprocess_requesthook for custom headers is less ergonomic than Starlette's direct header access. It requires understanding the library's internal upgrade flow. - Two server frameworks in the codebase (raw
websocketsfor legacy, ASGI for Phase 2) create maintenance burden and conceptual overhead. - Limited production tooling. The
websocketslibrary does not provide process management, signal handling, or monitoring hooks comparable to uvicorn.
Evaluation Against Criteria:
| Criterion | Score | Notes |
|---|---|---|
| RFC Compliance | 3 (Adequate) | Can add Acp-Connection-Id and initialize guard, but no path routing enforcement. |
| Extensibility | 1 (Inadequate) | Adding HTTP routes requires a separate server. Two frameworks in the codebase. |
| Implementation Cost | 5 (Excellent) | Smallest change delta. Enhance existing code. |
| Production Readiness | 2 (Poor) | No middleware, no process management, limited monitoring. Must build manually. |
| Dependency Impact | 5 (Excellent) | No new dependencies. |
Effort Estimate: Low. Approximately 50-80 lines of changes to existing _serve_websocket() for header return and initialize guard.
Risk Assessment:
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Phase 2 requires full rewrite | High | High | Accept technical debt now, plan rewrite later. Or reject this option. |
| Custom header hook API is fragile | Medium | Medium | Write tests for process_request hook; pin websockets version. |
| Dual-framework maintenance burden | High | Medium | Document both code paths; plan deprecation of raw websockets server. |
| Missing middleware for production use | High | Medium | Build custom middleware as needed, increasing code in transports.py. |
Option 3: FastAPI + uvicorn¶
Description: Use FastAPI's WebSocket support with Pydantic models for protocol message validation. Define request/response schemas for initialize, session/new, and other ACP methods. Serve via uvicorn.
Advantages:
- Automatic OpenAPI documentation generation for any HTTP routes added in Phase 2.
- Pydantic validation on JSON-RPC messages catches malformed requests before they reach
AgentSideConnection. - FastAPI is widely known in the Python ecosystem, potentially reducing onboarding time.
- Same ASGI foundation as Option 1, so middleware and routing benefits apply.
Disadvantages:
- FastAPI's WebSocket support does not perform Pydantic validation on WebSocket messages. The validation benefit is limited to HTTP routes, not the WebSocket transport that Phase 1 actually needs.
- Adds both
fastapiand itspydanticdependency (if not already present at the required version). This is heavier than Starlette alone. - FastAPI wraps Starlette, adding an abstraction layer that provides no value for a pure WebSocket endpoint. The overhead shows up in import time, startup time, and complexity.
AgentSideConnectionalready handles JSON-RPC message parsing and validation. Adding Pydantic validation at the transport layer duplicates this logic and creates two validation points that can drift out of sync.- FastAPI's opinionated patterns (dependency injection, path operation decorators) add ceremony that a single WebSocket endpoint does not need.
Evaluation Against Criteria:
| Criterion | Score | Notes |
|---|---|---|
| RFC Compliance | 4 (Good) | Same header control and routing as Starlette. |
| Extensibility | 5 (Excellent) | Full ASGI app with HTTP routes and middleware. |
| Implementation Cost | 2 (Poor) | FastAPI boilerplate plus Pydantic message models that duplicate existing validation. |
| Production Readiness | 4 (Good) | ASGI middleware and uvicorn benefits, plus OpenAPI docs. |
| Dependency Impact | 2 (Poor) | Two new dependencies: fastapi plus pydantic version alignment. |
Effort Estimate: Medium-High. Approximately 250-350 lines for the FastAPI app, Pydantic message models, and stream adapters. The Pydantic models for JSON-RPC messages add significant upfront work that duplicates existing validation.
Risk Assessment:
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Pydantic validation drift from AgentSideConnection | High | Medium | Keep validation in one place (AgentSideConnection); don't duplicate in FastAPI. But this removes the primary benefit of Option 3. |
| FastAPI version coupling with Pydantic v2 | Medium | Medium | Pin both versions; test upgrade paths. |
| Unnecessary abstraction for a WebSocket endpoint | High | Low | Accept added complexity, or simplify to Starlette. |
| Import/startup time regression | Low | Low | Measure impact; FastAPI's import chain is heavier than Starlette's. |
Options Comparison Summary¶
| Criterion | Weight | Option 1 (Starlette) | Option 2 (Raw websockets) | Option 3 (FastAPI) |
|---|---|---|---|---|
| RFC Compliance | High | 4 | 3 | 4 |
| Extensibility | High | 5 | 1 | 5 |
| Implementation Cost | Medium | 3 | 5 | 2 |
| Production Readiness | Medium | 4 | 2 | 4 |
| Dependency Impact | Low | 3 | 5 | 2 |
| Weighted Total | 3.9 | 2.8 | 3.6 |
Recommendation¶
Recommended Option¶
Option 1: Starlette + uvicorn
Justification¶
The deciding factor is Extensibility, weighted High. Phase 2 requires adding Streamable HTTP routes (POST/GET/DELETE with SSE) alongside the WebSocket endpoint. Starlette's ASGI Router composes these naturally: the WebSocket route at /acp and HTTP routes at the same path share middleware, connection state, and the server framework. Option 2 (raw websockets) cannot serve HTTP routes, meaning Phase 2 would require introducing a second server framework or rewriting the transport entirely. Option 3 (FastAPI) provides the same extensibility but adds Pydantic validation overhead that the WebSocket transport cannot use, since FastAPI does not validate WebSocket message payloads.
On RFC Compliance, Options 1 and 3 score equally (⅘), while Option 2 scores lower (⅗) due to the inability to enforce path-based routing cleanly.
On Implementation Cost, Option 2 scores best (5/5) because it is the smallest change. However, this cost advantage is temporary: the Phase 2 rewrite needed under Option 2 would exceed the total cost of Option 1's Phase 1 plus Phase 2.
On Production Readiness, Option 1 scores well (⅘) due to ASGI middleware support, uvicorn's process management, and graceful shutdown. Option 2 scores poorly (⅖) because these capabilities must be built manually.
On Dependency Impact, Option 2 has no new dependencies. Option 1 adds starlette (lightweight, no Pydantic). Option 3 adds both fastapi and pydantic version constraints. Starlette is the smallest addition among the options that provide ASGI extensibility.
Accepted Trade-offs¶
- HTTP/1.1 deviation: Acceptable because WebSocket upgrade works over HTTP/1.1. The deviation is functionally harmless for Phase 1. Migration to Hypercorn (ASGI-compatible HTTP/2 server) requires zero app-code changes; only the server runner call changes.
- New starlette dependency: Acceptable because starlette is lightweight (~30KB, no Pydantic), well-maintained (sibling project of FastAPI), and provides the ASGI foundation needed for Phase 2. The alternative of maintaining two server frameworks (Option 2 + separate HTTP server for Phase 2) carries higher long-term cost.
Conditions¶
- HTTP/1.1 deviation must be documented in server startup logs.
- starlette dependency must be audited for transitive dependencies before pinning.
- Initialize enforcement must be configurable (allow opt-out for backward compatibility during migration).
Technical Design¶
Architecture Overview¶
ACP Streamable HTTP WebSocket Transport
======================================
Client (IDE, CLI, etc.)
|
| HTTP/1.1 Upgrade: ws://host:port/acp
| Response Header: Acp-Connection-Id: <uuid-v4>
v
+----------------------------------------------------------+
| Starlette ASGI App |
| |
| WebSocket Route: /acp |
| +-----------------------------------------------------+ |
| | | |
| | 1. Accept upgrade, generate Acp-Connection-Id | |
| | 2. Create stream adapters | |
| | - _StarletteWebSocketReadStream -> ByteRecv | |
| | - _StarletteWebSocketWriteStream -> ByteSend | |
| | 3. Create AgentSideConnection(agent_factory, ...) | |
| | - Initialize guard lives in Connection layer | |
| | 4. Monitor connection lifecycle | |
| | - Graceful close -> AgentSideConnection.close()| |
| | - Abnormal close -> AgentSideConnection.close()| |
| +-----------------------------------------------------+ |
| |
| (Phase 2: HTTP Route: /acp for Streamable HTTP/SSE) |
+----------------------------------------------------------+
|
| uvicorn serves the ASGI app
v
Network (host:port)
Key Components¶
ACPWebSocketTransport¶
- Responsibility: Configuration dataclass for the transport
- Fields:
host: str = "localhost",port: int = 8080 - Type:
@dataclass - Note: Phase 1 is WebSocket-only. Phase 2 will extend this class to support Streamable HTTP (POST/GET/DELETE with SSE).
_StarletteWebSocketReadStream¶
- Responsibility: Adapt Starlette WebSocket receive to
ByteReceiveStream - Pattern: Wraps
WebSocket.receive_text(), translatesWebSocketDisconnecttoanyio.EndOfStream, appends trailing newline for JSON-RPC line protocol - Base:
ByteReceiveStream
_StarletteWebSocketWriteStream¶
- Responsibility: Adapt Starlette WebSocket send to
ByteSendStream - Pattern: Strips trailing newline, sends as complete WebSocket text message via
WebSocket.send_text() - Base:
ByteSendStream
Initialize Guard (Protocol Layer)¶
- Responsibility: Enforce
initializeas the first JSON-RPC request after WebSocket upgrade - Location: Integrated into
AgentSideConnection/Connectionlayer, NOT the transport layer - Rationale: The transport layer should not duplicate JSON-RPC parsing logic.
Connection._receive_loop()already parses JSON-RPC messages. The guard intercepts at the protocol layer after parsing. - State:
initialized: boolper connection - Behavior:
- Before initialize: Any request with
method != "initialize"returns JSON-RPC error{"jsonrpc":"2.0","error":{"code":-32002,"message":"initialize required"},"id":<request_id>} - After successful initialize: All messages pass through unmodified
- Batch requests: If the first message is a batch, the entire batch is rejected with
-32002unless all elements areinitialize(Phase 1 does not support batching) - Implementation notes:
- The guard inspects the parsed JSON-RPC request object (already available in
Connection._process_message()). No additional stream parsing is needed. - To detect a successful
initializeresponse, the guard wraps the agent'sinitialize()handler. When the handler returns anInitializeResponse(or the response is serialized successfully), the guard flipsinitialized = True. If the handler raises an exception, the connection remains uninitialized and the client may retry. - The guard does not intercept outgoing bytes on
ByteSendStream. It operates on the structured JSON-RPC response object before serialization, avoiding stream adapter complexity.
Data Model¶
@dataclass
class ACPWebSocketTransport:
"""Configuration for ACP WebSocket transport (Phase 1).
Implements the WebSocket subset of the ACP RFC's Streamable HTTP
WebSocket Transport profile. Phase 1 covers WebSocket only;
Streamable HTTP (POST/GET/DELETE with SSE) is planned for Phase 2.
Attributes:
host: Host to bind the server to.
port: Port for the server. Note: defaults to 8080 (standard HTTP port).
This differs from the legacy `WebSocketTransport` default (8765)
because the transports serve different protocols and use-cases.
When migrating from legacy `--ws-port`, update to `--port`.
"""
host: str = "localhost"
port: int = 8080
Type Union Extension¶
Transport = (
StdioTransport
| WebSocketTransport # Legacy; deprecated, will be removed
| StreamTransport
| ACPWebSocketTransport
| Literal["stdio", "websocket", "streamable-http"]
)
The "streamable-http" string literal normalizes to ACPWebSocketTransport() with defaults. The "websocket" literal continues to normalize to the legacy WebSocketTransport() for backward compatibility but emits a deprecation warning.
API Design¶
WebSocket Upgrade¶
GET /acp HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <key>
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <accept>
Acp-Connection-Id: 550e8400-e29b-41d4-a716-446655440000
Initialize Lifecycle Flow¶
Client connects via WebSocket upgrade at /acp
|
v
+------------------+
| initialized=False |
+------------------+
|
v
Receive first JSON-RPC message
|
+---> method == "initialize" ?
| |
| YES | NO
| v v
| Pass to Return JSON-RPC error:
| AgentSide {"jsonrpc":"2.0","error":
| Connection {"code":-32002,
| "message":"initialize required"},
| "id":<request_id>}
| |
| v
| Initialize succeeds?
| |
| YES | NO
| v v
| Set Connection remains
| initialized uninitialized;
| = True client may retry
| |
v v
Subsequent messages pass through
to AgentSideConnection unmodified
CLI Integration¶
agentpool serve-acp config.yml # Default: stdio transport
agentpool serve-acp config.yml --transport streamable-http # WebSocket on localhost:8080
agentpool serve-acp config.yml --transport streamable-http --port 9000 # WebSocket on localhost:9000
agentpool serve-acp config.yml --transport streamable-http --host 0.0.0.0 --port 9000 # WebSocket on 0.0.0.0:9000
The --transport flag accepts stdio (default), websocket (legacy, deprecated), or streamable-http (new compliant transport). When --transport streamable-http is used, --host and --port control the bind address. When --transport websocket is used, the legacy non-compliant _serve_websocket() transport is instantiated; a deprecation warning is emitted directing the user to switch to --transport streamable-http. The legacy --ws-host and --ws-port parameters are still accepted for backward compatibility but emit deprecation warnings directing users to --host and --port.
YAML Config Schema Extension¶
ACPPoolServerConfig Fields¶
The ACPPoolServerConfig model in src/agentpool_config/pool_server.py is extended with:
class ACPPoolServerConfig(BasePoolServerConfig):
# ... existing fields ...
transport: Literal["stdio", "streamable-http"] = Field(
default="stdio",
title="Transport type",
examples=["stdio", "streamable-http"],
)
"""Transport type for the ACP server."""
host: str = Field(
default="localhost",
title="Server host",
examples=["localhost", "0.0.0.0", "127.0.0.1"],
)
"""Host to bind the server to (streamable-http only)."""
port: int = Field(
default=8080,
gt=0,
title="Server port",
examples=[8080, 9000],
)
"""Port to listen on (streamable-http only)."""
Example YAML¶
pool_server:
type: acp
enabled: true
transport: streamable-http # or "stdio" (default)
host: "0.0.0.0" # optional, defaults to "localhost"
port: 8080 # optional, defaults to 8080
When transport: streamable-http is specified, ACPServer.from_config() resolves the config to an ACPWebSocketTransport instance with the provided or default host and port values.
Uvicorn Shutdown Integration¶
ACPServer._start_async() passes self._shutdown_event (an asyncio.Event from BaseServer) to serve(). The _serve_streamable_http() function bridges this event to uvicorn's shutdown mechanism:
async def _serve_streamable_http(
agent: Agent | Callable[[AgentSideConnection], Agent],
host: str,
port: int,
shutdown_event: asyncio.Event | None,
debug_file: str | None,
**kwargs: Any,
) -> None:
from starlette.applications import Starlette
from starlette.websockets import WebSocket
from starlette.routing import WebSocketRoute
import uvicorn
app = Starlette(routes=[WebSocketRoute("/acp", _acp_websocket_endpoint)])
config = uvicorn.Config(app, host=host, port=port, log_level="info")
server = uvicorn.Server(config)
shutdown = shutdown_event or asyncio.Event()
async def shutdown_watcher() -> None:
await shutdown.wait()
server.should_exit = True
watcher_task = asyncio.create_task(shutdown_watcher())
try:
await server.serve()
finally:
watcher_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await watcher_task
# Clean up any remaining connections
# _active_connections: set[AgentSideConnection] maintained by the endpoint handler
for conn in _active_connections:
await conn.close()
The shutdown sequence is:
- External signal (SIGTERM) or
ACPServer.stop()setsshutdown_event shutdown_watcher()detects the event and setsserver.should_exit = True- uvicorn initiates graceful shutdown, cancelling all in-flight ASGI tasks
- Each WebSocket endpoint's
try/finallyblock callsAgentSideConnection.close() _serve_streamable_http()awaits uvicorn exit, then cleans up any leaked connections
Security Considerations¶
Threat Analysis¶
| Threat | Impact | Likelihood | Mitigation |
|---|---|---|---|
| Unauthenticated network access | High | High (if bound to 0.0.0.0) | Default to localhost; document risk of 0.0.0.0 binding |
| WebSocket connection flood (DoS) | Medium | Medium | Add max_connections parameter; reject with HTTP 503 when exceeded |
| Unencrypted transport (ws://) | High | Medium | Support TLS via uvicorn flags; recommend reverse proxy for production |
| CORS bypass for future SSE routes | Medium | Low | Add cors_origins config parameter; apply Starlette CORSMiddleware |
Security Measures¶
- Default bind to
localhost; document0.0.0.0risk - Implement
max_connectionslimit with configurable default (100) - Support TLS via
--ssl-keyfile/--ssl-certfileCLI flags - Add
cors_originsparameter for Phase 2 SSE routes - Log connection ID on connect/disconnect for audit trail
Compliance¶
No regulatory requirements apply in Phase 1. Production deployments should run behind a TLS-terminating reverse proxy with authentication middleware.
Implementation Plan¶
Phase 1: Transport Config + ASGI WebSocket Server¶
- Scope: New transport type, stream adapters, initialize guard in protocol layer, ASGI app
- Deliverables:
ACPWebSocketTransportdataclass,_StarletteWebSocketReadStream/_StarletteWebSocketWriteStreamadapters, initialize guard inConnection, Starlette ASGI app with/acpWebSocket route - Dependencies: None (can start immediately)
Tasks:
- 1.1: Add
ACPWebSocketTransportdataclass tosrc/acp/transports.py - 1.2: Extend
Transporttype union withACPWebSocketTransportand"streamable-http"literal - 1.3: Add
"streamable-http"normalization case inserve() - 1.4: Add
ACPWebSocketTransportdispatch case inserve() - 2.1: Create
_StarletteWebSocketReadStreamadapter (handleWebSocketDisconnect->anyio.EndOfStream) - 2.2: Create
_StarletteWebSocketWriteStreamadapter - 2.3: Implement initialize lifecycle guard in
Connection._process_message()or_agent_handler() - 2.4: Create Starlette ASGI app with
/acpWebSocket route - 2.5: Implement connection cleanup with
try/finallyin endpoint handler
Phase 2: Server Runner + ACPServer Integration¶
- Scope: Server runner function, dependency addition, ACPServer integration
- Deliverables:
_serve_streamable_http()function, starlette dependency, verified ACPServer passthrough, uvicorn shutdown bridging - Dependencies: Phase 1 complete
Tasks:
- 3.1: Implement
_serve_streamable_http()function with uvicornServerdirect instantiation - 3.2: Implement
shutdown_event->server.should_exitbridging via background watcher task - 3.3: Add
starlettedependency topyproject.toml - 4.1: Verify
ACPServer.__init__()type hints - 4.2: Verify
ACPServer.from_config()type hints - 4.3: Verify
_start_async()transport passthrough
Phase 3: CLI + YAML Config¶
- Scope: CLI flags, YAML schema extension, deprecation of legacy transport
- Deliverables:
--transportextension,--host/--portflags, YAMLtransport: streamable-httpsupport, legacy--transport websocketdeprecation warning - Dependencies: Phase 2 complete
Tasks:
- 5.1: Extend
--transportchoices toLiteral["stdio", "websocket", "streamable-http"] - 5.2: Add
--hostand--portflags (effective when--transport streamable-http) - 5.3: Create
ACPWebSocketTransportfrom CLI flags when--transport streamable-http - 5.4: Emit deprecation warning when
--transport websocketis used (still runs legacy transport, warns user to switch to--transport streamable-http) - 6.1: Extend
ACPPoolServerConfigwithtransport,host,portfields followingMCPPoolServerConfigpattern - 6.2: Resolve YAML config to
ACPWebSocketTransportinACPServer.from_config()
Phase 4: Testing¶
- Scope: Unit and integration tests
- Deliverables: Test suite covering all requirements
- Dependencies: Phases 1-3 complete
Tasks:
- 7.1: Unit test
ACPWebSocketTransportdataclass - 7.2: Unit test
serve()dispatch for all transport types - 7.3: Integration test: WebSocket upgrade returns
Acp-Connection-Id - 7.4: Integration test: initialize lifecycle enforcement in protocol layer
- 7.5: Integration test: stream adapter round-trip with
WebSocketDisconnecttranslation - 7.6: Integration test: connection cleanup on disconnect
- 7.7: Integration test: graceful shutdown via
shutdown_event - 7.8: Integration test: CLI flags and legacy deprecation warning
- 7.9: Integration test: YAML config resolution to
ACPWebSocketTransport - 7.10: LSP diagnostics clean on all changed files
Milestones¶
| Milestone | Description | Target | Status |
|---|---|---|---|
| M1 | ASGI WebSocket server working end-to-end | Week 1 | Not Started |
| M2 | Server runner + ACPServer integration | Week 1-2 | Not Started |
| M3 | CLI + YAML config complete | Week 2 | Not Started |
| M4 | All tests passing | Week 2-3 | Not Started |
Rollback Strategy¶
The new transport is opt-in (requires --transport streamable-http or transport: streamable-http in YAML). Removing the feature requires:
- Revert CLI flag additions
- Revert YAML config model changes
- Remove
_serve_streamable_http()andACPWebSocketTransport - Remove starlette dependency
No existing functionality is affected since stdio remains the default.
Open Questions¶
- HTTP/2 migration timeline
- Context: The RFC may expect HTTP/2. uvicorn supports HTTP/1.1 only. Hypercorn is an ASGI-compatible HTTP/2 server that could replace uvicorn with zero app-code changes.
- Owner: Team lead
- Status: Open
- Multi-session over single connection design
- Context: Phase 1 maps one WebSocket connection to one agent session. The RFC allows multiple sessions over a single connection. The initialize guard's state model may need extension.
- Owner: Architect
- Status: Open
- Authentication/authorization at the transport layer
- Context: Phase 1 has no auth. What mechanisms should Phase 2 support (API key, JWT, mTLS)? Should middleware pipeline be designed now?
- Owner: Security lead
- Status: Open
- Starlette version pinning strategy
- Context: Starlette follows semantic versioning but is developed alongside FastAPI, which can create pressure for rapid minor releases.
- Owner: DevOps
- Status: Open
Decision Record¶
Complete this section after RFC review is concluded.
Decision¶
Status: Pending review Date: TBD Approvers: TBD
Key Discussion Points¶
- Framework choice: Starlette vs raw websockets vs FastAPI
- HTTP/1.1 deviation acceptance
- Phase 1 scope boundaries (no auth, no multi-session)
- Initialize enforcement backward compatibility
- Legacy
WebSocketTransportdeprecation timeline
Conditions of Approval¶
- HTTP/1.1 deviation accepted with documented migration path
- starlette dependency audited for transitive dependencies
- Initialize enforcement has opt-out mechanism for migration period
- Legacy
WebSocketTransportdeprecated with removal target v0.6.0 (2026-Q3) and clear migration message to--transport streamable-http
Rejected Options¶
| Option | Reason |
|---|---|
JSON-RPC error code -32600 for "initialize required" |
Rejected: -32600 is reserved for structural "Invalid Request" errors per JSON-RPC 2.0 spec (ยง5.1). State-related errors belong in the server error range (-32000 to -32099). -32002 is idiomatic for server-not-initialized states (also used by LSP). Preserving this record per repository RFC rules. |
Dissenting Opinions¶
None recorded yet.
References¶
Related Documents¶
- OpenSpec proposal:
openspec/changes/acp-streamable-http-ws-server/proposal.md - OpenSpec design:
openspec/changes/acp-streamable-http-ws-server/design.md - OpenSpec ws-transport spec:
openspec/changes/acp-streamable-http-ws-server/specs/ws-transport/spec.md - OpenSpec ws-server-integration spec:
openspec/changes/acp-streamable-http-ws-server/specs/ws-server-integration/spec.md - OpenSpec tasks:
openspec/changes/acp-streamable-http-ws-server/tasks.md
External Resources¶
- ACP RFC: Streamable HTTP WebSocket Transport profile (
streamable-http-websocket-transport.mdx) - Starlette documentation: https://www.starlette.io/
- uvicorn documentation: https://www.uvicorn.org/
- Hypercorn documentation: https://hypercorn.readthedocs.io/
Appendix¶
Current transport code: src/acp/transports.py
ACP server: src/agentpool_server/acp_server/server.py
Pool server config: src/agentpool_config/pool_server.py