Skip to content

Commit df59a0e

Browse files
Add no-result permission handling for extensions (#802)
1 parent 0dd6bfb commit df59a0e

22 files changed

+224
-65
lines changed

dotnet/src/Client.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ namespace GitHub.Copilot.SDK;
5454
/// </example>
5555
public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
5656
{
57+
internal const string NoResultPermissionV2ErrorMessage =
58+
"Permission handlers cannot return 'no-result' when connected to a protocol v2 server.";
59+
5760
/// <summary>
5861
/// Minimum protocol version this SDK can communicate with.
5962
/// </summary>
@@ -1394,8 +1397,16 @@ public async Task<PermissionRequestResponseV2> OnPermissionRequestV2(string sess
13941397
try
13951398
{
13961399
var result = await session.HandlePermissionRequestAsync(permissionRequest);
1400+
if (result.Kind == new PermissionRequestResultKind("no-result"))
1401+
{
1402+
throw new InvalidOperationException(NoResultPermissionV2ErrorMessage);
1403+
}
13971404
return new PermissionRequestResponseV2(result);
13981405
}
1406+
catch (InvalidOperationException ex) when (ex.Message == NoResultPermissionV2ErrorMessage)
1407+
{
1408+
throw;
1409+
}
13991410
catch (Exception)
14001411
{
14011412
return new PermissionRequestResponseV2(new PermissionRequestResult

dotnet/src/Session.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,10 @@ private async Task ExecutePermissionAndRespondAsync(string requestId, Permission
467467
};
468468

469469
var result = await handler(permissionRequest, invocation);
470+
if (result.Kind == new PermissionRequestResultKind("no-result"))
471+
{
472+
return;
473+
}
470474
await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, result);
471475
}
472476
catch (Exception)

dotnet/src/Types.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ public class PermissionRequestResult
350350
/// <item><description><c>"denied-by-rules"</c> — denied by configured permission rules.</description></item>
351351
/// <item><description><c>"denied-interactively-by-user"</c> — the user explicitly denied the request.</description></item>
352352
/// <item><description><c>"denied-no-approval-rule-and-could-not-request-from-user"</c> — no rule matched and user approval was unavailable.</description></item>
353+
/// <item><description><c>"no-result"</c> — leave the pending permission request unanswered.</description></item>
353354
/// </list>
354355
/// </summary>
355356
[JsonPropertyName("kind")]

dotnet/test/PermissionRequestResultKindTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public void WellKnownKinds_HaveExpectedValues()
2121
Assert.Equal("denied-by-rules", PermissionRequestResultKind.DeniedByRules.Value);
2222
Assert.Equal("denied-no-approval-rule-and-could-not-request-from-user", PermissionRequestResultKind.DeniedCouldNotRequestFromUser.Value);
2323
Assert.Equal("denied-interactively-by-user", PermissionRequestResultKind.DeniedInteractivelyByUser.Value);
24+
Assert.Equal("no-result", new PermissionRequestResultKind("no-result").Value);
2425
}
2526

2627
[Fact]
@@ -115,6 +116,7 @@ public void JsonRoundTrip_PreservesAllKinds()
115116
PermissionRequestResultKind.DeniedByRules,
116117
PermissionRequestResultKind.DeniedCouldNotRequestFromUser,
117118
PermissionRequestResultKind.DeniedInteractivelyByUser,
119+
new PermissionRequestResultKind("no-result"),
118120
};
119121

120122
foreach (var kind in kinds)

go/client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ import (
5151
"github.com/github/copilot-sdk/go/rpc"
5252
)
5353

54+
const noResultPermissionV2Error = "permission handlers cannot return 'no-result' when connected to a protocol v2 server"
55+
5456
// Client manages the connection to the Copilot CLI server and provides session management.
5557
//
5658
// The Client can either spawn a CLI server process or connect to an existing server.
@@ -1531,6 +1533,9 @@ func (c *Client) handlePermissionRequestV2(req permissionRequestV2) (*permission
15311533
},
15321534
}, nil
15331535
}
1536+
if result.Kind == "no-result" {
1537+
return nil, &jsonrpc2.Error{Code: -32603, Message: noResultPermissionV2Error}
1538+
}
15341539

15351540
return &permissionResponseV2{Result: result}, nil
15361541
}

go/session.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,9 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques
562562
})
563563
return
564564
}
565+
if result.Kind == "no-result" {
566+
return
567+
}
565568

566569
s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.SessionPermissionsHandlePendingPermissionRequestParams{
567570
RequestID: requestID,

go/types_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func TestPermissionRequestResultKind_Constants(t *testing.T) {
1515
{"DeniedByRules", PermissionRequestResultKindDeniedByRules, "denied-by-rules"},
1616
{"DeniedCouldNotRequestFromUser", PermissionRequestResultKindDeniedCouldNotRequestFromUser, "denied-no-approval-rule-and-could-not-request-from-user"},
1717
{"DeniedInteractivelyByUser", PermissionRequestResultKindDeniedInteractivelyByUser, "denied-interactively-by-user"},
18+
{"NoResult", PermissionRequestResultKind("no-result"), "no-result"},
1819
}
1920

2021
for _, tt := range tests {
@@ -42,6 +43,7 @@ func TestPermissionRequestResult_JSONRoundTrip(t *testing.T) {
4243
{"DeniedByRules", PermissionRequestResultKindDeniedByRules},
4344
{"DeniedCouldNotRequestFromUser", PermissionRequestResultKindDeniedCouldNotRequestFromUser},
4445
{"DeniedInteractivelyByUser", PermissionRequestResultKindDeniedInteractivelyByUser},
46+
{"NoResult", PermissionRequestResultKind("no-result")},
4547
{"Custom", PermissionRequestResultKind("custom")},
4648
}
4749

nodejs/docs/agent-author.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,9 @@ Discovery rules:
5959
## Minimal Skeleton
6060

6161
```js
62-
import { approveAll } from "@github/copilot-sdk";
6362
import { joinSession } from "@github/copilot-sdk/extension";
6463

6564
await joinSession({
66-
onPermissionRequest: approveAll, // Required — handle permission requests
6765
tools: [], // Optional — custom tools
6866
hooks: {}, // Optional — lifecycle hooks
6967
});

nodejs/docs/examples.md

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ A practical guide to writing extensions using the `@github/copilot-sdk` extensio
77
Every extension starts with the same boilerplate:
88

99
```js
10-
import { approveAll } from "@github/copilot-sdk";
1110
import { joinSession } from "@github/copilot-sdk/extension";
1211

1312
const session = await joinSession({
14-
onPermissionRequest: approveAll,
1513
hooks: { /* ... */ },
1614
tools: [ /* ... */ ],
1715
});
@@ -33,7 +31,6 @@ Use `session.log()` to surface messages to the user in the CLI timeline:
3331

3432
```js
3533
const session = await joinSession({
36-
onPermissionRequest: approveAll,
3734
hooks: {
3835
onSessionStart: async () => {
3936
await session.log("My extension loaded");
@@ -383,7 +380,6 @@ function copyToClipboard(text) {
383380
}
384381
385382
const session = await joinSession({
386-
onPermissionRequest: approveAll,
387383
hooks: {
388384
onUserPromptSubmitted: async (input) => {
389385
if (/\\bcopy\\b/i.test(input.prompt)) {
@@ -425,15 +421,12 @@ Correlate `tool.execution_start` / `tool.execution_complete` events by `toolCall
425421
```js
426422
import { existsSync, watchFile, readFileSync } from "node:fs";
427423
import { join } from "node:path";
428-
import { approveAll } from "@github/copilot-sdk";
429424
import { joinSession } from "@github/copilot-sdk/extension";
430425
431426
const agentEdits = new Set(); // toolCallIds for in-flight agent edits
432427
const recentAgentPaths = new Set(); // paths recently written by the agent
433428
434-
const session = await joinSession({
435-
onPermissionRequest: approveAll,
436-
});
429+
const session = await joinSession();
437430
438431
const workspace = session.workspacePath; // e.g. ~/.copilot/session-state/<id>
439432
if (workspace) {
@@ -480,14 +473,11 @@ Filter out agent edits by tracking `tool.execution_start` / `tool.execution_comp
480473
```js
481474
import { watch, readFileSync, statSync } from "node:fs";
482475
import { join, relative, resolve } from "node:path";
483-
import { approveAll } from "@github/copilot-sdk";
484476
import { joinSession } from "@github/copilot-sdk/extension";
485477
486478
const agentEditPaths = new Set();
487479
488-
const session = await joinSession({
489-
onPermissionRequest: approveAll,
490-
});
480+
const session = await joinSession();
491481
492482
const cwd = process.cwd();
493483
const IGNORE = new Set(["node_modules", ".git", "dist"]);
@@ -582,7 +572,6 @@ Register `onUserInputRequest` to enable the agent's `ask_user` tool:
582572
583573
```js
584574
const session = await joinSession({
585-
onPermissionRequest: approveAll,
586575
onUserInputRequest: async (request) => {
587576
// request.question has the agent's question
588577
// request.choices has the options (if multiple choice)
@@ -599,7 +588,6 @@ An extension that combines tools, hooks, and events.
599588

600589
```js
601590
import { execFile, exec } from "node:child_process";
602-
import { approveAll } from "@github/copilot-sdk";
603591
import { joinSession } from "@github/copilot-sdk/extension";
604592
605593
const isWindows = process.platform === "win32";
@@ -617,7 +605,6 @@ function openInEditor(filePath) {
617605
}
618606
619607
const session = await joinSession({
620-
onPermissionRequest: approveAll,
621608
hooks: {
622609
onUserPromptSubmitted: async (input) => {
623610
if (/\\bcopy this\\b/i.test(input.prompt)) {

nodejs/docs/extensions.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,9 @@ Extensions add custom tools, hooks, and behaviors to the Copilot CLI. They run a
3939
Extensions use `@github/copilot-sdk` for all interactions with the CLI:
4040

4141
```js
42-
import { approveAll } from "@github/copilot-sdk";
4342
import { joinSession } from "@github/copilot-sdk/extension";
4443

4544
const session = await joinSession({
46-
onPermissionRequest: approveAll,
4745
tools: [
4846
/* ... */
4947
],

0 commit comments

Comments
 (0)