Skip to content

Commit 2c77d58

Browse files
committed
fix: reject duplicate websocket initialize
1 parent 59320fd commit 2c77d58

2 files changed

Lines changed: 75 additions & 0 deletions

File tree

acp-streamable-http-jetty/src/main/java/com/agentclientprotocol/sdk/agent/transport/StreamableHttpAcpAgentTransport.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import com.agentclientprotocol.sdk.agent.AcpAgentFactory;
2525
import com.agentclientprotocol.sdk.error.AcpConnectionException;
26+
import com.agentclientprotocol.sdk.error.AcpErrorCodes;
2627
import com.agentclientprotocol.sdk.json.AcpJsonMapper;
2728
import com.agentclientprotocol.sdk.json.TypeRef;
2829
import com.agentclientprotocol.sdk.spec.AcpSchema;
@@ -927,6 +928,13 @@ void acceptFromClient(JSONRPCMessage message) {
927928
}
928929
initialized.set(true);
929930
}
931+
else if (message instanceof AcpSchema.JSONRPCRequest request
932+
&& AcpSchema.METHOD_INITIALIZE.equals(request.method())) {
933+
sendToClient(new AcpSchema.JSONRPCResponse(AcpSchema.JSONRPC_VERSION, request.id(), null,
934+
new AcpSchema.JSONRPCError(AcpErrorCodes.INVALID_REQUEST,
935+
"Initialize not allowed on existing connection", null)));
936+
return;
937+
}
930938
remoteConnection.acceptInbound(message);
931939
}
932940

acp-streamable-http-jetty/src/test/java/com/agentclientprotocol/sdk/agent/transport/StreamableHttpAcpAgentTransportWebSocketIntegrationTest.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.util.concurrent.CompletableFuture;
2525
import java.util.concurrent.CompletionStage;
2626
import java.util.concurrent.CountDownLatch;
27+
import java.util.concurrent.BlockingQueue;
28+
import java.util.concurrent.LinkedBlockingQueue;
2729
import java.util.concurrent.TimeUnit;
2830
import java.util.concurrent.atomic.AtomicInteger;
2931
import java.util.concurrent.atomic.AtomicReference;
@@ -285,6 +287,44 @@ void rejectsNonInitializeFirstMessage() throws Exception {
285287
}
286288
}
287289

290+
@Test
291+
void rejectsDuplicateInitializeWithoutForwardingItToTheAgent() throws Exception {
292+
AtomicInteger initializeCalls = new AtomicInteger();
293+
AcpAgentFactory agentFactory = AcpAgentFactory.async(transport -> AcpAgent.async(transport)
294+
.initializeHandler(request -> {
295+
initializeCalls.incrementAndGet();
296+
return Mono.just(new AcpSchema.InitializeResponse(AcpSchema.LATEST_PROTOCOL_VERSION,
297+
new AcpSchema.AgentCapabilities(true, null, null), List.of()));
298+
})
299+
.build());
300+
301+
try (FixtureServer server = FixtureServer.start(agentFactory)) {
302+
MessageRecordingListener listener = new MessageRecordingListener();
303+
WebSocket webSocket = HttpClient.newHttpClient()
304+
.newWebSocketBuilder()
305+
.connectTimeout(TIMEOUT)
306+
.buildAsync(server.endpoint(), listener)
307+
.get(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
308+
309+
assertThat(listener.openLatch.await(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)).isTrue();
310+
webSocket.sendText("""
311+
{"jsonrpc":"2.0","id":"init-1","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{}}}
312+
""", true).get(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
313+
assertThat(readJsonRpcMessage(listener)).isInstanceOf(AcpSchema.JSONRPCResponse.class);
314+
315+
webSocket.sendText("""
316+
{"jsonrpc":"2.0","id":"init-2","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{}}}
317+
""", true).get(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
318+
319+
AcpSchema.JSONRPCResponse response = (AcpSchema.JSONRPCResponse) readJsonRpcMessage(listener);
320+
assertThat(response.id()).isEqualTo("init-2");
321+
assertThat(response.error()).isNotNull();
322+
assertThat(response.error().code()).isEqualTo(-32600);
323+
assertThat(response.error().message()).isEqualTo("Initialize not allowed on existing connection");
324+
assertThat(initializeCalls).hasValue(1);
325+
}
326+
}
327+
288328
private static AcpAgentFactory simpleAgentFactory() {
289329
AtomicInteger sessionCounter = new AtomicInteger();
290330
return AcpAgentFactory.async(transport -> AcpAgent.async(transport)
@@ -318,6 +358,12 @@ private static void assertEventuallyNoConnections(StreamableHttpAcpAgentTranspor
318358
assertThat(transport.activeConnectionCount()).isEqualTo(0);
319359
}
320360

361+
private static AcpSchema.JSONRPCMessage readJsonRpcMessage(MessageRecordingListener listener) throws Exception {
362+
String message = listener.messages.poll(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
363+
assertThat(message).isNotNull();
364+
return AcpSchema.deserializeJsonRpcMessage(AcpJsonMapper.createDefault(), message);
365+
}
366+
321367
private static int freePort() {
322368
try (ServerSocket socket = new ServerSocket(0)) {
323369
return socket.getLocalPort();
@@ -395,4 +441,25 @@ public void onError(WebSocket webSocket, Throwable error) {
395441

396442
}
397443

444+
private static final class MessageRecordingListener implements WebSocket.Listener {
445+
446+
private final CountDownLatch openLatch = new CountDownLatch(1);
447+
448+
private final BlockingQueue<String> messages = new LinkedBlockingQueue<>();
449+
450+
@Override
451+
public void onOpen(WebSocket webSocket) {
452+
openLatch.countDown();
453+
webSocket.request(1);
454+
}
455+
456+
@Override
457+
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
458+
messages.add(data.toString());
459+
webSocket.request(1);
460+
return CompletableFuture.completedFuture(null);
461+
}
462+
463+
}
464+
398465
}

0 commit comments

Comments
 (0)