diff --git a/docs/getting-started.md b/docs/getting-started.md index f615e923b..56c6a9c46 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1193,14 +1193,14 @@ Once the CLI is running in server mode, configure your SDK client to connect to Node.js / TypeScript ```typescript -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; const client = new CopilotClient({ cliUrl: "localhost:4321" }); // Use the client normally -const session = await client.createSession(); +const session = await client.createSession({ onPermissionRequest: approveAll }); // ... ``` @@ -1210,7 +1210,7 @@ const session = await client.createSession(); Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler client = CopilotClient({ "cli_url": "localhost:4321" @@ -1218,7 +1218,7 @@ client = CopilotClient({ await client.start() # Use the client normally -session = await client.create_session() +session = await client.create_session({"on_permission_request": PermissionHandler.approve_all}) # ... ``` @@ -1241,7 +1241,9 @@ if err := client.Start(ctx); err != nil { defer client.Stop() // Use the client normally -session, err := client.CreateSession(ctx, nil) +session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, +}) // ... ``` @@ -1260,7 +1262,10 @@ using var client = new CopilotClient(new CopilotClientOptions }); // Use the client normally -await using var session = await client.CreateSessionAsync(); +await using var session = await client.CreateSessionAsync(new() +{ + OnPermissionRequest = PermissionHandler.ApproveAll +}); // ... ``` diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index cf29dd116..1f3a7fb43 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -38,7 +38,7 @@ namespace GitHub.Copilot.SDK; /// await using var client = new CopilotClient(); /// /// // Create a session -/// await using var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-4" }); +/// await using var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll, Model = "gpt-4" }); /// /// // Handle events /// using var subscription = session.On(evt => @@ -340,10 +340,9 @@ private async Task CleanupConnectionAsync(List? errors) /// /// Creates a new Copilot session with the specified configuration. /// - /// Configuration for the session. If null, default settings are used. + /// Configuration for the session, including the required handler. /// A that can be used to cancel the operation. /// A task that resolves to provide the . - /// Thrown when the client is not connected and AutoStart is disabled, or when a session with the same ID already exists. /// /// Sessions maintain conversation state, handle events, and manage tool execution. /// If the client is not connected and is enabled (default), @@ -352,21 +351,29 @@ private async Task CleanupConnectionAsync(List? errors) /// /// /// // Basic session - /// var session = await client.CreateSessionAsync(); + /// var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll }); /// /// // Session with model and tools - /// var session = await client.CreateSessionAsync(new SessionConfig + /// var session = await client.CreateSessionAsync(new() /// { + /// OnPermissionRequest = PermissionHandler.ApproveAll, /// Model = "gpt-4", /// Tools = [AIFunctionFactory.Create(MyToolMethod)] /// }); /// /// - public async Task CreateSessionAsync(SessionConfig? config = null, CancellationToken cancellationToken = default) + public async Task CreateSessionAsync(SessionConfig config, CancellationToken cancellationToken = default) { + if (config.OnPermissionRequest == null) + { + throw new ArgumentException( + "An OnPermissionRequest handler is required when creating a session. " + + "For example, to allow all permissions, use CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });"); + } + var connection = await EnsureConnectedAsync(cancellationToken); - var hasHooks = config?.Hooks != null && ( + var hasHooks = config.Hooks != null && ( config.Hooks.OnPreToolUse != null || config.Hooks.OnPostToolUse != null || config.Hooks.OnUserPromptSubmitted != null || @@ -375,42 +382,39 @@ public async Task CreateSessionAsync(SessionConfig? config = nul config.Hooks.OnErrorOccurred != null); var request = new CreateSessionRequest( - config?.Model, - config?.SessionId, - config?.ClientName, - config?.ReasoningEffort, - config?.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), - config?.SystemMessage, - config?.AvailableTools, - config?.ExcludedTools, - config?.Provider, + config.Model, + config.SessionId, + config.ClientName, + config.ReasoningEffort, + config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), + config.SystemMessage, + config.AvailableTools, + config.ExcludedTools, + config.Provider, (bool?)true, - config?.OnUserInputRequest != null ? true : null, + config.OnUserInputRequest != null ? true : null, hasHooks ? true : null, - config?.WorkingDirectory, - config?.Streaming == true ? true : null, - config?.McpServers, + config.WorkingDirectory, + config.Streaming is true ? true : null, + config.McpServers, "direct", - config?.CustomAgents, - config?.ConfigDir, - config?.SkillDirectories, - config?.DisabledSkills, - config?.InfiniteSessions); + config.CustomAgents, + config.ConfigDir, + config.SkillDirectories, + config.DisabledSkills, + config.InfiniteSessions); var response = await InvokeRpcAsync( connection.Rpc, "session.create", [request], cancellationToken); var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath); - session.RegisterTools(config?.Tools ?? []); - if (config?.OnPermissionRequest != null) - { - session.RegisterPermissionHandler(config.OnPermissionRequest); - } - if (config?.OnUserInputRequest != null) + session.RegisterTools(config.Tools ?? []); + session.RegisterPermissionHandler(config.OnPermissionRequest); + if (config.OnUserInputRequest != null) { session.RegisterUserInputHandler(config.OnUserInputRequest); } - if (config?.Hooks != null) + if (config.Hooks != null) { session.RegisterHooks(config.Hooks); } @@ -427,9 +431,10 @@ public async Task CreateSessionAsync(SessionConfig? config = nul /// Resumes an existing Copilot session with the specified configuration. /// /// The ID of the session to resume. - /// Configuration for the resumed session. If null, default settings are used. + /// Configuration for the resumed session, including the required handler. /// A that can be used to cancel the operation. /// A task that resolves to provide the . + /// Thrown when is not set. /// Thrown when the session does not exist or the client is not connected. /// /// This allows you to continue a previous conversation, maintaining all conversation history. @@ -438,20 +443,28 @@ public async Task CreateSessionAsync(SessionConfig? config = nul /// /// /// // Resume a previous session - /// var session = await client.ResumeSessionAsync("session-123"); + /// var session = await client.ResumeSessionAsync("session-123", new() { OnPermissionRequest = PermissionHandler.ApproveAll }); /// /// // Resume with new tools - /// var session = await client.ResumeSessionAsync("session-123", new ResumeSessionConfig + /// var session = await client.ResumeSessionAsync("session-123", new() /// { + /// OnPermissionRequest = PermissionHandler.ApproveAll, /// Tools = [AIFunctionFactory.Create(MyNewToolMethod)] /// }); /// /// - public async Task ResumeSessionAsync(string sessionId, ResumeSessionConfig? config = null, CancellationToken cancellationToken = default) + public async Task ResumeSessionAsync(string sessionId, ResumeSessionConfig config, CancellationToken cancellationToken = default) { + if (config.OnPermissionRequest == null) + { + throw new ArgumentException( + "An OnPermissionRequest handler is required when resuming a session. " + + "For example, to allow all permissions, use new() { OnPermissionRequest = PermissionHandler.ApproveAll }."); + } + var connection = await EnsureConnectedAsync(cancellationToken); - var hasHooks = config?.Hooks != null && ( + var hasHooks = config.Hooks != null && ( config.Hooks.OnPreToolUse != null || config.Hooks.OnPostToolUse != null || config.Hooks.OnUserPromptSubmitted != null || @@ -461,42 +474,39 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes var request = new ResumeSessionRequest( sessionId, - config?.ClientName, - config?.Model, - config?.ReasoningEffort, - config?.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), - config?.SystemMessage, - config?.AvailableTools, - config?.ExcludedTools, - config?.Provider, + config.ClientName, + config.Model, + config.ReasoningEffort, + config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), + config.SystemMessage, + config.AvailableTools, + config.ExcludedTools, + config.Provider, (bool?)true, - config?.OnUserInputRequest != null ? true : null, + config.OnUserInputRequest != null ? true : null, hasHooks ? true : null, - config?.WorkingDirectory, - config?.ConfigDir, - config?.DisableResume == true ? true : null, - config?.Streaming == true ? true : null, - config?.McpServers, + config.WorkingDirectory, + config.ConfigDir, + config.DisableResume is true ? true : null, + config.Streaming is true ? true : null, + config.McpServers, "direct", - config?.CustomAgents, - config?.SkillDirectories, - config?.DisabledSkills, - config?.InfiniteSessions); + config.CustomAgents, + config.SkillDirectories, + config.DisabledSkills, + config.InfiniteSessions); var response = await InvokeRpcAsync( connection.Rpc, "session.resume", [request], cancellationToken); var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath); - session.RegisterTools(config?.Tools ?? []); - if (config?.OnPermissionRequest != null) - { - session.RegisterPermissionHandler(config.OnPermissionRequest); - } - if (config?.OnUserInputRequest != null) + session.RegisterTools(config.Tools ?? []); + session.RegisterPermissionHandler(config.OnPermissionRequest); + if (config.OnUserInputRequest != null) { session.RegisterUserInputHandler(config.OnUserInputRequest); } - if (config?.Hooks != null) + if (config.Hooks != null) { session.RegisterHooks(config.Hooks); } @@ -516,7 +526,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes /// /// if (client.State == ConnectionState.Connected) /// { - /// var session = await client.CreateSessionAsync(); + /// var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll }); /// } /// /// @@ -630,7 +640,7 @@ public async Task> ListModelsAsync(CancellationToken cancellatio /// var lastId = await client.GetLastSessionIdAsync(); /// if (lastId != null) /// { - /// var session = await client.ResumeSessionAsync(lastId); + /// var session = await client.ResumeSessionAsync(lastId, new() { OnPermissionRequest = PermissionHandler.ApproveAll }); /// } /// /// diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 45f093b10..923b193cc 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -27,7 +27,7 @@ namespace GitHub.Copilot.SDK; /// /// /// -/// await using var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-4" }); +/// await using var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll, Model = "gpt-4" }); /// /// // Subscribe to events /// using var subscription = session.On(evt => @@ -557,10 +557,10 @@ await InvokeRpcAsync( /// /// /// // Using 'await using' for automatic disposal - /// await using var session = await client.CreateSessionAsync(); + /// await using var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll }); /// /// // Or manually dispose - /// var session2 = await client.CreateSessionAsync(); + /// var session2 = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll }); /// // ... use the session ... /// await session2.DisposeAsync(); /// diff --git a/dotnet/test/AskUserTests.cs b/dotnet/test/AskUserTests.cs index 55a563674..d3f273996 100644 --- a/dotnet/test/AskUserTests.cs +++ b/dotnet/test/AskUserTests.cs @@ -15,7 +15,7 @@ public async Task Should_Invoke_User_Input_Handler_When_Model_Uses_Ask_User_Tool { var userInputRequests = new List(); CopilotSession? session = null; - session = await Client.CreateSessionAsync(new SessionConfig + session = await CreateSessionAsync(new SessionConfig { OnUserInputRequest = (request, invocation) => { @@ -49,7 +49,7 @@ public async Task Should_Receive_Choices_In_User_Input_Request() { var userInputRequests = new List(); - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { OnUserInputRequest = (request, invocation) => { @@ -82,7 +82,7 @@ public async Task Should_Handle_Freeform_User_Input_Response() var userInputRequests = new List(); var freeformAnswer = "This is my custom freeform answer that was not in the choices"; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { OnUserInputRequest = (request, invocation) => { diff --git a/dotnet/test/ClientTests.cs b/dotnet/test/ClientTests.cs index bbb3e8544..91b7f9241 100644 --- a/dotnet/test/ClientTests.cs +++ b/dotnet/test/ClientTests.cs @@ -59,7 +59,7 @@ public async Task Should_Force_Stop_Without_Cleanup() { using var client = new CopilotClient(new CopilotClientOptions()); - await client.CreateSessionAsync(); + await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll }); await client.ForceStopAsync(); Assert.Equal(ConnectionState.Disconnected, client.State); @@ -220,7 +220,7 @@ public void Should_Throw_When_UseLoggedInUser_Used_With_CliUrl() public async Task Should_Not_Throw_When_Disposing_Session_After_Stopping_Client() { await using var client = new CopilotClient(new CopilotClientOptions()); - await using var session = await client.CreateSessionAsync(); + await using var session = await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll }); await client.StopAsync(); } @@ -247,7 +247,7 @@ public async Task Should_Report_Error_With_Stderr_When_CLI_Fails_To_Start() // Verify subsequent calls also fail (don't hang) var ex2 = await Assert.ThrowsAnyAsync(async () => { - var session = await client.CreateSessionAsync(); + var session = await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll }); await session.SendAsync(new MessageOptions { Prompt = "test" }); }); Assert.Contains("exited", ex2.Message, StringComparison.OrdinalIgnoreCase); @@ -255,4 +255,32 @@ public async Task Should_Report_Error_With_Stderr_When_CLI_Fails_To_Start() // Cleanup - ForceStop should handle the disconnected state gracefully try { await client.ForceStopAsync(); } catch (Exception) { /* Expected */ } } + + [Fact] + public async Task Should_Throw_When_CreateSession_Called_Without_PermissionHandler() + { + using var client = new CopilotClient(new CopilotClientOptions()); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.CreateSessionAsync(new SessionConfig()); + }); + + Assert.Contains("OnPermissionRequest", ex.Message); + Assert.Contains("is required", ex.Message); + } + + [Fact] + public async Task Should_Throw_When_ResumeSession_Called_Without_PermissionHandler() + { + using var client = new CopilotClient(new CopilotClientOptions()); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.ResumeSessionAsync("some-session-id", new ResumeSessionConfig()); + }); + + Assert.Contains("OnPermissionRequest", ex.Message); + Assert.Contains("is required", ex.Message); + } } diff --git a/dotnet/test/CompactionTests.cs b/dotnet/test/CompactionTests.cs index af76508c7..91551e550 100644 --- a/dotnet/test/CompactionTests.cs +++ b/dotnet/test/CompactionTests.cs @@ -15,7 +15,7 @@ public class CompactionTests(E2ETestFixture fixture, ITestOutputHelper output) : public async Task Should_Trigger_Compaction_With_Low_Threshold_And_Emit_Events() { // Create session with very low compaction thresholds to trigger compaction quickly - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { InfiniteSessions = new InfiniteSessionConfig { @@ -84,7 +84,7 @@ await session.SendAndWaitAsync(new MessageOptions [Fact] public async Task Should_Not_Emit_Compaction_Events_When_Infinite_Sessions_Disabled() { - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { InfiniteSessions = new InfiniteSessionConfig { diff --git a/dotnet/test/Harness/E2ETestBase.cs b/dotnet/test/Harness/E2ETestBase.cs index 8727e1239..dc1fa465d 100644 --- a/dotnet/test/Harness/E2ETestBase.cs +++ b/dotnet/test/Harness/E2ETestBase.cs @@ -42,6 +42,28 @@ public async Task InitializeAsync() public Task DisposeAsync() => Task.CompletedTask; + /// + /// Creates a session with a default config that approves all permissions. + /// Convenience wrapper for E2E tests. + /// + protected Task CreateSessionAsync(SessionConfig? config = null) + { + config ??= new SessionConfig(); + config.OnPermissionRequest ??= PermissionHandler.ApproveAll; + return Client.CreateSessionAsync(config); + } + + /// + /// Resumes a session with a default config that approves all permissions. + /// Convenience wrapper for E2E tests. + /// + protected Task ResumeSessionAsync(string sessionId, ResumeSessionConfig? config = null) + { + config ??= new ResumeSessionConfig(); + config.OnPermissionRequest ??= PermissionHandler.ApproveAll; + return Client.ResumeSessionAsync(sessionId, config); + } + protected static string GetSystemMessage(ParsedHttpExchange exchange) => exchange.Request.Messages.FirstOrDefault(m => m.Role == "system")?.Content ?? string.Empty; diff --git a/dotnet/test/HooksTests.cs b/dotnet/test/HooksTests.cs index 44a6e66c2..a37ef3c15 100644 --- a/dotnet/test/HooksTests.cs +++ b/dotnet/test/HooksTests.cs @@ -15,7 +15,7 @@ public async Task Should_Invoke_PreToolUse_Hook_When_Model_Runs_A_Tool() { var preToolUseInputs = new List(); CopilotSession? session = null; - session = await Client.CreateSessionAsync(new SessionConfig + session = await CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Hooks = new SessionHooks @@ -51,7 +51,7 @@ public async Task Should_Invoke_PostToolUse_Hook_After_Model_Runs_A_Tool() { var postToolUseInputs = new List(); CopilotSession? session = null; - session = await Client.CreateSessionAsync(new SessionConfig + session = await CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Hooks = new SessionHooks @@ -89,7 +89,7 @@ public async Task Should_Invoke_Both_PreToolUse_And_PostToolUse_Hooks_For_Single var preToolUseInputs = new List(); var postToolUseInputs = new List(); - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Hooks = new SessionHooks @@ -131,7 +131,7 @@ public async Task Should_Deny_Tool_Execution_When_PreToolUse_Returns_Deny() { var preToolUseInputs = new List(); - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Hooks = new SessionHooks diff --git a/dotnet/test/McpAndAgentsTests.cs b/dotnet/test/McpAndAgentsTests.cs index 644a70bf3..1d35ffda4 100644 --- a/dotnet/test/McpAndAgentsTests.cs +++ b/dotnet/test/McpAndAgentsTests.cs @@ -24,7 +24,7 @@ public async Task Should_Accept_MCP_Server_Configuration_On_Session_Create() } }; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { McpServers = mcpServers }); @@ -45,7 +45,7 @@ public async Task Should_Accept_MCP_Server_Configuration_On_Session_Create() public async Task Should_Accept_MCP_Server_Configuration_On_Session_Resume() { // Create a session first - var session1 = await Client.CreateSessionAsync(); + var session1 = await CreateSessionAsync(); var sessionId = session1.SessionId; await session1.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" }); @@ -61,7 +61,7 @@ public async Task Should_Accept_MCP_Server_Configuration_On_Session_Resume() } }; - var session2 = await Client.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var session2 = await ResumeSessionAsync(sessionId, new ResumeSessionConfig { McpServers = mcpServers }); @@ -96,7 +96,7 @@ public async Task Should_Handle_Multiple_MCP_Servers() } }; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { McpServers = mcpServers }); @@ -120,7 +120,7 @@ public async Task Should_Accept_Custom_Agent_Configuration_On_Session_Create() } }; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); @@ -141,7 +141,7 @@ public async Task Should_Accept_Custom_Agent_Configuration_On_Session_Create() public async Task Should_Accept_Custom_Agent_Configuration_On_Session_Resume() { // Create a session first - var session1 = await Client.CreateSessionAsync(); + var session1 = await CreateSessionAsync(); var sessionId = session1.SessionId; await session1.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" }); @@ -157,7 +157,7 @@ public async Task Should_Accept_Custom_Agent_Configuration_On_Session_Resume() } }; - var session2 = await Client.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var session2 = await ResumeSessionAsync(sessionId, new ResumeSessionConfig { CustomAgents = customAgents }); @@ -187,7 +187,7 @@ public async Task Should_Handle_Custom_Agent_With_Tools_Configuration() } }; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); @@ -220,7 +220,7 @@ public async Task Should_Handle_Custom_Agent_With_MCP_Servers() } }; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); @@ -251,7 +251,7 @@ public async Task Should_Handle_Multiple_Custom_Agents() } }; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); @@ -277,7 +277,7 @@ public async Task Should_Pass_Literal_Env_Values_To_Mcp_Server_Subprocess() } }; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { McpServers = mcpServers, OnPermissionRequest = PermissionHandler.ApproveAll, @@ -321,7 +321,7 @@ public async Task Should_Accept_Both_MCP_Servers_And_Custom_Agents() } }; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { McpServers = mcpServers, CustomAgents = customAgents diff --git a/dotnet/test/PermissionTests.cs b/dotnet/test/PermissionTests.cs index b1295be91..d2c04d1e8 100644 --- a/dotnet/test/PermissionTests.cs +++ b/dotnet/test/PermissionTests.cs @@ -15,7 +15,7 @@ public async Task Should_Invoke_Permission_Handler_For_Write_Operations() { var permissionRequests = new List(); CopilotSession? session = null; - session = await Client.CreateSessionAsync(new SessionConfig + session = await CreateSessionAsync(new SessionConfig { OnPermissionRequest = (request, invocation) => { @@ -44,7 +44,7 @@ await session.SendAsync(new MessageOptions [Fact] public async Task Should_Deny_Permission_When_Handler_Returns_Denied() { - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { OnPermissionRequest = (request, invocation) => { @@ -71,9 +71,13 @@ await session.SendAsync(new MessageOptions } [Fact] - public async Task Should_Deny_Tool_Operations_By_Default_When_No_Handler_Is_Provided() + public async Task Should_Deny_Tool_Operations_When_Handler_Explicitly_Denies() { - var session = await Client.CreateSessionAsync(new SessionConfig()); + var session = await CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = (_, _) => + Task.FromResult(new PermissionRequestResult { Kind = "denied-no-approval-rule-and-could-not-request-from-user" }) + }); var permissionDenied = false; session.On(evt => @@ -95,10 +99,9 @@ await session.SendAndWaitAsync(new MessageOptions } [Fact] - public async Task Should_Work_Without_Permission_Handler__Default_Behavior_() + public async Task Should_Work_With_Approve_All_Permission_Handler() { - // Create session without permission handler - var session = await Client.CreateSessionAsync(new SessionConfig()); + var session = await CreateSessionAsync(new SessionConfig()); await session.SendAsync(new MessageOptions { @@ -113,7 +116,7 @@ await session.SendAsync(new MessageOptions public async Task Should_Handle_Async_Permission_Handler() { var permissionRequestReceived = false; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { OnPermissionRequest = async (request, invocation) => { @@ -140,12 +143,12 @@ public async Task Should_Resume_Session_With_Permission_Handler() var permissionRequestReceived = false; // Create session without permission handler - var session1 = await Client.CreateSessionAsync(); + var session1 = await CreateSessionAsync(); var sessionId = session1.SessionId; await session1.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" }); // Resume with permission handler - var session2 = await Client.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var session2 = await ResumeSessionAsync(sessionId, new ResumeSessionConfig { OnPermissionRequest = (request, invocation) => { @@ -165,7 +168,7 @@ await session2.SendAndWaitAsync(new MessageOptions [Fact] public async Task Should_Handle_Permission_Handler_Errors_Gracefully() { - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { OnPermissionRequest = (request, invocation) => { @@ -186,16 +189,20 @@ await session.SendAsync(new MessageOptions } [Fact] - public async Task Should_Deny_Tool_Operations_By_Default_When_No_Handler_Is_Provided_After_Resume() + public async Task Should_Deny_Tool_Operations_When_Handler_Explicitly_Denies_After_Resume() { - var session1 = await Client.CreateSessionAsync(new SessionConfig + var session1 = await 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 session2 = await ResumeSessionAsync(sessionId, new ResumeSessionConfig + { + OnPermissionRequest = (_, _) => + Task.FromResult(new PermissionRequestResult { Kind = "denied-no-approval-rule-and-could-not-request-from-user" }) + }); var permissionDenied = false; session2.On(evt => @@ -220,7 +227,7 @@ await session2.SendAndWaitAsync(new MessageOptions public async Task Should_Receive_ToolCallId_In_Permission_Requests() { var receivedToolCallId = false; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { OnPermissionRequest = (request, invocation) => { diff --git a/dotnet/test/RpcTests.cs b/dotnet/test/RpcTests.cs index 818bc8760..a13695589 100644 --- a/dotnet/test/RpcTests.cs +++ b/dotnet/test/RpcTests.cs @@ -55,7 +55,7 @@ public async Task Should_Call_Rpc_Account_GetQuota_When_Authenticated() [Fact(Skip = "session.model.getCurrent not yet implemented in CLI")] public async Task Should_Call_Session_Rpc_Model_GetCurrent() { - var session = await Client.CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5" }); + var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5" }); var result = await session.Rpc.Model.GetCurrentAsync(); Assert.NotNull(result.ModelId); @@ -66,7 +66,7 @@ public async Task Should_Call_Session_Rpc_Model_GetCurrent() [Fact(Skip = "session.model.switchTo not yet implemented in CLI")] public async Task Should_Call_Session_Rpc_Model_SwitchTo() { - var session = await Client.CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5" }); + var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5" }); // Get initial model var before = await session.Rpc.Model.GetCurrentAsync(); @@ -84,7 +84,7 @@ public async Task Should_Call_Session_Rpc_Model_SwitchTo() [Fact] public async Task Should_Get_And_Set_Session_Mode() { - var session = await Client.CreateSessionAsync(); + var session = await CreateSessionAsync(); // Get initial mode (default should be interactive) var initial = await session.Rpc.Mode.GetAsync(); @@ -106,7 +106,7 @@ public async Task Should_Get_And_Set_Session_Mode() [Fact] public async Task Should_Read_Update_And_Delete_Plan() { - var session = await Client.CreateSessionAsync(); + var session = await CreateSessionAsync(); // Initially plan should not exist var initial = await session.Rpc.Plan.ReadAsync(); @@ -134,7 +134,7 @@ public async Task Should_Read_Update_And_Delete_Plan() [Fact] public async Task Should_Create_List_And_Read_Workspace_Files() { - var session = await Client.CreateSessionAsync(); + var session = await CreateSessionAsync(); // Initially no files var initialFiles = await session.Rpc.Workspace.ListFilesAsync(); diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index 7b7dcafd7..e4b13fff7 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -15,7 +15,7 @@ public class SessionTests(E2ETestFixture fixture, ITestOutputHelper output) : E2 [Fact] public async Task ShouldCreateAndDestroySessions() { - var session = await Client.CreateSessionAsync(new SessionConfig { Model = "fake-test-model" }); + var session = await CreateSessionAsync(new SessionConfig { Model = "fake-test-model" }); Assert.Matches(@"^[a-f0-9-]+$", session.SessionId); @@ -33,7 +33,7 @@ public async Task ShouldCreateAndDestroySessions() [Fact] public async Task Should_Have_Stateful_Conversation() { - var session = await Client.CreateSessionAsync(); + var session = await CreateSessionAsync(); var assistantMessage = await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" }); Assert.NotNull(assistantMessage); @@ -48,7 +48,7 @@ public async Task Should_Have_Stateful_Conversation() public async Task Should_Create_A_Session_With_Appended_SystemMessage_Config() { var systemMessageSuffix = "End each response with the phrase 'Have a nice day!'"; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { SystemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Append, Content = systemMessageSuffix } }); @@ -72,7 +72,7 @@ public async Task Should_Create_A_Session_With_Appended_SystemMessage_Config() public async Task Should_Create_A_Session_With_Replaced_SystemMessage_Config() { var testSystemMessage = "You are an assistant called Testy McTestface. Reply succinctly."; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { SystemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Replace, Content = testSystemMessage } }); @@ -93,7 +93,7 @@ public async Task Should_Create_A_Session_With_Replaced_SystemMessage_Config() [Fact] public async Task Should_Create_A_Session_With_AvailableTools() { - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { AvailableTools = new List { "view", "edit" } }); @@ -113,7 +113,7 @@ public async Task Should_Create_A_Session_With_AvailableTools() [Fact] public async Task Should_Create_A_Session_With_ExcludedTools() { - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { ExcludedTools = new List { "view" } }); @@ -133,7 +133,7 @@ public async Task Should_Create_A_Session_With_ExcludedTools() [Fact] public async Task Should_Create_Session_With_Custom_Tool() { - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { Tools = [ @@ -153,7 +153,7 @@ public async Task Should_Create_Session_With_Custom_Tool() [Fact] public async Task Should_Resume_A_Session_Using_The_Same_Client() { - var session1 = await Client.CreateSessionAsync(); + var session1 = await CreateSessionAsync(); var sessionId = session1.SessionId; await session1.SendAsync(new MessageOptions { Prompt = "What is 1+1?" }); @@ -161,7 +161,7 @@ public async Task Should_Resume_A_Session_Using_The_Same_Client() Assert.NotNull(answer); Assert.Contains("2", answer!.Data.Content ?? string.Empty); - var session2 = await Client.ResumeSessionAsync(sessionId); + var session2 = await ResumeSessionAsync(sessionId); Assert.Equal(sessionId, session2.SessionId); var answer2 = await TestHelper.GetFinalAssistantMessageAsync(session2); @@ -172,7 +172,7 @@ public async Task Should_Resume_A_Session_Using_The_Same_Client() [Fact] public async Task Should_Resume_A_Session_Using_A_New_Client() { - var session1 = await Client.CreateSessionAsync(); + var session1 = await CreateSessionAsync(); var sessionId = session1.SessionId; await session1.SendAsync(new MessageOptions { Prompt = "What is 1+1?" }); @@ -181,7 +181,7 @@ public async Task Should_Resume_A_Session_Using_A_New_Client() Assert.Contains("2", answer!.Data.Content ?? string.Empty); using var newClient = Ctx.CreateClient(); - var session2 = await newClient.ResumeSessionAsync(sessionId); + var session2 = await newClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll }); Assert.Equal(sessionId, session2.SessionId); var messages = await session2.GetMessagesAsync(); @@ -193,13 +193,13 @@ public async Task Should_Resume_A_Session_Using_A_New_Client() public async Task Should_Throw_Error_When_Resuming_Non_Existent_Session() { await Assert.ThrowsAsync(() => - Client.ResumeSessionAsync("non-existent-session-id")); + ResumeSessionAsync("non-existent-session-id")); } [Fact] public async Task Should_Abort_A_Session() { - var session = await Client.CreateSessionAsync(); + var session = await CreateSessionAsync(); // Set up wait for tool execution to start BEFORE sending var toolStartTask = TestHelper.GetNextEventOfTypeAsync(session); @@ -237,7 +237,7 @@ await session.SendAsync(new MessageOptions [Fact(Skip = "Requires schema update for AssistantMessageDeltaEvent type")] public async Task Should_Receive_Streaming_Delta_Events_When_Streaming_Is_Enabled() { - var session = await Client.CreateSessionAsync(new SessionConfig { Streaming = true }); + var session = await CreateSessionAsync(new SessionConfig { Streaming = true }); var deltaContents = new List(); var doneEvent = new TaskCompletionSource(); @@ -282,7 +282,7 @@ public async Task Should_Receive_Streaming_Delta_Events_When_Streaming_Is_Enable public async Task Should_Pass_Streaming_Option_To_Session_Creation() { // Verify that the streaming option is accepted without errors - var session = await Client.CreateSessionAsync(new SessionConfig { Streaming = true }); + var session = await CreateSessionAsync(new SessionConfig { Streaming = true }); Assert.Matches(@"^[a-f0-9-]+$", session.SessionId); @@ -296,7 +296,7 @@ public async Task Should_Pass_Streaming_Option_To_Session_Creation() [Fact] public async Task Should_Receive_Session_Events() { - var session = await Client.CreateSessionAsync(); + var session = await CreateSessionAsync(); var receivedEvents = new List(); var idleReceived = new TaskCompletionSource(); @@ -333,7 +333,7 @@ public async Task Should_Receive_Session_Events() [Fact] public async Task Send_Returns_Immediately_While_Events_Stream_In_Background() { - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -358,7 +358,7 @@ public async Task Send_Returns_Immediately_While_Events_Stream_In_Background() [Fact] public async Task SendAndWait_Blocks_Until_Session_Idle_And_Returns_Final_Assistant_Message() { - var session = await Client.CreateSessionAsync(); + var session = await CreateSessionAsync(); var events = new List(); session.On(evt => events.Add(evt.Type)); @@ -376,7 +376,7 @@ public async Task SendAndWait_Blocks_Until_Session_Idle_And_Returns_Final_Assist [Fact(Skip = "Needs test harness CAPI proxy support")] public async Task Should_List_Sessions_With_Context() { - var session = await Client.CreateSessionAsync(); + var session = await CreateSessionAsync(); var sessions = await Client.ListSessionsAsync(); Assert.NotEmpty(sessions); @@ -394,7 +394,7 @@ public async Task Should_List_Sessions_With_Context() [Fact] public async Task SendAndWait_Throws_On_Timeout() { - var session = await Client.CreateSessionAsync(); + var session = await CreateSessionAsync(); // Use a slow command to ensure timeout triggers before completion var ex = await Assert.ThrowsAsync(() => @@ -406,7 +406,7 @@ public async Task SendAndWait_Throws_On_Timeout() [Fact] public async Task SendAndWait_Throws_OperationCanceledException_When_Token_Cancelled() { - var session = await Client.CreateSessionAsync(); + var session = await CreateSessionAsync(); // Set up wait for tool execution to start BEFORE sending var toolStartTask = TestHelper.GetNextEventOfTypeAsync(session); @@ -431,7 +431,7 @@ public async Task SendAndWait_Throws_OperationCanceledException_When_Token_Cance public async Task Should_Create_Session_With_Custom_Config_Dir() { var customConfigDir = Path.Join(Ctx.HomeDir, "custom-config"); - var session = await Client.CreateSessionAsync(new SessionConfig { ConfigDir = customConfigDir }); + var session = await CreateSessionAsync(new SessionConfig { ConfigDir = customConfigDir }); Assert.Matches(@"^[a-f0-9-]+$", session.SessionId); diff --git a/dotnet/test/SkillsTests.cs b/dotnet/test/SkillsTests.cs index bba5e1e5f..d68eed79d 100644 --- a/dotnet/test/SkillsTests.cs +++ b/dotnet/test/SkillsTests.cs @@ -52,7 +52,7 @@ private string CreateSkillDir() public async Task Should_Load_And_Apply_Skill_From_SkillDirectories() { var skillsDir = CreateSkillDir(); - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { SkillDirectories = [skillsDir] }); @@ -71,7 +71,7 @@ public async Task Should_Load_And_Apply_Skill_From_SkillDirectories() public async Task Should_Not_Apply_Skill_When_Disabled_Via_DisabledSkills() { var skillsDir = CreateSkillDir(); - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { SkillDirectories = [skillsDir], DisabledSkills = ["test-skill"] @@ -93,7 +93,7 @@ public async Task Should_Apply_Skill_On_Session_Resume_With_SkillDirectories() var skillsDir = CreateSkillDir(); // Create a session without skills first - var session1 = await Client.CreateSessionAsync(); + var session1 = await CreateSessionAsync(); var sessionId = session1.SessionId; // First message without skill - marker should not appear @@ -102,7 +102,7 @@ public async Task Should_Apply_Skill_On_Session_Resume_With_SkillDirectories() Assert.DoesNotContain(SkillMarker, message1!.Data.Content); // Resume with skillDirectories - skill should now be active - var session2 = await Client.ResumeSessionAsync(sessionId, new ResumeSessionConfig + var session2 = await ResumeSessionAsync(sessionId, new ResumeSessionConfig { SkillDirectories = [skillsDir] }); diff --git a/dotnet/test/ToolsTests.cs b/dotnet/test/ToolsTests.cs index ad1ab7a21..942a09a09 100644 --- a/dotnet/test/ToolsTests.cs +++ b/dotnet/test/ToolsTests.cs @@ -21,7 +21,7 @@ await File.WriteAllTextAsync( Path.Combine(Ctx.WorkDir, "README.md"), "# ELIZA, the only chatbot you'll ever need"); - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -39,7 +39,7 @@ await session.SendAsync(new MessageOptions [Fact] public async Task Invokes_Custom_Tool() { - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { Tools = [AIFunctionFactory.Create(EncryptString, "encrypt_string")], }); @@ -64,7 +64,7 @@ public async Task Handles_Tool_Calling_Errors() var getUserLocation = AIFunctionFactory.Create( () => { throw new Exception("Melbourne"); }, "get_user_location", "Gets the user's location"); - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { Tools = [getUserLocation] }); @@ -105,7 +105,7 @@ public async Task Handles_Tool_Calling_Errors() public async Task Can_Receive_And_Return_Complex_Types() { ToolInvocation? receivedInvocation = null; - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { Tools = [AIFunctionFactory.Create(PerformDbQuery, "db_query", serializerOptions: ToolsTestsJsonContext.Default.Options)], }); @@ -151,7 +151,7 @@ private partial class ToolsTestsJsonContext : JsonSerializerContext; [Fact(Skip = "Behaves as if no content was in the result. Likely that binary results aren't fully implemented yet.")] public async Task Can_Return_Binary_Result() { - var session = await Client.CreateSessionAsync(new SessionConfig + var session = await CreateSessionAsync(new SessionConfig { Tools = [AIFunctionFactory.Create(GetImage, "get_image")], }); diff --git a/go/README.md b/go/README.md index c528e945c..e9355d559 100644 --- a/go/README.md +++ b/go/README.md @@ -99,7 +99,7 @@ That's it! When your application calls `copilot.NewClient` without a `CLIPath` n - `Stop() error` - Stop the CLI server - `ForceStop()` - Forcefully stop without graceful cleanup - `CreateSession(config *SessionConfig) (*Session, error)` - Create a new session -- `ResumeSession(sessionID string) (*Session, error)` - Resume an existing session +- `ResumeSession(sessionID string, config *ResumeSessionConfig) (*Session, error)` - Resume an existing session - `ResumeSessionWithOptions(sessionID string, config *ResumeSessionConfig) (*Session, error)` - Resume with additional configuration - `ListSessions(filter *SessionListFilter) ([]SessionMetadata, error)` - List sessions (with optional filter) - `DeleteSession(sessionID string) error` - Delete a session permanently diff --git a/go/client.go b/go/client.go index 82c095274..50e6b4ccb 100644 --- a/go/client.go +++ b/go/client.go @@ -12,6 +12,7 @@ // defer client.Stop() // // session, err := client.CreateSession(&copilot.SessionConfig{ +// OnPermissionRequest: copilot.PermissionHandler.ApproveAll, // Model: "gpt-4", // }) // if err != nil { @@ -426,17 +427,20 @@ func (c *Client) ensureConnected() error { // If the client is not connected and AutoStart is enabled, this will automatically // start the connection. // -// The config parameter is optional; pass nil for default settings. +// The config parameter is required and must include an OnPermissionRequest handler. // // Returns the created session or an error if session creation fails. // // Example: // // // Basic session -// session, err := client.CreateSession(context.Background(), nil) +// session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{ +// OnPermissionRequest: copilot.PermissionHandler.ApproveAll, +// }) // // // Session with model and tools // session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{ +// OnPermissionRequest: copilot.PermissionHandler.ApproveAll, // Model: "gpt-4", // Tools: []copilot.Tool{ // { @@ -447,44 +451,46 @@ func (c *Client) ensureConnected() error { // }, // }) func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Session, error) { + if config == nil || config.OnPermissionRequest == nil { + return nil, fmt.Errorf("an OnPermissionRequest handler is required when creating a session. For example, to allow all permissions, use &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}") + } + if err := c.ensureConnected(); err != nil { return nil, err } req := createSessionRequest{} - if config != nil { - req.Model = config.Model - req.SessionID = config.SessionID - req.ClientName = config.ClientName - req.ReasoningEffort = config.ReasoningEffort - req.ConfigDir = config.ConfigDir - req.Tools = config.Tools - req.SystemMessage = config.SystemMessage - req.AvailableTools = config.AvailableTools - req.ExcludedTools = config.ExcludedTools - req.Provider = config.Provider - req.WorkingDirectory = config.WorkingDirectory - req.MCPServers = config.MCPServers - req.EnvValueMode = "direct" - req.CustomAgents = config.CustomAgents - req.SkillDirectories = config.SkillDirectories - req.DisabledSkills = config.DisabledSkills - req.InfiniteSessions = config.InfiniteSessions - - if config.Streaming { - req.Streaming = Bool(true) - } - if config.OnUserInputRequest != nil { - req.RequestUserInput = Bool(true) - } - if config.Hooks != nil && (config.Hooks.OnPreToolUse != nil || - config.Hooks.OnPostToolUse != nil || - config.Hooks.OnUserPromptSubmitted != nil || - config.Hooks.OnSessionStart != nil || - config.Hooks.OnSessionEnd != nil || - config.Hooks.OnErrorOccurred != nil) { - req.Hooks = Bool(true) - } + req.Model = config.Model + req.SessionID = config.SessionID + req.ClientName = config.ClientName + req.ReasoningEffort = config.ReasoningEffort + req.ConfigDir = config.ConfigDir + req.Tools = config.Tools + req.SystemMessage = config.SystemMessage + req.AvailableTools = config.AvailableTools + req.ExcludedTools = config.ExcludedTools + req.Provider = config.Provider + req.WorkingDirectory = config.WorkingDirectory + req.MCPServers = config.MCPServers + req.EnvValueMode = "direct" + req.CustomAgents = config.CustomAgents + req.SkillDirectories = config.SkillDirectories + req.DisabledSkills = config.DisabledSkills + req.InfiniteSessions = config.InfiniteSessions + + if config.Streaming { + req.Streaming = Bool(true) + } + if config.OnUserInputRequest != nil { + req.RequestUserInput = Bool(true) + } + if config.Hooks != nil && (config.Hooks.OnPreToolUse != nil || + config.Hooks.OnPostToolUse != nil || + config.Hooks.OnUserPromptSubmitted != nil || + config.Hooks.OnSessionStart != nil || + config.Hooks.OnSessionEnd != nil || + config.Hooks.OnErrorOccurred != nil) { + req.Hooks = Bool(true) } req.RequestPermission = Bool(true) @@ -500,19 +506,13 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses session := newSession(response.SessionID, c.client, response.WorkspacePath) - if config != nil { - session.registerTools(config.Tools) - if config.OnPermissionRequest != nil { - session.registerPermissionHandler(config.OnPermissionRequest) - } - if config.OnUserInputRequest != nil { - session.registerUserInputHandler(config.OnUserInputRequest) - } - if config.Hooks != nil { - session.registerHooks(config.Hooks) - } - } else { - session.registerTools(nil) + session.registerTools(config.Tools) + session.registerPermissionHandler(config.OnPermissionRequest) + if config.OnUserInputRequest != nil { + session.registerUserInputHandler(config.OnUserInputRequest) + } + if config.Hooks != nil { + session.registerHooks(config.Hooks) } c.sessionsMux.Lock() @@ -522,15 +522,18 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses return session, nil } -// ResumeSession resumes an existing conversation session by its ID using default options. +// ResumeSession resumes an existing conversation session by its ID. // -// This is a convenience method that calls [Client.ResumeSessionWithOptions] with nil config. +// This is a convenience method that calls [Client.ResumeSessionWithOptions]. +// The config must include an OnPermissionRequest handler. // // Example: // -// session, err := client.ResumeSession(context.Background(), "session-123") -func (c *Client) ResumeSession(ctx context.Context, sessionID string) (*Session, error) { - return c.ResumeSessionWithOptions(ctx, sessionID, nil) +// session, err := client.ResumeSession(context.Background(), "session-123", &copilot.ResumeSessionConfig{ +// OnPermissionRequest: copilot.PermissionHandler.ApproveAll, +// }) +func (c *Client) ResumeSession(ctx context.Context, sessionID string, config *ResumeSessionConfig) (*Session, error) { + return c.ResumeSessionWithOptions(ctx, sessionID, config) } // ResumeSessionWithOptions resumes an existing conversation session with additional configuration. @@ -541,50 +544,53 @@ func (c *Client) ResumeSession(ctx context.Context, sessionID string) (*Session, // Example: // // session, err := client.ResumeSessionWithOptions(context.Background(), "session-123", &copilot.ResumeSessionConfig{ +// OnPermissionRequest: copilot.PermissionHandler.ApproveAll, // Tools: []copilot.Tool{myNewTool}, // }) func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, config *ResumeSessionConfig) (*Session, error) { + if config == nil || config.OnPermissionRequest == nil { + return nil, fmt.Errorf("an OnPermissionRequest handler is required when resuming a session. For example, to allow all permissions, use &copilot.ResumeSessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}") + } + if err := c.ensureConnected(); err != nil { return nil, err } var req resumeSessionRequest req.SessionID = sessionID - if config != nil { - req.ClientName = config.ClientName - req.Model = config.Model - req.ReasoningEffort = config.ReasoningEffort - req.SystemMessage = config.SystemMessage - req.Tools = config.Tools - req.Provider = config.Provider - req.AvailableTools = config.AvailableTools - req.ExcludedTools = config.ExcludedTools - if config.Streaming { - req.Streaming = Bool(true) - } - if config.OnUserInputRequest != nil { - req.RequestUserInput = Bool(true) - } - if config.Hooks != nil && (config.Hooks.OnPreToolUse != nil || - config.Hooks.OnPostToolUse != nil || - config.Hooks.OnUserPromptSubmitted != nil || - config.Hooks.OnSessionStart != nil || - config.Hooks.OnSessionEnd != nil || - config.Hooks.OnErrorOccurred != nil) { - req.Hooks = Bool(true) - } - req.WorkingDirectory = config.WorkingDirectory - req.ConfigDir = config.ConfigDir - if config.DisableResume { - req.DisableResume = Bool(true) - } - req.MCPServers = config.MCPServers - req.EnvValueMode = "direct" - req.CustomAgents = config.CustomAgents - req.SkillDirectories = config.SkillDirectories - req.DisabledSkills = config.DisabledSkills - req.InfiniteSessions = config.InfiniteSessions - } + req.ClientName = config.ClientName + req.Model = config.Model + req.ReasoningEffort = config.ReasoningEffort + req.SystemMessage = config.SystemMessage + req.Tools = config.Tools + req.Provider = config.Provider + req.AvailableTools = config.AvailableTools + req.ExcludedTools = config.ExcludedTools + if config.Streaming { + req.Streaming = Bool(true) + } + if config.OnUserInputRequest != nil { + req.RequestUserInput = Bool(true) + } + if config.Hooks != nil && (config.Hooks.OnPreToolUse != nil || + config.Hooks.OnPostToolUse != nil || + config.Hooks.OnUserPromptSubmitted != nil || + config.Hooks.OnSessionStart != nil || + config.Hooks.OnSessionEnd != nil || + config.Hooks.OnErrorOccurred != nil) { + req.Hooks = Bool(true) + } + req.WorkingDirectory = config.WorkingDirectory + req.ConfigDir = config.ConfigDir + if config.DisableResume { + req.DisableResume = Bool(true) + } + req.MCPServers = config.MCPServers + req.EnvValueMode = "direct" + req.CustomAgents = config.CustomAgents + req.SkillDirectories = config.SkillDirectories + req.DisabledSkills = config.DisabledSkills + req.InfiniteSessions = config.InfiniteSessions req.RequestPermission = Bool(true) result, err := c.client.Request("session.resume", req) @@ -598,19 +604,13 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, } session := newSession(response.SessionID, c.client, response.WorkspacePath) - if config != nil { - session.registerTools(config.Tools) - if config.OnPermissionRequest != nil { - session.registerPermissionHandler(config.OnPermissionRequest) - } - if config.OnUserInputRequest != nil { - session.registerUserInputHandler(config.OnUserInputRequest) - } - if config.Hooks != nil { - session.registerHooks(config.Hooks) - } - } else { - session.registerTools(nil) + session.registerTools(config.Tools) + session.registerPermissionHandler(config.OnPermissionRequest) + if config.OnUserInputRequest != nil { + session.registerUserInputHandler(config.OnUserInputRequest) + } + if config.Hooks != nil { + session.registerHooks(config.Hooks) } c.sessionsMux.Lock() @@ -881,7 +881,9 @@ func (c *Client) handleLifecycleEvent(event SessionLifecycleEvent) { // Example: // // if client.State() == copilot.StateConnected { -// session, err := client.CreateSession(context.Background(), nil) +// session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{ +// OnPermissionRequest: copilot.PermissionHandler.ApproveAll, +// }) // } func (c *Client) State() ConnectionState { return c.state diff --git a/go/client_test.go b/go/client_test.go index d21cc0185..2d198f224 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -21,7 +21,9 @@ func TestClient_HandleToolCallRequest(t *testing.T) { client := NewClient(&ClientOptions{CLIPath: cliPath}) t.Cleanup(func() { client.ForceStop() }) - session, err := client.CreateSession(t.Context(), nil) + session, err := client.CreateSession(t.Context(), &SessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + }) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -444,3 +446,43 @@ func TestResumeSessionRequest_ClientName(t *testing.T) { } }) } + +func TestClient_CreateSession_RequiresPermissionHandler(t *testing.T) { + t.Run("returns error when config is nil", func(t *testing.T) { + client := NewClient(nil) + _, err := client.CreateSession(t.Context(), nil) + if err == nil { + t.Fatal("Expected error when OnPermissionRequest is nil") + } + matched, _ := regexp.MatchString("OnPermissionRequest.*is required", err.Error()) + if !matched { + t.Errorf("Expected error about OnPermissionRequest being required, got: %v", err) + } + }) + + t.Run("returns error when OnPermissionRequest is not set", func(t *testing.T) { + client := NewClient(nil) + _, err := client.CreateSession(t.Context(), &SessionConfig{}) + if err == nil { + t.Fatal("Expected error when OnPermissionRequest is nil") + } + matched, _ := regexp.MatchString("OnPermissionRequest.*is required", err.Error()) + if !matched { + t.Errorf("Expected error about OnPermissionRequest being required, got: %v", err) + } + }) +} + +func TestClient_ResumeSession_RequiresPermissionHandler(t *testing.T) { + t.Run("returns error when config is nil", func(t *testing.T) { + client := NewClient(nil) + _, err := client.ResumeSessionWithOptions(t.Context(), "some-id", nil) + if err == nil { + t.Fatal("Expected error when OnPermissionRequest is nil") + } + matched, _ := regexp.MatchString("OnPermissionRequest.*is required", err.Error()) + if !matched { + t.Errorf("Expected error about OnPermissionRequest being required, got: %v", err) + } + }) +} diff --git a/go/internal/e2e/ask_user_test.go b/go/internal/e2e/ask_user_test.go index 305d9df8a..d5458483a 100644 --- a/go/internal/e2e/ask_user_test.go +++ b/go/internal/e2e/ask_user_test.go @@ -20,6 +20,7 @@ func TestAskUser(t *testing.T) { var mu sync.Mutex session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, OnUserInputRequest: func(request copilot.UserInputRequest, invocation copilot.UserInputInvocation) (copilot.UserInputResponse, error) { mu.Lock() userInputRequests = append(userInputRequests, request) @@ -80,6 +81,7 @@ func TestAskUser(t *testing.T) { var mu sync.Mutex session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, OnUserInputRequest: func(request copilot.UserInputRequest, invocation copilot.UserInputInvocation) (copilot.UserInputResponse, error) { mu.Lock() userInputRequests = append(userInputRequests, request) @@ -135,6 +137,7 @@ func TestAskUser(t *testing.T) { freeformAnswer := "This is my custom freeform answer that was not in the choices" session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, OnUserInputRequest: func(request copilot.UserInputRequest, invocation copilot.UserInputInvocation) (copilot.UserInputResponse, error) { mu.Lock() userInputRequests = append(userInputRequests, request) diff --git a/go/internal/e2e/client_test.go b/go/internal/e2e/client_test.go index 8f5cf2495..d2663d2fa 100644 --- a/go/internal/e2e/client_test.go +++ b/go/internal/e2e/client_test.go @@ -94,7 +94,9 @@ func TestClient(t *testing.T) { }) t.Cleanup(func() { client.ForceStop() }) - _, err := client.CreateSession(t.Context(), nil) + _, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -118,7 +120,9 @@ func TestClient(t *testing.T) { }) t.Cleanup(func() { client.ForceStop() }) - _, err := client.CreateSession(t.Context(), nil) + _, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) if err != nil { t.Fatalf("Failed to create session: %v", err) } diff --git a/go/internal/e2e/compaction_test.go b/go/internal/e2e/compaction_test.go index da9ea240c..239e1e128 100644 --- a/go/internal/e2e/compaction_test.go +++ b/go/internal/e2e/compaction_test.go @@ -21,6 +21,7 @@ func TestCompaction(t *testing.T) { bufferThreshold := 0.01 // 1% session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, InfiniteSessions: &copilot.InfiniteSessionConfig{ Enabled: &enabled, BackgroundCompactionThreshold: &backgroundThreshold, @@ -93,6 +94,7 @@ func TestCompaction(t *testing.T) { enabled := false session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, InfiniteSessions: &copilot.InfiniteSessionConfig{ Enabled: &enabled, }, diff --git a/go/internal/e2e/mcp_and_agents_test.go b/go/internal/e2e/mcp_and_agents_test.go index f8325b9f4..0f49a05c0 100644 --- a/go/internal/e2e/mcp_and_agents_test.go +++ b/go/internal/e2e/mcp_and_agents_test.go @@ -27,7 +27,8 @@ func TestMCPServers(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - MCPServers: mcpServers, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + MCPServers: mcpServers, }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -61,7 +62,7 @@ func TestMCPServers(t *testing.T) { ctx.ConfigureForTest(t) // Create a session first - session1, err := client.CreateSession(t.Context(), nil) + session1, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -83,7 +84,8 @@ func TestMCPServers(t *testing.T) { } session2, err := client.ResumeSessionWithOptions(t.Context(), sessionID, &copilot.ResumeSessionConfig{ - MCPServers: mcpServers, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + MCPServers: mcpServers, }) if err != nil { t.Fatalf("Failed to resume session: %v", err) @@ -170,7 +172,8 @@ func TestMCPServers(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - MCPServers: mcpServers, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + MCPServers: mcpServers, }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -204,7 +207,8 @@ func TestCustomAgents(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - CustomAgents: customAgents, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + CustomAgents: customAgents, }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -238,7 +242,7 @@ func TestCustomAgents(t *testing.T) { ctx.ConfigureForTest(t) // Create a session first - session1, err := client.CreateSession(t.Context(), nil) + session1, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -260,7 +264,8 @@ func TestCustomAgents(t *testing.T) { } session2, err := client.ResumeSessionWithOptions(t.Context(), sessionID, &copilot.ResumeSessionConfig{ - CustomAgents: customAgents, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + CustomAgents: customAgents, }) if err != nil { t.Fatalf("Failed to resume session: %v", err) @@ -298,7 +303,8 @@ func TestCustomAgents(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - CustomAgents: customAgents, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + CustomAgents: customAgents, }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -332,7 +338,8 @@ func TestCustomAgents(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - CustomAgents: customAgents, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + CustomAgents: customAgents, }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -368,7 +375,8 @@ func TestCustomAgents(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - CustomAgents: customAgents, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + CustomAgents: customAgents, }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -409,8 +417,9 @@ func TestCombinedConfiguration(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - MCPServers: mcpServers, - CustomAgents: customAgents, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + MCPServers: mcpServers, + CustomAgents: customAgents, }) if err != nil { t.Fatalf("Failed to create session: %v", err) diff --git a/go/internal/e2e/permissions_test.go b/go/internal/e2e/permissions_test.go index 1584f0244..d1d9134b1 100644 --- a/go/internal/e2e/permissions_test.go +++ b/go/internal/e2e/permissions_test.go @@ -157,10 +157,14 @@ func TestPermissions(t *testing.T) { } }) - t.Run("should deny tool operations by default when no handler is provided", func(t *testing.T) { + t.Run("should deny tool operations when handler explicitly denies", func(t *testing.T) { ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), nil) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: "denied-no-approval-rule-and-could-not-request-from-user"}, nil + }, + }) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -192,7 +196,7 @@ func TestPermissions(t *testing.T) { } }) - t.Run("should deny tool operations by default when no handler is provided after resume", func(t *testing.T) { + t.Run("should deny tool operations when handler explicitly denies after resume", func(t *testing.T) { ctx.ConfigureForTest(t) session1, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ @@ -206,7 +210,11 @@ func TestPermissions(t *testing.T) { t.Fatalf("Failed to send message: %v", err) } - session2, err := client.ResumeSession(t.Context(), sessionID) + session2, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: "denied-no-approval-rule-and-could-not-request-from-user"}, nil + }, + }) if err != nil { t.Fatalf("Failed to resume session: %v", err) } @@ -238,10 +246,12 @@ func TestPermissions(t *testing.T) { } }) - t.Run("without permission handler", func(t *testing.T) { + t.Run("should work with approve-all permission handler", func(t *testing.T) { ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), nil) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) if err != nil { t.Fatalf("Failed to create session: %v", err) } diff --git a/go/internal/e2e/rpc_test.go b/go/internal/e2e/rpc_test.go index 43b7cafa8..1f8f17c16 100644 --- a/go/internal/e2e/rpc_test.go +++ b/go/internal/e2e/rpc_test.go @@ -130,7 +130,8 @@ func TestSessionRpc(t *testing.T) { t.Skip("session.model.getCurrent not yet implemented in CLI") session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Model: "claude-sonnet-4.5", }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -151,7 +152,8 @@ func TestSessionRpc(t *testing.T) { t.Skip("session.model.switchTo not yet implemented in CLI") session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Model: "claude-sonnet-4.5", }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -188,7 +190,7 @@ func TestSessionRpc(t *testing.T) { }) t.Run("should get and set session mode", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), nil) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -231,7 +233,7 @@ func TestSessionRpc(t *testing.T) { }) t.Run("should read, update, and delete plan", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), nil) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -287,7 +289,7 @@ func TestSessionRpc(t *testing.T) { }) t.Run("should create, list, and read workspace files", func(t *testing.T) { - session, err := client.CreateSession(t.Context(), nil) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) if err != nil { t.Fatalf("Failed to create session: %v", err) } diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index 87341838a..f04307c2d 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -18,7 +18,7 @@ func TestSession(t *testing.T) { t.Run("should create and destroy sessions", func(t *testing.T) { ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{Model: "fake-test-model"}) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Model: "fake-test-model"}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -58,7 +58,7 @@ func TestSession(t *testing.T) { t.Run("should have stateful conversation", func(t *testing.T) { ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), nil) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -87,6 +87,7 @@ func TestSession(t *testing.T) { systemMessageSuffix := "End each response with the phrase 'Have a nice day!'" session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, SystemMessage: &copilot.SystemMessageConfig{ Mode: "append", Content: systemMessageSuffix, @@ -135,6 +136,7 @@ func TestSession(t *testing.T) { testSystemMessage := "You are an assistant called Testy McTestface. Reply succinctly." session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, SystemMessage: &copilot.SystemMessageConfig{ Mode: "replace", Content: testSystemMessage, @@ -184,7 +186,8 @@ func TestSession(t *testing.T) { ctx.ConfigureForTest(t) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - AvailableTools: []string{"view", "edit"}, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + AvailableTools: []string{"view", "edit"}, }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -222,7 +225,8 @@ func TestSession(t *testing.T) { ctx.ConfigureForTest(t) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - ExcludedTools: []string{"view"}, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + ExcludedTools: []string{"view"}, }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -260,6 +264,7 @@ func TestSession(t *testing.T) { ctx.ConfigureForTest(t) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Tools: []copilot.Tool{ { Name: "get_secret_number", @@ -323,7 +328,7 @@ func TestSession(t *testing.T) { ctx.ConfigureForTest(t) // Create initial session - session1, err := client.CreateSession(t.Context(), nil) + session1, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -344,7 +349,9 @@ func TestSession(t *testing.T) { } // Resume using the same client - session2, err := client.ResumeSession(t.Context(), sessionID) + session2, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) if err != nil { t.Fatalf("Failed to resume session: %v", err) } @@ -367,7 +374,7 @@ func TestSession(t *testing.T) { ctx.ConfigureForTest(t) // Create initial session - session1, err := client.CreateSession(t.Context(), nil) + session1, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -391,7 +398,9 @@ func TestSession(t *testing.T) { newClient := ctx.NewClient() defer newClient.ForceStop() - session2, err := newClient.ResumeSession(t.Context(), sessionID) + session2, err := newClient.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) if err != nil { t.Fatalf("Failed to resume session: %v", err) } @@ -428,7 +437,9 @@ func TestSession(t *testing.T) { t.Run("should throw error when resuming non-existent session", func(t *testing.T) { ctx.ConfigureForTest(t) - _, err := client.ResumeSession(t.Context(), "non-existent-session-id") + _, err := client.ResumeSession(t.Context(), "non-existent-session-id", &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) if err == nil { t.Error("Expected error when resuming non-existent session") } @@ -437,7 +448,7 @@ func TestSession(t *testing.T) { t.Run("should resume session with a custom provider", func(t *testing.T) { ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), nil) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -445,6 +456,7 @@ func TestSession(t *testing.T) { // Resume the session with a provider session2, err := client.ResumeSessionWithOptions(t.Context(), sessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Provider: &copilot.ProviderConfig{ Type: "openai", BaseURL: "https://api.openai.com/v1", @@ -557,7 +569,8 @@ func TestSession(t *testing.T) { ctx.ConfigureForTest(t) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Streaming: true, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Streaming: true, }) if err != nil { t.Fatalf("Failed to create session with streaming: %v", err) @@ -617,7 +630,8 @@ func TestSession(t *testing.T) { // Verify that the streaming option is accepted without errors session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Streaming: true, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Streaming: true, }) if err != nil { t.Fatalf("Failed to create session with streaming: %v", err) @@ -647,7 +661,7 @@ func TestSession(t *testing.T) { t.Run("should receive session events", func(t *testing.T) { ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), nil) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -722,7 +736,8 @@ func TestSession(t *testing.T) { customConfigDir := ctx.HomeDir + "/custom-config" session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - ConfigDir: customConfigDir, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + ConfigDir: customConfigDir, }) if err != nil { t.Fatalf("Failed to create session with custom config dir: %v", err) @@ -753,7 +768,7 @@ func TestSession(t *testing.T) { ctx.ConfigureForTest(t) // Create a couple of sessions and send messages to persist them - session1, err := client.CreateSession(t.Context(), nil) + session1, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) if err != nil { t.Fatalf("Failed to create session1: %v", err) } @@ -763,7 +778,7 @@ func TestSession(t *testing.T) { t.Fatalf("Failed to send message to session1: %v", err) } - session2, err := client.CreateSession(t.Context(), nil) + session2, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) if err != nil { t.Fatalf("Failed to create session2: %v", err) } @@ -829,7 +844,7 @@ func TestSession(t *testing.T) { ctx.ConfigureForTest(t) // Create a session and send a message to persist it - session, err := client.CreateSession(t.Context(), nil) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -881,7 +896,9 @@ func TestSession(t *testing.T) { } // Verify we cannot resume the deleted session - _, err = client.ResumeSession(t.Context(), sessionID) + _, err = client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) if err == nil { t.Error("Expected error when resuming deleted session") } diff --git a/go/internal/e2e/skills_test.go b/go/internal/e2e/skills_test.go index ed3578abd..10cd50028 100644 --- a/go/internal/e2e/skills_test.go +++ b/go/internal/e2e/skills_test.go @@ -57,7 +57,8 @@ func TestSkills(t *testing.T) { skillsDir := createTestSkillDir(t, ctx.WorkDir, skillMarker) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - SkillDirectories: []string{skillsDir}, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillDirectories: []string{skillsDir}, }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -84,8 +85,9 @@ func TestSkills(t *testing.T) { skillsDir := createTestSkillDir(t, ctx.WorkDir, skillMarker) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - SkillDirectories: []string{skillsDir}, - DisabledSkills: []string{"test-skill"}, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillDirectories: []string{skillsDir}, + DisabledSkills: []string{"test-skill"}, }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -113,7 +115,7 @@ func TestSkills(t *testing.T) { skillsDir := createTestSkillDir(t, ctx.WorkDir, skillMarker) // Create a session without skills first - session1, err := client.CreateSession(t.Context(), nil) + session1, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -131,7 +133,8 @@ func TestSkills(t *testing.T) { // Resume with skillDirectories - skill should now be active session2, err := client.ResumeSessionWithOptions(t.Context(), sessionID, &copilot.ResumeSessionConfig{ - SkillDirectories: []string{skillsDir}, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillDirectories: []string{skillsDir}, }) if err != nil { t.Fatalf("Failed to resume session: %v", err) diff --git a/go/internal/e2e/tools_test.go b/go/internal/e2e/tools_test.go index d54bdcb14..b38e41a60 100644 --- a/go/internal/e2e/tools_test.go +++ b/go/internal/e2e/tools_test.go @@ -55,6 +55,7 @@ func TestTools(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Tools: []copilot.Tool{ copilot.DefineTool("encrypt_string", "Encrypts a string", func(params EncryptParams, inv copilot.ToolInvocation) (string, error) { @@ -87,6 +88,7 @@ func TestTools(t *testing.T) { type EmptyParams struct{} session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Tools: []copilot.Tool{ copilot.DefineTool("get_user_location", "Gets the user's location", func(params EmptyParams, inv copilot.ToolInvocation) (any, error) { @@ -189,6 +191,7 @@ func TestTools(t *testing.T) { var receivedInvocation *copilot.ToolInvocation session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Tools: []copilot.Tool{ copilot.DefineTool("db_query", "Performs a database query", func(params DbQueryParams, inv copilot.ToolInvocation) ([]City, error) { diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index 3272df55b..db5bf57b2 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^0.0.411-0", + "@github/copilot": "^0.0.414", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 7df64e507..6d841c7cc 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -91,7 +91,7 @@ function toJsonSchema(parameters: Tool["parameters"]): Record | * const client = new CopilotClient({ cliUrl: "localhost:3000" }); * * // Create a session - * const session = await client.createSession({ model: "gpt-4" }); + * const session = await client.createSession({ onPermissionRequest: approveAll, model: "gpt-4" }); * * // Send messages and handle responses * session.on((event) => { @@ -494,10 +494,11 @@ export class CopilotClient { * @example * ```typescript * // Basic session - * const session = await client.createSession(); + * const session = await client.createSession({ onPermissionRequest: approveAll }); * * // Session with model and tools * const session = await client.createSession({ + * onPermissionRequest: approveAll, * model: "gpt-4", * tools: [{ * name: "get_weather", @@ -508,7 +509,13 @@ export class CopilotClient { * }); * ``` */ - async createSession(config: SessionConfig = {}): Promise { + async createSession(config: SessionConfig): Promise { + if (!config?.onPermissionRequest) { + throw new Error( + "An onPermissionRequest handler is required when creating a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }." + ); + } + if (!this.connection) { if (this.options.autoStart) { await this.start(); @@ -551,9 +558,7 @@ export class CopilotClient { }; const session = new CopilotSession(sessionId, this.connection!, workspacePath); session.registerTools(config.tools); - if (config.onPermissionRequest) { - session.registerPermissionHandler(config.onPermissionRequest); - } + session.registerPermissionHandler(config.onPermissionRequest); if (config.onUserInputRequest) { session.registerUserInputHandler(config.onUserInputRequest); } @@ -580,18 +585,22 @@ export class CopilotClient { * @example * ```typescript * // Resume a previous session - * const session = await client.resumeSession("session-123"); + * const session = await client.resumeSession("session-123", { onPermissionRequest: approveAll }); * * // Resume with new tools * const session = await client.resumeSession("session-123", { + * onPermissionRequest: approveAll, * tools: [myNewTool] * }); * ``` */ - async resumeSession( - sessionId: string, - config: ResumeSessionConfig = {} - ): Promise { + async resumeSession(sessionId: string, config: ResumeSessionConfig): Promise { + if (!config?.onPermissionRequest) { + throw new Error( + "An onPermissionRequest handler is required when resuming a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }." + ); + } + if (!this.connection) { if (this.options.autoStart) { await this.start(); @@ -635,9 +644,7 @@ export class CopilotClient { }; const session = new CopilotSession(resumedSessionId, this.connection!, workspacePath); session.registerTools(config.tools); - if (config.onPermissionRequest) { - session.registerPermissionHandler(config.onPermissionRequest); - } + session.registerPermissionHandler(config.onPermissionRequest); if (config.onUserInputRequest) { session.registerUserInputHandler(config.onUserInputRequest); } @@ -657,7 +664,7 @@ export class CopilotClient { * @example * ```typescript * if (client.getState() === "connected") { - * const session = await client.createSession(); + * const session = await client.createSession({ onPermissionRequest: approveAll }); * } * ``` */ @@ -802,7 +809,7 @@ export class CopilotClient { * ```typescript * const lastId = await client.getLastSessionId(); * if (lastId) { - * const session = await client.resumeSession(lastId); + * const session = await client.resumeSession(lastId, { onPermissionRequest: approveAll }); * } * ``` */ diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 79692b782..c016edff2 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -675,7 +675,7 @@ export interface SessionConfig { * Handler for permission requests from the server. * When provided, the server will call this handler to request permission for operations. */ - onPermissionRequest?: PermissionHandler; + onPermissionRequest: PermissionHandler; /** * Handler for user input requests from the agent. diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 5d1ed8ac3..6fa33e9ec 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -1,16 +1,37 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, expect, it, onTestFinished, vi } from "vitest"; -import { CopilotClient } from "../src/index.js"; +import { approveAll, CopilotClient } from "../src/index.js"; // This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.ts instead describe("CopilotClient", () => { + it("throws when createSession is called without onPermissionRequest", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + await expect((client as any).createSession({})).rejects.toThrow( + /onPermissionRequest.*is required/ + ); + }); + + it("throws when resumeSession is called without onPermissionRequest", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + await expect((client as any).resumeSession(session.sessionId, {})).rejects.toThrow( + /onPermissionRequest.*is required/ + ); + }); + it("returns a standardized failure result when a tool is not registered", async () => { const client = new CopilotClient(); await client.start(); onTestFinished(() => client.forceStop()); - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); const response = await ( client as unknown as { handleToolCallRequest: (typeof client)["handleToolCallRequest"] } @@ -33,7 +54,7 @@ describe("CopilotClient", () => { onTestFinished(() => client.forceStop()); const spy = vi.spyOn((client as any).connection!, "sendRequest"); - await client.createSession({ clientName: "my-app" }); + await client.createSession({ clientName: "my-app", onPermissionRequest: approveAll }); expect(spy).toHaveBeenCalledWith( "session.create", @@ -46,9 +67,12 @@ describe("CopilotClient", () => { await client.start(); onTestFinished(() => client.forceStop()); - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); const spy = vi.spyOn((client as any).connection!, "sendRequest"); - await client.resumeSession(session.sessionId, { clientName: "my-app" }); + await client.resumeSession(session.sessionId, { + clientName: "my-app", + onPermissionRequest: approveAll, + }); expect(spy).toHaveBeenCalledWith( "session.resume", diff --git a/nodejs/test/e2e/ask_user.test.ts b/nodejs/test/e2e/ask_user.test.ts index d6c89a249..c58daa00c 100644 --- a/nodejs/test/e2e/ask_user.test.ts +++ b/nodejs/test/e2e/ask_user.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest"; import type { UserInputRequest, UserInputResponse } from "../../src/index.js"; +import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; describe("User input (ask_user)", async () => { @@ -13,6 +14,7 @@ describe("User input (ask_user)", async () => { const userInputRequests: UserInputRequest[] = []; const session = await client.createSession({ + onPermissionRequest: approveAll, onUserInputRequest: async (request, invocation) => { userInputRequests.push(request); expect(invocation.sessionId).toBe(session.sessionId); @@ -43,6 +45,7 @@ describe("User input (ask_user)", async () => { const userInputRequests: UserInputRequest[] = []; const session = await client.createSession({ + onPermissionRequest: approveAll, onUserInputRequest: async (request) => { userInputRequests.push(request); // Pick the first choice @@ -74,6 +77,7 @@ describe("User input (ask_user)", async () => { const freeformAnswer = "This is my custom freeform answer that was not in the choices"; const session = await client.createSession({ + onPermissionRequest: approveAll, onUserInputRequest: async (request) => { userInputRequests.push(request); // Return a freeform answer (not from choices) diff --git a/nodejs/test/e2e/client.test.ts b/nodejs/test/e2e/client.test.ts index aa8ddcbd6..c7539fc0b 100644 --- a/nodejs/test/e2e/client.test.ts +++ b/nodejs/test/e2e/client.test.ts @@ -1,6 +1,6 @@ import { ChildProcess } from "child_process"; import { describe, expect, it, onTestFinished } from "vitest"; -import { CopilotClient } from "../../src/index.js"; +import { CopilotClient, approveAll } from "../../src/index.js"; function onTestFinishedForceStop(client: CopilotClient) { onTestFinished(async () => { @@ -51,9 +51,9 @@ describe("Client", () => { // the process has exited. const client = new CopilotClient({ useStdio: false }); - await client.createSession(); + await client.createSession({ onPermissionRequest: approveAll }); - // Kill the server process to force cleanup to fail + // Kill the server processto force cleanup to fail // eslint-disable-next-line @typescript-eslint/no-explicit-any const cliProcess = (client as any).cliProcess as ChildProcess; expect(cliProcess).toBeDefined(); @@ -69,7 +69,7 @@ describe("Client", () => { const client = new CopilotClient({}); onTestFinishedForceStop(client); - await client.createSession(); + await client.createSession({ onPermissionRequest: approveAll }); await client.forceStop(); expect(client.getState()).toBe("disconnected"); }); @@ -152,7 +152,7 @@ describe("Client", () => { // Verify subsequent calls also fail (don't hang) try { - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); await session.send("test"); expect.fail("Expected send() to throw an error after CLI exit"); } catch (error) { diff --git a/nodejs/test/e2e/compaction.test.ts b/nodejs/test/e2e/compaction.test.ts index 820b72ffb..e9ea287d3 100644 --- a/nodejs/test/e2e/compaction.test.ts +++ b/nodejs/test/e2e/compaction.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { SessionEvent } from "../../src/index.js"; +import { SessionEvent, approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; describe("Compaction", async () => { @@ -8,6 +8,7 @@ describe("Compaction", async () => { it("should trigger compaction with low threshold and emit events", async () => { // Create session with very low compaction thresholds to trigger compaction quickly const session = await client.createSession({ + onPermissionRequest: approveAll, infiniteSessions: { enabled: true, // Trigger background compaction at 0.5% context usage (~1000 tokens) @@ -63,6 +64,7 @@ describe("Compaction", async () => { it("should not emit compaction events when infinite sessions disabled", async () => { const session = await client.createSession({ + onPermissionRequest: approveAll, infiniteSessions: { enabled: false, }, diff --git a/nodejs/test/e2e/hooks.test.ts b/nodejs/test/e2e/hooks.test.ts index 18cc9fea0..b7d8d4dcd 100644 --- a/nodejs/test/e2e/hooks.test.ts +++ b/nodejs/test/e2e/hooks.test.ts @@ -120,6 +120,7 @@ describe("Session hooks", async () => { const preToolUseInputs: PreToolUseHookInput[] = []; const session = await client.createSession({ + onPermissionRequest: approveAll, hooks: { onPreToolUse: async (input) => { preToolUseInputs.push(input); diff --git a/nodejs/test/e2e/mcp_and_agents.test.ts b/nodejs/test/e2e/mcp_and_agents.test.ts index 7b7aabf06..cc626e325 100644 --- a/nodejs/test/e2e/mcp_and_agents.test.ts +++ b/nodejs/test/e2e/mcp_and_agents.test.ts @@ -28,6 +28,7 @@ describe("MCP Servers and Custom Agents", async () => { }; const session = await client.createSession({ + onPermissionRequest: approveAll, mcpServers, }); @@ -44,7 +45,7 @@ describe("MCP Servers and Custom Agents", async () => { it("should accept MCP server configuration on session resume", async () => { // Create a session first - const session1 = await client.createSession(); + const session1 = await client.createSession({ onPermissionRequest: approveAll }); const sessionId = session1.sessionId; await session1.sendAndWait({ prompt: "What is 1+1?" }); @@ -59,6 +60,7 @@ describe("MCP Servers and Custom Agents", async () => { }; const session2 = await client.resumeSession(sessionId, { + onPermissionRequest: approveAll, mcpServers, }); @@ -89,6 +91,7 @@ describe("MCP Servers and Custom Agents", async () => { }; const session = await client.createSession({ + onPermissionRequest: approveAll, mcpServers, }); @@ -136,6 +139,7 @@ describe("MCP Servers and Custom Agents", async () => { ]; const session = await client.createSession({ + onPermissionRequest: approveAll, customAgents, }); @@ -152,7 +156,7 @@ describe("MCP Servers and Custom Agents", async () => { it("should accept custom agent configuration on session resume", async () => { // Create a session first - const session1 = await client.createSession(); + const session1 = await client.createSession({ onPermissionRequest: approveAll }); const sessionId = session1.sessionId; await session1.sendAndWait({ prompt: "What is 1+1?" }); @@ -167,6 +171,7 @@ describe("MCP Servers and Custom Agents", async () => { ]; const session2 = await client.resumeSession(sessionId, { + onPermissionRequest: approveAll, customAgents, }); @@ -193,6 +198,7 @@ describe("MCP Servers and Custom Agents", async () => { ]; const session = await client.createSession({ + onPermissionRequest: approveAll, customAgents, }); @@ -219,6 +225,7 @@ describe("MCP Servers and Custom Agents", async () => { ]; const session = await client.createSession({ + onPermissionRequest: approveAll, customAgents, }); @@ -244,6 +251,7 @@ describe("MCP Servers and Custom Agents", async () => { ]; const session = await client.createSession({ + onPermissionRequest: approveAll, customAgents, }); @@ -273,6 +281,7 @@ describe("MCP Servers and Custom Agents", async () => { ]; const session = await client.createSession({ + onPermissionRequest: approveAll, mcpServers, customAgents, }); diff --git a/nodejs/test/e2e/permissions.test.ts b/nodejs/test/e2e/permissions.test.ts index b68446ee9..ea23bc071 100644 --- a/nodejs/test/e2e/permissions.test.ts +++ b/nodejs/test/e2e/permissions.test.ts @@ -64,10 +64,14 @@ describe("Permission callbacks", async () => { await session.destroy(); }); - it("should deny tool operations by default when no handler is provided", async () => { + it("should deny tool operations when handler explicitly denies", async () => { let permissionDenied = false; - const session = await client.createSession(); + const session = await client.createSession({ + onPermissionRequest: () => ({ + kind: "denied-no-approval-rule-and-could-not-request-from-user", + }), + }); session.on((event) => { if ( event.type === "tool.execution_complete" && @@ -85,12 +89,16 @@ describe("Permission callbacks", async () => { await session.destroy(); }); - it("should deny tool operations by default when no handler is provided after resume", async () => { + it("should deny tool operations when handler explicitly denies after resume", async () => { const session1 = await client.createSession({ onPermissionRequest: approveAll }); const sessionId = session1.sessionId; await session1.sendAndWait({ prompt: "What is 1+1?" }); - const session2 = await client.resumeSession(sessionId); + const session2 = await client.resumeSession(sessionId, { + onPermissionRequest: () => ({ + kind: "denied-no-approval-rule-and-could-not-request-from-user", + }), + }); let permissionDenied = false; session2.on((event) => { if ( @@ -109,9 +117,8 @@ describe("Permission callbacks", async () => { await session2.destroy(); }); - it("should work without permission handler (default behavior)", async () => { - // Create session without onPermissionRequest handler - const session = await client.createSession(); + it("should work with approve-all permission handler", async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); const message = await session.sendAndWait({ prompt: "What is 2+2?", @@ -147,8 +154,8 @@ describe("Permission callbacks", async () => { it("should resume session with permission handler", async () => { const permissionRequests: PermissionRequest[] = []; - // Create session without permission handler - const session1 = await client.createSession(); + // Create initial session + const session1 = await client.createSession({ onPermissionRequest: approveAll }); const sessionId = session1.sessionId; await session1.sendAndWait({ prompt: "What is 1+1?" }); diff --git a/nodejs/test/e2e/rpc.test.ts b/nodejs/test/e2e/rpc.test.ts index b7acbaf66..62a885d05 100644 --- a/nodejs/test/e2e/rpc.test.ts +++ b/nodejs/test/e2e/rpc.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, onTestFinished } from "vitest"; -import { CopilotClient } from "../../src/index.js"; +import { CopilotClient, approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; function onTestFinishedForceStop(client: CopilotClient) { @@ -71,7 +71,10 @@ describe("Session RPC", async () => { // session.model.getCurrent is defined in schema but not yet implemented in CLI it.skip("should call session.rpc.model.getCurrent", async () => { - const session = await client.createSession({ model: "claude-sonnet-4.5" }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + model: "claude-sonnet-4.5", + }); const result = await session.rpc.model.getCurrent(); expect(result.modelId).toBeDefined(); @@ -80,7 +83,10 @@ describe("Session RPC", async () => { // session.model.switchTo is defined in schema but not yet implemented in CLI it.skip("should call session.rpc.model.switchTo", async () => { - const session = await client.createSession({ model: "claude-sonnet-4.5" }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + model: "claude-sonnet-4.5", + }); // Get initial model const before = await session.rpc.model.getCurrent(); @@ -96,7 +102,7 @@ describe("Session RPC", async () => { }); it("should get and set session mode", async () => { - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); // Get initial mode (default should be interactive) const initial = await session.rpc.mode.get(); @@ -116,7 +122,7 @@ describe("Session RPC", async () => { }); it("should read, update, and delete plan", async () => { - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); // Initially plan should not exist const initial = await session.rpc.plan.read(); @@ -142,7 +148,7 @@ describe("Session RPC", async () => { }); it("should create, list, and read workspace files", async () => { - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); // Initially no files const initialFiles = await session.rpc.workspace.listFiles(); diff --git a/nodejs/test/e2e/session.test.ts b/nodejs/test/e2e/session.test.ts index 09c293a53..1bf095085 100644 --- a/nodejs/test/e2e/session.test.ts +++ b/nodejs/test/e2e/session.test.ts @@ -8,7 +8,10 @@ describe("Sessions", async () => { const { copilotClient: client, openAiEndpoint, homeDir, env } = await createSdkTestContext(); it("should create and destroy sessions", async () => { - const session = await client.createSession({ model: "fake-test-model" }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + model: "fake-test-model", + }); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); expect(await session.getMessages()).toMatchObject([ @@ -25,7 +28,7 @@ describe("Sessions", async () => { // TODO: Re-enable once test harness CAPI proxy supports this test's session lifecycle it.skip("should list sessions with context field", { timeout: 60000 }, async () => { // Create a session — just creating it is enough for it to appear in listSessions - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); // Verify it has a start event (confirms session is active) @@ -44,7 +47,7 @@ describe("Sessions", async () => { }); it("should have stateful conversation", async () => { - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); const assistantMessage = await session.sendAndWait({ prompt: "What is 1+1?" }); expect(assistantMessage?.data.content).toContain("2"); @@ -57,6 +60,7 @@ describe("Sessions", async () => { it("should create a session with appended systemMessage config", async () => { const systemMessageSuffix = "End each response with the phrase 'Have a nice day!'"; const session = await client.createSession({ + onPermissionRequest: approveAll, systemMessage: { mode: "append", content: systemMessageSuffix, @@ -77,6 +81,7 @@ describe("Sessions", async () => { it("should create a session with replaced systemMessage config", async () => { const testSystemMessage = "You are an assistant called Testy McTestface. Reply succinctly."; const session = await client.createSession({ + onPermissionRequest: approveAll, systemMessage: { mode: "replace", content: testSystemMessage }, }); @@ -92,6 +97,7 @@ describe("Sessions", async () => { it("should create a session with availableTools", async () => { const session = await client.createSession({ + onPermissionRequest: approveAll, availableTools: ["view", "edit"], }); @@ -107,6 +113,7 @@ describe("Sessions", async () => { it("should create a session with excludedTools", async () => { const session = await client.createSession({ + onPermissionRequest: approveAll, excludedTools: ["view"], }); @@ -128,9 +135,9 @@ describe("Sessions", async () => { // we stopped all the clients (one or more child processes were left orphaned). it.skip("should handle multiple concurrent sessions", async () => { const [s1, s2, s3] = await Promise.all([ - client.createSession(), - client.createSession(), - client.createSession(), + client.createSession({ onPermissionRequest: approveAll }), + client.createSession({ onPermissionRequest: approveAll }), + client.createSession({ onPermissionRequest: approveAll }), ]); // All sessions should have unique IDs @@ -156,13 +163,13 @@ describe("Sessions", async () => { it("should resume a session using the same client", async () => { // Create initial session - const session1 = await client.createSession(); + const session1 = await client.createSession({ onPermissionRequest: approveAll }); const sessionId = session1.sessionId; const answer = await session1.sendAndWait({ prompt: "What is 1+1?" }); expect(answer?.data.content).toContain("2"); // Resume using the same client - const session2 = await client.resumeSession(sessionId); + const session2 = await client.resumeSession(sessionId, { onPermissionRequest: approveAll }); expect(session2.sessionId).toBe(sessionId); const messages = await session2.getMessages(); const assistantMessages = messages.filter((m) => m.type === "assistant.message"); @@ -171,7 +178,7 @@ describe("Sessions", async () => { it("should resume a session using a new client", async () => { // Create initial session - const session1 = await client.createSession(); + const session1 = await client.createSession({ onPermissionRequest: approveAll }); const sessionId = session1.sessionId; const answer = await session1.sendAndWait({ prompt: "What is 1+1?" }); expect(answer?.data.content).toContain("2"); @@ -183,7 +190,9 @@ describe("Sessions", async () => { }); onTestFinished(() => newClient.forceStop()); - const session2 = await newClient.resumeSession(sessionId); + const session2 = await newClient.resumeSession(sessionId, { + onPermissionRequest: approveAll, + }); expect(session2.sessionId).toBe(sessionId); // TODO: There's an inconsistency here. When resuming with a new client, we don't see @@ -195,11 +204,14 @@ describe("Sessions", async () => { }); it("should throw error when resuming non-existent session", async () => { - await expect(client.resumeSession("non-existent-session-id")).rejects.toThrow(); + await expect( + client.resumeSession("non-existent-session-id", { onPermissionRequest: approveAll }) + ).rejects.toThrow(); }); it("should create session with custom tool", async () => { const session = await client.createSession({ + onPermissionRequest: approveAll, tools: [ { name: "get_secret_number", @@ -229,11 +241,12 @@ describe("Sessions", async () => { }); it("should resume session with a custom provider", async () => { - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); const sessionId = session.sessionId; // Resume the session with a provider const session2 = await client.resumeSession(sessionId, { + onPermissionRequest: approveAll, provider: { type: "openai", baseUrl: "https://api.openai.com/v1", @@ -245,7 +258,7 @@ describe("Sessions", async () => { }); it("should abort a session", async () => { - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); // Set up event listeners BEFORE sending to avoid race conditions const nextToolCallStart = getNextEventOfType(session, "tool.execution_start"); @@ -272,6 +285,7 @@ describe("Sessions", async () => { it("should receive streaming delta events when streaming is enabled", async () => { const session = await client.createSession({ + onPermissionRequest: approveAll, streaming: true, }); @@ -308,6 +322,7 @@ describe("Sessions", async () => { it("should pass streaming option to session creation", async () => { // Verify that the streaming option is accepted without errors const session = await client.createSession({ + onPermissionRequest: approveAll, streaming: true, }); @@ -319,7 +334,7 @@ describe("Sessions", async () => { }); it("should receive session events", async () => { - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); const receivedEvents: Array<{ type: string }> = []; session.on((event) => { @@ -342,6 +357,7 @@ describe("Sessions", async () => { it("should create session with custom config dir", async () => { const customConfigDir = `${homeDir}/custom-config`; const session = await client.createSession({ + onPermissionRequest: approveAll, configDir: customConfigDir, }); @@ -390,7 +406,7 @@ describe("Send Blocking Behavior", async () => { }); it("sendAndWait blocks until session.idle and returns final assistant message", async () => { - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); const events: string[] = []; session.on((event) => { @@ -409,7 +425,7 @@ describe("Send Blocking Behavior", async () => { // This test validates client-side timeout behavior. // The snapshot has no assistant response since we expect timeout before completion. it("sendAndWait throws on timeout", async () => { - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); // Use a slow command to ensure timeout triggers before completion await expect( diff --git a/nodejs/test/e2e/skills.test.ts b/nodejs/test/e2e/skills.test.ts index 92186ec0b..654f429aa 100644 --- a/nodejs/test/e2e/skills.test.ts +++ b/nodejs/test/e2e/skills.test.ts @@ -5,6 +5,7 @@ import * as fs from "fs"; import * as path from "path"; import { beforeEach, describe, expect, it } from "vitest"; +import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; describe("Skills Configuration", async () => { @@ -44,6 +45,7 @@ IMPORTANT: You MUST include the exact text "${SKILL_MARKER}" somewhere in EVERY it("should load and apply skill from skillDirectories", async () => { const skillsDir = createSkillDir(); const session = await client.createSession({ + onPermissionRequest: approveAll, skillDirectories: [skillsDir], }); @@ -62,6 +64,7 @@ IMPORTANT: You MUST include the exact text "${SKILL_MARKER}" somewhere in EVERY it("should not apply skill when disabled via disabledSkills", async () => { const skillsDir = createSkillDir(); const session = await client.createSession({ + onPermissionRequest: approveAll, skillDirectories: [skillsDir], disabledSkills: ["test-skill"], }); @@ -93,7 +96,7 @@ IMPORTANT: You MUST include the exact text "${SKILL_MARKER}" somewhere in EVERY const skillsDir = createSkillDir(); // Create a session without skills first - const session1 = await client.createSession(); + const session1 = await client.createSession({ onPermissionRequest: approveAll }); const sessionId = session1.sessionId; // First message without skill - marker should not appear @@ -102,6 +105,7 @@ IMPORTANT: You MUST include the exact text "${SKILL_MARKER}" somewhere in EVERY // Resume with skillDirectories - skill should now be active const session2 = await client.resumeSession(sessionId, { + onPermissionRequest: approveAll, skillDirectories: [skillsDir], }); diff --git a/nodejs/test/e2e/tools.test.ts b/nodejs/test/e2e/tools.test.ts index 3db24dff7..a6ad0c049 100644 --- a/nodejs/test/e2e/tools.test.ts +++ b/nodejs/test/e2e/tools.test.ts @@ -26,6 +26,7 @@ describe("Custom tools", async () => { it("invokes custom tool", async () => { const session = await client.createSession({ + onPermissionRequest: approveAll, tools: [ defineTool("encrypt_string", { description: "Encrypts a string", @@ -45,6 +46,7 @@ describe("Custom tools", async () => { it("handles tool calling errors", async () => { const session = await client.createSession({ + onPermissionRequest: approveAll, tools: [ defineTool("get_user_location", { description: "Gets the user's location", @@ -85,6 +87,7 @@ describe("Custom tools", async () => { it("can receive and return complex types", async () => { const session = await client.createSession({ + onPermissionRequest: approveAll, tools: [ defineTool("db_query", { description: "Performs a database query", diff --git a/python/copilot/client.py b/python/copilot/client.py index 90260ffbd..774569afb 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -91,7 +91,10 @@ class CopilotClient: >>> await client.start() >>> >>> # Create a session and send a message - >>> session = await client.create_session({"model": "gpt-4"}) + >>> session = await client.create_session({ + ... "on_permission_request": PermissionHandler.approve_all, + ... "model": "gpt-4", + ... }) >>> session.on(lambda event: print(event.type)) >>> await session.send({"prompt": "Hello!"}) >>> @@ -414,7 +417,7 @@ async def force_stop(self) -> None: if not self._is_external_server: self._actual_port = None - async def create_session(self, config: Optional[SessionConfig] = None) -> CopilotSession: + async def create_session(self, config: SessionConfig) -> CopilotSession: """ Create a new conversation session with the Copilot CLI. @@ -434,10 +437,12 @@ async def create_session(self, config: Optional[SessionConfig] = None) -> Copilo Example: >>> # Basic session - >>> session = await client.create_session() + >>> config = {"on_permission_request": PermissionHandler.approve_all} + >>> session = await client.create_session(config) >>> >>> # Session with model and streaming >>> session = await client.create_session({ + ... "on_permission_request": PermissionHandler.approve_all, ... "model": "gpt-4", ... "streaming": True ... }) @@ -448,7 +453,14 @@ async def create_session(self, config: Optional[SessionConfig] = None) -> Copilo else: raise RuntimeError("Client not connected. Call start() first.") - cfg = config or {} + cfg = config + + if not cfg.get("on_permission_request"): + raise ValueError( + "An on_permission_request handler is required when creating a session. " + "For example, to allow all permissions, use " + '{"on_permission_request": PermissionHandler.approve_all}.' + ) tool_defs = [] tools = cfg.get("tools") @@ -568,8 +580,7 @@ async def create_session(self, config: Optional[SessionConfig] = None) -> Copilo workspace_path = response.get("workspacePath") session = CopilotSession(session_id, self._client, workspace_path) session._register_tools(tools) - if on_permission_request: - session._register_permission_handler(on_permission_request) + session._register_permission_handler(on_permission_request) if on_user_input_request: session._register_user_input_handler(on_user_input_request) if hooks: @@ -579,9 +590,7 @@ async def create_session(self, config: Optional[SessionConfig] = None) -> Copilo return session - async def resume_session( - self, session_id: str, config: Optional[ResumeSessionConfig] = None - ) -> CopilotSession: + async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> CopilotSession: """ Resume an existing conversation session by its ID. @@ -601,10 +610,12 @@ async def resume_session( Example: >>> # Resume a previous session - >>> session = await client.resume_session("session-123") + >>> config = {"on_permission_request": PermissionHandler.approve_all} + >>> session = await client.resume_session("session-123", config) >>> >>> # Resume with new tools >>> session = await client.resume_session("session-123", { + ... "on_permission_request": PermissionHandler.approve_all, ... "tools": [my_new_tool] ... }) """ @@ -614,7 +625,14 @@ async def resume_session( else: raise RuntimeError("Client not connected. Call start() first.") - cfg = config or {} + cfg = config + + if not cfg.get("on_permission_request"): + raise ValueError( + "An on_permission_request handler is required when resuming a session. " + "For example, to allow all permissions, use " + '{"on_permission_request": PermissionHandler.approve_all}.' + ) tool_defs = [] tools = cfg.get("tools") @@ -744,8 +762,7 @@ async def resume_session( workspace_path = response.get("workspacePath") session = CopilotSession(resumed_session_id, self._client, workspace_path) session._register_tools(cfg.get("tools")) - if on_permission_request: - session._register_permission_handler(on_permission_request) + session._register_permission_handler(on_permission_request) if on_user_input_request: session._register_user_input_handler(on_user_input_request) if hooks: diff --git a/python/e2e/test_ask_user.py b/python/e2e/test_ask_user.py index 93036ea4c..f409e460c 100644 --- a/python/e2e/test_ask_user.py +++ b/python/e2e/test_ask_user.py @@ -4,6 +4,8 @@ import pytest +from copilot import PermissionHandler + from .testharness import E2ETestContext pytestmark = pytest.mark.asyncio(loop_scope="module") @@ -27,7 +29,12 @@ async def on_user_input_request(request, invocation): "wasFreeform": not bool(choices), } - session = await ctx.client.create_session({"on_user_input_request": on_user_input_request}) + session = await ctx.client.create_session( + { + "on_user_input_request": on_user_input_request, + "on_permission_request": PermissionHandler.approve_all, + } + ) await session.send_and_wait( { @@ -61,7 +68,12 @@ async def on_user_input_request(request, invocation): "wasFreeform": False, } - session = await ctx.client.create_session({"on_user_input_request": on_user_input_request}) + session = await ctx.client.create_session( + { + "on_user_input_request": on_user_input_request, + "on_permission_request": PermissionHandler.approve_all, + } + ) await session.send_and_wait( { @@ -97,7 +109,12 @@ async def on_user_input_request(request, invocation): "wasFreeform": True, } - session = await ctx.client.create_session({"on_user_input_request": on_user_input_request}) + session = await ctx.client.create_session( + { + "on_user_input_request": on_user_input_request, + "on_permission_request": PermissionHandler.approve_all, + } + ) response = await session.send_and_wait( { diff --git a/python/e2e/test_client.py b/python/e2e/test_client.py index c18764e55..cc5d31ac6 100644 --- a/python/e2e/test_client.py +++ b/python/e2e/test_client.py @@ -2,7 +2,7 @@ import pytest -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler from .testharness import CLI_PATH @@ -51,7 +51,7 @@ async def test_should_return_errors_on_failed_cleanup(self): client = CopilotClient({"cli_path": CLI_PATH}) try: - await client.create_session() + await client.create_session({"on_permission_request": PermissionHandler.approve_all}) # Kill the server process to force cleanup to fail process = client._process @@ -69,7 +69,7 @@ async def test_should_return_errors_on_failed_cleanup(self): async def test_should_force_stop_without_cleanup(self): client = CopilotClient({"cli_path": CLI_PATH}) - await client.create_session() + await client.create_session({"on_permission_request": PermissionHandler.approve_all}) await client.force_stop() assert client.get_state() == "disconnected" @@ -206,7 +206,9 @@ async def test_should_report_error_with_stderr_when_cli_fails_to_start(self): # Verify subsequent calls also fail (don't hang) with pytest.raises(Exception) as exc_info2: - session = await client.create_session() + session = await client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) await session.send("test") # Error message varies by platform (EINVAL on Windows, EPIPE on Linux) error_msg = str(exc_info2.value).lower() diff --git a/python/e2e/test_compaction.py b/python/e2e/test_compaction.py index b2463e447..dc95b6855 100644 --- a/python/e2e/test_compaction.py +++ b/python/e2e/test_compaction.py @@ -2,6 +2,7 @@ import pytest +from copilot import PermissionHandler from copilot.generated.session_events import SessionEventType from .testharness import E2ETestContext @@ -23,7 +24,8 @@ async def test_should_trigger_compaction_with_low_threshold_and_emit_events( "background_compaction_threshold": 0.005, # Block at 1% to ensure compaction runs "buffer_exhaustion_threshold": 0.01, - } + }, + "on_permission_request": PermissionHandler.approve_all, } ) @@ -71,7 +73,12 @@ def on_event(event): async def test_should_not_emit_compaction_events_when_infinite_sessions_disabled( self, ctx: E2ETestContext ): - session = await ctx.client.create_session({"infinite_sessions": {"enabled": False}}) + session = await ctx.client.create_session( + { + "infinite_sessions": {"enabled": False}, + "on_permission_request": PermissionHandler.approve_all, + } + ) compaction_events = [] diff --git a/python/e2e/test_mcp_and_agents.py b/python/e2e/test_mcp_and_agents.py index 7ca4b8c2b..b29a54827 100644 --- a/python/e2e/test_mcp_and_agents.py +++ b/python/e2e/test_mcp_and_agents.py @@ -32,7 +32,9 @@ async def test_should_accept_mcp_server_configuration_on_session_create( } } - session = await ctx.client.create_session({"mcp_servers": mcp_servers}) + session = await ctx.client.create_session( + {"mcp_servers": mcp_servers, "on_permission_request": PermissionHandler.approve_all} + ) assert session.session_id is not None @@ -48,7 +50,9 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( ): """Test that MCP server configuration is accepted on session resume""" # Create a session first - session1 = await ctx.client.create_session() + session1 = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) session_id = session1.session_id await session1.send_and_wait({"prompt": "What is 1+1?"}) @@ -62,7 +66,10 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( } } - session2 = await ctx.client.resume_session(session_id, {"mcp_servers": mcp_servers}) + session2 = await ctx.client.resume_session( + session_id, + {"mcp_servers": mcp_servers, "on_permission_request": PermissionHandler.approve_all}, + ) assert session2.session_id == session_id @@ -123,7 +130,9 @@ async def test_should_accept_custom_agent_configuration_on_session_create( } ] - session = await ctx.client.create_session({"custom_agents": custom_agents}) + session = await ctx.client.create_session( + {"custom_agents": custom_agents, "on_permission_request": PermissionHandler.approve_all} + ) assert session.session_id is not None @@ -139,7 +148,9 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( ): """Test that custom agent configuration is accepted on session resume""" # Create a session first - session1 = await ctx.client.create_session() + session1 = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) session_id = session1.session_id await session1.send_and_wait({"prompt": "What is 1+1?"}) @@ -153,7 +164,13 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( } ] - session2 = await ctx.client.resume_session(session_id, {"custom_agents": custom_agents}) + session2 = await ctx.client.resume_session( + session_id, + { + "custom_agents": custom_agents, + "on_permission_request": PermissionHandler.approve_all, + }, + ) assert session2.session_id == session_id @@ -186,7 +203,11 @@ async def test_should_accept_both_mcp_servers_and_custom_agents(self, ctx: E2ETe ] session = await ctx.client.create_session( - {"mcp_servers": mcp_servers, "custom_agents": custom_agents} + { + "mcp_servers": mcp_servers, + "custom_agents": custom_agents, + "on_permission_request": PermissionHandler.approve_all, + } ) assert session.session_id is not None diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py index 80b69ebba..c116053ba 100644 --- a/python/e2e/test_permissions.py +++ b/python/e2e/test_permissions.py @@ -68,10 +68,15 @@ def on_permission_request( await session.destroy() - async def test_should_deny_tool_operations_by_default_when_no_handler_is_provided( + async def test_should_deny_tool_operations_when_handler_explicitly_denies( self, ctx: E2ETestContext ): - session = await ctx.client.create_session() + """Test that tool operations are denied when handler explicitly denies""" + + def deny_all(request, invocation): + return {"kind": "denied-no-approval-rule-and-could-not-request-from-user"} + + session = await ctx.client.create_session({"on_permission_request": deny_all}) denied_events = [] done_event = asyncio.Event() @@ -98,16 +103,20 @@ def on_event(event): await session.destroy() - async def test_should_deny_tool_operations_by_default_when_no_handler_is_provided_after_resume( + async def test_should_deny_tool_operations_when_handler_explicitly_denies_after_resume( self, ctx: E2ETestContext ): + """Test that tool operations are denied after resume when handler explicitly denies""" session1 = await ctx.client.create_session( {"on_permission_request": PermissionHandler.approve_all} ) session_id = session1.session_id await session1.send_and_wait({"prompt": "What is 1+1?"}) - session2 = await ctx.client.resume_session(session_id) + def deny_all(request, invocation): + return {"kind": "denied-no-approval-rule-and-could-not-request-from-user"} + + session2 = await ctx.client.resume_session(session_id, {"on_permission_request": deny_all}) denied_events = [] done_event = asyncio.Event() @@ -134,12 +143,11 @@ def on_event(event): await session2.destroy() - async def test_should_work_without_permission_handler__default_behavior_( - self, ctx: E2ETestContext - ): - """Test that sessions work without permission handler (default behavior)""" - # Create session without on_permission_request handler - session = await ctx.client.create_session() + async def test_should_work_with_approve_all_permission_handler(self, ctx: E2ETestContext): + """Test that sessions work with approve-all permission handler""" + session = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) message = await session.send_and_wait({"prompt": "What is 2+2?"}) @@ -172,8 +180,10 @@ async def test_should_resume_session_with_permission_handler(self, ctx: E2ETestC """Test resuming session with permission handler""" permission_requests = [] - # Create session without permission handler - session1 = await ctx.client.create_session() + # Create initial session + session1 = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) session_id = session1.session_id await session1.send_and_wait({"prompt": "What is 1+1?"}) diff --git a/python/e2e/test_rpc.py b/python/e2e/test_rpc.py index da2ba3eb6..240cd3730 100644 --- a/python/e2e/test_rpc.py +++ b/python/e2e/test_rpc.py @@ -2,7 +2,7 @@ import pytest -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler from copilot.generated.rpc import PingParams from .testharness import CLI_PATH, E2ETestContext @@ -77,7 +77,9 @@ class TestSessionRpc: @pytest.mark.skip(reason="session.model.getCurrent not yet implemented in CLI") async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext): """Test calling session.rpc.model.getCurrent""" - session = await ctx.client.create_session({"model": "claude-sonnet-4.5"}) + session = await ctx.client.create_session( + {"model": "claude-sonnet-4.5", "on_permission_request": PermissionHandler.approve_all} + ) result = await session.rpc.model.get_current() assert result.model_id is not None @@ -89,7 +91,9 @@ async def test_should_call_session_rpc_model_switch_to(self, ctx: E2ETestContext """Test calling session.rpc.model.switchTo""" from copilot.generated.rpc import SessionModelSwitchToParams - session = await ctx.client.create_session({"model": "claude-sonnet-4.5"}) + session = await ctx.client.create_session( + {"model": "claude-sonnet-4.5", "on_permission_request": PermissionHandler.approve_all} + ) # Get initial model before = await session.rpc.model.get_current() @@ -112,7 +116,9 @@ async def test_get_and_set_session_mode(self): try: await client.start() - session = await client.create_session({}) + session = await client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) # Get initial mode (default should be interactive) initial = await session.rpc.mode.get() @@ -146,7 +152,9 @@ async def test_read_update_and_delete_plan(self): try: await client.start() - session = await client.create_session({}) + session = await client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) # Initially plan should not exist initial = await session.rpc.plan.read() @@ -187,7 +195,9 @@ async def test_create_list_and_read_workspace_files(self): try: await client.start() - session = await client.create_session({}) + session = await client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) # Initially no files initial_files = await session.rpc.workspace.list_files() diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index 0998298f4..4842d7829 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -14,7 +14,9 @@ class TestSessions: async def test_should_create_and_destroy_sessions(self, ctx: E2ETestContext): - session = await ctx.client.create_session({"model": "fake-test-model"}) + session = await ctx.client.create_session( + {"model": "fake-test-model", "on_permission_request": PermissionHandler.approve_all} + ) assert session.session_id messages = await session.get_messages() @@ -29,7 +31,9 @@ async def test_should_create_and_destroy_sessions(self, ctx: E2ETestContext): await session.get_messages() async def test_should_have_stateful_conversation(self, ctx: E2ETestContext): - session = await ctx.client.create_session() + session = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) assistant_message = await session.send_and_wait({"prompt": "What is 1+1?"}) assert assistant_message is not None @@ -46,7 +50,10 @@ async def test_should_create_a_session_with_appended_systemMessage_config( ): system_message_suffix = "End each response with the phrase 'Have a nice day!'" session = await ctx.client.create_session( - {"system_message": {"mode": "append", "content": system_message_suffix}} + { + "system_message": {"mode": "append", "content": system_message_suffix}, + "on_permission_request": PermissionHandler.approve_all, + } ) await session.send({"prompt": "What is your full name?"}) @@ -65,7 +72,10 @@ async def test_should_create_a_session_with_replaced_systemMessage_config( ): test_system_message = "You are an assistant called Testy McTestface. Reply succinctly." session = await ctx.client.create_session( - {"system_message": {"mode": "replace", "content": test_system_message}} + { + "system_message": {"mode": "replace", "content": test_system_message}, + "on_permission_request": PermissionHandler.approve_all, + } ) await session.send({"prompt": "What is your full name?"}) @@ -79,7 +89,12 @@ async def test_should_create_a_session_with_replaced_systemMessage_config( assert system_message == test_system_message # Exact match async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestContext): - session = await ctx.client.create_session({"available_tools": ["view", "edit"]}) + session = await ctx.client.create_session( + { + "available_tools": ["view", "edit"], + "on_permission_request": PermissionHandler.approve_all, + } + ) await session.send({"prompt": "What is 1+1?"}) await get_final_assistant_message(session) @@ -93,7 +108,9 @@ async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestCon assert "edit" in tool_names async def test_should_create_a_session_with_excludedTools(self, ctx: E2ETestContext): - session = await ctx.client.create_session({"excluded_tools": ["view"]}) + session = await ctx.client.create_session( + {"excluded_tools": ["view"], "on_permission_request": PermissionHandler.approve_all} + ) await session.send({"prompt": "What is 1+1?"}) await get_final_assistant_message(session) @@ -115,9 +132,9 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont import asyncio s1, s2, s3 = await asyncio.gather( - ctx.client.create_session(), - ctx.client.create_session(), - ctx.client.create_session(), + ctx.client.create_session({"on_permission_request": PermissionHandler.approve_all}), + ctx.client.create_session({"on_permission_request": PermissionHandler.approve_all}), + ctx.client.create_session({"on_permission_request": PermissionHandler.approve_all}), ) # All sessions should have unique IDs @@ -139,21 +156,27 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestContext): # Create initial session - session1 = await ctx.client.create_session() + session1 = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) session_id = session1.session_id answer = await session1.send_and_wait({"prompt": "What is 1+1?"}) assert answer is not None assert "2" in answer.data.content # Resume using the same client - session2 = await ctx.client.resume_session(session_id) + session2 = await ctx.client.resume_session( + session_id, {"on_permission_request": PermissionHandler.approve_all} + ) assert session2.session_id == session_id answer2 = await get_final_assistant_message(session2) assert "2" in answer2.data.content async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestContext): # Create initial session - session1 = await ctx.client.create_session() + session1 = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) session_id = session1.session_id answer = await session1.send_and_wait({"prompt": "What is 1+1?"}) assert answer is not None @@ -171,7 +194,9 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont ) try: - session2 = await new_client.resume_session(session_id) + session2 = await new_client.resume_session( + session_id, {"on_permission_request": PermissionHandler.approve_all} + ) assert session2.session_id == session_id # TODO: There's an inconsistency here. When resuming with a new client, @@ -186,15 +211,21 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont async def test_should_throw_error_resuming_nonexistent_session(self, ctx: E2ETestContext): with pytest.raises(Exception): - await ctx.client.resume_session("non-existent-session-id") + await ctx.client.resume_session( + "non-existent-session-id", {"on_permission_request": PermissionHandler.approve_all} + ) async def test_should_list_sessions(self, ctx: E2ETestContext): import asyncio # Create a couple of sessions and send messages to persist them - session1 = await ctx.client.create_session() + session1 = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) await session1.send_and_wait({"prompt": "Say hello"}) - session2 = await ctx.client.create_session() + session2 = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) await session2.send_and_wait({"prompt": "Say goodbye"}) # Small delay to ensure session files are written to disk @@ -231,7 +262,9 @@ async def test_should_delete_session(self, ctx: E2ETestContext): import asyncio # Create a session and send a message to persist it - session = await ctx.client.create_session() + session = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) await session.send_and_wait({"prompt": "Hello"}) session_id = session.session_id @@ -253,7 +286,9 @@ async def test_should_delete_session(self, ctx: E2ETestContext): # Verify we cannot resume the deleted session with pytest.raises(Exception): - await ctx.client.resume_session(session_id) + await ctx.client.resume_session( + session_id, {"on_permission_request": PermissionHandler.approve_all} + ) async def test_should_create_session_with_custom_tool(self, ctx: E2ETestContext): # This test uses the low-level Tool() API to show that Pydantic is optional @@ -277,7 +312,8 @@ def get_secret_number_handler(invocation): "required": ["key"], }, ) - ] + ], + "on_permission_request": PermissionHandler.approve_all, } ) @@ -292,7 +328,8 @@ async def test_should_create_session_with_custom_provider(self, ctx: E2ETestCont "type": "openai", "base_url": "https://api.openai.com/v1", "api_key": "fake-key", - } + }, + "on_permission_request": PermissionHandler.approve_all, } ) assert session.session_id @@ -307,13 +344,16 @@ async def test_should_create_session_with_azure_provider(self, ctx: E2ETestConte "azure": { "api_version": "2024-02-15-preview", }, - } + }, + "on_permission_request": PermissionHandler.approve_all, } ) assert session.session_id async def test_should_resume_session_with_custom_provider(self, ctx: E2ETestContext): - session = await ctx.client.create_session() + session = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) session_id = session.session_id # Resume the session with a provider @@ -324,7 +364,8 @@ async def test_should_resume_session_with_custom_provider(self, ctx: E2ETestCont "type": "openai", "base_url": "https://api.openai.com/v1", "api_key": "fake-key", - } + }, + "on_permission_request": PermissionHandler.approve_all, }, ) @@ -381,7 +422,9 @@ async def test_should_receive_streaming_delta_events_when_streaming_is_enabled( ): import asyncio - session = await ctx.client.create_session({"streaming": True}) + session = await ctx.client.create_session( + {"streaming": True, "on_permission_request": PermissionHandler.approve_all} + ) delta_contents = [] done_event = asyncio.Event() @@ -422,7 +465,9 @@ def on_event(event): async def test_should_pass_streaming_option_to_session_creation(self, ctx: E2ETestContext): # Verify that the streaming option is accepted without errors - session = await ctx.client.create_session({"streaming": True}) + session = await ctx.client.create_session( + {"streaming": True, "on_permission_request": PermissionHandler.approve_all} + ) assert session.session_id @@ -434,7 +479,9 @@ async def test_should_pass_streaming_option_to_session_creation(self, ctx: E2ETe async def test_should_receive_session_events(self, ctx: E2ETestContext): import asyncio - session = await ctx.client.create_session() + session = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) received_events = [] idle_event = asyncio.Event() @@ -469,7 +516,12 @@ async def test_should_create_session_with_custom_config_dir(self, ctx: E2ETestCo import os custom_config_dir = os.path.join(ctx.home_dir, "custom-config") - session = await ctx.client.create_session({"config_dir": custom_config_dir}) + session = await ctx.client.create_session( + { + "config_dir": custom_config_dir, + "on_permission_request": PermissionHandler.approve_all, + } + ) assert session.session_id diff --git a/python/e2e/test_skills.py b/python/e2e/test_skills.py index 7f05140eb..10d32695c 100644 --- a/python/e2e/test_skills.py +++ b/python/e2e/test_skills.py @@ -7,6 +7,8 @@ import pytest +from copilot import PermissionHandler + from .testharness import E2ETestContext pytestmark = pytest.mark.asyncio(loop_scope="module") @@ -53,7 +55,12 @@ class TestSkillBehavior: async def test_should_load_and_apply_skill_from_skilldirectories(self, ctx: E2ETestContext): """Test that skills are loaded and applied from skillDirectories""" skills_dir = create_skill_dir(ctx.work_dir) - session = await ctx.client.create_session({"skill_directories": [skills_dir]}) + session = await ctx.client.create_session( + { + "skill_directories": [skills_dir], + "on_permission_request": PermissionHandler.approve_all, + } + ) assert session.session_id is not None @@ -70,7 +77,11 @@ async def test_should_not_apply_skill_when_disabled_via_disabledskills( """Test that disabledSkills prevents skill from being applied""" skills_dir = create_skill_dir(ctx.work_dir) session = await ctx.client.create_session( - {"skill_directories": [skills_dir], "disabled_skills": ["test-skill"]} + { + "skill_directories": [skills_dir], + "disabled_skills": ["test-skill"], + "on_permission_request": PermissionHandler.approve_all, + } ) assert session.session_id is not None @@ -93,7 +104,9 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories( skills_dir = create_skill_dir(ctx.work_dir) # Create a session without skills first - session1 = await ctx.client.create_session() + session1 = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) session_id = session1.session_id # First message without skill - marker should not appear @@ -102,7 +115,13 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories( assert SKILL_MARKER not in message1.data.content # Resume with skillDirectories - skill should now be active - session2 = await ctx.client.resume_session(session_id, {"skill_directories": [skills_dir]}) + session2 = await ctx.client.resume_session( + session_id, + { + "skill_directories": [skills_dir], + "on_permission_request": PermissionHandler.approve_all, + }, + ) assert session2.session_id == session_id diff --git a/python/e2e/test_tools.py b/python/e2e/test_tools.py index 10e61cf15..485998e00 100644 --- a/python/e2e/test_tools.py +++ b/python/e2e/test_tools.py @@ -34,7 +34,9 @@ class EncryptParams(BaseModel): def encrypt_string(params: EncryptParams, invocation: ToolInvocation) -> str: return params.input.upper() - session = await ctx.client.create_session({"tools": [encrypt_string]}) + session = await ctx.client.create_session( + {"tools": [encrypt_string], "on_permission_request": PermissionHandler.approve_all} + ) await session.send({"prompt": "Use encrypt_string to encrypt this string: Hello"}) assistant_message = await get_final_assistant_message(session) @@ -45,7 +47,9 @@ async def test_handles_tool_calling_errors(self, ctx: E2ETestContext): def get_user_location() -> str: raise Exception("Melbourne") - session = await ctx.client.create_session({"tools": [get_user_location]}) + session = await ctx.client.create_session( + {"tools": [get_user_location], "on_permission_request": PermissionHandler.approve_all} + ) await session.send( {"prompt": "What is my location? If you can't find out, just say 'unknown'."} @@ -108,7 +112,9 @@ def db_query(params: DbQueryParams, invocation: ToolInvocation) -> list[City]: City(countryId=12, cityName="San Lorenzo", population=204356), ] - session = await ctx.client.create_session({"tools": [db_query]}) + session = await ctx.client.create_session( + {"tools": [db_query], "on_permission_request": PermissionHandler.approve_all} + ) expected_session_id = session.session_id await session.send( diff --git a/python/test_client.py b/python/test_client.py index 0bc99ea69..c6ad027f5 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -6,10 +6,35 @@ import pytest -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler from e2e.testharness import CLI_PATH +class TestPermissionHandlerRequired: + @pytest.mark.asyncio + async def test_create_session_raises_without_permission_handler(self): + client = CopilotClient({"cli_path": CLI_PATH}) + await client.start() + try: + with pytest.raises(ValueError, match="on_permission_request.*is required"): + await client.create_session({}) + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_resume_session_raises_without_permission_handler(self): + client = CopilotClient({"cli_path": CLI_PATH}) + await client.start() + try: + session = await client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) + with pytest.raises(ValueError, match="on_permission_request.*is required"): + await client.resume_session(session.session_id, {}) + finally: + await client.force_stop() + + class TestHandleToolCallRequest: @pytest.mark.asyncio async def test_returns_failure_when_tool_not_registered(self): @@ -17,7 +42,9 @@ async def test_returns_failure_when_tool_not_registered(self): await client.start() try: - session = await client.create_session() + session = await client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) response = await client._handle_tool_call_request( { @@ -164,7 +191,9 @@ async def mock_request(method, params): return await original_request(method, params) client._client.request = mock_request - await client.create_session({"client_name": "my-app"}) + await client.create_session( + {"client_name": "my-app", "on_permission_request": PermissionHandler.approve_all} + ) assert captured["session.create"]["clientName"] == "my-app" finally: await client.force_stop() @@ -175,7 +204,9 @@ async def test_resume_session_forwards_client_name(self): await client.start() try: - session = await client.create_session() + session = await client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) captured = {} original_request = client._client.request @@ -185,7 +216,10 @@ async def mock_request(method, params): return await original_request(method, params) client._client.request = mock_request - await client.resume_session(session.session_id, {"client_name": "my-app"}) + await client.resume_session( + session.session_id, + {"client_name": "my-app", "on_permission_request": PermissionHandler.approve_all}, + ) assert captured["session.resume"]["clientName"] == "my-app" finally: await client.force_stop() diff --git a/test/scenarios/sessions/session-resume/csharp/Program.cs b/test/scenarios/sessions/session-resume/csharp/Program.cs index adb7b1f12..73979669d 100644 --- a/test/scenarios/sessions/session-resume/csharp/Program.cs +++ b/test/scenarios/sessions/session-resume/csharp/Program.cs @@ -13,6 +13,7 @@ // 1. Create a session await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", AvailableTools = new List(), }); @@ -27,7 +28,10 @@ await session.SendAndWaitAsync(new MessageOptions var sessionId = session.SessionId; // 4. Resume the session with the same ID - await using var resumed = await client.ResumeSessionAsync(sessionId); + await using var resumed = await client.ResumeSessionAsync(sessionId, new ResumeSessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + }); Console.WriteLine("Session resumed"); // 5. Ask for the secret word diff --git a/test/scenarios/sessions/session-resume/go/main.go b/test/scenarios/sessions/session-resume/go/main.go index 796694ec4..6ec4bb02d 100644 --- a/test/scenarios/sessions/session-resume/go/main.go +++ b/test/scenarios/sessions/session-resume/go/main.go @@ -22,8 +22,9 @@ func main() { // 1. Create a session session, err := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "claude-haiku-4.5", - AvailableTools: []string{}, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Model: "claude-haiku-4.5", + AvailableTools: []string{}, }) if err != nil { log.Fatal(err) @@ -41,7 +42,9 @@ func main() { sessionID := session.SessionID // 4. Resume the session with the same ID - resumed, err := client.ResumeSession(ctx, sessionID) + resumed, err := client.ResumeSession(ctx, sessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) if err != nil { log.Fatal(err) } diff --git a/test/snapshots/permissions/should_deny_tool_operations_by_default_when_no_handler_is_provided.yaml b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml similarity index 83% rename from test/snapshots/permissions/should_deny_tool_operations_by_default_when_no_handler_is_provided.yaml rename to test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml index 4413bb20a..c0fc46a9a 100644 --- a/test/snapshots/permissions/should_deny_tool_operations_by_default_when_no_handler_is_provided.yaml +++ b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml @@ -44,6 +44,5 @@ conversations: tool_call_id: toolcall_1 content: Permission denied and could not request permission from user - role: assistant - content: I received a permission denied error. It appears I don't have permission to execute the `node --version` - command in this environment. This might be due to security restrictions or the command not being available in - the current context. + content: Permission was denied to run the command. This may be due to security policies or execution restrictions in the + current environment. diff --git a/test/snapshots/permissions/should_deny_tool_operations_by_default_when_no_handler_is_provided_after_resume.yaml b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml similarity index 88% rename from test/snapshots/permissions/should_deny_tool_operations_by_default_when_no_handler_is_provided_after_resume.yaml rename to test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml index 788a1a783..551ba8f91 100644 --- a/test/snapshots/permissions/should_deny_tool_operations_by_default_when_no_handler_is_provided_after_resume.yaml +++ b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml @@ -7,7 +7,7 @@ conversations: - role: user content: What is 1+1? - role: assistant - content: 1+1 = 2 + content: 1+1 equals 2. - role: user content: Run 'node --version' - role: assistant @@ -30,7 +30,7 @@ conversations: - role: user content: What is 1+1? - role: assistant - content: 1+1 = 2 + content: 1+1 equals 2. - role: user content: Run 'node --version' - role: assistant @@ -52,4 +52,5 @@ conversations: tool_call_id: toolcall_1 content: Permission denied and could not request permission from user - role: assistant - content: Permission was denied to run the command. I don't have access to execute shell commands in this environment. + content: The command was denied due to insufficient permissions. You'll need to grant permission to run commands in this + session. diff --git a/test/snapshots/permissions/should_work_without_permission_handler__default_behavior_.yaml b/test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml similarity index 86% rename from test/snapshots/permissions/should_work_without_permission_handler__default_behavior_.yaml rename to test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml index 9fe2fcd07..9199977db 100644 --- a/test/snapshots/permissions/should_work_without_permission_handler__default_behavior_.yaml +++ b/test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml @@ -7,4 +7,4 @@ conversations: - role: user content: What is 2+2? - role: assistant - content: 2 + 2 = 4 + content: 2+2 = 4 diff --git a/test/snapshots/permissions/without_permission_handler.yaml b/test/snapshots/permissions/without_permission_handler.yaml deleted file mode 100644 index 9fe2fcd07..000000000 --- a/test/snapshots/permissions/without_permission_handler.yaml +++ /dev/null @@ -1,10 +0,0 @@ -models: - - claude-sonnet-4.5 -conversations: - - messages: - - role: system - content: ${system} - - role: user - content: What is 2+2? - - role: assistant - content: 2 + 2 = 4