Skip to content

Commit 2d85705

Browse files
committed
On branch copilot-pr-20 Address "5. **NO_RESULT permission handling — silent no-op** — MEDIUM RISK"
modified: .gitignore modified: src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java modified: src/main/java/com/github/copilot/sdk/json/PermissionRequestResultKind.java modified: src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java modified: src/test/java/com/github/copilot/sdk/SessionHandlerTest.java
1 parent 1324222 commit 2d85705

File tree

5 files changed

+52
-3
lines changed

5 files changed

+52
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ blog-copilotsdk/
77
smoke-test
88
*job-logs.txt
99
temporary-prompts/
10+
changebundle.txt*

src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,11 @@ private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNo
194194
session.handlePermissionRequest(permissionRequest).thenAccept(result -> {
195195
try {
196196
if (PermissionRequestResultKind.NO_RESULT.getValue().equalsIgnoreCase(result.getKind())) {
197-
// Handler explicitly abstains — do not send a response,
198-
// allowing another client to handle the request.
199-
return;
197+
// Protocol v2 does not support NO_RESULT — the server
198+
// expects exactly one response per request, so abstaining
199+
// would leave it hanging.
200+
throw new IllegalStateException(
201+
"Permission handlers cannot return 'no-result' when connected to a protocol v2 server.");
200202
}
201203
rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result));
202204
} catch (IOException e) {

src/main/java/com/github/copilot/sdk/json/PermissionRequestResultKind.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ public final class PermissionRequestResultKind {
5858
* cannot or chooses not to handle a given permission request, it can return
5959
* {@code NO_RESULT} to leave the request unanswered, allowing another client to
6060
* handle it.
61+
* <p>
62+
* <strong>Warning:</strong> This kind is only valid with protocol v3 servers
63+
* (broadcast permission model). When connected to a protocol v2 server, the SDK
64+
* will throw {@link IllegalStateException} because v2 expects exactly one
65+
* response per permission request.
6166
*/
6267
public static final PermissionRequestResultKind NO_RESULT = new PermissionRequestResultKind("no-result");
6368

src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.fasterxml.jackson.databind.ObjectMapper;
2626
import com.fasterxml.jackson.databind.node.ObjectNode;
2727
import com.github.copilot.sdk.json.PermissionRequestResult;
28+
import com.github.copilot.sdk.json.PermissionRequestResultKind;
2829
import com.github.copilot.sdk.json.PreToolUseHookOutput;
2930
import com.github.copilot.sdk.json.SessionHooks;
3031
import com.github.copilot.sdk.json.SessionLifecycleEvent;
@@ -341,6 +342,25 @@ void permissionRequestHandlerFails() throws Exception {
341342
assertEquals("denied-no-approval-rule-and-could-not-request-from-user", result.get("kind").asText());
342343
}
343344

345+
@Test
346+
void permissionRequestV2RejectsNoResult() throws Exception {
347+
CopilotSession session = createSession("s1");
348+
session.registerPermissionHandler((request, invocation) -> CompletableFuture
349+
.completedFuture(new PermissionRequestResult().setKind(PermissionRequestResultKind.NO_RESULT)));
350+
351+
ObjectNode params = MAPPER.createObjectNode();
352+
params.put("sessionId", "s1");
353+
params.putObject("permissionRequest");
354+
355+
invokeHandler("permission.request", "13", params);
356+
357+
// V2 protocol does not support NO_RESULT — the handler should fall through
358+
// to the exception path and respond with denied.
359+
JsonNode response = readResponse();
360+
JsonNode result = response.get("result").get("result");
361+
assertEquals("denied-no-approval-rule-and-could-not-request-from-user", result.get("kind").asText());
362+
}
363+
344364
// ===== userInput.request tests =====
345365

346366
@Test

src/test/java/com/github/copilot/sdk/SessionHandlerTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.fasterxml.jackson.databind.JsonNode;
1818
import com.fasterxml.jackson.databind.ObjectMapper;
1919
import com.github.copilot.sdk.json.PermissionRequestResult;
20+
import com.github.copilot.sdk.json.PermissionRequestResultKind;
2021
import com.github.copilot.sdk.json.SessionEndHookOutput;
2122
import com.github.copilot.sdk.json.SessionHooks;
2223
import com.github.copilot.sdk.json.SessionStartHookOutput;
@@ -115,6 +116,26 @@ void testHandlePermissionRequestHandlerSucceeds() throws Exception {
115116
assertEquals("allow", result.getKind());
116117
}
117118

119+
// ===== handlePermissionRequest: handler returns NO_RESULT (v3 path) =====
120+
121+
@Test
122+
void testHandlePermissionRequestNoResultPassesThrough() throws Exception {
123+
session.registerPermissionHandler((request, invocation) -> {
124+
var res = new PermissionRequestResult();
125+
res.setKind(PermissionRequestResultKind.NO_RESULT);
126+
return CompletableFuture.completedFuture(res);
127+
});
128+
129+
JsonNode data = MAPPER.valueToTree(Map.of("tool", "read_file"));
130+
131+
PermissionRequestResult result = session.handlePermissionRequest(data).get();
132+
133+
// In v3, NO_RESULT is a valid response — the session just returns it
134+
// and the caller (CopilotSession.executePermissionAndRespondAsync) decides
135+
// to skip sending the RPC response.
136+
assertEquals("no-result", result.getKind());
137+
}
138+
118139
// ===== handleUserInputRequest: no handler registered =====
119140

120141
@Test

0 commit comments

Comments
 (0)