Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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.
Copy link
Contributor

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
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,7 @@ 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)
}
req.RequestPermission = Bool(true)
if config.OnUserInputRequest != nil {
req.RequestUserInput = Bool(true)
}
Expand Down Expand Up @@ -562,9 +560,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
if config.Streaming {
req.Streaming = Bool(true)
}
if config.OnPermissionRequest != nil {
req.RequestPermission = Bool(true)
}
req.RequestPermission = Bool(true)
if config.OnUserInputRequest != nil {
req.RequestUserInput = Bool(true)
}
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
11 changes: 11 additions & 0 deletions go/permissions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package copilot

// PermissionHandler provides pre-built OnPermissionRequest implementations.
var PermissionHandler = struct {
// ApproveAll approves all permission requests.
ApproveAll func(PermissionRequest, PermissionInvocation) (PermissionRequestResult, error)
}{
ApproveAll: func(_ PermissionRequest, _ PermissionInvocation) (PermissionRequestResult, error) {
return PermissionRequestResult{Kind: "approved"}, nil
},
}
5 changes: 4 additions & 1 deletion go/samples/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ func main() {
}
defer client.Stop()

session, err := client.CreateSession(ctx, nil)
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
CLIPath: cliPath,
Copy link
Contributor

Choose a reason for hiding this comment

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

Invalid field in SessionConfig

CLIPath is not a valid field of SessionConfig. According to go/types.go, CLIPath belongs to ClientOptions (where it's already set on line 20), not SessionConfig.

This line should be removed. The correct code should be:

session, err := client.CreateSession(ctx, &copilot.SessionConfig{
    OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
})

This appears to be a copy-paste error. The other language samples (Node.js, Python, .NET) correctly omit the CLI path from their session configuration.

AI generated by SDK Consistency Review Agent for #509

OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
})
if err != nil {
panic(err)
}
Expand Down
8 changes: 6 additions & 2 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,9 @@ type SessionConfig struct {
// ExcludedTools is a list of tool names to disable. All other tools remain available.
// Ignored if AvailableTools is specified.
ExcludedTools []string
// OnPermissionRequest is a handler for permission requests from the server
// OnPermissionRequest is a handler for permission requests from the server.
// If nil, all permission requests are denied by default.
// Provide a handler to approve operations (file writes, shell commands, URL fetches, etc.).
OnPermissionRequest PermissionHandler
// OnUserInputRequest is a handler for user input requests from the agent (enables ask_user tool)
OnUserInputRequest UserInputHandler
Expand Down Expand Up @@ -426,7 +428,9 @@ type ResumeSessionConfig struct {
// ReasoningEffort level for models that support it.
// Valid values: "low", "medium", "high", "xhigh"
ReasoningEffort string
// OnPermissionRequest is a handler for permission requests from the server
// OnPermissionRequest is a handler for permission requests from the server.
// If nil, all permission requests are denied by default.
// Provide a handler to approve operations (file writes, shell commands, URL fetches, etc.).
OnPermissionRequest PermissionHandler
// OnUserInputRequest is a handler for user input requests from the agent (enables ask_user tool)
OnUserInputRequest UserInputHandler
Expand Down
6 changes: 4 additions & 2 deletions nodejs/samples/chat.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as readline from "node:readline";
import { CopilotClient, type SessionEvent } from "@github/copilot-sdk";
import { CopilotClient, approveAll, type SessionEvent } from "@github/copilot-sdk";

async function main() {
const client = new CopilotClient();
const session = await client.createSession();
const session = await client.createSession({
onPermissionRequest: approveAll,
});

session.on((event: SessionEvent) => {
let output: string | null = null;
Expand Down
4 changes: 2 additions & 2 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ export class CopilotClient {
availableTools: config.availableTools,
excludedTools: config.excludedTools,
provider: config.provider,
requestPermission: !!config.onPermissionRequest,
requestPermission: true,
requestUserInput: !!config.onUserInputRequest,
hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
workingDirectory: config.workingDirectory,
Expand Down Expand Up @@ -605,7 +605,7 @@ export class CopilotClient {
parameters: toJsonSchema(tool.parameters),
})),
provider: config.provider,
requestPermission: !!config.onPermissionRequest,
requestPermission: true,
requestUserInput: !!config.onUserInputRequest,
hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
workingDirectory: config.workingDirectory,
Expand Down
2 changes: 1 addition & 1 deletion nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

export { CopilotClient } from "./client.js";
export { CopilotSession, type AssistantMessageEvent } from "./session.js";
export { defineTool } from "./types.js";
export { defineTool, approveAll } from "./types.js";
export type {
ConnectionState,
CopilotClientOptions,
Expand Down
2 changes: 2 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ export type PermissionHandler = (
invocation: { sessionId: string }
) => Promise<PermissionRequestResult> | PermissionRequestResult;

export const approveAll: PermissionHandler = () => ({ kind: "approved" });

// ============================================================================
// User Input Request Types
// ============================================================================
Expand Down
4 changes: 4 additions & 0 deletions nodejs/test/e2e/hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
PostToolUseHookInput,
PostToolUseHookOutput,
} from "../../src/index.js";
import { approveAll } from "../../src/index.js";
import { createSdkTestContext } from "./harness/sdkTestContext.js";

describe("Session hooks", async () => {
Expand All @@ -20,6 +21,7 @@ describe("Session hooks", async () => {
const preToolUseInputs: PreToolUseHookInput[] = [];

const session = await client.createSession({
onPermissionRequest: approveAll,
hooks: {
onPreToolUse: async (input, invocation) => {
preToolUseInputs.push(input);
Expand Down Expand Up @@ -50,6 +52,7 @@ describe("Session hooks", async () => {
const postToolUseInputs: PostToolUseHookInput[] = [];

const session = await client.createSession({
onPermissionRequest: approveAll,
hooks: {
onPostToolUse: async (input, invocation) => {
postToolUseInputs.push(input);
Expand Down Expand Up @@ -81,6 +84,7 @@ describe("Session hooks", async () => {
const postToolUseInputs: PostToolUseHookInput[] = [];

const session = await client.createSession({
onPermissionRequest: approveAll,
hooks: {
onPreToolUse: async (input) => {
preToolUseInputs.push(input);
Expand Down
2 changes: 2 additions & 0 deletions nodejs/test/e2e/mcp_and_agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
import { describe, expect, it } from "vitest";
import type { CustomAgentConfig, MCPLocalServerConfig, MCPServerConfig } from "../../src/index.js";
import { approveAll } from "../../src/index.js";
import { createSdkTestContext } from "./harness/sdkTestContext.js";

const __filename = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -108,6 +109,7 @@ describe("MCP Servers and Custom Agents", async () => {

const session = await client.createSession({
mcpServers,
onPermissionRequest: approveAll,
});

expect(session.sessionId).toBeDefined();
Expand Down
Loading
Loading