Skip to content

Commit ba9c9e0

Browse files
Copilotedburns
andauthored
Fix hooks and permission dispatch for sub-agent sessions
When the CLI sends hooks.invoke or permission.request with a session ID that is not registered (e.g. for CLI-created sub-agent sessions), fall back to a registered session that has the appropriate handler. If no session with a handler exists, hooks returns null output (no-op) and permissions returns denied. Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/7e1546a6-1f1c-4d86-b564-5d98d7586407 Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
1 parent 50d42ea commit ba9c9e0

3 files changed

Lines changed: 110 additions & 5 deletions

File tree

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,15 @@ void registerPermissionHandler(PermissionHandler handler) {
12191219
permissionHandler.set(handler);
12201220
}
12211221

1222+
/**
1223+
* Returns whether this session has a permission handler registered.
1224+
*
1225+
* @return {@code true} if a permission handler is registered
1226+
*/
1227+
boolean hasPermissionHandler() {
1228+
return permissionHandler.get() != null;
1229+
}
1230+
12221231
/**
12231232
* Handles a permission request from the Copilot CLI.
12241233
* <p>
@@ -1350,6 +1359,15 @@ void registerHooks(SessionHooks hooks) {
13501359
hooksHandler.set(hooks);
13511360
}
13521361

1362+
/**
1363+
* Returns whether this session has hooks registered.
1364+
*
1365+
* @return {@code true} if a {@link SessionHooks} instance is registered
1366+
*/
1367+
boolean hasHooksHandler() {
1368+
return hooksHandler.get() != null;
1369+
}
1370+
13531371
/**
13541372
* Registers transform callbacks for system message sections.
13551373
* <p>

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNo
190190
JsonNode permissionRequest = params.get("permissionRequest");
191191

192192
CopilotSession session = sessions.get(sessionId);
193+
if (session == null) {
194+
session = findSessionWithPermissionHandler();
195+
}
193196
if (session == null) {
194197
var result = new PermissionRequestResult()
195198
.setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER);
@@ -292,7 +295,11 @@ private void handleHooksInvoke(JsonRpcClient rpc, String requestId, JsonNode par
292295

293296
CopilotSession session = sessions.get(sessionId);
294297
if (session == null) {
295-
rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session " + sessionId);
298+
session = findSessionWithHooks();
299+
}
300+
if (session == null) {
301+
// No registered session has hooks — return null output (no-op).
302+
rpc.sendResponse(Long.parseLong(requestId), Collections.singletonMap("output", null));
296303
return;
297304
}
298305

@@ -366,6 +373,42 @@ private void handleSystemMessageTransform(JsonRpcClient rpc, String requestId, J
366373
});
367374
}
368375

376+
/**
377+
* Finds a registered session that has hooks handlers.
378+
* <p>
379+
* Used as a fallback when the CLI sends a {@code hooks.invoke} call with a
380+
* session ID that is not in the registry (e.g. for CLI-created sub-agent
381+
* sessions).
382+
*
383+
* @return a session with hooks registered, or {@code null} if none exists
384+
*/
385+
private CopilotSession findSessionWithHooks() {
386+
for (CopilotSession s : sessions.values()) {
387+
if (s.hasHooksHandler()) {
388+
return s;
389+
}
390+
}
391+
return null;
392+
}
393+
394+
/**
395+
* Finds a registered session that has a permission handler.
396+
* <p>
397+
* Used as a fallback when the CLI sends a {@code permission.request} call with
398+
* a session ID that is not in the registry (e.g. for CLI-created sub-agent
399+
* sessions).
400+
*
401+
* @return a session with a permission handler, or {@code null} if none exists
402+
*/
403+
private CopilotSession findSessionWithPermissionHandler() {
404+
for (CopilotSession s : sessions.values()) {
405+
if (s.hasPermissionHandler()) {
406+
return s;
407+
}
408+
}
409+
return null;
410+
}
411+
369412
private void runAsync(Runnable task) {
370413
try {
371414
if (executor != null) {

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

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,8 @@ void toolCallHandlerFails() throws Exception {
295295
// ===== permission.request tests =====
296296

297297
@Test
298-
void permissionRequestWithUnknownSession() throws Exception {
298+
void permissionRequestWithUnknownSessionNoFallback() throws Exception {
299+
// No sessions registered at all — returns denied
299300
ObjectNode params = MAPPER.createObjectNode();
300301
params.put("sessionId", "nonexistent");
301302
params.putObject("permissionRequest");
@@ -307,6 +308,25 @@ void permissionRequestWithUnknownSession() throws Exception {
307308
assertEquals("denied-no-approval-rule-and-could-not-request-from-user", result.get("kind").asText());
308309
}
309310

311+
@Test
312+
void permissionRequestFallsBackToSessionWithHandler() throws Exception {
313+
// Register a session with a permission handler under a different ID
314+
CopilotSession session = createSession("parent-session");
315+
session.registerPermissionHandler((request, invocation) -> CompletableFuture
316+
.completedFuture(new PermissionRequestResult().setKind("allow")));
317+
318+
// Send permission.request with an unknown sub-agent session ID
319+
ObjectNode params = MAPPER.createObjectNode();
320+
params.put("sessionId", "sub-agent-session");
321+
params.putObject("permissionRequest");
322+
323+
invokeHandler("permission.request", "14", params);
324+
325+
JsonNode response = readResponse();
326+
JsonNode result = response.get("result").get("result");
327+
assertEquals("allow", result.get("kind").asText());
328+
}
329+
310330
@Test
311331
void permissionRequestWithHandler() throws Exception {
312332
CopilotSession session = createSession("s1");
@@ -453,7 +473,8 @@ void userInputRequestHandlerFails() throws Exception {
453473
// ===== hooks.invoke tests =====
454474

455475
@Test
456-
void hooksInvokeWithUnknownSession() throws Exception {
476+
void hooksInvokeWithUnknownSessionNoFallback() throws Exception {
477+
// No sessions registered at all — returns null output (no-op)
457478
ObjectNode params = MAPPER.createObjectNode();
458479
params.put("sessionId", "nonexistent");
459480
params.put("hookType", "preToolUse");
@@ -462,8 +483,31 @@ void hooksInvokeWithUnknownSession() throws Exception {
462483
invokeHandler("hooks.invoke", "30", params);
463484

464485
JsonNode response = readResponse();
465-
assertNotNull(response.get("error"));
466-
assertEquals(-32602, response.get("error").get("code").asInt());
486+
JsonNode output = response.get("result").get("output");
487+
assertTrue(output == null || output.isNull(), "Output should be null when no session has hooks");
488+
}
489+
490+
@Test
491+
void hooksInvokeFallsBackToSessionWithHooks() throws Exception {
492+
// Register a session with hooks under a different ID
493+
CopilotSession session = createSession("parent-session");
494+
session.registerHooks(new SessionHooks().setOnPreToolUse(
495+
(input, invocation) -> CompletableFuture.completedFuture(PreToolUseHookOutput.allow())));
496+
497+
// Send hooks.invoke with an unknown sub-agent session ID
498+
ObjectNode params = MAPPER.createObjectNode();
499+
params.put("sessionId", "sub-agent-session");
500+
params.put("hookType", "preToolUse");
501+
ObjectNode input = params.putObject("input");
502+
input.put("toolName", "glob");
503+
input.put("toolCallId", "tc-sub");
504+
505+
invokeHandler("hooks.invoke", "35", params);
506+
507+
JsonNode response = readResponse();
508+
JsonNode output = response.get("result").get("output");
509+
assertNotNull(output, "Should fall back to the registered session's hooks");
510+
assertEquals("allow", output.get("permissionDecision").asText());
467511
}
468512

469513
@Test

0 commit comments

Comments
 (0)