Skip to content

Commit 3e2d2b2

Browse files
Breaking change: deny all permissions by default (#509)
1 parent 19d8cea commit 3e2d2b2

40 files changed

+516
-74
lines changed

docs/compatibility.md

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -124,20 +124,13 @@ The `--share` option is not available via SDK. Workarounds:
124124

125125
### Permission Control
126126

127+
The SDK uses a **deny-by-default** permission model. All permission requests (file writes, shell commands, URL fetches, etc.) are denied unless your app provides an `onPermissionRequest` handler.
128+
127129
Instead of `--allow-all-paths` or `--yolo`, use the permission handler:
128130

129131
```typescript
130132
const session = await client.createSession({
131-
onPermissionRequest: async (request) => {
132-
// Auto-approve everything (equivalent to --yolo)
133-
return { approved: true };
134-
135-
// Or implement custom logic
136-
if (request.kind === "shell") {
137-
return { approved: request.command.startsWith("git") };
138-
}
139-
return { approved: true };
140-
},
133+
onPermissionRequest: approveAll,
141134
});
142135
```
143136

dotnet/samples/Chat.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using GitHub.Copilot.SDK;
22

33
await using var client = new CopilotClient();
4-
await using var session = await client.CreateSessionAsync();
4+
await using var session = await client.CreateSessionAsync(new SessionConfig
5+
{
6+
OnPermissionRequest = PermissionHandler.ApproveAll
7+
});
58

69
using var _ = session.On(evt =>
710
{

dotnet/src/Client.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = nul
377377
config?.AvailableTools,
378378
config?.ExcludedTools,
379379
config?.Provider,
380-
config?.OnPermissionRequest != null ? true : null,
380+
(bool?)true,
381381
config?.OnUserInputRequest != null ? true : null,
382382
hasHooks ? true : null,
383383
config?.WorkingDirectory,
@@ -461,7 +461,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
461461
config?.AvailableTools,
462462
config?.ExcludedTools,
463463
config?.Provider,
464-
config?.OnPermissionRequest != null ? true : null,
464+
(bool?)true,
465465
config?.OnUserInputRequest != null ? true : null,
466466
hasHooks ? true : null,
467467
config?.WorkingDirectory,

dotnet/src/PermissionHandlers.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
namespace GitHub.Copilot.SDK;
6+
7+
/// <summary>Provides pre-built <see cref="PermissionRequestHandler"/> implementations.</summary>
8+
public static class PermissionHandler
9+
{
10+
/// <summary>A <see cref="PermissionRequestHandler"/> that approves all permission requests.</summary>
11+
public static PermissionRequestHandler ApproveAll { get; } =
12+
(_, _) => Task.FromResult(new PermissionRequestResult { Kind = "approved" });
13+
}

dotnet/src/Session.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public partial class CopilotSession : IAsyncDisposable
4747
private readonly HashSet<SessionEventHandler> _eventHandlers = new();
4848
private readonly Dictionary<string, AIFunction> _toolHandlers = new();
4949
private readonly JsonRpc _rpc;
50-
private PermissionHandler? _permissionHandler;
50+
private PermissionRequestHandler? _permissionHandler;
5151
private readonly SemaphoreSlim _permissionHandlerLock = new(1, 1);
5252
private UserInputHandler? _userInputHandler;
5353
private readonly SemaphoreSlim _userInputHandlerLock = new(1, 1);
@@ -292,7 +292,7 @@ internal void RegisterTools(ICollection<AIFunction> tools)
292292
/// When the assistant needs permission to perform certain actions (e.g., file operations),
293293
/// this handler is called to approve or deny the request.
294294
/// </remarks>
295-
internal void RegisterPermissionHandler(PermissionHandler handler)
295+
internal void RegisterPermissionHandler(PermissionRequestHandler handler)
296296
{
297297
_permissionHandlerLock.Wait();
298298
try
@@ -313,7 +313,7 @@ internal void RegisterPermissionHandler(PermissionHandler handler)
313313
internal async Task<PermissionRequestResult> HandlePermissionRequestAsync(JsonElement permissionRequestData)
314314
{
315315
await _permissionHandlerLock.WaitAsync();
316-
PermissionHandler? handler;
316+
PermissionRequestHandler? handler;
317317
try
318318
{
319319
handler = _permissionHandler;

dotnet/src/Types.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ public class PermissionInvocation
166166
public string SessionId { get; set; } = string.Empty;
167167
}
168168

169-
public delegate Task<PermissionRequestResult> PermissionHandler(PermissionRequest request, PermissionInvocation invocation);
169+
public delegate Task<PermissionRequestResult> PermissionRequestHandler(PermissionRequest request, PermissionInvocation invocation);
170170

171171
// ============================================================================
172172
// User Input Handler Types
@@ -793,7 +793,7 @@ protected SessionConfig(SessionConfig? other)
793793
/// Handler for permission requests from the server.
794794
/// When provided, the server will call this handler to request permission for operations.
795795
/// </summary>
796-
public PermissionHandler? OnPermissionRequest { get; set; }
796+
public PermissionRequestHandler? OnPermissionRequest { get; set; }
797797

798798
/// <summary>
799799
/// Handler for user input requests from the agent.
@@ -932,7 +932,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
932932
/// Handler for permission requests from the server.
933933
/// When provided, the server will call this handler to request permission for operations.
934934
/// </summary>
935-
public PermissionHandler? OnPermissionRequest { get; set; }
935+
public PermissionRequestHandler? OnPermissionRequest { get; set; }
936936

937937
/// <summary>
938938
/// Handler for user input requests from the agent.

dotnet/test/HooksTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public async Task Should_Invoke_PreToolUse_Hook_When_Model_Runs_A_Tool()
1717
CopilotSession? session = null;
1818
session = await Client.CreateSessionAsync(new SessionConfig
1919
{
20+
OnPermissionRequest = PermissionHandler.ApproveAll,
2021
Hooks = new SessionHooks
2122
{
2223
OnPreToolUse = (input, invocation) =>
@@ -52,6 +53,7 @@ public async Task Should_Invoke_PostToolUse_Hook_After_Model_Runs_A_Tool()
5253
CopilotSession? session = null;
5354
session = await Client.CreateSessionAsync(new SessionConfig
5455
{
56+
OnPermissionRequest = PermissionHandler.ApproveAll,
5557
Hooks = new SessionHooks
5658
{
5759
OnPostToolUse = (input, invocation) =>
@@ -89,6 +91,7 @@ public async Task Should_Invoke_Both_PreToolUse_And_PostToolUse_Hooks_For_Single
8991

9092
var session = await Client.CreateSessionAsync(new SessionConfig
9193
{
94+
OnPermissionRequest = PermissionHandler.ApproveAll,
9295
Hooks = new SessionHooks
9396
{
9497
OnPreToolUse = (input, invocation) =>
@@ -130,6 +133,7 @@ public async Task Should_Deny_Tool_Execution_When_PreToolUse_Returns_Deny()
130133

131134
var session = await Client.CreateSessionAsync(new SessionConfig
132135
{
136+
OnPermissionRequest = PermissionHandler.ApproveAll,
133137
Hooks = new SessionHooks
134138
{
135139
OnPreToolUse = (input, invocation) =>

dotnet/test/McpAndAgentsTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,8 @@ public async Task Should_Pass_Literal_Env_Values_To_Mcp_Server_Subprocess()
279279

280280
var session = await Client.CreateSessionAsync(new SessionConfig
281281
{
282-
McpServers = mcpServers
282+
McpServers = mcpServers,
283+
OnPermissionRequest = PermissionHandler.ApproveAll,
283284
});
284285

285286
Assert.Matches(@"^[a-f0-9-]+$", session.SessionId);

dotnet/test/PermissionTests.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,30 @@ await session.SendAsync(new MessageOptions
7070
Assert.Equal("protected content", content);
7171
}
7272

73+
[Fact]
74+
public async Task Should_Deny_Tool_Operations_By_Default_When_No_Handler_Is_Provided()
75+
{
76+
var session = await Client.CreateSessionAsync(new SessionConfig());
77+
var permissionDenied = false;
78+
79+
session.On(evt =>
80+
{
81+
if (evt is ToolExecutionCompleteEvent toolEvt &&
82+
!toolEvt.Data.Success &&
83+
toolEvt.Data.Error?.Message.Contains("Permission denied") == true)
84+
{
85+
permissionDenied = true;
86+
}
87+
});
88+
89+
await session.SendAndWaitAsync(new MessageOptions
90+
{
91+
Prompt = "Run 'node --version'"
92+
});
93+
94+
Assert.True(permissionDenied, "Expected a tool.execution_complete event with Permission denied result");
95+
}
96+
7397
[Fact]
7498
public async Task Should_Work_Without_Permission_Handler__Default_Behavior_()
7599
{
@@ -161,6 +185,37 @@ await session.SendAsync(new MessageOptions
161185
Assert.Matches("fail|cannot|unable|permission", message?.Data.Content?.ToLowerInvariant() ?? string.Empty);
162186
}
163187

188+
[Fact]
189+
public async Task Should_Deny_Tool_Operations_By_Default_When_No_Handler_Is_Provided_After_Resume()
190+
{
191+
var session1 = await Client.CreateSessionAsync(new SessionConfig
192+
{
193+
OnPermissionRequest = PermissionHandler.ApproveAll
194+
});
195+
var sessionId = session1.SessionId;
196+
await session1.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" });
197+
198+
var session2 = await Client.ResumeSessionAsync(sessionId);
199+
var permissionDenied = false;
200+
201+
session2.On(evt =>
202+
{
203+
if (evt is ToolExecutionCompleteEvent toolEvt &&
204+
!toolEvt.Data.Success &&
205+
toolEvt.Data.Error?.Message.Contains("Permission denied") == true)
206+
{
207+
permissionDenied = true;
208+
}
209+
});
210+
211+
await session2.SendAndWaitAsync(new MessageOptions
212+
{
213+
Prompt = "Run 'node --version'"
214+
});
215+
216+
Assert.True(permissionDenied, "Expected a tool.execution_complete event with Permission denied result");
217+
}
218+
164219
[Fact]
165220
public async Task Should_Receive_ToolCallId_In_Permission_Requests()
166221
{

dotnet/test/SessionTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,10 @@ public async Task Should_Receive_Session_Events()
333333
[Fact]
334334
public async Task Send_Returns_Immediately_While_Events_Stream_In_Background()
335335
{
336-
var session = await Client.CreateSessionAsync();
336+
var session = await Client.CreateSessionAsync(new SessionConfig
337+
{
338+
OnPermissionRequest = PermissionHandler.ApproveAll,
339+
});
337340
var events = new List<string>();
338341

339342
session.On(evt => events.Add(evt.Type));

0 commit comments

Comments
 (0)