Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c88bd32
Expose managed approval requirement on permission requests
joshspicer Jul 24, 2026
cedcd23
docs: align managed selector name with Domain
joshspicer Jul 28, 2026
dbbb8d3
Fix managed permission approval surfaces
joshspicer Jul 28, 2026
f48c5c7
docs: clarify managed approval behavior
joshspicer Jul 28, 2026
00658d6
docs: guard managed custom approvals
joshspicer Jul 29, 2026
8b9647e
Expose managed approvals across SDKs
joshspicer Jul 29, 2026
2c9086a
Respect managed approval in permission policies
joshspicer Jul 29, 2026
c5dacda
Minimize Java permission overlay
joshspicer Jul 29, 2026
bf6c00e
Clarify Java managed permission deferral
joshspicer Jul 29, 2026
241decc
Preserve Rust permission event metadata
joshspicer Jul 29, 2026
9027d19
Fix managed approval C# example
joshspicer Jul 30, 2026
53f44eb
Fix managed approval documentation guards
joshspicer Jul 30, 2026
3c6d73d
Fix .NET managed permission event test
joshspicer Jul 30, 2026
4ac0667
Fail approve-all in managed sessions
joshspicer Jul 30, 2026
c1ae85f
Avoid expanding Rust permission result API
joshspicer Jul 30, 2026
f306071
Keep managed permission helpers fail-closed
joshspicer Jul 30, 2026
24dd043
Fix Java Gradle snapshot coordinate
joshspicer Jul 30, 2026
33e1bb4
Align managed permission documentation
joshspicer Jul 30, 2026
d34362c
Clarify Java managed approval handling
joshspicer Jul 30, 2026
8b4ba8c
Surface permission handler failures
joshspicer Jul 30, 2026
909a516
Log Java permission handler failures
joshspicer Jul 30, 2026
13bb69d
Clarify permission failure diagnostics
joshspicer Jul 30, 2026
bee8b3a
Clarify Rust managed approval fallback
joshspicer Jul 30, 2026
48f7959
Reject no-result in legacy Python callbacks
joshspicer Jul 30, 2026
b4525ff
Fail closed on managed approval metadata
joshspicer Jul 30, 2026
c80aade
Initialize managed settings before Go events
joshspicer Jul 30, 2026
565f247
Fail closed on unknown permission requests
joshspicer Jul 30, 2026
6048480
Preserve legacy Python approve-all calls
joshspicer Jul 30, 2026
fb0c782
Clarify Rust permission event payload
joshspicer Jul 30, 2026
67527da
Harden managed permission fallbacks
joshspicer Jul 30, 2026
a7fab6a
Regenerate managed approval outputs
Jul 31, 2026
eec1b5f
Regenerate managed approval outputs
Jul 31, 2026
7439562
Align managed approval helpers
Jul 31, 2026
afcd1e6
Fix managed approval hierarchy and diagnostics
joshspicer Jul 31, 2026
bc2fbb9
Restore managed settings approval guard
joshspicer Jul 31, 2026
bdefea7
Fail closed on malformed managed metadata
joshspicer Jul 31, 2026
bd67de2
Preserve managed approval accessor compatibility
joshspicer Jul 31, 2026
83be79b
Preserve Python permission constructor order
joshspicer Jul 31, 2026
3da6c28
Preserve managed metadata compatibility
joshspicer Jul 31, 2026
f662524
Fail closed on malformed Java managed metadata
joshspicer Jul 31, 2026
a9601f7
Merge origin/main into joshspicer/managed-approval-required-sdk
joshspicer Jul 31, 2026
6d0de63
Retrigger CI
joshspicer Jul 31, 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
15 changes: 11 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 @@ -132,7 +132,7 @@ Create a new conversation session.
- `Streaming` - Enable streaming of response chunks (default: false)
- `InfiniteSessions` - Configure automatic context compaction (see below)
- `EnableSessionStore` - Enables the cross-session store for search and retrieval across sessions. When unset in `CopilotClientMode.CopilotCli`, the runtime default applies (enabled). In `CopilotClientMode.Empty`, defaults to disabled.
- `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 @@ -782,7 +782,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 @@ -794,16 +794,23 @@ 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.ManagedApprovalRequired is 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
37 changes: 29 additions & 8 deletions dotnet/src/Generated/SessionEvents.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 29 additions & 2 deletions dotnet/src/PermissionHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,34 @@ 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)
{
if (request.ManagedApprovalRequired is true)
{
return true;
}

return request.GetType() == typeof(PermissionRequest)
&& request.Kind is not ("shell"
or "write"
or "read"
or "mcp"
or "url"
or "memory"
or "custom-tool"
or "hook"
or "extension-management"
or "extension-permission-access");
}
}
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 or response delivery 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
127 changes: 127 additions & 0 deletions dotnet/test/Unit/PermissionHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*---------------------------------------------------------------------------------------------
* 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);
PermissionRequest genericRequest = request;
Assert.True(genericRequest.ManagedApprovalRequired);
}

[Fact]
public async Task ApproveAllThrowsWhenManagedSettingsEnabled()
{
var request = new PermissionRequest
{
Kind = "read",
ManagedApprovalRequired = true,
};

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

[Fact]
public async Task ApproveAllApprovesOrdinaryRequest()
{
var request = new PermissionRequest { Kind = "read" };

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 ApproveAllLeavesManagedKnownVariantPendingThroughBaseType()
{
PermissionRequest 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 void DerivedManagedApprovalAccessorForwardsToBaseStorage()
{
var request = new PermissionRequestRead
{
Intention = "Read managed content",
ManagedApprovalRequired = true,
Path = "/workspace/file.txt",
};

PermissionRequest genericRequest = request;
Assert.True(genericRequest.ManagedApprovalRequired);

genericRequest.ManagedApprovalRequired = false;
Assert.False(request.ManagedApprovalRequired);
}

[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);
}
}
Loading
Loading