Skip to content

Commit 279f6c4

Browse files
Require permission handler on session creation (#554)
* Improve discoverability of permission handler being required * Update TypeScript tests * Formatting * Test updates * Formatting * More doc updates * Fix E2E tests: add permission handler to all session calls across Python, Go, C# - Add on_permission_request/OnPermissionRequest to all Python and Go E2E test create_session/resume_session calls - Fix pre-existing deny tests: restore 'denied-interactively-by-user' kind (was accidentally changed by blanket replace) - Fix session-resume scenario builds for Go and C# Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix docs validation: add permission handler to getting-started.md examples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix remaining E2E tests missing permission handler - Go: client_test.go CreateSession calls need OnPermissionRequest - Python: test_client.py create_session call needs config arg Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f0909a7 commit 279f6c4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+914
-470
lines changed

docs/getting-started.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,14 +1193,14 @@ Once the CLI is running in server mode, configure your SDK client to connect to
11931193
<summary><strong>Node.js / TypeScript</strong></summary>
11941194

11951195
```typescript
1196-
import { CopilotClient } from "@github/copilot-sdk";
1196+
import { CopilotClient, approveAll } from "@github/copilot-sdk";
11971197

11981198
const client = new CopilotClient({
11991199
cliUrl: "localhost:4321"
12001200
});
12011201

12021202
// Use the client normally
1203-
const session = await client.createSession();
1203+
const session = await client.createSession({ onPermissionRequest: approveAll });
12041204
// ...
12051205
```
12061206

@@ -1210,15 +1210,15 @@ const session = await client.createSession();
12101210
<summary><strong>Python</strong></summary>
12111211

12121212
```python
1213-
from copilot import CopilotClient
1213+
from copilot import CopilotClient, PermissionHandler
12141214

12151215
client = CopilotClient({
12161216
"cli_url": "localhost:4321"
12171217
})
12181218
await client.start()
12191219

12201220
# Use the client normally
1221-
session = await client.create_session()
1221+
session = await client.create_session({"on_permission_request": PermissionHandler.approve_all})
12221222
# ...
12231223
```
12241224

@@ -1241,7 +1241,9 @@ if err := client.Start(ctx); err != nil {
12411241
defer client.Stop()
12421242

12431243
// Use the client normally
1244-
session, err := client.CreateSession(ctx, nil)
1244+
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
1245+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
1246+
})
12451247
// ...
12461248
```
12471249

@@ -1260,7 +1262,10 @@ using var client = new CopilotClient(new CopilotClientOptions
12601262
});
12611263

12621264
// Use the client normally
1263-
await using var session = await client.CreateSessionAsync();
1265+
await using var session = await client.CreateSessionAsync(new()
1266+
{
1267+
OnPermissionRequest = PermissionHandler.ApproveAll
1268+
});
12641269
// ...
12651270
```
12661271

dotnet/src/Client.cs

Lines changed: 74 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ namespace GitHub.Copilot.SDK;
3838
/// await using var client = new CopilotClient();
3939
///
4040
/// // Create a session
41-
/// await using var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-4" });
41+
/// await using var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll, Model = "gpt-4" });
4242
///
4343
/// // Handle events
4444
/// using var subscription = session.On(evt =>
@@ -340,10 +340,9 @@ private async Task CleanupConnectionAsync(List<Exception>? errors)
340340
/// <summary>
341341
/// Creates a new Copilot session with the specified configuration.
342342
/// </summary>
343-
/// <param name="config">Configuration for the session. If null, default settings are used.</param>
343+
/// <param name="config">Configuration for the session, including the required <see cref="SessionConfig.OnPermissionRequest"/> handler.</param>
344344
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
345345
/// <returns>A task that resolves to provide the <see cref="CopilotSession"/>.</returns>
346-
/// <exception cref="InvalidOperationException">Thrown when the client is not connected and AutoStart is disabled, or when a session with the same ID already exists.</exception>
347346
/// <remarks>
348347
/// Sessions maintain conversation state, handle events, and manage tool execution.
349348
/// If the client is not connected and <see cref="CopilotClientOptions.AutoStart"/> is enabled (default),
@@ -352,21 +351,29 @@ private async Task CleanupConnectionAsync(List<Exception>? errors)
352351
/// <example>
353352
/// <code>
354353
/// // Basic session
355-
/// var session = await client.CreateSessionAsync();
354+
/// var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });
356355
///
357356
/// // Session with model and tools
358-
/// var session = await client.CreateSessionAsync(new SessionConfig
357+
/// var session = await client.CreateSessionAsync(new()
359358
/// {
359+
/// OnPermissionRequest = PermissionHandler.ApproveAll,
360360
/// Model = "gpt-4",
361361
/// Tools = [AIFunctionFactory.Create(MyToolMethod)]
362362
/// });
363363
/// </code>
364364
/// </example>
365-
public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = null, CancellationToken cancellationToken = default)
365+
public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, CancellationToken cancellationToken = default)
366366
{
367+
if (config.OnPermissionRequest == null)
368+
{
369+
throw new ArgumentException(
370+
"An OnPermissionRequest handler is required when creating a session. " +
371+
"For example, to allow all permissions, use CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });");
372+
}
373+
367374
var connection = await EnsureConnectedAsync(cancellationToken);
368375

369-
var hasHooks = config?.Hooks != null && (
376+
var hasHooks = config.Hooks != null && (
370377
config.Hooks.OnPreToolUse != null ||
371378
config.Hooks.OnPostToolUse != null ||
372379
config.Hooks.OnUserPromptSubmitted != null ||
@@ -375,42 +382,39 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = nul
375382
config.Hooks.OnErrorOccurred != null);
376383

377384
var request = new CreateSessionRequest(
378-
config?.Model,
379-
config?.SessionId,
380-
config?.ClientName,
381-
config?.ReasoningEffort,
382-
config?.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
383-
config?.SystemMessage,
384-
config?.AvailableTools,
385-
config?.ExcludedTools,
386-
config?.Provider,
385+
config.Model,
386+
config.SessionId,
387+
config.ClientName,
388+
config.ReasoningEffort,
389+
config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
390+
config.SystemMessage,
391+
config.AvailableTools,
392+
config.ExcludedTools,
393+
config.Provider,
387394
(bool?)true,
388-
config?.OnUserInputRequest != null ? true : null,
395+
config.OnUserInputRequest != null ? true : null,
389396
hasHooks ? true : null,
390-
config?.WorkingDirectory,
391-
config?.Streaming == true ? true : null,
392-
config?.McpServers,
397+
config.WorkingDirectory,
398+
config.Streaming is true ? true : null,
399+
config.McpServers,
393400
"direct",
394-
config?.CustomAgents,
395-
config?.ConfigDir,
396-
config?.SkillDirectories,
397-
config?.DisabledSkills,
398-
config?.InfiniteSessions);
401+
config.CustomAgents,
402+
config.ConfigDir,
403+
config.SkillDirectories,
404+
config.DisabledSkills,
405+
config.InfiniteSessions);
399406

400407
var response = await InvokeRpcAsync<CreateSessionResponse>(
401408
connection.Rpc, "session.create", [request], cancellationToken);
402409

403410
var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath);
404-
session.RegisterTools(config?.Tools ?? []);
405-
if (config?.OnPermissionRequest != null)
406-
{
407-
session.RegisterPermissionHandler(config.OnPermissionRequest);
408-
}
409-
if (config?.OnUserInputRequest != null)
411+
session.RegisterTools(config.Tools ?? []);
412+
session.RegisterPermissionHandler(config.OnPermissionRequest);
413+
if (config.OnUserInputRequest != null)
410414
{
411415
session.RegisterUserInputHandler(config.OnUserInputRequest);
412416
}
413-
if (config?.Hooks != null)
417+
if (config.Hooks != null)
414418
{
415419
session.RegisterHooks(config.Hooks);
416420
}
@@ -427,9 +431,10 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = nul
427431
/// Resumes an existing Copilot session with the specified configuration.
428432
/// </summary>
429433
/// <param name="sessionId">The ID of the session to resume.</param>
430-
/// <param name="config">Configuration for the resumed session. If null, default settings are used.</param>
434+
/// <param name="config">Configuration for the resumed session, including the required <see cref="ResumeSessionConfig.OnPermissionRequest"/> handler.</param>
431435
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
432436
/// <returns>A task that resolves to provide the <see cref="CopilotSession"/>.</returns>
437+
/// <exception cref="ArgumentException">Thrown when <see cref="ResumeSessionConfig.OnPermissionRequest"/> is not set.</exception>
433438
/// <exception cref="InvalidOperationException">Thrown when the session does not exist or the client is not connected.</exception>
434439
/// <remarks>
435440
/// This allows you to continue a previous conversation, maintaining all conversation history.
@@ -438,20 +443,28 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = nul
438443
/// <example>
439444
/// <code>
440445
/// // Resume a previous session
441-
/// var session = await client.ResumeSessionAsync("session-123");
446+
/// var session = await client.ResumeSessionAsync("session-123", new() { OnPermissionRequest = PermissionHandler.ApproveAll });
442447
///
443448
/// // Resume with new tools
444-
/// var session = await client.ResumeSessionAsync("session-123", new ResumeSessionConfig
449+
/// var session = await client.ResumeSessionAsync("session-123", new()
445450
/// {
451+
/// OnPermissionRequest = PermissionHandler.ApproveAll,
446452
/// Tools = [AIFunctionFactory.Create(MyNewToolMethod)]
447453
/// });
448454
/// </code>
449455
/// </example>
450-
public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSessionConfig? config = null, CancellationToken cancellationToken = default)
456+
public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSessionConfig config, CancellationToken cancellationToken = default)
451457
{
458+
if (config.OnPermissionRequest == null)
459+
{
460+
throw new ArgumentException(
461+
"An OnPermissionRequest handler is required when resuming a session. " +
462+
"For example, to allow all permissions, use new() { OnPermissionRequest = PermissionHandler.ApproveAll }.");
463+
}
464+
452465
var connection = await EnsureConnectedAsync(cancellationToken);
453466

454-
var hasHooks = config?.Hooks != null && (
467+
var hasHooks = config.Hooks != null && (
455468
config.Hooks.OnPreToolUse != null ||
456469
config.Hooks.OnPostToolUse != null ||
457470
config.Hooks.OnUserPromptSubmitted != null ||
@@ -461,42 +474,39 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
461474

462475
var request = new ResumeSessionRequest(
463476
sessionId,
464-
config?.ClientName,
465-
config?.Model,
466-
config?.ReasoningEffort,
467-
config?.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
468-
config?.SystemMessage,
469-
config?.AvailableTools,
470-
config?.ExcludedTools,
471-
config?.Provider,
477+
config.ClientName,
478+
config.Model,
479+
config.ReasoningEffort,
480+
config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
481+
config.SystemMessage,
482+
config.AvailableTools,
483+
config.ExcludedTools,
484+
config.Provider,
472485
(bool?)true,
473-
config?.OnUserInputRequest != null ? true : null,
486+
config.OnUserInputRequest != null ? true : null,
474487
hasHooks ? true : null,
475-
config?.WorkingDirectory,
476-
config?.ConfigDir,
477-
config?.DisableResume == true ? true : null,
478-
config?.Streaming == true ? true : null,
479-
config?.McpServers,
488+
config.WorkingDirectory,
489+
config.ConfigDir,
490+
config.DisableResume is true ? true : null,
491+
config.Streaming is true ? true : null,
492+
config.McpServers,
480493
"direct",
481-
config?.CustomAgents,
482-
config?.SkillDirectories,
483-
config?.DisabledSkills,
484-
config?.InfiniteSessions);
494+
config.CustomAgents,
495+
config.SkillDirectories,
496+
config.DisabledSkills,
497+
config.InfiniteSessions);
485498

486499
var response = await InvokeRpcAsync<ResumeSessionResponse>(
487500
connection.Rpc, "session.resume", [request], cancellationToken);
488501

489502
var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath);
490-
session.RegisterTools(config?.Tools ?? []);
491-
if (config?.OnPermissionRequest != null)
492-
{
493-
session.RegisterPermissionHandler(config.OnPermissionRequest);
494-
}
495-
if (config?.OnUserInputRequest != null)
503+
session.RegisterTools(config.Tools ?? []);
504+
session.RegisterPermissionHandler(config.OnPermissionRequest);
505+
if (config.OnUserInputRequest != null)
496506
{
497507
session.RegisterUserInputHandler(config.OnUserInputRequest);
498508
}
499-
if (config?.Hooks != null)
509+
if (config.Hooks != null)
500510
{
501511
session.RegisterHooks(config.Hooks);
502512
}
@@ -516,7 +526,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
516526
/// <code>
517527
/// if (client.State == ConnectionState.Connected)
518528
/// {
519-
/// var session = await client.CreateSessionAsync();
529+
/// var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });
520530
/// }
521531
/// </code>
522532
/// </example>
@@ -630,7 +640,7 @@ public async Task<List<ModelInfo>> ListModelsAsync(CancellationToken cancellatio
630640
/// var lastId = await client.GetLastSessionIdAsync();
631641
/// if (lastId != null)
632642
/// {
633-
/// var session = await client.ResumeSessionAsync(lastId);
643+
/// var session = await client.ResumeSessionAsync(lastId, new() { OnPermissionRequest = PermissionHandler.ApproveAll });
634644
/// }
635645
/// </code>
636646
/// </example>

dotnet/src/Session.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ namespace GitHub.Copilot.SDK;
2727
/// </remarks>
2828
/// <example>
2929
/// <code>
30-
/// await using var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-4" });
30+
/// await using var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll, Model = "gpt-4" });
3131
///
3232
/// // Subscribe to events
3333
/// using var subscription = session.On(evt =>
@@ -557,10 +557,10 @@ await InvokeRpcAsync<object>(
557557
/// <example>
558558
/// <code>
559559
/// // Using 'await using' for automatic disposal
560-
/// await using var session = await client.CreateSessionAsync();
560+
/// await using var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });
561561
///
562562
/// // Or manually dispose
563-
/// var session2 = await client.CreateSessionAsync();
563+
/// var session2 = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });
564564
/// // ... use the session ...
565565
/// await session2.DisposeAsync();
566566
/// </code>

dotnet/test/AskUserTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public async Task Should_Invoke_User_Input_Handler_When_Model_Uses_Ask_User_Tool
1515
{
1616
var userInputRequests = new List<UserInputRequest>();
1717
CopilotSession? session = null;
18-
session = await Client.CreateSessionAsync(new SessionConfig
18+
session = await CreateSessionAsync(new SessionConfig
1919
{
2020
OnUserInputRequest = (request, invocation) =>
2121
{
@@ -49,7 +49,7 @@ public async Task Should_Receive_Choices_In_User_Input_Request()
4949
{
5050
var userInputRequests = new List<UserInputRequest>();
5151

52-
var session = await Client.CreateSessionAsync(new SessionConfig
52+
var session = await CreateSessionAsync(new SessionConfig
5353
{
5454
OnUserInputRequest = (request, invocation) =>
5555
{
@@ -82,7 +82,7 @@ public async Task Should_Handle_Freeform_User_Input_Response()
8282
var userInputRequests = new List<UserInputRequest>();
8383
var freeformAnswer = "This is my custom freeform answer that was not in the choices";
8484

85-
var session = await Client.CreateSessionAsync(new SessionConfig
85+
var session = await CreateSessionAsync(new SessionConfig
8686
{
8787
OnUserInputRequest = (request, invocation) =>
8888
{

0 commit comments

Comments
 (0)