Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
ca0e7f7
feat!: default-deny permissions across all SDK languages
SteveSandersonMS Feb 18, 2026
07bdcd1
fix: add onPermissionRequest handlers to E2E tests for default-deny b…
SteveSandersonMS Feb 18, 2026
7ba9d36
fix: add OnPermissionRequest handlers to Go hooks tests
SteveSandersonMS Feb 18, 2026
64256b9
refactor: add PermissionHandlers.approveAll helper, remove verbose RE…
SteveSandersonMS Feb 18, 2026
a7bee9d
Rename PermissionHandlers to PermissionHandler across all SDK languages
SteveSandersonMS Feb 18, 2026
ae9a038
Fix ruff import sort in session.py
SteveSandersonMS Feb 18, 2026
1135709
Node.js: export approveAll directly instead of PermissionHandler name…
SteveSandersonMS Feb 18, 2026
2ade8c3
go fmt
SteveSandersonMS Feb 18, 2026
f124485
debug: log all events in default-deny test to inspect event structure
SteveSandersonMS Feb 18, 2026
9941b3a
Fix default-deny e2e tests: check Permission denied in tool.execution…
SteveSandersonMS Feb 18, 2026
6091fd7
fix: check error.message not result.content for permission denied det…
SteveSandersonMS Feb 18, 2026
9ca4554
fix: move requestPermission outside config nil-guard in Go; add resum…
SteveSandersonMS Feb 18, 2026
a5cc0b7
fix: import PermissionHandler in Python e2e test
SteveSandersonMS Feb 18, 2026
594196b
fix: remove redundant inline asyncio imports in Python e2e tests
SteveSandersonMS Feb 18, 2026
63852b5
fix: add approveAll to invokes_built-in_tools test and regenerate sna…
SteveSandersonMS Feb 18, 2026
8aa82f4
fix(go): add permission handler to abort test and regenerate snapshot
SteveSandersonMS Feb 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,20 +124,13 @@ The `--share` option is not available via SDK. Workarounds:

### Permission Control

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.

Instead of `--allow-all-paths` or `--yolo`, use the permission handler:

```typescript
const session = await client.createSession({
onPermissionRequest: async (request) => {
// Auto-approve everything (equivalent to --yolo)
return { approved: true };

// Or implement custom logic
if (request.kind === "shell") {
return { approved: request.command.startsWith("git") };
}
return { approved: true };
},
onPermissionRequest: approveAll,
});
```

Expand Down
5 changes: 4 additions & 1 deletion dotnet/samples/Chat.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using GitHub.Copilot.SDK;

await using var client = new CopilotClient();
await using var session = await client.CreateSessionAsync();
await using var session = await client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll
});

