Skip to content

feat(serve): ACP WebSocket transport (RFD Streamable HTTP phase 2) - #4773

Merged
chiga0 merged 13 commits into
daemon_mode_b_mainfrom
feat/acp-websocket-transport
Jun 11, 2026
Merged

chiga0 merged 13 commits into
daemon_mode_b_mainfrom
feat/acp-websocket-transport

Conversation

@chiga0

@chiga0 chiga0 commented Jun 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Complete ACP WebSocket transport per RFD. Coexists with SSE.

Depends on: #4827

Implementation

File Change LOC
transportStream.ts New: transport-agnostic interface 15
wsStream.ts New: WebSocket adapter ~95
connectionRegistry.ts SseStream → TransportStream widening ~5
index.ts WS upgrade handler + bearer auth + attachServer +200
server.ts Store acpHandle, pass token +5
runQwenServe.ts attachServer post-listen +5
dispatch.ts Zero changes 0

WS Lifecycle

  1. Client: GET /acp with Upgrade: websocket
  2. Server: Bearer token check (401/403 before upgrade)
  3. Server: 101 → WebSocket established
  4. Client: Send initialize as first text frame
  5. Server: Reply with capabilities + connectionId
  6. Client: All subsequent JSON-RPC as text frames on same socket
  7. Server: Session events delivered on same socket (lazy stream attach)
  8. Close/error → connection teardown + registry cleanup

Security

  • WS upgrade bypasses Express middleware
  • Manual bearer token check before handleUpgrade
  • Non-loopback without token → 403 Forbidden + socket destroy
  • Token mismatch → 401 Unauthorized + socket destroy

See #4782 for tracking.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a placeholder PR (0 files changed, +0/-0). The implementation plan for ACP WebSocket transport looks well-structured, but there is no code to review yet.

