Files
cleveragents-core/features/lsp_server_stub.feature
T
HAL9000 db389a7304 fix(lsp): add per-message read timeout to prevent DoS in _read_message()
Resolves the DoS vulnerability in LspServer._read_message() where a
malicious client could send a valid Content-Length header but stall
without delivering the body, blocking the server indefinitely.

Changes:
- Add MESSAGE_READ_TIMEOUT constant (default: 30 seconds)
- Add _read_body_with_timeout() method using select() for timeout enforcement
- Add read_timeout constructor parameter for configurability
- Replace blocking self._input.read(content_length) with timeout-based read
- Remove the SEC: comment that documented the vulnerability
- Add BDD tests covering timeout behavior and constant export

The fix uses select() for streams with a real file descriptor (e.g.
sys.stdin) and falls back to direct blocking read for in-memory streams
(e.g. io.BytesIO used in tests), preserving full test compatibility.

Closes #7083
2026-06-04 21:00:09 -04:00

486 lines
21 KiB
Gherkin

@phase2 @domain @lsp @lsp_server_stub
Feature: LSP server stub JSON-RPC protocol
As a developer integrating LSP into CleverAgents
I want a minimal LSP server stub that handles core lifecycle methods
So that I can test the JSON-RPC transport and protocol handshake
Background:
Given a mock LSP transport
# ---------------------------------------------------------------------------
# Initialize handshake
# ---------------------------------------------------------------------------
@lsp_initialize
Scenario: LSP initialize returns server info and capabilities
Given an initialize request with id 1
When the LSP server processes all messages
Then the response for id 1 should have result
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
And the result for id 1 should contain serverInfo with version "0.1.0"
And the result for id 1 should contain capabilities key "textDocumentSync"
@lsp_initialize
Scenario: LSP initialize with client info is accepted
Given an initialize request with id 10 and clientInfo name "test-client"
When the LSP server processes all messages
Then the response for id 10 should have result
And the result for id 10 should contain serverInfo with name "cleveragents-lsp-stub"
@lsp_initialize
Scenario: Initialized notification is silently accepted
Given an initialize request with id 1
And a notification for method "initialized"
And an exit notification
When the LSP server processes all messages
Then the response for id 1 should have result
And there should be no error responses
@lsp_initialize
Scenario: Initialized notification before initialize is silently ignored
Given a notification for method "initialized"
And an exit notification
When the LSP server processes all messages
Then there should be no error responses
And the server exit code should be 1
# ---------------------------------------------------------------------------
# Shutdown handshake
# ---------------------------------------------------------------------------
@lsp_shutdown
Scenario: LSP shutdown returns null result
Given an initialize request with id 1
And a shutdown request with id 2
And an exit notification
When the LSP server processes all messages
Then the response for id 2 should have null result
And the server exit code should be 0
@lsp_shutdown
Scenario: LSP exit without prior shutdown returns exit code 1
Given an initialize request with id 1
And an exit notification
When the LSP server processes all messages
Then the server exit code should be 1
# ---------------------------------------------------------------------------
# Unsupported methods
# ---------------------------------------------------------------------------
@lsp_unsupported
Scenario: Unsupported method after initialize returns MethodNotFound error
Given an initialize request with id 1
And a request for method "textDocument/completion" with id 5
And an exit notification
When the LSP server processes all messages
Then the response for id 5 should have error code -32601
And the error message for id 5 should contain "Not implemented: textDocument/completion"
@lsp_unsupported
Scenario: Unsupported method textDocument/hover after initialize returns error
Given an initialize request with id 1
And a request for method "textDocument/hover" with id 6
And an exit notification
When the LSP server processes all messages
Then the response for id 6 should have error code -32601
And the error message for id 6 should contain "Not implemented: textDocument/hover"
@lsp_unsupported
Scenario: Unsupported method before initialize returns ServerNotInitialized
Given a request for method "textDocument/completion" with id 5
And an exit notification
When the LSP server processes all messages
Then the response for id 5 should have error code -32002
And the error message for id 5 should contain "Server not initialized"
@lsp_unsupported
Scenario: Unsupported notification is silently ignored
Given a notification for method "textDocument/didOpen"
And an exit notification
When the LSP server processes all messages
Then there should be no error responses
# ---------------------------------------------------------------------------
# Invalid JSON-RPC messages
# ---------------------------------------------------------------------------
@lsp_invalid
Scenario: Invalid JSON returns parse error response
Given raw bytes that are not valid JSON-RPC
And an exit notification
When the LSP server processes all messages
Then a parse error response should have been sent
And the server should have stopped
@lsp_invalid
Scenario: Missing jsonrpc version field returns error
Given a message with missing jsonrpc version and id 7
When the LSP server processes all messages
Then the response for id 7 should have error code -32600
And the error message for id 7 should contain "jsonrpc version"
@lsp_invalid
Scenario: Missing method field returns error
Given a message with missing method and id 8
When the LSP server processes all messages
Then the response for id 8 should have error code -32600
And the error message for id 8 should contain "method"
# ---------------------------------------------------------------------------
# Server lifecycle
# ---------------------------------------------------------------------------
@lsp_lifecycle
Scenario: Server startup reports PID
Given an initialize request with id 1
And an exit notification
When the LSP server processes all messages
Then the server should have logged its PID
@lsp_lifecycle
Scenario: Server handles EOF gracefully
When the LSP server processes all messages with empty input
Then the server should have stopped
And the server exit code should be 1
# ---------------------------------------------------------------------------
# A2A facade wiring
# ---------------------------------------------------------------------------
@lsp_a2a
Scenario: LSP server stores provided A2A facade
Given an LSP server with A2A facade
And an initialize request with id 1
When the LSP server processes all messages
Then the response for id 1 should have result
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
And the server should hold the provided A2A facade
@lsp_a2a
Scenario: LSP server lazily creates A2A facade on property access
Given an initialize request with id 1
And an exit notification
When the LSP server processes all messages
Then the response for id 1 should have result
And the server facade should be lazily created
# ---------------------------------------------------------------------------
# Full handshake sequence
# ---------------------------------------------------------------------------
@lsp_full_handshake
Scenario: Complete LSP lifecycle (initialize, shutdown, exit)
Given an initialize request with id 1
And a shutdown request with id 2
And an exit notification
When the LSP server processes all messages
Then the response for id 1 should have result
And the response for id 2 should have null result
And the server exit code should be 0
# ---------------------------------------------------------------------------
# Server construction validation
# ---------------------------------------------------------------------------
@lsp_construction
Scenario: LspServer rejects invalid input_stream
When I try to create an LspServer with invalid input stream
Then a TypeError should be raised for input stream
@lsp_construction
Scenario: LspServer rejects invalid output_stream
When I try to create an LspServer with invalid output stream
Then a TypeError should be raised for output stream
# ---------------------------------------------------------------------------
# Transport edge cases
# ---------------------------------------------------------------------------
@lsp_transport_edge
Scenario: Server recovers from invalid message and handles subsequent request
Given a message with missing jsonrpc version and id 7
And an initialize request with id 1
And an exit notification
When the LSP server processes all messages
Then the response for id 7 should have error code -32600
And the response for id 1 should have result
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
@lsp_transport_edge
Scenario: Request with non-dict params uses empty params
Given an initialize request with id 1
And a request with non-dict params and id 20
And an exit notification
When the LSP server processes all messages
Then the response for id 20 should have error code -32601
@lsp_transport_edge
Scenario: Negative content length is handled gracefully
Given a message with negative content length
When the LSP server processes all messages
Then the server should have stopped
And the server exit code should be 1
And a transport warning should have been logged for "lsp.transport.negative_content_length"
@lsp_transport_edge
Scenario: Incomplete message body is handled gracefully
Given a message with incomplete body
When the LSP server processes all messages
Then the server should have stopped
And the server exit code should be 1
And a transport warning should have been logged for "lsp.transport.incomplete_read"
@lsp_transport_edge
Scenario: Unknown method returns MethodNotFound without data field
Given an initialize request with id 1
And a request for method "custom/withData" with id 30
And an exit notification
When the LSP server processes all messages
Then the response for id 30 should have error code -32601
And the error response for id 30 should not contain a data field
@lsp_transport_edge
Scenario: Missing content length header is handled
Given a message with missing content length header
When the LSP server processes all messages
Then the server should have stopped
And the server exit code should be 1
And a transport warning should have been logged for "lsp.transport.missing_content_length"
@lsp_transport_edge
Scenario: Oversized content length is rejected
Given a message with content length exceeding the maximum
And an exit notification
When the LSP server processes all messages
Then the server should have stopped
And the server exit code should be 1
And a transport warning should have been logged for "lsp.transport.content_length_exceeded"
@lsp_transport_edge
Scenario: Too many header lines are rejected
Given a message with too many header lines
When the LSP server processes all messages
Then the server should have stopped
And the server exit code should be 1
And a transport warning should have been logged for "lsp.transport.too_many_header_lines"
@lsp_transport_edge
Scenario: JSON array body returns InvalidRequest error
Given a message with a JSON array body
And an exit notification
When the LSP server processes all messages
Then an InvalidRequest error response should have been sent
@lsp_transport_edge
Scenario: Non-integer content length is handled gracefully
Given a message with non-integer content length
When the LSP server processes all messages
Then the server should have stopped
And the server exit code should be 1
And a transport warning should have been logged for "lsp.transport.invalid_content_length"
@lsp_transport_edge
Scenario: Zero content length returns parse error
Given a message with content length zero
And an exit notification
When the LSP server processes all messages
Then a parse error response should have been sent
And the server should have stopped
@lsp_transport_edge
Scenario: Duplicate content-length headers uses last value
Given a message with duplicate content-length headers and id 60
And an exit notification
When the LSP server processes all messages
Then the response for id 60 should have result
And the result for id 60 should contain serverInfo with name "cleveragents-lsp-stub"
@lsp_transport_edge
Scenario: Shutdown without prior initialize returns ServerNotInitialized
Given a shutdown request with id 1
And an exit notification
When the LSP server processes all messages
Then the response for id 1 should have error code -32002
And the error message for id 1 should contain "Server not initialized"
@lsp_transport_edge
Scenario: Initialize without params key uses empty params
Given a message with initialize request without params key and id 50
And an exit notification
When the LSP server processes all messages
Then the response for id 50 should have result
And the result for id 50 should contain serverInfo with name "cleveragents-lsp-stub"
# ---------------------------------------------------------------------------
# Post-shutdown request rejection (LSP spec §3.16)
# ---------------------------------------------------------------------------
@lsp_shutdown
Scenario: Request after shutdown returns InvalidRequest error
Given an initialize request with id 1
And a shutdown request with id 2
And a request for method "textDocument/completion" with id 3
And an exit notification
When the LSP server processes all messages
Then the response for id 3 should have error code -32600
And the error message for id 3 should contain "shutting down"
@lsp_shutdown
Scenario: Notification after shutdown is silently ignored
Given an initialize request with id 1
And a shutdown request with id 2
And a notification for method "textDocument/didOpen"
And an exit notification
When the LSP server processes all messages
Then the response for id 1 should have result
And the response for id 2 should have null result
And there should be no error responses
And the server exit code should be 0
# ---------------------------------------------------------------------------
# Double initialize rejection
# ---------------------------------------------------------------------------
@lsp_initialize
Scenario: Second initialize request returns InvalidRequest error
Given an initialize request with id 1
And an initialize request with id 2
And an exit notification
When the LSP server processes all messages
Then the response for id 1 should have result
And the response for id 2 should have error code -32600
And the error message for id 2 should contain "already initialized"
# ---------------------------------------------------------------------------
# Stream desync recovery (transport error followed by valid message)
# ---------------------------------------------------------------------------
@lsp_transport_edge
Scenario: Server recovers after oversized message with body and processes next request
Given an oversized message with real body followed by an initialize request with id 1
And an exit notification
When the LSP server processes all messages
Then the response for id 1 should have result
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
@lsp_transport_edge
Scenario: Server recovers after invalid content-length header and processes next request
Given a message with non-integer content length followed by an initialize request with id 1
And an exit notification
When the LSP server processes all messages
Then the response for id 1 should have result
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
@lsp_transport_edge
Scenario: Server recovers after too many header lines and processes next request
Given a message with too many header lines followed by an initialize request with id 1
And an exit notification
When the LSP server processes all messages
Then the response for id 1 should have result
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
@lsp_transport_edge
Scenario: EOF during body discard of oversized message is handled gracefully
Given an oversized message with a short body that triggers EOF during discard
When the LSP server processes all messages
Then the server should have stopped
And the server exit code should be 1
And a transport warning should have been logged for "lsp.transport.content_length_exceeded"
@lsp_transport_edge
Scenario: Oversized single header line is handled gracefully
Given a message with a header line exceeding the maximum line length
And an exit notification
When the LSP server processes all messages
Then the server should have stopped
And the server exit code should be 1
And a transport warning should have been logged for "lsp.transport.too_many_header_lines"
@lsp_transport_edge
Scenario: Deeply nested JSON triggers parse error response
Given a message with deeply nested JSON
And an exit notification
When the LSP server processes all messages
Then a parse error response should have been sent
And a transport warning should have been logged for "lsp.transport.json_recursion_error"
And the server should have stopped
# ---------------------------------------------------------------------------
# CLI serve command
# ---------------------------------------------------------------------------
@lsp_cli_serve
Scenario: CLI serve command rejects invalid log level
When I run lsp CLI serve with log level "banana"
Then the lsp serve CLI should have failed
And the lsp serve CLI output should contain "Invalid log level"
@lsp_cli_serve
Scenario: CLI serve command accepts valid log level and runs
When I run lsp CLI serve with piped empty stdin
Then the lsp serve CLI should have exited with code 1
And the lsp serve CLI output should contain "CleverAgents LSP Server"
@lsp_cli_serve
Scenario: CLI serve command with debug log level
When I run lsp CLI serve with log level "debug" and piped empty stdin
Then the lsp serve CLI should have exited with code 1
And the lsp serve CLI output should contain "CleverAgents LSP Server"
# ---------------------------------------------------------------------------
# Broken output stream (pipe failure)
# ---------------------------------------------------------------------------
@lsp_transport_edge
Scenario: Broken output stream stops the server gracefully
Given an initialize request with id 1
And an exit notification
When the LSP server processes all messages with a broken output stream
Then the server should have stopped
And the server exit code should be 1
And a transport warning should have been logged for "lsp.transport.write_error"
# ---------------------------------------------------------------------------
# String-valued JSON-RPC request id
# ---------------------------------------------------------------------------
@lsp_initialize
Scenario: String request id is echoed correctly
Given an initialize request with string id "alpha-1"
And an exit notification
When the LSP server processes all messages
Then the response for string id "alpha-1" should have result
And the result for string id "alpha-1" should contain serverInfo with name "cleveragents-lsp-stub"
# ---------------------------------------------------------------------------
# EOF after shutdown without exit notification
# ---------------------------------------------------------------------------
@lsp_shutdown
Scenario: EOF after shutdown without exit returns exit code 0
Given an initialize request with id 1
And a shutdown request with id 2
When the LSP server processes all messages
Then the response for id 2 should have null result
And the server exit code should be 0
# ---------------------------------------------------------------------------
# Per-message read timeout (DoS protection)
# ---------------------------------------------------------------------------
@lsp_transport_edge @lsp_dos_protection
Scenario: Read timeout is configurable via constructor parameter
Given an initialize request with id 1
And an exit notification
When the LSP server processes all messages with read timeout 60.0
Then the response for id 1 should have result
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
And the server exit code should be 1
@lsp_transport_edge @lsp_dos_protection
Scenario: MESSAGE_READ_TIMEOUT constant is exported and has correct default value
When I check the MESSAGE_READ_TIMEOUT constant
Then the MESSAGE_READ_TIMEOUT should be 30
And MESSAGE_READ_TIMEOUT should be importable from lsp server module