using var _ = session.On(evt =>
{
Expand Down
4 changes: 2 additions & 2 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = nul
config?.AvailableTools,
config?.ExcludedTools,
config?.Provider,
config?.OnPermissionRequest != null ? true : null,
(bool?)true,
config?.OnUserInputRequest != null ? true : null,
hasHooks ? true : null,
config?.WorkingDirectory,
Expand Down Expand Up @@ -461,7 +461,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
config?.AvailableTools,
config?.ExcludedTools,
config?.Provider,
config?.OnPermissionRequest != null ? true : null,
(bool?)true,
config?.OnUserInputRequest != null ? true : null,
hasHooks ? true : null,
config?.WorkingDirectory,
Expand Down
13 changes: 13 additions & 0 deletions dotnet/src/PermissionHandlers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

namespace GitHub.Copilot.SDK;

/// <summary>Provides pre-built <see cref="PermissionRequestHandler"/> implementations.</summary>
public static class PermissionHandler
{
/// <summary>A <see cref="PermissionRequestHandler"/> that approves all permission requests.</summary>
public static PermissionRequestHandler ApproveAll { get; } =
(_, _) => Task.FromResult(new PermissionRequestResult { Kind = "approved" });
}
6 changes: 3 additions & 3 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public partial class CopilotSession : IAsyncDisposable
private readonly HashSet<SessionEventHandler> _eventHandlers = new();
private readonly Dictionary<string, AIFunction> _toolHandlers = new();
private readonly JsonRpc _rpc;
private PermissionHandler? _permissionHandler;
private PermissionRequestHandler? _permissionHandler;
private readonly SemaphoreSlim _permissionHandlerLock = new(1, 1);
private UserInputHandler? _userInputHandler;
private readonly SemaphoreSlim _userInputHandlerLock = new(1, 1);
Expand Down Expand Up @@ -292,7 +292,7 @@ internal void RegisterTools(ICollection<AIFunction> tools)
/// When the assistant needs permission to perform certain actions (e.g., file operations),
/// this handler is called to approve or deny the request.
/// </remarks>
internal void RegisterPermissionHandler(PermissionHandler handler)
internal void RegisterPermissionHandler(PermissionRequestHandler handler)
{
_permissionHandlerLock.Wait();
try
Expand All @@ -313,7 +313,7 @@ internal void RegisterPermissionHandler(PermissionHandler handler)
internal async Task<PermissionRequestResult> HandlePermissionRequestAsync(JsonElement permissionRequestData)
{
await _permissionHandlerLock.WaitAsync();
PermissionHandler? handler;
PermissionRequestHandler? handler;
try
{
handler = _permissionHandler;
Expand Down
6 changes: 3 additions & 3 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ public class PermissionInvocation
public string SessionId { get; set; } = string.Empty;
}

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

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

/// <summary>
/// Handler for user input requests from the agent.
Expand Down Expand Up @@ -932,7 +932,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
/// Handler for permission requests from the server.
/// When provided, the server will call this handler to request permission for operations.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same documentation enhancement suggestion as for SessionConfig.OnPermissionRequest above - clarify the deny-by-default behavior.

AI generated by SDK Consistency Review Agent for #509

/// </summary>
public PermissionHandler? OnPermissionRequest { get; set; }
public PermissionRequestHandler? OnPermissionRequest { get; set; }

/// <summary>
/// Handler for user input requests from the agent.
Expand Down
4 changes: 4 additions & 0 deletions dotnet/test/HooksTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public async Task Should_Invoke_PreToolUse_Hook_When_Model_Runs_A_Tool()
CopilotSession? session = null;
session = await Client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Hooks = new SessionHooks
{
OnPreToolUse = (input, invocation) =>
Expand Down Expand Up @@ -52,6 +53,7 @@ public async Task Should_Invoke_PostToolUse_Hook_After_Model_Runs_A_Tool()
CopilotSession? session = null;
session = await Client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Hooks = new SessionHooks
{
OnPostToolUse = (input, invocation) =>
Expand Down Expand Up @@ -89,6 +91,7 @@ public async Task Should_Invoke_Both_PreToolUse_And_PostToolUse_Hooks_For_Single

var session = await Client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Hooks = new SessionHooks
{
OnPreToolUse = (input, invocation) =>
Expand Down Expand Up @@ -130,6 +133,7 @@ public async Task Should_Deny_Tool_Execution_When_PreToolUse_Returns_Deny()

var session = await Client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Hooks = new SessionHooks
{
OnPreToolUse = (input, invocation) =>
Expand Down
3 changes: 2 additions & 1 deletion dotnet/test/McpAndAgentsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ public async Task Should_Pass_Literal_Env_Values_To_Mcp_Server_Subprocess()

var session = await Client.CreateSessionAsync(new SessionConfig
{
McpServers = mcpServers
McpServers = mcpServers,
OnPermissionRequest = PermissionHandler.ApproveAll,
});

Assert.Matches(@"^[a-f0-9-]+$", session.SessionId);
Expand Down
55 changes: 55 additions & 0 deletions dotnet/test/PermissionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,30 @@ await session.SendAsync(new MessageOptions
Assert.Equal("protected content", content);
}

[Fact]
public async Task Should_Deny_Tool_Operations_By_Default_When_No_Handler_Is_Provided()
{
var session = await Client.CreateSessionAsync(new SessionConfig());
var permissionDenied = false;

session.On(evt =>
{
if (evt is ToolExecutionCompleteEvent toolEvt &&
!toolEvt.Data.Success &&
toolEvt.Data.Error?.Message.Contains("Permission denied") == true)
{
permissionDenied = true;
}
});

await session.SendAndWaitAsync(new MessageOptions
{
Prompt = "Run 'node --version'"
});

Assert.True(permissionDenied, "Expected a tool.execution_complete event with Permission denied result");
}

[Fact]
public async Task Should_Work_Without_Permission_Handler__Default_Behavior_()
{
Expand Down Expand Up @@ -161,6 +185,37 @@ await session.SendAsync(new MessageOptions
Assert.Matches("fail|cannot|unable|permission", message?.Data.Content?.ToLowerInvariant() ?? string.Empty);
}

[Fact]
public async Task Should_Deny_Tool_Operations_By_Default_When_No_Handler_Is_Provided_After_Resume()
{
var session1 = await Client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll
});
var sessionId = session1.SessionId;
await session1.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" });

var session2 = await Client.ResumeSessionAsync(sessionId);
var permissionDenied = false;

session2.On(evt =>
{
if (evt is ToolExecutionCompleteEvent toolEvt &&
!toolEvt.Data.Success &&
toolEvt.Data.Error?.Message.Contains("Permission denied") == true)
{
permissionDenied = true;
}
});

await session2.SendAndWaitAsync(new MessageOptions
{
Prompt = "Run 'node --version'"
});

Assert.True(permissionDenied, "Expected a tool.execution_complete event with Permission denied result");
}

[Fact]
public async Task Should_Receive_ToolCallId_In_Permission_Requests()
{
Expand Down
5 changes: 4 additions & 1 deletion dotnet/test/SessionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,10 @@ public async Task Should_Receive_Session_Events()
[Fact]
public async Task Send_Returns_Immediately_While_Events_Stream_In_Background()
{
var session = await Client.CreateSessionAsync();
var session = await Client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
});
var events = new List<string>();

session.On(evt => events.Add(evt.Type));
Expand Down
5 changes: 4 additions & 1 deletion dotnet/test/ToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ await File.WriteAllTextAsync(
Path.Combine(Ctx.WorkDir, "README.md"),
"# ELIZA, the only chatbot you'll ever need");

var session = await Client.CreateSessionAsync();
var session = await Client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
});

await session.SendAsync(new MessageOptions
{
Expand Down
8 changes: 2 additions & 6 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,9 +473,6 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
if config.Streaming {
req.Streaming = Bool(true)
}
if config.OnPermissionRequest != nil {
req.RequestPermission = Bool(true)
}
if config.OnUserInputRequest != nil {
req.RequestUserInput = Bool(true)
}
Expand All @@ -488,6 +485,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
req.Hooks = Bool(true)
}
}
req.RequestPermission = Bool(true)

result, err := c.client.Request("session.create", req)
if err != nil {
Expand Down Expand Up @@ -562,9 +560,6 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
if config.Streaming {
req.Streaming = Bool(true)
}
if config.OnPermissionRequest != nil {
req.RequestPermission = Bool(true)
}
if config.OnUserInputRequest != nil {
req.RequestUserInput = Bool(true)
}
Expand All @@ -588,6 +583,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
req.DisabledSkills = config.DisabledSkills
req.InfiniteSessions = config.InfiniteSessions
}
req.RequestPermission = Bool(true)

result, err := c.client.Request("session.resume", req)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions go/internal/e2e/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func TestHooks(t *testing.T) {
var mu sync.Mutex

session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
Hooks: &copilot.SessionHooks{
OnPreToolUse: func(input copilot.PreToolUseHookInput, invocation copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) {
mu.Lock()
Expand Down Expand Up @@ -80,6 +81,7 @@ func TestHooks(t *testing.T) {
var mu sync.Mutex

session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
Hooks: &copilot.SessionHooks{
OnPostToolUse: func(input copilot.PostToolUseHookInput, invocation copilot.HookInvocation) (*copilot.PostToolUseHookOutput, error) {
mu.Lock()
Expand Down Expand Up @@ -145,6 +147,7 @@ func TestHooks(t *testing.T) {
var mu sync.Mutex

session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
Hooks: &copilot.SessionHooks{
OnPreToolUse: func(input copilot.PreToolUseHookInput, invocation copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) {
mu.Lock()
Expand Down Expand Up @@ -214,6 +217,7 @@ func TestHooks(t *testing.T) {
var mu sync.Mutex

session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
Hooks: &copilot.SessionHooks{
OnPreToolUse: func(input copilot.PreToolUseHookInput, invocation copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) {
mu.Lock()
Expand Down
3 changes: 2 additions & 1 deletion go/internal/e2e/mcp_and_agents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ func TestMCPServers(t *testing.T) {
}

session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
MCPServers: mcpServers,
MCPServers: mcpServers,
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
})
if err != nil {
t.Fatalf("Failed to create session: %v", err)
Expand Down
Loading
Loading