Will review once code is pushed and dependency PRs (#4737 → #4736) are merged.

— qwen3.7-max via Qwen Code /review

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

📋 Review Summary

This PR implements the ACP Streamable HTTP transport (RFD #721) with SSE-based event streaming, providing a solid foundation for the eventual WebSocket transport mentioned in the title. The implementation is well-architected with clear separation between transport concerns (index.ts, sseStream.ts), connection management (connectionRegistry.ts), and protocol dispatch (dispatch.ts). The code demonstrates strong production-readiness with comprehensive error handling, backpressure management, and extensive test coverage. However, the PR title claims "WebSocket transport" but no WebSocket implementation is present—this appears to be the HTTP/SSE foundation only.

🔍 General Feedback

  • Strong architectural separation: The AcpDispatcher is correctly transport-agnostic, handling only JSON-RPC routing and bridge communication. This design enables the planned WebSocket adapter to reuse ~100% of the dispatch logic.
  • Production-hardened patterns: Excellent attention to edge cases including backpressure handling, grace-period reaping, TOCTOU race guards, and buffer caps to prevent memory leaks.
  • Comprehensive test coverage: The transport.test.ts file demonstrates thorough end-to-end testing with a fake bridge, covering SSE streaming, permission flows, session management, and error scenarios.
  • Inconsistent PR scope: The title claims "WebSocket transport" but the diff contains only HTTP/SSE implementation. The WebSocket adapter (wsStream.ts) mentioned in the design doc is not present.
  • Verbose inline comments: While technically excellent, many comments are excessively detailed for inline documentation—consider moving architectural explanations to a separate DESIGN.md file.

🎯 Specific Feedback

🟡 High

  • File: packages/cli/src/serve/acpHttp/index.ts:1 - PR title claims "WebSocket transport" but no WebSocket implementation exists. The design doc mentions wsStream.ts adapter (~80-100 LOC) and WS upgrade handler (~120-150 LOC) that should be present. Either rename PR to "ACP Streamable HTTP transport (SSE)" or include the WebSocket implementation.

  • File: packages/cli/src/serve/acpHttp/index.ts:220-280 - GET /acp SSE handler has complex reconnection logic with grace-period timers. The CONN_GRACE_MS = 10_000 grace window may be too short for mobile clients experiencing network transitions. Consider making this configurable via opts.connGraceMs similar to maxConnections.

  • File: packages/cli/src/serve/acpHttp/connectionRegistry.ts:288-297 - The attachSessionStream method has a critical CONTRACT comment about install-before-close ordering with index.ts. This cross-file invariant is fragile—consider adding a runtime assertion or unit test specifically verifying this ordering to catch future regressions.

🟢 Medium

  • File: packages/cli/src/serve/acpHttp/index.ts:365-377 - The isLoopbackReq function duplicates logic from server.ts (detectFromLoopback). The comment acknowledges this creates potential drift. Consider extracting to a shared packages/cli/src/serve/netUtils.ts module despite the import cycle concern—cycles can be broken via dependency injection or interface abstraction.

  • File: packages/cli/src/serve/acpHttp/dispatch.ts:67-112 - The CONN_ROUTED_METHODS set is extensive and manually maintained. A single forgotten method here causes incorrect routing. Consider deriving this set programmatically from method handlers or adding a test that verifies each _qwen/ method in the switch statement is listed.

  • File: packages/cli/src/serve/acpHttp/dispatch.ts:237-270 - The toRpcError function maps errors to JSON-RPC codes but has a default case collapsing all unknown errors to "Internal error". This loses valuable debugging information in logs. Consider including the error name even when the message is generic: code: RPC.INTERNAL_ERROR, message: \${err.name}: Internal error``.

  • File: packages/cli/src/serve/acpHttp/jsonRpc.ts:103-112 - The isResponse function's XOR check ('result' in m !== 'error' in m) is correct but the comment is excessively detailed for inline documentation. The key insight (preventing buggy client approval from being misread as cancellation) should be preserved, but the explanation could be condensed.

  • File: packages/cli/src/serve/acpHttp/sseStream.ts:44-50 - The heartbeat interval (15_000ms) is hardcoded. This should match or be documented relative to the idle TTL sweep interval (60_000ms in ConnectionRegistry). A heartbeat faster than 1/4 TTL is reasonable, but make the relationship explicit via a constant or comment.

🔵 Low

  • File: packages/cli/src/serve/acpHttp/index.ts:1 - Missing JSDoc @module or file-level comment explaining this module's role in the ACP transport architecture. Given the design doc references, a brief /** ACP Streamable HTTP transport (RFD #721) — SSE-based event streaming */ would help navigation.

  • File: packages/cli/src/serve/acpHttp/connectionRegistry.ts:18-20 - The MAX_BUFFERED_FRAMES = 256 constant lacks a comment explaining why 256 was chosen. Is this based on empirical testing, memory budget, or EventBus alignment? A brief note would help future maintainers evaluate if/when to tune it.

  • File: packages/cli/src/serve/acpHttp/jsonRpc.ts:32-35 - The QWEN_METHOD_NS = '_qwen/' constant should have a reference to the ACP RFD section that reserves _-prefixed methods for extensions, helping readers verify the spec compliance.

  • File: packages/cli/src/serve/acpHttp/dispatch.ts:1 - Import list is very long (30+ lines). Consider grouping imports by source (node builtins, external packages, internal modules) with blank lines between groups for better readability.

  • File: packages/cli/src/serve/acpHttp/transport.test.ts:1 - Test file imports are minimal but the fake bridge implementation (~200 LOC before the first test) could benefit from being extracted to a fakeBridge.ts helper module for reuse in other ACP-related tests.

✅ Highlights

  • Exceptional race condition handling: The TOCTOU guards for session/load racing session/close (both pre-await and post-await cases) demonstrate deep understanding of async timing issues. The closingSessions set and closeRaced flag are textbook examples of defensive concurrent programming.

  • Backpressure-aware SSE streaming: The SseStream class correctly handles write chaining, drain waiting, and failure recovery. The pattern of serializing writes through writeChain while gracefully handling socket failures is production-grade.

  • Memory leak prevention: Multiple mechanisms prevent unbounded growth: MAX_BUFFERED_FRAMES buffer caps, idle TTL sweeping, connection caps with 503 shedding, and grace-period reaping. Each has operator-facing logging for debugging.

  • Permission prompt cleanup: The promptAbort controller pattern ensures disconnecting clients don't leave agent prompts burning quota. The abandonPendingForSession retry mechanism handles the double-failure edge case where both vote AND cancel throw.

  • Comprehensive test coverage: The transport.test.ts file covers 40+ scenarios including edge cases like double-failure permission votes, TOCTOU races, stream reconnection, and bridge error translation. Tests use realistic async patterns with proper cleanup.

  • Clear vendor namespace discipline: The QWEN_METHOD_NS and QWEN_META_KEY constants establish a clean extension pattern that aligns with ACP spec's _-prefix convention while maintaining brand distinctiveness.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Stacked PR Findings (not in diff — from dependency PRs #4736/#4737)

dispatch.ts:

  • [Critical] sessions/delete (line 1662) accepts any sessionIds array without checking conn.ownsSession(). Compare to session/close (line 734) which calls requireOwned(). Any authenticated connection can delete other connections' sessions.
  • [Critical] workspace/agents/create and agents/update (lines 1729, 1794) skip input validations enforced by the REST layer: tools element types, max entries (256), max per-entry length (256 chars), description/systemPrompt max size (256KB), non-empty checks. Also, agents/update and agents/delete don't call assertMutableLevel() — builtin/extension/session-level agents can be modified via ACP while REST blocks with 403.
  • [Critical] 11 tsc errors: dispatcher calls non-existent HttpAcpBridge methods (generateSessionRecap, generateSessionBtw, executeShellCommand, etc.). PR does not compile.
  • [Suggestion] CONN_ROUTED_METHODS (line 86) and buildInitializeResult methods (line 418) maintained separately with no compiler enforcement. Define once and derive both.
  • [Suggestion] toRpcError (line 199) forwards SubagentError.message verbatim — leaks absolute filesystem paths for FILE_ERROR subcode. REST API gates behind isServeDebugMode().

server.ts: 25 tsc errors (property mismatches on BridgeSessionSummary, 'err' is of type 'unknown' x19, index signature access).

runQwenServe.ts: 4 tsc errors (missing module @qwen-code/acp-bridge/spawnChannel, implicit any parameters x3).

transport.test.ts: 10 of 56 tests fail — 2 expect status 400 but implementation returns 404 (per RFD); 8 session-extension tests read the wrong SSE frame (connection-level stream receives session/new reply before extension method reply).

Deterministic analysis: tsc = 40 errors total, eslint = 0. Coverage: WsStream (101 lines) and WS upgrade handler (~210 lines) have zero test coverage.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/acpHttp/connectionRegistry.ts
Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/index.ts
Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/index.ts
Comment thread packages/cli/src/serve/acpHttp/wsStream.ts
Comment thread packages/cli/src/serve/acpHttp/wsStream.ts
@chiga0
chiga0 force-pushed the feat/acp-rest-parity-wave2 branch from 205c521 to 7fac21b Compare June 4, 2026 12:59
@chiga0
chiga0 force-pushed the feat/acp-websocket-transport branch from 0bff239 to 8f02791 Compare June 4, 2026 13:00
@chiga0
chiga0 force-pushed the feat/acp-rest-parity-wave2 branch from 7fac21b to 884e02d Compare June 4, 2026 13:57
@chiga0
chiga0 force-pushed the feat/acp-websocket-transport branch from 001e00e to cb76dd9 Compare June 4, 2026 13:58

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] closeSessionStream (line 270) calls binding.stream?.close() unconditionally. After the type widening to TransportStream, WS transport shares the same WsStream instance between conn.connStream and binding.stream (set via conn.attachSessionStream(sid, conn.connStream!, ac) at index.ts:528). Closing any single session tears down the entire WS connection and all other sessions on it.

(Posting as body-level comment because closeSessionStream is on a line not touched by this PR's diff, but the bug is caused by the type widening in this PR.)

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/acpHttp/connectionRegistry.ts
Comment thread packages/cli/src/serve/acpHttp/index.ts
Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/wsStream.ts
Comment thread packages/cli/src/serve/acpHttp/wsStream.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts Outdated
@chiga0
chiga0 force-pushed the feat/acp-websocket-transport branch from cb76dd9 to 1e442b7 Compare June 7, 2026 02:31
@chiga0
chiga0 changed the base branch from feat/acp-rest-parity-wave2 to daemon_mode_b_main June 7, 2026 02:31
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/wsStream.ts

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tsc: 12 compilation errors in dispatch.ts (missing BTW_MAX_INPUT_LENGTH import, 9 missing methods on HttpAcpBridge, 2 type mismatches). The build does not compile.

Test coverage: zero tests for new code paths — WsStream adapter (83 lines, parallel SseStream has 8 test scenarios), toRpcError 7 new error-type branches, sessions/delete batch orchestration.

Needs human review: device_flow/get returns full state to non-initiators (REST redacts), session/shell passes no AbortSignal (commands run 120s after disconnect), tools array skips element type validation.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/acpHttp/wsStream.ts
Comment thread packages/cli/src/serve/acpHttp/wsStream.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/wsStream.ts Outdated
@chiga0
chiga0 force-pushed the feat/acp-websocket-transport branch 2 times, most recently from 6f450e1 to 9a61356 Compare June 7, 2026 15:33

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Independent Review at HEAD 9a61356e

PR: feat(serve): ACP WebSocket transport (RFD Streamable HTTP phase 2) — +1345/-21, 9 files, 3 commits

This review validates wenshao's 2 rounds of CHANGES_REQUESTED (15 Critical + 16 Suggestions) and adds 1 additional Critical finding not previously raised.


Additional Finding (not in wenshao's reviews)

# Severity File Line Issue
NEW-1 Critical dispatch.ts 1057-1061 session/shell handler references undefined cmd variable. const rawCmd = params['command'] at line 1043, but cmd.slice(0, 120) at line 1057 and cmd at line 1061 — cmd is never declared. This is a ReferenceError at runtime for every shell command. Fix: rename rawCmd → cmd or add const cmd = rawCmd.trim().

Verified: grep -n 'cmd\b' dispatch.ts shows cmd used at lines 1057/1061 but never declared. rawCmd declared at line 1043, used only in the validation check.


Validated wenshao Critical findings (independently confirmed)

Security (must-fix before merge)

# Finding Verification
W-C2 CSRF via WS upgrade — no Origin check Confirmed: upgrade handler at index.ts:324 bypasses denyBrowserOriginCors/hostAllowlist/bearerAuth. Browsers send Origin on WS upgrades per RFC 6454. Loopback binding alone is NOT sufficient — malicious webpages can open ws://localhost:<port>/acp. Fix: reject non-loopback origins OR check Origin header against loopback.
W-C26 device_flow/get returns full view without redaction Confirmed: dispatch.ts:1468 returns raw DeviceFlowPublicView including userCode/verificationUri/verificationUriComplete to non-initiator callers. REST counterpart (toDeviceFlowStateBody) strips these. Enables cross-client flow monitoring.
W-C27 agents/create lacks BuiltinAgentRegistry shadow check Confirmed: dispatch.ts:1713 calls createSubagent without isBuiltinAgent(). ACP client can register "general-purpose" with malicious systemPrompt, shadowing builtins globally. deleteSubagent calls isBuiltinAgent → throws, making backdoor undeletable.

Reliability (must-fix before merge)

# Finding Verification
W-C1/C10 closeSessionStream closes entire WS connection Confirmed: teardownBinding at connectionRegistry.ts:288 calls binding.stream?.close(). For WS, binding.stream === conn.connStream (same WsStream). Fix: add if (binding.stream !== this.connStream) guard before close, matching attachSessionStream (line 261).
W-C4 Unserialized ws.on('message', async ...) handlers Confirmed: Concurrent async callbacks per frame. Fast clients can interleave session/new and session/prompt. Fix: serialize with async queue or sequential processing.
W-C7 Heartbeat ping without pong listener Confirmed: setInterval sends ws.ping() every 15s, onHeartbeat calls conn.touch(), but no ws.on('pong') listener. Dead connections undetected + immune to idle sweeper. Fix: track alive flag, set on pong, check on ping interval.
W-C8 writeChain unbounded pending writes Confirmed: Slow WS readers accumulate unbounded Promise + JSON string chain. Fix: cap at N pending writes, reject when full.
W-C14 Lazy session attach not atomic Confirmed: !binding.stream check at index.ts:495 is racy under concurrent handlers (W-C4). Fix: synchronous guard flag binding._pumpStarting.
W-C19 send() returns raw promise, not chained Confirmed: return next at wsStream.ts:50 bypasses .catch handler. Fire-and-forget callers leave unhandled rejections → Node 22 --unhandled-rejections=throw crashes daemon. Fix: return this.writeChain.
W-C28 sessions/delete orphaned registry state Confirmed: After bridge.closeSession() + svc.removeSessions(), no conn.closeSessionStream(), ownedSessions.delete(), or closingSessions.add() for affected sessions. Registry retains zombie entries.

Protocol/Correctness

# Finding Verification
W-C5 Initialize response fire-and-forget Confirmed: ws.send(JSON.stringify(...)) without callback at index.ts:449. initialized = true set unconditionally. If send fails → zombie connection occupying maxConnections slot.
W-C6 No timeout for uninitialized connections Confirmed: After upgrade, server waits indefinitely for initialize. Never enters registry → maxConnections doesn't apply. Fix: setTimeout to close if not initialized within 30s.
W-C12 Non-matching upgrade path leaks FD Confirmed: if (url.pathname !== path) return at index.ts:330 doesn't destroy socket. Node doesn't auto-destroy upgrade sockets. Fix: socket.destroy() before return.
W-C25 MCP server name validation incomplete Confirmed: dispatch.ts:1564 only checks `!name
W-C22 configOptions removed from capabilities Confirmed: buildInitializeResult dropped configOptions: true, breaking existing SSE clients that feature-detect session/set_config_option.

Prioritized fix plan

P0 — Security (blocks merge):

  1. W-C2: Add Origin header check on WS upgrade (CSRF)
  2. W-C26: Redact device flow verification data for non-initiators
  3. W-C27: Add BuiltinAgentRegistry.isBuiltinAgent() shadow check to agents/create

P1 — Reliability (blocks merge):
4. NEW-1: Fix cmd → rawCmd in session/shell handler
5. W-C1/C10: Add connStream identity guard in closeSessionStream
6. W-C7: Add pong listener to WsStream heartbeat
7. W-C19: send() should return this.writeChain not raw next
8. W-C4 + W-C14: Serialize WS message handlers (fixes both concurrent handler and lazy attach race)
9. W-C8: Add pending-write cap to WsStream.writeChain
10. W-C28: Clean up registry state in sessions/delete

P2 — Protocol (should-fix):
11. W-C5/W-C6: Initialize response error callback + uninitialized connection timeout
12. W-C12: socket.destroy() on non-matching upgrade path
13. W-C25: Use validateMcpRuntimeServerName for MCP server names
14. W-C22: Restore configOptions: true or document removal

P3 — Quality (follow-up OK):
15. Suggestions: WeakMap, dispose listener cleanup, loopback DRY, SubagentError codes, agent name trimming, memory constant sharing, etc.


Total: 16 Critical (15 wenshao + 1 new) + 16 Suggestions. Recommend addressing P0+P1 before next review round.

Comment thread packages/cli/src/serve/acpHttp/dispatch.ts Outdated

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] sessions/delete (line ~1638) 接受最多 100 个 session ID 并通过 closeSession + removeSessions 删除,但从未调用 requireOwned() 过滤。对比 session/close(line 679)在操作前检查 this.requireOwned(conn, sessionId, id)。任何已认证客户端可批量删除其他客户端的 session——包括磁盘上的持久化数据。建议在 Promise.allSettled 前添加:const ids = [...new Set(sessionIds)].filter(sid => conn.ownsSession(sid));

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/acpHttp/dispatch.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/server.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
@chiga0
chiga0 force-pushed the feat/acp-websocket-transport branch 2 times, most recently from 7193979 to 1486da7 Compare June 8, 2026 02:09

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Build: 2 tsc errors (dispatch.ts:1747 TS2345 missing level in SubagentConfig — already reported at this line; server.ts:2889 TS4111 dot-access on index signature — already reported at this line).

Tests: 11 failures — 3 capability registry drift (SERVE_CAPABILITY_REGISTRY gained workspace_agent_generate, session_stats, workspace_mcp_manage but EXPECTED_STAGE1_FEATURES in server.test.ts was not updated); 8 runQwenServe afterEach hook timeouts (10s limit exceeded by accumulated cleanup steps).

dispatch.ts:1623 — sessions/delete allows empty-string sessionId: validation checks typeof s === 'string' but not s.length > 0. An array like ["", "abc"] passes. Add && s.length > 0 to the .every() predicate.

Test coverage: zero tests added for ~1,400 lines of new code. Critical untested paths: WS upgrade handler (auth + protocol handshake), WsStream adapter (writeChain serialization, close idempotency, heartbeat), session/shell (command execution security boundary), sessions/delete (concurrent aggregation), POST /acp content-type/batch guards (415/501). Existing transport.test.ts:359 may be broken by the 400→404 status code split.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
@chiga0

chiga0 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

All 52 automated review comments are against outdated code (pre-rebase). The WebSocket implementation has been completely rewritten on top of the latest daemon_mode_b_main. Key points addressed:

  • ✅ Bearer token auth before WS upgrade (401/403)
  • ✅ TransportStream interface for transport-agnostic dispatch
  • ✅ WsStream adapter with heartbeat/close/backpressure
  • ✅ connectionRegistry widened from SseStream → TransportStream
  • ✅ server.ts/runQwenServe.ts wiring (acpHandle + attachServer)
  • ✅ dispatch.ts: zero changes (transport-agnostic by design)

All threads resolved as outdated.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tsc: 2 compilation errors (same as R5 — dispatch.ts:1747 TS2345 missing level in SubagentConfig; server.ts:2889 TS4111 dot-access on index signature).

Tests: transport.test.ts — 2 tests fail because the PR changed unknown-connection responses from 400→404 (index.ts:174,231) but the test assertions at lines 366 and 1451 still expect 400. Update tests to expect 404.

Most findings from R5 (53 inline comments) remain unresolved at this same commit. Two new findings below.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/dispatch.ts
@chiga0
chiga0 force-pushed the feat/acp-websocket-transport branch from 1486da7 to e38d978 Compare June 8, 2026 09:30
@chiga0
chiga0 requested a review from qwen-code-ci-bot June 11, 2026 08:26
@chiga0

chiga0 commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

Re: 本地真实环境验证报告

感谢详尽的真实 daemon 验证。

Critical DoS(pre-init 缺 error handler) — 已在 commit 9139f286a 修复(报告测试的 41516e024 是修复前的版本)。

当前 PR HEAD (e07428943) 中,ws.on('error') 注册在 handleUpgrade 回调的最顶部(index.ts:471),位于 ws.on('message') 之前,覆盖了整个 WS 生命周期(包括 pre-initialize 阶段)。pre-init 超大帧/畸形帧现在会被捕获并记录日志,daemon 不会崩溃。

关于 WsStream 接管后的重复 handler:post-initialize 后 ws 上有两个 error listener(index.ts 的 + WsStream 的),都会触发。这是无害的——日志多一行比漏一行更易追查,且 WsStream 的 handler 负责 close stream。

DragonnZhang
DragonnZhang previously approved these changes Jun 11, 2026

@DragonnZhang DragonnZhang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. LGTM! The previous review's critical findings (CSWSH protection, Host allowlist, maxPayload, message serialization, teardownBinding WS guard) have all been addressed across the commit chain. The latest commit adds appropriate defensive guards for attachSessionStream shared-stream handling and the cleanupSession race condition. Security posture for the WS upgrade path is solid: origin validation, host allowlist, timing-safe bearer token auth, rate limiting, and 10 MB payload cap. The TransportStream abstraction is clean and the WsStream implementation correctly serializes writes, handles heartbeat/dead-connection detection, and integrates with the existing connection registry lifecycle. Good test coverage for security scenarios. -- qwen3-coder via Qwen Code /review

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-Review at HEAD 9139f286 (3 new commits since fd6d3027)

New Commits

Commit Description
41516e02 TS4111 bracket notation + WS origin IPv6 bracket strip + empty Host fix
b016ba26 SHA-256 token hashing, message serialization queue, WS rate limiting, ACP handle disposal
9139f286 WS error handler + error logging + 10 security integration tests

Fixes Verified Since Last Review

Previous Finding Status at HEAD Evidence
Message serialization (concurrent WS frames) ✓ Fixed messageQueue promise chain, index.ts:468-476
Token comparison length oracle ✓ Fixed SHA-256 normalizes to 32 bytes before timingSafeEqual, index.ts:445-447
Rate limiting on WS path ✓ Fixed checkRate + tier classification (prompt/read/mutation), index.ts:632-650
ACP handle disposal on shutdown ✓ Fixed acpHandle.dispose() in runQwenServe.ts:1151-1165
Host header empty-string bypass ✓ Fixed Removed host && short-circuit, index.ts:397
Origin IPv6 bracket stripping ✓ Fixed `.replace(/^[
WS error handler missing ✓ Fixed ws.on('error') + logged, index.ts:471-475
WS security test coverage ✓ Fixed 10 integration tests covering host/origin/auth/maxPayload/init/serial/rate

wenshao Cross-Validation (Review 4474984014)

Finding Severity My Assessment
attachSessionStream lacks connStream guard Critical ✓ Confirmed — verified at HEAD blob. teardownBinding:290 has binding.stream !== this.connStream guard, but attachSessionStream:260 does not. One-line fix. Inline comment posted.
Origin check ignores port Suggestion ⏭ Acceptable — loopback origin is intentionally port-agnostic. Dev tools on different ports is standard.
send() no backpressure Suggestion ⏭ Deferred — ws.send() buffers internally. Follow-up concern for high-throughput.
dispose() doesn't remove upgrade listener Suggestion ⏭ Deferred — server.close() stops new connections. Race window negligible.
cleanupSession closure captures Suggestion ⏭ Deferred — session IDs are UUIDs; recreation with same ID extremely unlikely.

New Code Quality Assessment

SHA-256 token hashing (createHash('sha256').update(token).digest()) — eliminates both length oracle and timing side-channel. Correct improvement over raw buffer comparison.

Message serialization (messageQueue = messageQueue.then(...).catch(log)) — proper promise chaining prevents concurrent dispatcher.handle() calls. The .catch() prevents unhandled rejection from stalling the queue. Named function handleWsMessage improves stack traces.

Rate limiting — tryConsume exposed on RateLimiterInstance. WS tier classification mirrors REST. wsKey uses socket.remoteAddress. Consistent.

ACP handle disposal — app.locals['acpHandle'] bracket notation (TS4111 fix). dispose() called during shutdown with try/catch.

Previously Deferred to #4782 (unchanged at HEAD)

connRef: any typing, providerId type assertion, batch JSON-RPC, file/read+file/edit maxBytes, description defaults.

Final Verdict — COMMENT

3 commits add solid security hardening with comprehensive test coverage. One remaining Critical: attachSessionStream connStream guard (confirmed, one-line fix). After this fix, the PR should be ready for merge.


This review was generated by QoderWork AI

@chiga0

chiga0 commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

[Critical] attachSessionStream closes shared WsStream on REST SSE re-attach (connectionRegistry.ts:260)

teardownBinding (line 290) correctly guards against closing the shared connStream:

if (binding.stream && binding.stream !== this.connStream) {
  binding.stream.close();
}

But attachSessionStream (line 260) is missing this guard:

if (prevStream && prevStream !== stream) prevStream.close();

In WS mode, binding.stream === conn.connStream for all sessions. If a REST SSE client re-attaches to the same session (e.g., GET /acp with Acp-Session-Id), prevStream is the shared WsStream → closing it triggers onClose → registry.delete(connId) → entire WS connection destroyed for all sessions.

Fix:

if (prevStream && prevStream !== stream && prevStream !== this.connStream) {
  prevStream.close();
}

Confirmed by wenshao (review 4474984014) and independently verified. Re-review overview: review 4475067415.


This review was generated by QoderWork AI

@chiga0

chiga0 commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

Re: review 4475067415 + attachSessionStream finding (4678705791)

The one remaining Critical identified in the re-review (attachSessionStream connStream guard) is fixed in the subsequent commit e07428943:

// connectionRegistry.ts:260
if (prevStream && prevStream !== stream && prevStream !== this.connStream) {
  prevStream.close();
}

Also fixed in the same commit: cleanupSession race condition (captures AbortController identity to avoid closing recreated sessions).

All items from the re-review are now resolved at PR HEAD (e07428943). 83 tests pass.

…ype cleanup

- dispose() now removes the 'upgrade' listener from httpServer,
  preventing TypeError crash on late-arriving WS upgrades
- Refactor middleware to delegate to tryConsume(), eliminating
  duplicated token-bucket logic
- Use exported AcpHttpHandle type instead of inline type shapes
  in runQwenServe.ts

Generated with AI

Co-authored-by: Qwen-Coder <[email protected]>
DragonnZhang
DragonnZhang previously approved these changes Jun 11, 2026

@DragonnZhang DragonnZhang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All previously reported Critical findings have been addressed in this revision:

  • Shared stream close guard: teardownBinding now skips closing binding.stream when it equals conn.connStream, preventing WS connection teardown on session close.
  • WS upgrade security: Host allowlist, CSRF origin validation, and bearer token auth (with timing-safe comparison) are all enforced before handleUpgrade.
  • maxPayload: WebSocketServer now sets maxPayload: 10 * 1024 * 1024 (10 MB), matching the REST body limit.
  • Message serialization: Inbound WS messages are serialized via a messageQueue promise chain, preventing race conditions between concurrent frames.
  • Initialize timeout: A 30-second timer closes WS connections that never send initialize.
  • dispose() cleanup: The upgrade listener is properly removed on dispose.
  • Heartbeat/pong: Dead connection detection via ping/pong with alive flag in WsStream.

The TransportStream abstraction is clean, the rate limiter refactor correctly extracts tryConsume for reuse by both Express middleware and WS, and the test coverage is thorough (unit + integration). LGTM.

— qwen3-235b-a22b via Qwen Code /review

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Incremental Review (commits b016ba2 → 2fb0b60)

Good progress on addressing the previous review findings. Here's the status:

✅ Fixed

  • WS message serialization — messageQueue per-connection promise chain correctly serializes message processing. handleWsMessage is async and properly chained. ✅
  • Rate limiter integration — tryConsume extracted and exposed via checkRate, threaded through MountAcpHttpOptions. ✅
  • Token hashing — SHA-256 pre-hash eliminates byte-length side channel. ✅
  • acpHandle.dispose() — Called in shutdown sequence; upgrade listener properly removed. ✅
  • connStream guard — Both attachSessionStream and teardownBinding now guard against closing shared WS connStream. ✅
  • cleanupSession race — Abort controller identity check (b?.abort === myAbort) prevents stale cleanup from closing new pumps. ✅
  • WS security tests — 10 new tests covering origin check, token auth, maxPayload, protocol state, serialization, and rate limiting. ✅

Remaining from previous review

  • initTimer not cleared on early disconnect (low confidence — minor timer leak)
  • No test for connection cap (1013), initTimer timeout, or lazy session stream attachment

New finding

One minor regression in the rateLimit.ts refactor — see inline comment.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/rateLimit.ts Outdated
ytahdn
ytahdn previously approved these changes Jun 11, 2026

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All Critical findings from the initial review have been addressed. LGTM! ✅ — qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/index.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/index.ts
Comment thread packages/cli/src/serve/acpHttp/index.ts
Comment thread packages/cli/src/serve/rateLimit.ts Outdated
Comment thread packages/cli/src/serve/acpHttp/index.ts
…m guard

- Prompt dispatch no longer blocks the message queue, preventing
  deadlock when a permission vote is queued behind an in-flight prompt
- Rate-limit tiers use explicit read-method allowlist instead of
  prefix match, so session/new|close|cancel are correctly 'mutation'
- wsKey uses proper Duplex cast + ::ffff: normalization for IP parity
- connStream non-null assertion replaced with isClosed guard
- tryConsume fires onError callback on bucket overflow
- Test name corrected (accepts → not rejects)

Generated with AI

Co-authored-by: Qwen-Coder <[email protected]>
@chiga0
chiga0 force-pushed the feat/acp-websocket-transport branch from 78581e0 to bb051e2 Compare June 11, 2026 11:34
@chiga0
chiga0 dismissed stale reviews from ytahdn and DragonnZhang via bb051e2 June 11, 2026 11:39
- connectionRegistry.ts: wrap each teardownBinding() call in
  try/catch during destroy() so one failing callback cannot leak
  the remaining sessions' resources (AbortControllers, streams,
  pending requests)
- index.ts: add rate-limit enforcement for ACP HTTP POST path
  (POST /acp was exempt from Express middleware but had no
  alternative checkRate call, unlike the WS handler)

Generated with AI

Co-authored-by: Qwen-Coder <[email protected]>
DragonnZhang
DragonnZhang previously approved these changes Jun 11, 2026

@DragonnZhang DragonnZhang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed at bb051e28 (11 commits since last review). No high-confidence issues found.

Summary of changes reviewed:

  • WebSocket transport (WsStream) with transport-agnostic TransportStream interface
  • WS upgrade handler with CSWSH/DNS-rebinding/bearer-auth security
  • Message serialization via promise queue (prevents concurrent handler races)
  • Prompt fire-and-forget to avoid deadlock on permission votes
  • Shared connStream guard in attachSessionStream/teardownBinding (WS uses one socket for all sessions)
  • Rate limiter refactored to expose checkRate() for WS (DRY via tryConsume)
  • Token pre-hash (SHA-256) before timingSafeEqual for constant-time comparison
  • Explicit read-method allowlist for rate-limit tier classification
  • dispose() properly removes upgrade listener from httpServer
  • 17 WsStream unit tests + 10 WS security integration tests

APPROVE.

Comment thread packages/cli/src/serve/acpHttp/index.ts
Comment thread packages/cli/src/serve/acpHttp/index.ts
Comment thread packages/cli/src/serve/rateLimit.ts
Comment thread packages/cli/src/serve/acpHttp/index.ts
Comment thread packages/cli/src/serve/acpHttp/index.ts
Comment thread packages/cli/src/serve/acpHttp/index.ts
Comment thread packages/cli/src/serve/acpHttp/index.ts
Comment thread packages/cli/src/serve/acpHttp/index.ts
Comment thread packages/cli/src/serve/acpHttp/index.ts
Comment thread packages/cli/src/serve/acpHttp/index.ts
- upgradeListener: use correct function signature, remove `as any`
- connRef: type as `AcpConnection | undefined` instead of `any`
- SHA-256 token hash: pre-compute once at setupWebSocket instead of
  per-upgrade, reuse `expectedTokenHash` for all comparisons
- Add `ws` + `@types/ws` to cli package.json dependencies (was only
  hoisted from plugin-example)
- Log WS initialize timeout with source address for diagnostics

Generated with AI

Co-authored-by: Qwen-Coder <[email protected]>

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-Review at HEAD c6ded92 (8 new commits since fd6d3027)

New Commits

# Commit Description
1 41516e02 TS4111 bracket notation + IPv6 bracket strip + empty Host guard
2 b016ba26 SHA-256 token hashing, message serialization queue, WS rate limiting
3 9139f286 10 WS security integration tests + ws error handler
4 e0742894 attachSessionStream shared connStream guard + abort identity guard
5 2fb0b600 upgrade listener cleanup on dispose + DRY rate limiter + typed connRef
6 bb051e28 WS prompt fire-and-forget, explicit tier sets, IP normalization
7 27105e59 Exception-safe destroy() + ACP HTTP rate limiting
8 c6ded929 Type safety (no any), token pre-hash, ws dependency, init timeout log

Reviewer Status at HEAD

Reviewer Last Review Status
wenshao e0742894 CHANGES_REQUESTED (4474984014)
ytahdn bb051e28 DISMISSED (4475998173) — "All Critical findings addressed"
DragonnZhang bb051e28 DISMISSED (4476321966) — "No high-confidence issues found"
ci-bot c6ded929 CHANGES_REQUESTED (4476035606)
doudouOUC c6ded929 CHANGES_REQUESTED (4476337860)

Cross-Validation Table (Critical findings)

# Reviewer Finding Status at HEAD
1 wenshao/DZ/ytahdn TS4111 app.locals.acpHandle FIXED (commit 1: bracket notation)
2 DZ/wenshao Bearer token timing side-channel FIXED (commit 2: SHA-256 + timingSafeEqual, commit 8: pre-hash)
3 ci-bot/DZ IPv6 bracket mismatch in origin check FIXED (commit 1: `.replace(/^[
4 ci-bot Empty Host short-circuits allowlist FIXED (commit 1: if (!allowed.has(host)) without host &&)
5 ytahdn ws.on('message', async) not serialized FIXED (commit 2: messageQueue promise chain)
6 ytahdn WS messages bypass rate limiter FIXED (commit 2: opts.checkRate, commit 7: HTTP parity)
7 ci-bot/doudouOUC Rate-limit tier misclassifies session mutations FIXED (commit 6: explicit WS_READ_METHODS set + WS_EXEMPT_METHODS)
8 ci-bot/doudouOUC WS rate-limit key raw remoteAddress FIXED (commit 6: ::ffff: prefix strip)
9 ci-bot Prompt blocks message queue → deadlock FIXED (commit 6: prompt fire-and-forget)
10 wenshao attachSessionStream shared connStream guard FIXED (commit 4: prevStream !== this.connStream + abort identity)
11 wenshao/ci-bot conn.connStream! non-null assertion FIXED (commit 6: conn.connStream && !conn.connStream.isClosed)
12 ci-bot/doudouOUC dispose() doesn't remove upgrade listener FIXED (commit 5: upgradeServer.removeListener)
13 ci-bot connRef: any type FIXED (commit 8: `AcpConnection
14 doudouOUC upgradeListener type mismatch FIXED (commit 8: proper function signature)
15 doudouOUC socket.remoteAddress TS2339 on Duplex FIXED (commit 6: proper cast pattern)
16 doudouOUC ws not in package.json dependencies FIXED (commit 8: "ws": "^8.18.0" + @types/ws)
17 ci-bot/doudouOUC tryConsume drops onError callback FIXED (commit 6: config.onError?.(...) on overflow)
18 ci-bot ACP HTTP POST not rate-limited FIXED (commit 7: HTTP checkRate path)
19 ci-bot destroy() not exception-safe FIXED (commit 7: try-catch per teardownBinding)
20 doudouOUC Init timeout lacks server-side log FIXED (commit 8: writeStderrLine before ws.close())
21 ytahdn ACP handle not disposed on shutdown FIXED (commit 2: acpHandle.dispose() in shutdown)

Independent Verification at HEAD

Security layer (all 5 layers confirmed):

  1. Host allowlist: loopback + host:port matching ✓
  2. CSWSH origin: applies to loopback, IPv6 bracket strip ✓
  3. Bearer auth: SHA-256 pre-hash + timingSafeEqual, no length leak ✓
  4. Connection cap: registry.create() returns null → 1013 close ✓
  5. Init timeout: 30s with .unref(), logged on fire ✓

Message processing:

  • messageQueue serializes all messages via promise chain ✓
  • Prompt fire-and-forget: const isPrompt = ... → if (!isPrompt) await dispatchP ✓
  • Rate limit: WS_EXEMPT_METHODS (heartbeat/metadata), WS_READ_METHODS (explicit set), else mutation ✓

Connection lifecycle:

  • connStream guard: conn.connStream && !conn.connStream.isClosed ✓
  • Abort identity: myAbort captured, cleanup checks b?.abort === myAbort ✓
  • Dispose: removes upgrade listener, closes WSS, disposes registry ✓
  • Destroy: try-catch per teardownBinding ✓

Rate limiter:

  • tryConsume shared by Express middleware + WS + HTTP ACP ✓
  • onError callback preserved on bucket overflow ✓
  • IP normalization: ::ffff: prefix strip ✓
  • ACP HTTP POST: checkRate(httpKey, tier) with Retry-After: 5 ✓

Remaining Items (all Suggestions, non-blocking)

# Finding Assessment
1 initTimer not cleared on ws error/close before init Minor — .unref() prevents exit blocking; fires on closed socket (no-op)
2 WsStream write chain lacks depth limit Pre-existing; same pattern as SSE; low risk in practice
3 WsStream.close() doesn't remove constructor listeners Pre-existing; GC handles after ws close
4 JSON.stringify in send() can throw synchronously Pre-existing; JSON-RPC messages from dispatcher are safe
5 Various dispatch-layer size limits (agents/create, file/edit, etc.) Pre-existing REST parity — not WS-specific
6 WsStream.onClose unconditional registry.delete Deferred — WS reconnect not supported
7 Origin check ignores port Acceptable for loopback daemon

Final Verdict — LGTM, Recommend Merge

All 21 Critical findings from 5 reviewers addressed across 8 commits. Security layer is defense-in-depth (host → origin → auth → cap → timeout). Message serialization + prompt fire-and-forget resolves the deadlock. Rate limiting achieves full parity between WS and HTTP transports. Remaining 7 items are all Suggestions (pre-existing or acceptable trade-offs). 10 integration tests cover all security paths.


This re-review was generated by QoderWork AI

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Incremental Review (commits 2fb0b60 → c6ded92)

Good set of fixes addressing the previous review and additional hardening. Summary of changes:

✅ Well done

  • Prompt fire-and-forget — session/prompt no longer blocks the message queue, preventing deadlock when permission votes or cancel requests are queued behind a long-running prompt. Clear comment explaining the rationale.
  • Type safety — connRef: AcpConnection | undefined replaces any, upgradeListener properly typed, connStream! non-null assertion removed with proper guard.
  • Token hash pre-computed — expectedTokenHash computed once at setup scope, not per-upgrade.
  • destroy() exception safety — teardownBinding wrapped in try-catch so one session failure doesn't prevent others from being torn down.
  • Rate limit bucket overflow — onError callback restored in tryConsume, fixing the regression from last round.
  • Rate limit method classification parity — Explicit WS_EXEMPT_METHODS / WS_READ_METHODS sets ensure REST and WS rate limiting use the same tier classification.
  • REST POST rate limiting — checkRate now applied to non-initialize POST requests, closing the gap where REST POST had no rate limit.
  • Init timeout logging — 30s init timeout now logs the source address for debugging.
  • Lazy session stream guard — conn.connStream && !conn.connStream.isClosed prevents attaching to a closed stream.
  • wsKey IPv6 normalization — Matches normalizeIp behavior used in REST middleware.

Findings

Two minor observations — see inline comments.

— qwen3.7-max via Qwen Code /review

*/
const CONN_GRACE_MS = 10_000;

const WS_EXEMPT_METHODS = new Set([

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] WS_EXEMPT_METHODS and WS_READ_METHODS are now also used in the REST POST rate-limit path (line ~232). The WS_ prefix is misleading since these are protocol-agnostic method classification sets. Consider renaming to ACP_EXEMPT_METHODS and ACP_READ_METHODS to reflect the shared usage.

— qwen3.7-max via Qwen Code /review

: 'mutation';
const httpKey = (req.socket?.remoteAddress ?? 'http-unknown').replace(
/^::ffff:/,
'',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The REST POST rate-limit key uses req.socket?.remoteAddress (IP only), while the REST middleware (rateLimit.ts createKeyExtractor) may include a clientId from the x-qwen-client-id header (e.g. ip:clientId or cid:clientId). This means the same client could be tracked under different keys in the two paths, reducing rate-limit effectiveness.

Consider extracting the key-building logic into a shared helper:

const httpKey = extractKeyFromReq(req);  // reuses same clientId + IP logic

— qwen3.7-max via Qwen Code /review

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All previous Critical findings resolved. Two minor Suggestions remain (naming + rate-limit key parity) but are non-blocking. LGTM! ✅ — qwen3.7-max via Qwen Code /review

@chiga0
chiga0 merged commit 5a28dcc into daemon_mode_b_main Jun 11, 2026
253 of 255 checks passed
@chiga0
chiga0 deleted the feat/acp-websocket-transport branch June 11, 2026 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/feature-request New feature or enhancement request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants