Skip to content

Commit 59320fd

Browse files
committed
fix: validate initialize response identity
1 parent 6dad4cf commit 59320fd

2 files changed

Lines changed: 49 additions & 4 deletions

File tree

acp-core/src/main/java/com/agentclientprotocol/sdk/client/transport/StreamableHttpAcpClientTransport.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,17 +282,24 @@ private Mono<Void> initialize(AcpSchema.JSONRPCRequest request) {
282282
return Mono.error(new AcpConnectionException(
283283
"Expected " + CONTENT_TYPE_JSON + " initialize response, got " + contentType));
284284
}
285-
this.connectionId = response.headers()
286-
.firstValue(HEADER_CONNECTION_ID)
287-
.orElseThrow(() -> new AcpConnectionException(
288-
"Initialize response missing " + HEADER_CONNECTION_ID));
289285
JSONRPCMessage responseMessage;
290286
try {
291287
responseMessage = AcpSchema.deserializeJsonRpcMessage(jsonMapper, response.body());
292288
}
293289
catch (Exception e) {
294290
return Mono.error(new AcpConnectionException("Failed to deserialize initialize response", e));
295291
}
292+
if (!(responseMessage instanceof AcpSchema.JSONRPCResponse initializeResponse)) {
293+
return Mono.error(new AcpConnectionException("ACP initialize response was not a JSON-RPC response"));
294+
}
295+
if (!Objects.equals(request.id(), initializeResponse.id())) {
296+
return Mono.error(
297+
new AcpConnectionException("ACP initialize response id did not match initialize request"));
298+
}
299+
this.connectionId = response.headers()
300+
.firstValue(HEADER_CONNECTION_ID)
301+
.orElseThrow(() -> new AcpConnectionException(
302+
"Initialize response missing " + HEADER_CONNECTION_ID));
296303
return openConnectionStream().then(emitInbound(responseMessage));
297304
})
298305
.doOnError(error -> {

acp-core/src/test/java/com/agentclientprotocol/sdk/client/transport/StreamableHttpAcpClientTransportTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,35 @@ void defaultAcpPathIsCorrect() {
9191
assertThat(StreamableHttpAcpClientTransport.DEFAULT_ACP_PATH).isEqualTo("/acp");
9292
}
9393

94+
@Test
95+
void initializeRejectsNonResponseBody() throws Exception {
96+
StreamableHttpAcpClientTransport transport = new StreamableHttpAcpClientTransport(
97+
URI.create("https://localhost:8443/acp"), jsonMapper,
98+
initializeHttpClient(new AcpSchema.JSONRPCNotification("session/update", Map.of())));
99+
transport.setExceptionHandler(error -> {
100+
});
101+
102+
assertThatThrownBy(() -> transport.sendMessage(AcpTestFixtures.createJsonRpcRequest(AcpSchema.METHOD_INITIALIZE,
103+
"init-1", AcpTestFixtures.createInitializeRequest())).block())
104+
.isInstanceOf(AcpConnectionException.class)
105+
.hasMessage("ACP initialize response was not a JSON-RPC response");
106+
}
107+
108+
@Test
109+
void initializeRejectsResponseWithDifferentId() throws Exception {
110+
StreamableHttpAcpClientTransport transport = new StreamableHttpAcpClientTransport(
111+
URI.create("https://localhost:8443/acp"), jsonMapper,
112+
initializeHttpClient(AcpTestFixtures.createJsonRpcResponse("wrong-id",
113+
AcpTestFixtures.createInitializeResponse())));
114+
transport.setExceptionHandler(error -> {
115+
});
116+
117+
assertThatThrownBy(() -> transport.sendMessage(AcpTestFixtures.createJsonRpcRequest(AcpSchema.METHOD_INITIALIZE,
118+
"init-1", AcpTestFixtures.createInitializeRequest())).block())
119+
.isInstanceOf(AcpConnectionException.class)
120+
.hasMessage("ACP initialize response id did not match initialize request");
121+
}
122+
94123
@Test
95124
void concurrentSessionLoadsReuseInFlightSessionStreamOpen() throws Exception {
96125
HttpClient httpClient = mock(HttpClient.class);
@@ -572,6 +601,15 @@ private InputStream emptyBody() {
572601
return new ByteArrayInputStream(new byte[0]);
573602
}
574603

604+
private HttpClient initializeHttpClient(AcpSchema.JSONRPCMessage initializeResponse) throws Exception {
605+
HttpClient httpClient = mock(HttpClient.class);
606+
String body = jsonMapper.writeValueAsString(initializeResponse);
607+
HttpResponse<Object> response = response(200,
608+
Map.of("Content-Type", "application/json", "Acp-Connection-Id", "conn-1"), body);
609+
when(httpClient.sendAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(response));
610+
return httpClient;
611+
}
612+
575613
private <T> HttpResponse<T> response(int statusCode, Map<String, String> headers, T body) {
576614
HttpResponse<T> response = mock(HttpResponse.class);
577615
when(response.statusCode()).thenReturn(statusCode);

0 commit comments

Comments
 (0)