Skip to content

Commit b0c1b8e

Browse files
Add no-result permission handling
Add a public no-result permission outcome across the SDKs, default TypeScript extension joinSession() to it, and make v2 permission adapters fail loudly if asked to return no-result. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0dd6bfb commit b0c1b8e

23 files changed

Lines changed: 216 additions & 15 deletions

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 == PermissionRequestResultKind.NoResult)
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/PermissionHandlers.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ public static class PermissionHandler
1010
/// <summary>A <see cref="PermissionRequestHandler"/> that approves all permission requests.</summary>
1111
public static PermissionRequestHandler ApproveAll { get; } =
1212
(_, _) => Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved });
13+
14+
/// <summary>A <see cref="PermissionRequestHandler"/> that leaves permission requests unanswered.</summary>
15+
public static PermissionRequestHandler NoResult { get; } =
16+
(_, _) => Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.NoResult });
1317
}

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 == PermissionRequestResultKind.NoResult)
471+
{
472+
return;
473+
}
470474
await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, result);
471475
}
472476
catch (Exception)

dotnet/src/Types.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,9 @@ public class ToolInvocation
283283
/// <summary>Gets the kind indicating the permission was denied interactively by the user.</summary>
284284
public static PermissionRequestResultKind DeniedInteractivelyByUser { get; } = new("denied-interactively-by-user");
285285

286+
/// <summary>Gets the kind indicating the SDK should not answer the pending permission request.</summary>
287+
public static PermissionRequestResultKind NoResult { get; } = new("no-result");
288+
286289
/// <summary>Gets the underlying string value of this <see cref="PermissionRequestResultKind"/>.</summary>
287290
public string Value => _value ?? string.Empty;
288291

@@ -350,6 +353,7 @@ public class PermissionRequestResult
350353
/// <item><description><c>"denied-by-rules"</c> — denied by configured permission rules.</description></item>
351354
/// <item><description><c>"denied-interactively-by-user"</c> — the user explicitly denied the request.</description></item>
352355
/// <item><description><c>"denied-no-approval-rule-and-could-not-request-from-user"</c> — no rule matched and user approval was unavailable.</description></item>
356+
/// <item><description><c>"no-result"</c> — leave the pending permission request unanswered.</description></item>
353357
/// </list>
354358
/// </summary>
355359
[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", PermissionRequestResultKind.NoResult.Value);
2425
}
2526

2627
[Fact]
@@ -115,6 +116,7 @@ public void JsonRoundTrip_PreservesAllKinds()
115116
PermissionRequestResultKind.DeniedByRules,
116117
PermissionRequestResultKind.DeniedCouldNotRequestFromUser,
117118
PermissionRequestResultKind.DeniedInteractivelyByUser,
119+
PermissionRequestResultKind.NoResult,
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 == PermissionRequestResultKindNoResult {
1537+
return nil, &jsonrpc2.Error{Code: -32603, Message: noResultPermissionV2Error}
1538+
}
15341539

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

go/permissions.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ package copilot
44
var PermissionHandler = struct {
55
// ApproveAll approves all permission requests.
66
ApproveAll PermissionHandlerFunc
7+
// NoResult leaves the pending permission request unanswered.
8+
NoResult PermissionHandlerFunc
79
}{
810
ApproveAll: func(_ PermissionRequest, _ PermissionInvocation) (PermissionRequestResult, error) {
911
return PermissionRequestResult{Kind: PermissionRequestResultKindApproved}, nil
1012
},
13+
NoResult: func(_ PermissionRequest, _ PermissionInvocation) (PermissionRequestResult, error) {
14+
return PermissionRequestResult{Kind: PermissionRequestResultKindNoResult}, nil
15+
},
1116
}

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 == PermissionRequestResultKindNoResult {
566+
return
567+
}
565568

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

go/types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ const (
123123

124124
// PermissionRequestResultKindDeniedInteractivelyByUser indicates the permission was denied interactively by the user.
125125
PermissionRequestResultKindDeniedInteractivelyByUser PermissionRequestResultKind = "denied-interactively-by-user"
126+
127+
// PermissionRequestResultKindNoResult indicates the SDK should not answer the pending permission request.
128+
PermissionRequestResultKindNoResult PermissionRequestResultKind = "no-result"
126129
)
127130

128131
// PermissionRequestResult represents the result of a permission request

go/types_test.go

Lines changed: 12 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", PermissionRequestResultKindNoResult, "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", PermissionRequestResultKindNoResult},
4547
{"Custom", PermissionRequestResultKind("custom")},
4648
}
4749

@@ -89,3 +91,13 @@ func TestPermissionRequestResult_JSONSerialize(t *testing.T) {
8991
t.Errorf("expected %s, got %s", expected, string(data))
9092
}
9193
}
94+
95+
func TestPermissionHandler_NoResult(t *testing.T) {
96+
result, err := PermissionHandler.NoResult(PermissionRequest{}, PermissionInvocation{})
97+
if err != nil {
98+
t.Fatalf("expected no error, got %v", err)
99+
}
100+
if result.Kind != PermissionRequestResultKindNoResult {
101+
t.Errorf("expected %q, got %q", PermissionRequestResultKindNoResult, result.Kind)
102+
}
103+
}

0 commit comments

Comments
 (0)