Skip to content

feat(acp-nats-ws): unblock remote ACP clients#126

Merged
yordis merged 23 commits intomainfrom
yordis/acp-websocket-spec
Apr 23, 2026
Merged

feat(acp-nats-ws): unblock remote ACP clients#126
yordis merged 23 commits intomainfrom
yordis/acp-websocket-spec

Conversation

@yordis
Copy link
Copy Markdown
Member

@yordis yordis commented Apr 22, 2026

  • unblock ACP clients that need the draft Streamable HTTP transport alongside WebSocket
  • close the remaining transport gap so /acp works across proxy-friendly and stateless deployments

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 22, 2026

PR Summary

Medium Risk
Adds a new HTTP/SSE transport path with per-connection/session state management and origin/protocol header validation, increasing surface area and complexity. Risk is moderate because it changes the public endpoint (/ws -> /acp) and introduces new connection lifecycle/routing logic that could affect interoperability and resource cleanup.

Overview
Adds draft ACP remote transport support on a single /acp endpoint by serving Streamable HTTP (POST/GET/DELETE returning SSE) alongside WebSocket upgrade, replacing the previous WebSocket-only /ws route.

Introduces a connection manager and per-HTTP-connection runtime (transport.rs) that tracks Acp-Connection-Id, negotiated Acp-Protocol-Version, and Acp-Session-Id, enforces content negotiation, validates Origin against the bind host, supports session-scoped GET listeners, and allows terminating connections via DELETE.

WebSocket handling is updated to emit/propagate connection IDs in logs and headers, ignore binary frames, and route through the new manager; extensive new tests cover HTTP flows, header validation, origin checks, and lifecycle behaviors.

Reviewed by Cursor Bugbot for commit 8805ee2. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Replaces the WebSocket-only upgrade with a unified ACP transport at /acp (HTTP SSE + WebSocket), adds per-connection UUIDs (AcpConnectionId) and protocol/header constants, moves wiring into a manager/transport model, implements HTTP handlers (GET/POST/DELETE), and adds extensive parsing, routing, and tests.

Changes

Cohort / File(s) Summary
Manifest
rsworkspace/crates/acp-nats-ws/Cargo.toml
Added runtime deps: serde (derive), serde_json, uuid; moved serde_json out of dev-deps; added tower to dev-dependencies.
Docs
rsworkspace/crates/acp-nats-ws/README.md
Updated README to document /acp transport (Streamable HTTP + WebSocket upgrade), header conventions, SSE examples, and changed example endpoints from /ws/acp.
Identifiers & Constants
rsworkspace/crates/acp-nats-ws/src/acp_connection_id.rs, rsworkspace/crates/acp-nats-ws/src/constants.rs
New AcpConnectionId wrapper around uuid::Uuid with new(), parse(), Default, Display, error type and unit tests; added ACP header/endpoint constants.
Connection Logic
rsworkspace/crates/acp-nats-ws/src/connection.rs
handle now accepts connection_id: AcpConnectionId; logs include connection ID; binary WebSocket frames are ignored; tests updated to use ACP_ENDPOINT; added binary-frame test.
App Entry / Routing
rsworkspace/crates/acp-nats-ws/src/main.rs
Replaced previous upgrade wiring with a manager channel and transport::AppState; router exposes ACP_ENDPOINT with GET/POST/DELETE and legacy /ws routing preserved in tests; manager delegates lifecycle to transport::process_manager_request.
Removed Module
rsworkspace/crates/acp-nats-ws/src/upgrade.rs
Deleted the former websocket upgrade module and its types/tests; functionality replaced by transport handlers.
New Transport Module
rsworkspace/crates/acp-nats-ws/src/transport.rs
Large new module providing AppState, manager requests, Axum handlers (get, post, delete), HTTP outcome/error types, JSON-RPC parsing/validation, SSE vs buffered semantics, run_http_connection, process_manager_request, session listener management, and many unit tests covering HTTP/WebSocket/SSE flows.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant HTTP as Transport HTTP Server
    participant Manager as Connection Manager
    participant Conn as Connection Runtime
    participant NATS as NATS/JetStream

    Client->>HTTP: POST/GET/DELETE /acp or WebSocket upgrade
    HTTP->>Manager: send ManagerRequest (HttpPost/HttpGet/WebSocket/HttpDelete)
    Manager->>Conn: spawn or attach run_http_connection / handoff WebSocket
    Conn->>NATS: publish/subscribe ACP messages
    NATS-->>Conn: inbound ACP messages
    Conn-->>Client: SSE events or WebSocket text frames
    Client->>Conn: JSON-RPC messages (POST bodies or WS frames)
    Conn->>NATS: forward outbound messages
    Client->>HTTP: DELETE /acp
    HTTP->>Manager: ManagerRequest::HttpDelete -> Manager closes Conn
    Conn->>NATS: teardown subscriptions
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I found a UUID, snug and bright,

I hop each stream from HTTP to WS by night,
SSE sprinkles, headers sing, NATS hums in tune,
I tend each session under acp's moon,
A rabbit cheering every connection's light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.72% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: adding Streamable HTTP transport support alongside WebSocket to enable remote ACP clients, which is the primary objective of this PR.
Description check ✅ Passed The description is directly related to the changeset, explaining the two key objectives: adding Streamable HTTP transport support and enabling the /acp endpoint across diverse deployments.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yordis/acp-websocket-spec

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
rsworkspace/crates/acp-nats-ws/src/main.rs (1)

52-53: Add a regression test for the legacy /ws alias.

The alias is key to the PR’s compatibility goal, but the updated tests only exercise /acp. A small handshake test for LEGACY_WS_ENDPOINT would lock this in.

🧪 Proposed test coverage addition
@@
     #[tokio::test]
     async fn test_websocket_connection_lifecycle() {
@@
     }
+
+    #[tokio::test]
+    async fn legacy_ws_endpoint_still_upgrades() {
+        let (shutdown_tx, _shutdown_rx) = watch::channel(false);
+        let (conn_tx, mut conn_rx) = mpsc::unbounded_channel::<ConnectionRequest>();
+
+        let state = UpgradeState {
+            conn_tx,
+            shutdown_tx,
+        };
+
+        let app = axum::Router::new()
+            .route(ACP_ENDPOINT, axum::routing::get(upgrade::handle))
+            .route(LEGACY_WS_ENDPOINT, axum::routing::get(upgrade::handle))
+            .with_state(state);
+
+        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+        let addr = listener.local_addr().unwrap();
+        tokio::spawn(async move {
+            axum::serve(listener, app).await.unwrap();
+        });
+
+        let ws_url = format!("ws://{}{}", addr, LEGACY_WS_ENDPOINT);
+        let (_ws_stream, response) = connect_async(ws_url).await.unwrap();
+
+        assert!(response.headers().contains_key(ACP_CONNECTION_ID_HEADER));
+        let _req = tokio::time::timeout(Duration::from_secs(2), conn_rx.recv())
+            .await
+            .expect("timeout waiting for legacy ConnectionRequest")
+            .expect("channel closed");
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/acp-nats-ws/src/main.rs` around lines 52 - 53, The test
suite lacks a regression test verifying the legacy LEGACY_WS_ENDPOINT ("/ws")
handshake; add a new async integration test that mirrors the existing
ACP_ENDPOINT handshake test but targets LEGACY_WS_ENDPOINT, asserting that an
HTTP GET to the route handled by upgrade::handle completes the websocket
handshake and returns the expected status/headers; place the test alongside the
existing handshake tests and reference ACP_ENDPOINT, LEGACY_WS_ENDPOINT and
upgrade::handle so the behavior for the alias is locked in.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@rsworkspace/crates/acp-nats-ws/src/main.rs`:
- Around line 52-53: The test suite lacks a regression test verifying the legacy
LEGACY_WS_ENDPOINT ("/ws") handshake; add a new async integration test that
mirrors the existing ACP_ENDPOINT handshake test but targets LEGACY_WS_ENDPOINT,
asserting that an HTTP GET to the route handled by upgrade::handle completes the
websocket handshake and returns the expected status/headers; place the test
alongside the existing handshake tests and reference ACP_ENDPOINT,
LEGACY_WS_ENDPOINT and upgrade::handle so the behavior for the alias is locked
in.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5f732cd0-7551-452b-921b-d892f6763050

📥 Commits

Reviewing files that changed from the base of the PR and between cb7673a and c3aad6f.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • rsworkspace/crates/acp-nats-ws/Cargo.toml
  • rsworkspace/crates/acp-nats-ws/README.md
  • rsworkspace/crates/acp-nats-ws/src/acp_connection_id.rs
  • rsworkspace/crates/acp-nats-ws/src/connection.rs
  • rsworkspace/crates/acp-nats-ws/src/constants.rs
  • rsworkspace/crates/acp-nats-ws/src/main.rs
  • rsworkspace/crates/acp-nats-ws/src/upgrade.rs

Comment thread rsworkspace/crates/acp-nats-ws/src/connection.rs
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis yordis changed the title fix(acp-nats-ws): align websocket bridge with acp transport draft feat(acp-nats-ws): unblock remote ACP clients Apr 22, 2026
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

badge

Code Coverage Summary

Details
Filename                                                                      Stmts    Miss  Cover    Missing
--------------------------------------------------------------------------  -------  ------  -------  ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
crates/trogon-source-linear/src/config.rs                                        17       0  100.00%
crates/trogon-source-linear/src/server.rs                                       392       0  100.00%
crates/trogon-source-linear/src/signature.rs                                     54       1  98.15%   16
crates/trogon-source-twitter/src/server.rs                                      567       0  100.00%
crates/trogon-source-twitter/src/signature.rs                                    77       0  100.00%
crates/trogon-source-twitter/src/config.rs                                       17       0  100.00%
crates/acp-nats/src/nats/subjects/global/logout.rs                                8       0  100.00%
crates/acp-nats/src/nats/subjects/global/ext.rs                                  12       0  100.00%
crates/acp-nats/src/nats/subjects/global/ext_notify.rs                           12       0  100.00%
crates/acp-nats/src/nats/subjects/global/session_new.rs                           8       0  100.00%
crates/acp-nats/src/nats/subjects/global/session_list.rs                          8       0  100.00%
crates/acp-nats/src/nats/subjects/global/authenticate.rs                          8       0  100.00%
crates/acp-nats/src/nats/subjects/global/initialize.rs                            8       0  100.00%
crates/trogon-source-notion/src/signature.rs                                     63       1  98.41%   32
crates/trogon-source-notion/src/notion_verification_token.rs                     17       0  100.00%
crates/trogon-source-notion/src/server.rs                                       351       8  97.72%   100-104, 137-138, 162-163
crates/trogon-source-notion/src/notion_event_type.rs                             48       3  93.75%   49-51
crates/acp-nats/src/nats/subjects/commands/set_model.rs                          18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/resume.rs                             18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/fork.rs                               18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/close.rs                              18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_mode.rs                           18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_config_option.rs                  18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/load.rs                               18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/prompt.rs                             18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/cancel.rs                             18       0  100.00%
crates/trogon-nats/src/nats_token.rs                                            161       0  100.00%
crates/trogon-nats/src/token.rs                                                   8       0  100.00%
crates/trogon-nats/src/mocks.rs                                                 304       0  100.00%
crates/trogon-nats/src/connect.rs                                               105      11  89.52%   22-24, 37, 49, 68-73
crates/trogon-nats/src/auth.rs                                                  119       0  100.00%
crates/trogon-nats/src/client.rs                                                 25      25  0.00%    50-89
crates/trogon-nats/src/messaging.rs                                             598       2  99.67%   153, 163
crates/trogon-std/src/time/mock.rs                                              129       0  100.00%
crates/trogon-std/src/time/system.rs                                             35       0  100.00%
crates/trogon-std/src/telemetry/http.rs                                         239       0  100.00%
crates/acp-nats/src/agent/list_sessions.rs                                       50       0  100.00%
crates/acp-nats/src/agent/test_support.rs                                       299       0  100.00%
crates/acp-nats/src/agent/js_request.rs                                         304       0  100.00%
crates/acp-nats/src/agent/set_session_model.rs                                   71       0  100.00%
crates/acp-nats/src/agent/new_session.rs                                         91       0  100.00%
crates/acp-nats/src/agent/ext_method.rs                                          92       0  100.00%
crates/acp-nats/src/agent/ext_notification.rs                                    88       0  100.00%
crates/acp-nats/src/agent/authenticate.rs                                        52       0  100.00%
crates/acp-nats/src/agent/mod.rs                                                 65       0  100.00%
crates/acp-nats/src/agent/prompt.rs                                             633       0  100.00%
crates/acp-nats/src/agent/fork_session.rs                                       106       0  100.00%
crates/acp-nats/src/agent/cancel.rs                                             105       0  100.00%
crates/acp-nats/src/agent/initialize.rs                                          83       0  100.00%
crates/acp-nats/src/agent/load_session.rs                                       101       0  100.00%
crates/acp-nats/src/agent/logout.rs                                              49       0  100.00%
crates/acp-nats/src/agent/close_session.rs                                       67       0  100.00%
crates/acp-nats/src/agent/resume_session.rs                                     102       0  100.00%
crates/acp-nats/src/agent/set_session_config_option.rs                           71       0  100.00%
crates/acp-nats/src/agent/bridge.rs                                             123       4  96.75%   109-112
crates/acp-nats/src/agent/set_session_mode.rs                                    71       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_wait_for_exit.rs           15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/fs_read_text_file.rs                15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_output.rs                  15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/session_request_permission.rs       15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/fs_write_text_file.rs               15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/session_update.rs                   15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_kill.rs                    15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_create.rs                  15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_release.rs                 15       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_session.rs                   18       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_agent.rs                     18       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_agent_ext.rs                 11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_agent.rs                     11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_session.rs                   11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/global_all.rs                    11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_client.rs                    11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/prompt_wildcard.rs               11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_client.rs                    18       0  100.00%
crates/acp-nats/src/telemetry/metrics.rs                                         65       0  100.00%
crates/acp-nats/src/nats/parsing.rs                                             285       1  99.65%   153
crates/acp-nats/src/nats/extensions.rs                                            3       0  100.00%
crates/acp-nats/src/nats/mod.rs                                                  23       0  100.00%
crates/trogon-source-slack/src/server.rs                                        954       0  100.00%
crates/trogon-source-slack/src/config.rs                                         17       0  100.00%
crates/trogon-source-slack/src/signature.rs                                      80       0  100.00%
crates/trogon-source-sentry/src/signature.rs                                     56       0  100.00%
crates/trogon-source-sentry/src/sentry_client_secret.rs                          17       0  100.00%
crates/trogon-source-sentry/src/server.rs                                       359       0  100.00%
crates/trogon-nats/src/lease/release.rs                                           5       5  0.00%    8-12
crates/trogon-nats/src/lease/mod.rs                                             603      13  97.84%   182-195
crates/trogon-nats/src/lease/renew.rs                                           263      19  92.78%   21-27, 46-57
crates/trogon-nats/src/lease/lease_bucket.rs                                     19       0  100.00%
crates/trogon-nats/src/lease/acquire.rs                                           8       8  0.00%    9-18
crates/trogon-nats/src/lease/nats_kv_lease_config.rs                             30       0  100.00%
crates/trogon-nats/src/lease/provision.rs                                       210      10  95.24%   84-94
crates/trogon-nats/src/lease/lease_key.rs                                        19       0  100.00%
crates/trogon-nats/src/lease/lease_config_error.rs                               13       0  100.00%
crates/trogon-nats/src/lease/lease_timing.rs                                     21       0  100.00%
crates/trogon-nats/src/lease/renew_interval.rs                                   61       0  100.00%
crates/trogon-nats/src/lease/ttl.rs                                              76       0  100.00%
crates/acp-telemetry/src/metric.rs                                               35       3  91.43%   30-31, 39
crates/acp-telemetry/src/log.rs                                                  71       1  98.59%   43
crates/acp-telemetry/src/lib.rs                                                 169      32  81.07%   28-34, 56-63, 98, 103, 108, 122-137, 174, 177, 180, 186
crates/acp-telemetry/src/service_name.rs                                         49       0  100.00%
crates/acp-telemetry/src/trace.rs                                                32       3  90.62%   23-24, 32
crates/acp-telemetry/src/signal.rs                                                7       1  85.71%   43
crates/trogon-std/src/args.rs                                                    10       0  100.00%
crates/trogon-std/src/json.rs                                                    30       0  100.00%
crates/trogon-std/src/http.rs                                                    19       0  100.00%
crates/trogon-std/src/secret_string.rs                                           35       0  100.00%
crates/trogon-std/src/duration.rs                                                45       0  100.00%
crates/trogon-source-discord/src/gateway.rs                                     491       1  99.80%   157
crates/trogon-source-discord/src/config.rs                                      109       0  100.00%
crates/acp-nats/src/jetstream/provision.rs                                       61       0  100.00%
crates/acp-nats/src/jetstream/consumers.rs                                       99       0  100.00%
crates/acp-nats/src/jetstream/ext_policy.rs                                      26       0  100.00%
crates/acp-nats/src/jetstream/streams.rs                                        194       4  97.94%   254-256, 266
crates/trogon-std/src/fs/system.rs                                              102       0  100.00%
crates/trogon-std/src/fs/mem.rs                                                 220      10  95.45%   61-63, 77-79, 133-135, 158
crates/acp-nats-ws/src/transport.rs                                            2002     190  90.51%   246-254, 332, 385-386, 507-508, 518, 542, 576-577, 586-587, 595-603, 626-631, 649, 668, 673, 692, 710-712, 733-734, 737, 774, 786-787, 790-791, 815, 827-828, 831-832, 857, 875, 898-900, 956, 973-976, 999-1000, 1022-1026, 1051-1052, 1055-1056, 1217, 1220, 1223, 1232, 1236, 1239, 1242-1245, 1266, 1281-1284, 1295-1300, 1306, 1317-1321, 1330-1369, 1379-1380, 1390, 1421, 1441, 1464-1470, 1473-1477, 1480-1485, 1495-1497, 1506, 1508-1509, 1593-1594, 1606-1607, 1621-1631, 1684-1705, 1744, 1985, 2024, 2054, 2376, 2419, 2476, 2545, 2557
crates/acp-nats-ws/src/connection.rs                                            184      32  82.61%   77-84, 89-100, 116, 118-119, 124, 136-137, 142, 146, 150, 153, 164, 168, 171, 174-178, 214
crates/acp-nats-ws/src/config.rs                                                 83       0  100.00%
crates/acp-nats-ws/src/acp_connection_id.rs                                      45       0  100.00%
crates/acp-nats-ws/src/main.rs                                                  543      18  96.69%   96, 241-262, 410
crates/acp-nats/src/acp_prefix.rs                                                51       0  100.00%
crates/acp-nats/src/client_proxy.rs                                             200       0  100.00%
crates/acp-nats/src/error.rs                                                     84       0  100.00%
crates/acp-nats/src/jsonrpc.rs                                                    6       0  100.00%
crates/acp-nats/src/ext_method_name.rs                                           70       0  100.00%
crates/acp-nats/src/config.rs                                                   204       0  100.00%
crates/acp-nats/src/lib.rs                                                       73       0  100.00%
crates/acp-nats/src/pending_prompt_waiters.rs                                   141       0  100.00%
crates/acp-nats/src/req_id.rs                                                    39       0  100.00%
crates/acp-nats/src/in_flight_slot_guard.rs                                      32       0  100.00%
crates/acp-nats/src/session_id.rs                                                72       0  100.00%
crates/trogon-nats/src/telemetry/messaging.rs                                   102       0  100.00%
crates/trogon-std/src/dirs/system.rs                                             76       0  100.00%
crates/trogon-std/src/dirs/fixed.rs                                              84       0  100.00%
crates/trogon-service-config/src/lib.rs                                          98       0  100.00%
crates/acp-nats/src/nats/subjects/responses/cancelled.rs                         18       0  100.00%
crates/acp-nats/src/nats/subjects/responses/response.rs                          20       0  100.00%
crates/acp-nats/src/nats/subjects/responses/update.rs                            27       0  100.00%
crates/acp-nats/src/nats/subjects/responses/ext_ready.rs                         15       0  100.00%
crates/acp-nats/src/nats/subjects/responses/prompt_response.rs                   27       0  100.00%
crates/trogon-nats/src/jetstream/stream_max_age.rs                               18       0  100.00%
crates/trogon-nats/src/jetstream/traits.rs                                       43      43  0.00%    140-208
crates/trogon-nats/src/jetstream/publish.rs                                      64       0  100.00%
crates/trogon-nats/src/jetstream/mocks.rs                                       740      44  94.05%   368-382, 388-396, 411-422, 436-446, 514-516
crates/trogon-nats/src/jetstream/claim_check.rs                                 372       0  100.00%
crates/trogon-source-gitlab/src/config.rs                                        17       0  100.00%
crates/trogon-source-gitlab/src/server.rs                                       431       0  100.00%
crates/trogon-source-gitlab/src/signature.rs                                     30       0  100.00%
crates/trogon-source-incidentio/src/incidentio_signing_secret.rs                 67       0  100.00%
crates/trogon-source-incidentio/src/signature.rs                                455       0  100.00%
crates/trogon-source-incidentio/src/config.rs                                    16       0  100.00%
crates/trogon-source-incidentio/src/incidentio_event_type.rs                     67       0  100.00%
crates/trogon-source-incidentio/src/server.rs                                   365       0  100.00%
crates/acp-nats-agent/src/connection.rs                                        1434       1  99.93%   686
crates/trogon-gateway/src/source_status.rs                                       32       0  100.00%
crates/trogon-gateway/src/streams.rs                                            148       0  100.00%
crates/trogon-gateway/src/config.rs                                            1682       0  100.00%
crates/trogon-gateway/src/http.rs                                               132       0  100.00%
crates/trogon-gateway/src/main.rs                                                 4       0  100.00%
crates/trogon-std/src/env/system.rs                                              17       0  100.00%
crates/trogon-std/src/env/in_memory.rs                                           81       0  100.00%
crates/acp-nats/src/client/terminal_wait_for_exit.rs                            396       0  100.00%
crates/acp-nats/src/client/session_update.rs                                     55       0  100.00%
crates/acp-nats/src/client/fs_read_text_file.rs                                 384       0  100.00%
crates/acp-nats/src/client/terminal_kill.rs                                     309       0  100.00%
crates/acp-nats/src/client/fs_write_text_file.rs                                451       0  100.00%
crates/acp-nats/src/client/request_permission.rs                                338       0  100.00%
crates/acp-nats/src/client/ext_session_prompt_response.rs                       157       0  100.00%
crates/acp-nats/src/client/ext.rs                                               365       8  97.81%   193-204, 229-240
crates/acp-nats/src/client/mod.rs                                              2987       0  100.00%
crates/acp-nats/src/client/rpc_reply.rs                                          71       0  100.00%
crates/acp-nats/src/client/terminal_create.rs                                   294       0  100.00%
crates/acp-nats/src/client/terminal_output.rs                                   223       0  100.00%
crates/acp-nats/src/client/terminal_release.rs                                  357       0  100.00%
crates/trogon-source-telegram/src/server.rs                                     387       0  100.00%
crates/trogon-source-telegram/src/signature.rs                                   38       0  100.00%
crates/trogon-source-telegram/src/config.rs                                      17       0  100.00%
crates/acp-nats/src/nats/subjects/mod.rs                                        380       0  100.00%
crates/acp-nats/src/nats/subjects/stream.rs                                      58       0  100.00%
crates/trogon-source-github/src/signature.rs                                     64       0  100.00%
crates/trogon-source-github/src/server.rs                                       351       0  100.00%
crates/trogon-source-github/src/config.rs                                        17       0  100.00%
crates/acp-nats-stdio/src/config.rs                                              72       0  100.00%
crates/acp-nats-stdio/src/main.rs                                               141      27  80.85%   64, 116-123, 129-131, 148, 179-200
TOTAL                                                                         29844     529  98.23%

Diff against main

Filename                                       Stmts    Miss  Cover
-------------------------------------------  -------  ------  --------
crates/acp-nats-ws/src/transport.rs            +2002    +190  +90.51%
crates/acp-nats-ws/src/connection.rs             +18      -3  +3.69%
crates/acp-nats-ws/src/acp_connection_id.rs      +45       0  +100.00%
crates/acp-nats-ws/src/main.rs                  +354       0  +6.21%
TOTAL                                          +2419    +187  -0.52%

Results for commit: 8805ee2

Minimum allowed coverage is 95%

♻️ This comment has been updated with latest results

Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs Outdated
Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs
Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis yordis added the rust:coverage-baseline-reset Relax Rust coverage gate to establish a new baseline label Apr 22, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
rsworkspace/crates/acp-nats-ws/src/transport.rs (1)

86-100: Prefer implementing IntoResponse rather than an inherent into_response.

HttpTransportError::into_response shadows the trait method name but is inherent, so Err(error).into_response() only works because of autoref. Implementing axum::response::IntoResponse directly lets the type be returned in Result<_, HttpTransportError> from handlers (Axum handles the conversion), removes the repetitive match … { Ok(r) => r, Err(e) => e.into_response() } in get/post/delete, and is idiomatic for axum 0.8.

♻️ Sketch
-impl HttpTransportError {
-    fn into_response(self) -> Response {
+impl axum::response::IntoResponse for HttpTransportError {
+    fn into_response(self) -> Response {
         let (status, message) = match self { /* … */ };
         (status, message).into_response()
     }
 }

Handlers then become:

pub async fn post(headers: HeaderMap, State(state): State<AppState>, body: String)
    -> Result<Response, HttpTransportError>
{
    http_post(headers, state, body).await
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/acp-nats-ws/src/transport.rs` around lines 86 - 100,
Replace the inherent method with an implementation of
axum::response::IntoResponse for HttpTransportError: implement impl IntoResponse
for HttpTransportError { fn into_response(self) -> Response { ... } } and move
the existing match logic into that trait method (or keep the inherent helper but
have the trait call it), so HttpTransportError can be returned directly from
handlers as Result<Response, HttpTransportError>; update handlers to return
Result<_, HttpTransportError> where needed and remove the repetitive explicit
error-to-response mapping such as e.into_response() in get/post/delete paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rsworkspace/crates/acp-nats-ws/src/transport.rs`:
- Around line 25-26: The SSE and internal messaging channels use unbounded mpsc
which can exhaust memory for slow/stalled clients; replace SseSender/SseReceiver
and all uses that create unbounded channels (notably stream_tx, each
AttachListener subscriber, input_tx and output_tx) with bounded mpsc::channel(N)
(choose a sensible N, e.g. 64 to match the notification channel) and implement
an explicit overflow policy: when send fails due to full buffer either drop that
listener/subscription (remove/close the AttachListener and clean up resources)
or return backpressure/error upstream so the outbound pump slows; ensure send
errors are handled where SseSender::send / input_tx / output_tx are used so
subscribers aren’t left accumulating frames indefinitely.
- Around line 736-786: The request-handling paths in HttpConnectionCommand::Post
(creating PendingRequest::Buffered and PendingRequest::Live) currently
fire-and-forget input_tx.send(message.raw) and can hang the HTTP caller if send
fails; change the flow to check input_tx.send(...) result and fail fast: for
both Buffered and Live, attempt input_tx.send(message.raw) before committing
pending_request (or if you must assign pending_request first, on Err
remove/reset pending_request and take the stored response to send
Err(HttpTransportError::Internal(...))); for Live also ensure you only send the
Ok(HttpPostOutcome::Live { ... }) after a successful send; in short, on send
error reset pending_request and respond with
Err(HttpTransportError::Internal(...)) via the response channel instead of
silently dropping the message so callers fail immediately.
- Around line 75-116: HttpTransportError currently holds only &'static str
messages and all call sites use map_err(|_| HttpTransportError::X("literal")),
which discards underlying errors; change HttpTransportError into structured
variants that carry the source error (e.g. BadRequest(#[source] Box<dyn
std::error::Error + Send + Sync>) or concrete error types) and derive
Display/Error via thiserror, update into_response to match on these variants and
map them to status codes, remove the manual Display impl, and replace the
map_err(...) call sites (e.g. where serde_json::Error, AcpSessionIdError,
AcpConnectionIdError::InvalidUuid, mpsc::SendError, oneshot::RecvError are
mapped) to propagate the typed errors (use ? or map_err(|e|
HttpTransportError::X(e.into())) so the original error is preserved) so callers
can see the source chain.
- Around line 382-417: The SSE GET handler http_get currently only sets
X_ACCEL_BUFFERING_HEADER and omits echoing Acp-Connection-Id/Acp-Session-Id like
the POST paths; update http_get to call
set_transport_headers(response.headers_mut(), &connection_id, &session_id) (or
equivalent) after building the response so the Acp-Connection-Id and
Acp-Session-Id are included in the response headers, matching build_sse_response
/ build_buffered_sse_response behavior.

---

Nitpick comments:
In `@rsworkspace/crates/acp-nats-ws/src/transport.rs`:
- Around line 86-100: Replace the inherent method with an implementation of
axum::response::IntoResponse for HttpTransportError: implement impl IntoResponse
for HttpTransportError { fn into_response(self) -> Response { ... } } and move
the existing match logic into that trait method (or keep the inherent helper but
have the trait call it), so HttpTransportError can be returned directly from
handlers as Result<Response, HttpTransportError>; update handlers to return
Result<_, HttpTransportError> where needed and remove the repetitive explicit
error-to-response mapping such as e.into_response() in get/post/delete paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 04c76481-b35c-4aa2-823b-4b35cdb3ae74

📥 Commits

Reviewing files that changed from the base of the PR and between c3aad6f and 32deb24.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • rsworkspace/crates/acp-nats-ws/Cargo.toml
  • rsworkspace/crates/acp-nats-ws/README.md
  • rsworkspace/crates/acp-nats-ws/src/acp_connection_id.rs
  • rsworkspace/crates/acp-nats-ws/src/constants.rs
  • rsworkspace/crates/acp-nats-ws/src/main.rs
  • rsworkspace/crates/acp-nats-ws/src/transport.rs
  • rsworkspace/crates/acp-nats-ws/src/upgrade.rs
💤 Files with no reviewable changes (1)
  • rsworkspace/crates/acp-nats-ws/src/upgrade.rs
✅ Files skipped from review due to trivial changes (2)
  • rsworkspace/crates/acp-nats-ws/src/constants.rs
  • rsworkspace/crates/acp-nats-ws/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • rsworkspace/crates/acp-nats-ws/Cargo.toml
  • rsworkspace/crates/acp-nats-ws/src/acp_connection_id.rs
  • rsworkspace/crates/acp-nats-ws/src/main.rs

Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs Outdated
Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs Outdated
Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs
Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs
Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
rsworkspace/crates/acp-nats-ws/src/main.rs (2)

83-86: ⚠️ Potential issue | 🟡 Minor

Stale doc comment — AppState.conn_tx renamed to manager_tx.

The comment still references AppState.conn_tx, which no longer exists. Update to AppState.manager_tx to avoid confusing future readers.

📝 Proposed fix
-    // `serve` returning drops the Router (and its AppState.conn_tx), which
+    // `serve` returning drops the Router (and its AppState.manager_tx), which
     // closes the channel and lets the connection thread's recv-loop exit and
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/acp-nats-ws/src/main.rs` around lines 83 - 86, The doc
comment mentions the old field name `AppState.conn_tx`; update this comment to
reference the new field `AppState.manager_tx` so it accurately describes that
dropping `serve` (and the Router, which owns `AppState.manager_tx`) closes the
channel and lets the connection thread's recv-loop exit and drain active
connections before telemetry teardown; locate the comment near the
`serve`/`Router` code in main.rs and replace `conn_tx` with `manager_tx`.

106-178: ⚠️ Potential issue | 🔴 Critical

Add Clone bound to J in both run_connection_thread and process_connections.

process_manager_request in transport.rs requires J: Clone + 'static, but both functions pass references to js_client without declaring the Clone bound. This will fail to compile when a concrete generic type is monomorphized.

Proposed fix
 fn run_connection_thread<N, J>(
     manager_rx: mpsc::UnboundedReceiver<ManagerRequest>,
     nats_client: N,
     js_client: J,
     config: acp_nats::Config,
 ) where
     N: acp_nats::RequestClient
         + acp_nats::PublishClient
         + acp_nats::FlushClient
         + acp_nats::SubscribeClient
         + Clone
         + Send
         + 'static,
-    J: acp_nats::JetStreamPublisher + acp_nats::JetStreamGetStream + Send + 'static,
+    J: acp_nats::JetStreamPublisher + acp_nats::JetStreamGetStream + Clone + Send + 'static,
     trogon_nats::jetstream::JsMessageOf<J>: trogon_nats::jetstream::JsRequestMessage,
 
 async fn process_connections<N, J>(
     mut manager_rx: mpsc::UnboundedReceiver<ManagerRequest>,
     nats_client: N,
     js_client: J,
     config: acp_nats::Config,
 ) where
     N: acp_nats::RequestClient
         + acp_nats::PublishClient
         + acp_nats::FlushClient
         + acp_nats::SubscribeClient
         + Clone
         + Send
         + 'static,
-    J: acp_nats::JetStreamPublisher + acp_nats::JetStreamGetStream + 'static,
+    J: acp_nats::JetStreamPublisher + acp_nats::JetStreamGetStream + Clone + 'static,
     trogon_nats::jetstream::JsMessageOf<J>: trogon_nats::jetstream::JsRequestMessage,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/acp-nats-ws/src/main.rs` around lines 106 - 178, The error
is that both run_connection_thread and process_connections use js_client by
reference but their generic bound for J is missing Clone; update the where
clauses for J in both functions (run_connection_thread and process_connections)
to include Clone (e.g., change J: acp_nats::JetStreamPublisher +
acp_nats::JetStreamGetStream + Send + 'static to J: acp_nats::JetStreamPublisher
+ acp_nats::JetStreamGetStream + Clone + Send + 'static in
run_connection_thread, and add Clone to the J bound in process_connections
similarly) so process_manager_request can clone/passthrough js_client as
required.
🧹 Nitpick comments (1)
rsworkspace/crates/acp-nats-ws/src/transport.rs (1)

943-948: Defensive branch is unreachable for HTTP posts routed via post().

http_post already calls validate_http_context (line 346), which rejects non-initialize messages with a missing connection id before the ManagerRequest is ever sent. This fallback in process_manager_request only fires if a future caller bypasses http_post. Fine as defense-in-depth, but worth a one-line comment so readers don't assume validation is centralized here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/acp-nats-ws/src/transport.rs` around lines 943 - 948, The
branch in process_manager_request that checks if !message.is_initialize() and
returns a BadRequest is unreachable for HTTP posts routed via http_post because
http_post already calls validate_http_context (which rejects non-initialize
messages missing Acp-Connection-Id); add a one-line comment above this defensive
if in process_manager_request explaining that validate_http_context (called by
http_post) enforces this for normal HTTP paths and that this check is only
defense-in-depth for callers that bypass http_post/validate_http_context
(reference process_manager_request, http_post, and validate_http_context to
locate the code).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rsworkspace/crates/acp-nats-ws/src/transport.rs`:
- Around line 787-798: AttachListener currently pushes a new sender into
get_listeners[session_id] but code that prunes dropped senders
(listeners.retain(...)) leaves empty Vec entries and Close doesn't clear maps,
causing a leak; update the logic so after any retain/filter step you check if
the Vec for that session_id is empty and remove the HashMap entry
(get_listeners.remove(&session_id)), and in the handler for
HttpConnectionCommand::Close clear or drain get_listeners and sessions to free
all entries; ensure the retain logic that removes dead senders runs wherever
listeners are cleaned so empty vectors are always evicted.
- Around line 805-855: The outbound-path currently short-circuits when
pending_request is Some(PendingRequest::Live { request_id, sender, .. }) and
continues before dispatching to per-session GET listeners; change the logic so
that for Live pending requests you still check parsed.params_session_id() and
route the frame to get_listeners for that session when the frame's sessionId
does not match the Live request_id (or when the frame has a sessionId for some
other session), while still delivering frames that match request_id to the Live
sender and clearing pending_request; in practice update the PendingRequest::Live
branch in the outbound handling to (1) attempt sender.send(frame.clone()) as
now, (2) if parsed.params_session_id() exists and is not the same as the Live
request's request_id (or parsed.id != request_id), additionally look up
get_listeners.get_mut(&session_id) and retain/send to those listeners, and (3)
only skip the get_listeners dispatch entirely when the frame both was delivered
to the Live sender and its parsed id/session matches the Live request_id.

---

Outside diff comments:
In `@rsworkspace/crates/acp-nats-ws/src/main.rs`:
- Around line 83-86: The doc comment mentions the old field name
`AppState.conn_tx`; update this comment to reference the new field
`AppState.manager_tx` so it accurately describes that dropping `serve` (and the
Router, which owns `AppState.manager_tx`) closes the channel and lets the
connection thread's recv-loop exit and drain active connections before telemetry
teardown; locate the comment near the `serve`/`Router` code in main.rs and
replace `conn_tx` with `manager_tx`.
- Around line 106-178: The error is that both run_connection_thread and
process_connections use js_client by reference but their generic bound for J is
missing Clone; update the where clauses for J in both functions
(run_connection_thread and process_connections) to include Clone (e.g., change
J: acp_nats::JetStreamPublisher + acp_nats::JetStreamGetStream + Send + 'static
to J: acp_nats::JetStreamPublisher + acp_nats::JetStreamGetStream + Clone + Send
+ 'static in run_connection_thread, and add Clone to the J bound in
process_connections similarly) so process_manager_request can clone/passthrough
js_client as required.

---

Nitpick comments:
In `@rsworkspace/crates/acp-nats-ws/src/transport.rs`:
- Around line 943-948: The branch in process_manager_request that checks if
!message.is_initialize() and returns a BadRequest is unreachable for HTTP posts
routed via http_post because http_post already calls validate_http_context
(which rejects non-initialize messages missing Acp-Connection-Id); add a
one-line comment above this defensive if in process_manager_request explaining
that validate_http_context (called by http_post) enforces this for normal HTTP
paths and that this check is only defense-in-depth for callers that bypass
http_post/validate_http_context (reference process_manager_request, http_post,
and validate_http_context to locate the code).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 823b2979-ce85-4a22-a569-6563854ad1c3

📥 Commits

Reviewing files that changed from the base of the PR and between 32deb24 and 0646d6b.

📒 Files selected for processing (3)
  • rsworkspace/crates/acp-nats-ws/src/connection.rs
  • rsworkspace/crates/acp-nats-ws/src/main.rs
  • rsworkspace/crates/acp-nats-ws/src/transport.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • rsworkspace/crates/acp-nats-ws/src/connection.rs

Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs Outdated
Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs Outdated
yordis added 2 commits April 22, 2026 16:28
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
rsworkspace/crates/acp-nats-ws/src/transport.rs (2)

439-473: ⚠️ Potential issue | 🟡 Minor

Echo ACP transport headers on GET SSE responses.

http_get already has both IDs but only sets X-Accel-Buffering; POST SSE paths include Acp-Connection-Id and Acp-Session-Id. Keep GET consistent by calling set_transport_headers.

🐛 Proposed fix
     let mut response = Sse::new(stream::unfold(stream, |mut stream| async move {
         stream
             .recv()
             .await
             .map(|item| (Ok::<Event, Infallible>(item.into_event()), stream))
     }))
     .into_response();
+    set_transport_headers(response.headers_mut(), &connection_id, Some(&session_id));
     response
         .headers_mut()
         .insert(X_ACCEL_BUFFERING_HEADER, HeaderValue::from_static("no"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/acp-nats-ws/src/transport.rs` around lines 439 - 473,
http_get constructs the SSE response but only sets X-Accel-Buffering; update it
to also echo the ACP transport headers by calling set_transport_headers on the
response (use the existing connection_id and session_id values). Locate http_get
and, after creating response via Sse::new(...).into_response(), invoke
set_transport_headers(&mut response, &connection_id, &session_id) (or the
project's equivalent signature) before returning so Acp-Connection-Id and
Acp-Session-Id are included on GET SSE responses.

893-909: ⚠️ Potential issue | 🟠 Major

Do not buffer unrelated session frames into session/new responses.

The Buffered path still captures every outbound frame and skips GET listener dispatch until the matching response arrives. During session/new, notifications for existing sessions can be starved from their GET listeners and incorrectly included in the buffered POST response.

Route frames with params.sessionId for known existing sessions to get_listeners, and only buffer frames that belong to the pending request or have no better session route.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/acp-nats-ws/src/transport.rs` around lines 893 - 909, The
Buffered branch in PendingRequest (the match arm handling
PendingRequest::Buffered { request_id, events, .. }) is currently pushing every
outbound frame into events and delaying GET listener dispatch; change it so
frames that carry params.sessionId for an already-known session (check
parsed.as_ref().and_then(|m| m.params.sessionId) or equivalent) are routed to
get_listeners instead of being pushed into events, and only push into events
when the frame belongs to the pending request (message.id == request_id) or when
no known-session route applies; preserve existing behavior for matching
request_id (keep extracting session_id, inserting into sessions, taking events
and sending response via response.send(Ok(HttpPostOutcome::Buffered { ... })))
but ensure known-session frames are dispatched to get_listeners before
buffering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rsworkspace/crates/acp-nats-ws/src/transport.rs`:
- Around line 166-204: IncomingHttpMessage currently doesn't validate the
"jsonrpc" envelope and allows responses with both result and error; update the
IncomingHttpMessage struct to include a jsonrpc field (e.g., pub jsonrpc:
Option<String>), then in IncomingHttpMessage::parse after deserializing check
that jsonrpc == Some("2.0") and return an HttpTransportError::BadRequest for any
other/missing value, and update IncomingHttpMessage::is_response to require
exactly one of result or error (i.e., result.is_some() != error.is_some()) so
responses with both fields are rejected.
- Around line 497-541: The Content-Type check in validate_post_headers currently
does an exact string compare and rejects parameterized types like
"application/json; charset=utf-8"; change validate_post_headers to strip
media-type parameters (split on ';' and trim, case-insensitive) before comparing
or reuse the existing accept_contains logic to test that the media type equals
"application/json" so parameterized Content-Type headers are accepted; update
the branch that returns HttpTransportError::UnsupportedMediaType accordingly
while keeping the same error message.

---

Duplicate comments:
In `@rsworkspace/crates/acp-nats-ws/src/transport.rs`:
- Around line 439-473: http_get constructs the SSE response but only sets
X-Accel-Buffering; update it to also echo the ACP transport headers by calling
set_transport_headers on the response (use the existing connection_id and
session_id values). Locate http_get and, after creating response via
Sse::new(...).into_response(), invoke set_transport_headers(&mut response,
&connection_id, &session_id) (or the project's equivalent signature) before
returning so Acp-Connection-Id and Acp-Session-Id are included on GET SSE
responses.
- Around line 893-909: The Buffered branch in PendingRequest (the match arm
handling PendingRequest::Buffered { request_id, events, .. }) is currently
pushing every outbound frame into events and delaying GET listener dispatch;
change it so frames that carry params.sessionId for an already-known session
(check parsed.as_ref().and_then(|m| m.params.sessionId) or equivalent) are
routed to get_listeners instead of being pushed into events, and only push into
events when the frame belongs to the pending request (message.id == request_id)
or when no known-session route applies; preserve existing behavior for matching
request_id (keep extracting session_id, inserting into sessions, taking events
and sending response via response.send(Ok(HttpPostOutcome::Buffered { ... })))
but ensure known-session frames are dispatched to get_listeners before
buffering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d57b3219-21ca-4796-ab6f-39d4df2ed62f

📥 Commits

Reviewing files that changed from the base of the PR and between 0646d6b and bb6d8a0.

📒 Files selected for processing (1)
  • rsworkspace/crates/acp-nats-ws/src/transport.rs

Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs
Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs
Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs
Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs
yordis added 3 commits April 22, 2026 16:49
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs
Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs
yordis added 2 commits April 22, 2026 17:36
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis yordis removed the rust:coverage-baseline-reset Relax Rust coverage gate to establish a new baseline label Apr 22, 2026
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
yordis added 4 commits April 22, 2026 18:05
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rsworkspace/crates/acp-nats-ws/src/main.rs (1)

102-157: ⚠️ Potential issue | 🟠 Major

Add Clone to the J trait bounds in both run_connection_thread and process_connections.

process_connections passes &js_client to transport::process_manager_request (line 164), which requires J: Clone + 'static (transport.rs line 1394). However, process_connections bounds J without Clone (line 156), creating a trait mismatch that will prevent compilation. The same issue applies to run_connection_thread (line 115), which passes J through to process_connections.

Additionally, the doc comment at lines 99–101 states that "All WebSocket connections are processed here" due to the ?Send trait, but the function now also handles HTTP connections. Update the comment to reflect both connection types.

♻️ Proposed fix
-    J: acp_nats::JetStreamPublisher + acp_nats::JetStreamGetStream + Send + 'static,
+    J: acp_nats::JetStreamPublisher + acp_nats::JetStreamGetStream + Clone + Send + 'static,

(apply to both run_connection_thread at line 115 and process_connections at line 156)

Update the doc comment to indicate both WebSocket and HTTP connections are handled here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/acp-nats-ws/src/main.rs` around lines 102 - 157, Add Clone
to the generic bound for J in both run_connection_thread and process_connections
(so J: acp_nats::JetStreamPublisher + acp_nats::JetStreamGetStream + Clone +
Send + 'static) because process_connections passes &js_client into
transport::process_manager_request which requires J: Clone; update the
corresponding doc comment above run_connection_thread to say it handles both
WebSocket and HTTP connections (not only WebSocket) to reflect the current
behavior.
🧹 Nitpick comments (3)
rsworkspace/crates/acp-nats-ws/src/transport.rs (2)

1473-1510: Minor: silent drop on HTTP command dispatch failure surfaces as Internal instead of NotFound.

For HttpGet (line 1491-1494) and implicitly HttpDelete (line 1506-1509), when command_tx.send(...) fails, the response oneshot is consumed into the SendError and discarded, so the caller's response_rx.await resolves to a RecvError which is mapped to HttpTransportError::Internal("connection manager dropped the request") in http_get / http_delete. The actual condition here is a dead HTTP connection task (stale handle not yet pruned), i.e. it should read as NotFound("unknown ACP connection") — matching the "removed" cleanup on the very next line.

Consider extracting the response before the send so you can emit a typed NotFound on failure, which also makes the HttpPost branch (line 1460-1471) more consistent.

♻️ Sketch for the HttpGet branch
-            if handle
-                .command_tx
-                .send(HttpConnectionCommand::AttachListener {
-                    session_id,
-                    protocol_version,
-                    response,
-                })
-                .is_err()
-            {
-                http_connections.remove(&connection_id);
-            }
+            if let Err(err) = handle.command_tx.send(HttpConnectionCommand::AttachListener {
+                session_id,
+                protocol_version,
+                response,
+            }) {
+                http_connections.remove(&connection_id);
+                if let HttpConnectionCommand::AttachListener { response, .. } = err.0 {
+                    let _ = response.send(Err(HttpTransportError::not_found(
+                        "unknown ACP connection",
+                    )));
+                }
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/acp-nats-ws/src/transport.rs` around lines 1473 - 1510,
The send failure currently consumes the response oneshot and causes callers to
observe an Internal error; in ManagerRequest::HttpGet (and similarly in
ManagerRequest::HttpDelete) extract the response oneshot out of the
HttpGet/HttpDelete match arm before calling
handle.command_tx.send(HttpConnectionCommand::AttachListener { ... response })
so you can detect a SendError and explicitly reply with
response.send(Err(HttpTransportError::not_found("unknown ACP connection"))) (and
then remove the stale handle from http_connections), ensuring a NotFound is
returned instead of losing the oneshot and surfacing Internal; apply the same
pattern to the HttpDelete branch when sending HttpConnectionCommand::Close.

316-338: Double JSON parse for has_result/has_error projection.

parse deserializes the body twice: once to serde_json::Value to read the result/error keys, and again via from_value to populate Self. For request/notification-heavy traffic this is a measurable tax. You can do one pass by deriving has_result/has_error from Option<Value> fields with #[serde(default)], or by projecting from the single Value round-trip manually.

rsworkspace/crates/acp-nats-ws/src/main.rs (1)

99-101: Stale doc comment — this thread now also hosts HTTP transport connections.

The comment still claims "All WebSocket connections are processed here", but process_manager_request now also dispatches HttpPost / HttpGet / HttpDelete and spawns run_http_connection on the same LocalSet (also why tokio::task::spawn_local continues to be required). Tighten the wording so future readers aren't misled about what lives on this runtime.

♻️ Proposed fix
-/// Runs a single-threaded tokio runtime with a
-/// `LocalSet`. All WebSocket connections are processed here because the ACP
-/// `Agent` trait is `?Send`, requiring `spawn_local` / `Rc`.
+/// Runs a single-threaded tokio runtime with a `LocalSet`. All ACP
+/// connections (WebSocket and streamable HTTP) are processed here because
+/// the ACP `Agent` trait is `?Send`, requiring `spawn_local` / `Rc`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/acp-nats-ws/src/main.rs` around lines 99 - 101, Update the
stale doc comment that says "All WebSocket connections are processed here" to
reflect that this single-threaded tokio runtime / LocalSet also hosts HTTP
transport connections; mention that process_manager_request dispatches
HttpPost/HttpGet/HttpDelete and spawns run_http_connection on the same LocalSet,
which is why spawn_local (and Rc) is still required for the ACP Agent trait
being ?Send. Locate the comment above the runtime setup (the LocalSet creation
and spawn_local usage) and change the wording to encompass both WebSocket and
HTTP connection handling and the reason spawn_local is needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@rsworkspace/crates/acp-nats-ws/src/main.rs`:
- Around line 102-157: Add Clone to the generic bound for J in both
run_connection_thread and process_connections (so J:
acp_nats::JetStreamPublisher + acp_nats::JetStreamGetStream + Clone + Send +
'static) because process_connections passes &js_client into
transport::process_manager_request which requires J: Clone; update the
corresponding doc comment above run_connection_thread to say it handles both
WebSocket and HTTP connections (not only WebSocket) to reflect the current
behavior.

---

Nitpick comments:
In `@rsworkspace/crates/acp-nats-ws/src/main.rs`:
- Around line 99-101: Update the stale doc comment that says "All WebSocket
connections are processed here" to reflect that this single-threaded tokio
runtime / LocalSet also hosts HTTP transport connections; mention that
process_manager_request dispatches HttpPost/HttpGet/HttpDelete and spawns
run_http_connection on the same LocalSet, which is why spawn_local (and Rc) is
still required for the ACP Agent trait being ?Send. Locate the comment above the
runtime setup (the LocalSet creation and spawn_local usage) and change the
wording to encompass both WebSocket and HTTP connection handling and the reason
spawn_local is needed.

In `@rsworkspace/crates/acp-nats-ws/src/transport.rs`:
- Around line 1473-1510: The send failure currently consumes the response
oneshot and causes callers to observe an Internal error; in
ManagerRequest::HttpGet (and similarly in ManagerRequest::HttpDelete) extract
the response oneshot out of the HttpGet/HttpDelete match arm before calling
handle.command_tx.send(HttpConnectionCommand::AttachListener { ... response })
so you can detect a SendError and explicitly reply with
response.send(Err(HttpTransportError::not_found("unknown ACP connection"))) (and
then remove the stale handle from http_connections), ensuring a NotFound is
returned instead of losing the oneshot and surfacing Internal; apply the same
pattern to the HttpDelete branch when sending HttpConnectionCommand::Close.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49f98959-ddb5-4bf0-9230-9f1ec5445b25

📥 Commits

Reviewing files that changed from the base of the PR and between bb6d8a0 and f5f83ff.

📒 Files selected for processing (4)
  • rsworkspace/crates/acp-nats-ws/README.md
  • rsworkspace/crates/acp-nats-ws/src/constants.rs
  • rsworkspace/crates/acp-nats-ws/src/main.rs
  • rsworkspace/crates/acp-nats-ws/src/transport.rs
✅ Files skipped from review due to trivial changes (1)
  • rsworkspace/crates/acp-nats-ws/src/constants.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • rsworkspace/crates/acp-nats-ws/README.md

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Comment thread rsworkspace/crates/acp-nats-ws/src/transport.rs
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis yordis added the rust:coverage-baseline-reset Relax Rust coverage gate to establish a new baseline label Apr 23, 2026
yordis added 3 commits April 22, 2026 22:05
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8805ee2. Configure here.


fn is_initialize(&self) -> bool {
self.method_name() == Some("initialize")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initialize notification creates orphaned HTTP connection resource leak

Medium Severity

is_initialize() only checks the method name without verifying the message is a request (has an id). An initialize notification (no id) passes both validate_http_context and process_manager_request's is_initialize() gate, spawning a new run_http_connection task. Since the notification isn't a request, run_http_connection returns HttpPostOutcome::Accepted (202) with no Acp-Connection-Id header, so the client can never address or DELETE the connection. The orphaned task persists until server shutdown, leaking resources.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8805ee2. Configure here.

@yordis yordis merged commit 3a70d78 into main Apr 23, 2026
7 checks passed
@yordis yordis deleted the yordis/acp-websocket-spec branch April 23, 2026 02:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rust:coverage-baseline-reset Relax Rust coverage gate to establish a new baseline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant