Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
03184d2
Expose managed approval requirement on permission requests
joshspicer Jul 24, 2026
e5417bd
docs: align managed selector name with Domain
joshspicer Jul 28, 2026
c942cf1
Fix managed permission approval surfaces
joshspicer Jul 28, 2026
0da1c59
docs: clarify managed approval behavior
joshspicer Jul 28, 2026
dea8119
docs: guard managed custom approvals
joshspicer Jul 29, 2026
0b9b0ff
Expose managed approvals across SDKs
joshspicer Jul 29, 2026
4b42e45
Respect managed approval in permission policies
joshspicer Jul 29, 2026
8bbf5b1
Minimize Java permission overlay
joshspicer Jul 29, 2026
2161da8
Clarify Java managed permission deferral
joshspicer Jul 29, 2026
679c6a5
Preserve Rust permission event metadata
joshspicer Jul 29, 2026
df66c97
Merge main into managed approval support
joshspicer Jul 30, 2026
d214c8b
Fix managed approval C# example
joshspicer Jul 30, 2026
b70b4fe
Fix managed approval documentation guards
joshspicer Jul 30, 2026
5356120
Fix .NET managed permission event test
joshspicer Jul 30, 2026
63c0e8f
Fix Java version references to align with main
joshspicer Jul 30, 2026
363ad33
Fail approve-all in managed sessions
joshspicer Jul 30, 2026
af2e19b
Avoid expanding Rust permission result API
joshspicer Jul 30, 2026
347a170
Keep managed permission helpers fail-closed
joshspicer Jul 30, 2026
3c72a45
Fix Java Gradle snapshot coordinate
joshspicer Jul 30, 2026
c2c3abd
Merge origin/main into joshspicer/managed-approval-required-sdk
joshspicer Jul 30, 2026
2f6d52e
Align managed permission documentation
joshspicer Jul 30, 2026
6dba2c5
Clarify Java managed approval handling
joshspicer Jul 30, 2026
1ff6326
Merge origin/main into joshspicer/managed-approval-required-sdk
joshspicer Jul 30, 2026
b987639
Surface permission handler failures
joshspicer Jul 30, 2026
0762739
Log Java permission handler failures
joshspicer Jul 30, 2026
c41594b
Clarify permission failure diagnostics
joshspicer Jul 30, 2026
f2e484a
Clarify Rust managed approval fallback
joshspicer Jul 30, 2026
c7eeb11
Reject no-result in legacy Python callbacks
joshspicer Jul 30, 2026
4157ef1
Fail closed on managed approval metadata
joshspicer Jul 30, 2026
b1578a7
Initialize managed settings before Go events
joshspicer Jul 30, 2026
08fdb39
Fail closed on unknown permission requests
joshspicer Jul 30, 2026
06920e4
Preserve legacy Python approve-all calls
joshspicer Jul 30, 2026
e740cf3
Clarify Rust permission event payload
joshspicer Jul 30, 2026
8e7c16f
Harden managed permission fallbacks
joshspicer Jul 30, 2026
02bb10e
Merge origin/main into joshspicer/managed-approval-required-sdk
joshspicer Jul 30, 2026
4fc0669
Merge origin/main into joshspicer/managed-approval-required-sdk
joshspicer Jul 30, 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
18 changes: 14 additions & 4 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ using GitHub.Copilot;
await using var client = new CopilotClient();
await client.StartAsync();

// Create a session (OnPermissionRequest is optional; ApproveAll allows every tool)
// ApproveAll is only valid when managed settings are disabled.
await using var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
Expand Down Expand Up @@ -131,7 +131,7 @@ Create a new conversation session.
- `Provider` - Custom API provider configuration (BYOK)
- `Streaming` - Enable streaming of response chunks (default: false)
- `InfiniteSessions` - Configure automatic context compaction (see below)
- `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. Use `PermissionHandler.ApproveAll` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section.
- `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `PermissionHandler.ApproveAll` approves requests when managed settings are disabled and throws when `EnableManagedSettings` is true. Custom handlers can inspect `ManagedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section.
- `OnUserInputRequest` - Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section.
- `Hooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.

Expand Down Expand Up @@ -781,7 +781,7 @@ An `OnPermissionRequest` handler is optional when you create or resume a session

### Approve All (simplest)

Use the built-in `PermissionHandler.ApproveAll` helper to allow every tool call without any checks:
Use the built-in `PermissionHandler.ApproveAll` helper to approve ordinary permission requests automatically:

```csharp
using GitHub.Copilot;
Expand All @@ -793,16 +793,26 @@ var session = await client.CreateSessionAsync(new SessionConfig
});
```

When `EnableManagedSettings` is true for the session, `ApproveAll` throws on the first permission request. Use a custom handler for managed sessions; request-level `ManagedApprovalRequired` remains available for human-facing confirmation logic.

### Custom Permission Handler

Provide your own permission handler (`Func<PermissionRequest, PermissionInvocation, Task<PermissionDecision>>`) to inspect each request and apply custom logic:
Provide your own permission handler (`Func<PermissionRequest, PermissionInvocation, Task<PermissionDecision>>`) to inspect each request and apply custom logic. Check `ManagedApprovalRequired` before any automatic approval:

```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
OnPermissionRequest = async (request, invocation) =>
{
if (request is PermissionRequestShell { ManagedApprovalRequired: true }
or PermissionRequestWrite { ManagedApprovalRequired: true }
or PermissionRequestRead { ManagedApprovalRequired: true }
or PermissionRequestUrl { ManagedApprovalRequired: true })
{
return PermissionDecision.NoResult();
}

// Pattern-match on the discriminated PermissionRequest union to access
// per-kind fields (FullCommandText, Path, ToolName, …).
return request switch
Expand Down
4 changes: 3 additions & 1 deletion dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,9 @@ private CopilotSession InitializeSession(
_logger,
this);
session.RegisterTools(config.Tools ?? []);
session.RegisterPermissionHandler(config.OnPermissionRequest);
session.RegisterPermissionHandler(
config.OnPermissionRequest,
config.EnableManagedSettings is true);
session.RegisterMcpAuthHandler(config.OnMcpAuthRequest);
session.RegisterCommands(config.Commands);
session.RegisterElicitationHandler(config.OnElicitationRequest);
Expand Down
26 changes: 24 additions & 2 deletions dotnet/src/PermissionHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,29 @@ namespace GitHub.Copilot;
/// <summary>Provides pre-built permission request handlers.</summary>
public static class PermissionHandler
{
/// <summary>A permission handler that approves all permission requests.</summary>
/// <summary>
/// A permission handler that approves requests when managed settings are disabled.
/// </summary>
public static Func<PermissionRequest, PermissionInvocation, Task<PermissionDecision>> ApproveAll { get; } =
(_, _) => Task.FromResult<PermissionDecision>(PermissionDecision.ApproveOnce());
(request, invocation) => invocation.ManagedSettingsEnabled
? Task.FromException<PermissionDecision>(
new InvalidOperationException("ApproveAll cannot be used when managed settings are enabled"))
: RequiresManagedApproval(request)
? Task.FromResult(PermissionDecision.NoResult())
: Task.FromResult(PermissionDecision.ApproveOnce());

private static bool RequiresManagedApproval(PermissionRequest request) => request switch
{
PermissionRequestShell shell => shell.ManagedApprovalRequired is true,
PermissionRequestWrite write => write.ManagedApprovalRequired is true,
PermissionRequestRead read => read.ManagedApprovalRequired is true,
PermissionRequestUrl url => url.ManagedApprovalRequired is true,
PermissionRequestMcp
or PermissionRequestMemory
or PermissionRequestCustomTool
or PermissionRequestHook
or PermissionRequestExtensionManagement
or PermissionRequestExtensionPermissionAccess => false,
_ => true,
};
}
16 changes: 12 additions & 4 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
private readonly CopilotClient _parentClient;

private volatile Func<PermissionRequest, PermissionInvocation, Task<PermissionDecision>>? _permissionHandler;
private bool _managedSettingsEnabled;
private volatile Func<McpAuthContext, Task<McpAuthResult?>>? _mcpAuthHandler;
private volatile Func<UserInputRequest, UserInputInvocation, Task<UserInputResponse>>? _userInputHandler;
private volatile Func<ElicitationContext, Task<ElicitationResult>>? _elicitationHandler;
Expand Down Expand Up @@ -557,13 +558,17 @@
/// Registers a handler for permission requests.
/// </summary>
/// <param name="handler">The permission handler function.</param>
/// <param name="managedSettingsEnabled">Whether managed settings are enabled for the session.</param>
/// <remarks>
/// 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(Func<PermissionRequest, PermissionInvocation, Task<PermissionDecision>>? handler)
internal void RegisterPermissionHandler(
Func<PermissionRequest, PermissionInvocation, Task<PermissionDecision>>? handler,
bool managedSettingsEnabled)
{
_permissionHandler = handler;
_managedSettingsEnabled = managedSettingsEnabled;
}

internal void RegisterMcpAuthHandler(Func<McpAuthContext, Task<McpAuthResult?>>? handler)
Expand All @@ -590,7 +595,8 @@

var invocation = new PermissionInvocation
{
SessionId = SessionId
SessionId = SessionId,
ManagedSettingsEnabled = _managedSettingsEnabled
};

var permissionTimestamp = Stopwatch.GetTimestamp();
Expand Down Expand Up @@ -932,7 +938,8 @@
{
var invocation = new PermissionInvocation
{
SessionId = SessionId
SessionId = SessionId,
ManagedSettingsEnabled = _managedSettingsEnabled
};

var permissionTimestamp = Stopwatch.GetTimestamp();
Expand All @@ -954,21 +961,22 @@
SessionId,
requestId);
}
catch (Exception)
catch (Exception ex)
{
_logger.LogError(ex, "Permission handler failed. SessionId={SessionId}, RequestId={RequestId}", SessionId, requestId);
try
{
await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, PermissionDecision.UserNotAvailable());
}
catch (IOException)
{
// Connection lost or RPC error — nothing we can do
}
catch (ObjectDisposedException)
{
// Connection already disposed — nothing we can do
}
}

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.
}

/// <summary>
Expand Down
3 changes: 3 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,9 @@ public sealed class PermissionInvocation
/// Identifier of the session that triggered the permission request.
/// </summary>
public string SessionId { get; set; } = string.Empty;

/// <summary>Whether managed settings are enabled for this session.</summary>
public bool ManagedSettingsEnabled { get; set; }
}

// ============================================================================
Expand Down
98 changes: 98 additions & 0 deletions dotnet/test/Unit/PermissionHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using GitHub.Copilot.Rpc;
using Xunit;

namespace GitHub.Copilot.Test.Unit;

public class PermissionHandlerTests
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
};

[Fact]
public void PermissionEventExposesManagedApprovalRequired()
{
const string json = """
{
"permissionRequest": {
"kind": "read",
"intention": "Read managed content",
"path": "/workspace/file.txt",
"managedApprovalRequired": true
},
"requestId": "permission-1"
}
""";

var data = JsonSerializer.Deserialize<PermissionRequestedData>(
json,
SerializerOptions);

Assert.NotNull(data);
var request = Assert.IsType<PermissionRequestRead>(data.PermissionRequest);
Assert.True(request.ManagedApprovalRequired);
}

[Fact]
public async Task ApproveAllThrowsWhenManagedSettingsEnabled()
{
var request = new PermissionRequestRead
{
Intention = "Read managed content",
ManagedApprovalRequired = true,
Path = "/workspace/file.txt",
};

await Assert.ThrowsAsync<InvalidOperationException>(() =>
PermissionHandler.ApproveAll(request, new PermissionInvocation
{
ManagedSettingsEnabled = true,
}));
}

[Fact]
public async Task ApproveAllApprovesOrdinaryRequest()
{
var request = new PermissionRequestRead
{
Intention = "Read ordinary content",
Path = "/workspace/file.txt",
};

var decision = await PermissionHandler.ApproveAll(request, new PermissionInvocation());

Assert.IsType<PermissionDecisionApproveOnce>(decision);
}

[Fact]
public async Task ApproveAllLeavesManagedRequestPendingWhenSessionFlagIsAbsent()
{
var request = new PermissionRequestRead
{
Intention = "Read managed content",
ManagedApprovalRequired = true,
Path = "/workspace/file.txt",
};

var decision = await PermissionHandler.ApproveAll(request, new PermissionInvocation());

Assert.IsType<PermissionDecisionNoResult>(decision);
}

[Fact]
public async Task ApproveAllLeavesUnknownRequestPending()
{
var request = new PermissionRequest { Kind = "future-managed-kind" };

var decision = await PermissionHandler.ApproveAll(request, new PermissionInvocation());

Assert.IsType<PermissionDecisionNoResult>(decision);
}
}
14 changes: 10 additions & 4 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func main() {
}
defer client.Stop()

// Create a session (OnPermissionRequest is optional; ApproveAll allows every tool)
// ApproveAll is only valid when managed settings are disabled.
session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{
Model: "gpt-5",
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
Expand Down Expand Up @@ -221,7 +221,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec
- `Provider` (\*ProviderConfig): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section.
- `Streaming` (*bool): Enable streaming delta events (nil = runtime default)
- `InfiniteSessions` (\*InfiniteSessionConfig): Automatic context compaction configuration
- `OnPermissionRequest` (PermissionHandlerFunc): Optional handler called before each tool execution to approve or deny it. When nil, permission requests are emitted as events and left pending for manual resolution. Use `copilot.PermissionHandler.ApproveAll` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section.
- `OnPermissionRequest` (PermissionHandlerFunc): Optional handler called before each tool execution to approve or deny it. When nil, permission requests are emitted as events and left pending for manual resolution. `copilot.PermissionHandler.ApproveAll` approves requests when managed settings are disabled and returns an error when `EnableManagedSettings` is true. Custom handlers can inspect `RequiresManagedApproval()` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section.
- `OnUserInputRequest` (UserInputHandler): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section.
- `Hooks` (\*SessionHooks): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
- `Commands` ([]CommandDefinition): Slash-commands registered for this session. See [Commands](#commands) section.
Expand Down Expand Up @@ -680,7 +680,7 @@ An `OnPermissionRequest` handler is optional when you create or resume a session

### Approve All (simplest)

Use the built-in `PermissionHandler.ApproveAll` helper to allow every tool call without any checks:
Use the built-in `PermissionHandler.ApproveAll` helper to approve ordinary permission requests automatically:

```go
session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{
Expand All @@ -689,9 +689,11 @@ session, err := client.CreateSession(context.Background(), &copilot.SessionConfi
})
```

When `EnableManagedSettings` is true for the session, `ApproveAll` returns an error on the first permission request. Use a custom handler for managed sessions; request-level `RequiresManagedApproval()` remains available for human-facing confirmation logic.

### Custom Permission Handler

Provide your own `PermissionHandlerFunc` to inspect each request and apply custom logic:
Provide your own `PermissionHandlerFunc` to inspect each request and apply custom logic. Check `RequiresManagedApproval()` before any automatic approval:

```go
import (
Expand All @@ -704,6 +706,10 @@ import (
session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{
Model: "gpt-5",
OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) {
if request.RequiresManagedApproval() {
return &rpc.PermissionDecisionNoResult{}, nil
}

// Type-switch on the discriminated PermissionRequest variants to
// access per-kind fields:
if shell, ok := request.(*copilot.PermissionRequestShell); ok {
Expand Down
14 changes: 12 additions & 2 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,12 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
// message is dispatched) so notifications for the new session id are
// routed to a registered session.
initializeSession := func(sessionID string) (*Session, error) {
s := newSession(sessionID, c.client, "")
s := newSession(
sessionID,
c.client,
"",
config.EnableManagedSettings != nil && *config.EnableManagedSettings,
)

s.registerTools(config.Tools)
s.registerPermissionHandler(config.OnPermissionRequest)
Expand Down Expand Up @@ -1227,7 +1232,12 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,

// Create and register the session before issuing the RPC so that
// events emitted by the CLI (e.g. session.start) are not dropped.
session := newSession(sessionID, c.client, "")
session := newSession(
sessionID,
c.client,
"",
config.EnableManagedSettings != nil && *config.EnableManagedSettings,
)

session.registerTools(config.Tools)
session.registerPermissionHandler(config.OnPermissionRequest)
Expand Down
12 changes: 10 additions & 2 deletions go/permissions.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
package copilot

import (
"errors"

"github.com/github/copilot-sdk/go/rpc"
)

// PermissionHandler provides pre-built OnPermissionRequest implementations.
var PermissionHandler = struct {
// ApproveAll approves all permission requests.
// ApproveAll approves permission requests when managed settings are disabled.
ApproveAll PermissionHandlerFunc
}{
ApproveAll: func(_ PermissionRequest, _ PermissionInvocation) (rpc.PermissionDecision, error) {
ApproveAll: func(request PermissionRequest, invocation PermissionInvocation) (rpc.PermissionDecision, error) {
if invocation.ManagedSettingsEnabled {
return nil, errors.New("approveAll cannot be used when managed settings are enabled")
}
if request.RequiresManagedApproval() {
return &rpc.PermissionDecisionNoResult{}, nil
}
return &rpc.PermissionDecisionApproveOnce{}, nil
},
}
Loading
Loading