From 4418e85f0282786eacd06892538949e08461d899 Mon Sep 17 00:00:00 2001 From: Tamir Dresher Date: Wed, 10 Jun 2026 07:09:16 +0300 Subject: [PATCH] fix(Squad.Agents.AI): expose SessionConfig + default OnPermissionRequest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squad.Agents.AI 0.1.0-preview.2 calls `CopilotClient.AsAIAgent(instructions, name)` which leaves `SessionConfig.OnPermissionRequest` unset. The first call to `SquadAgent.CreateSessionAsync()` therefore throws: System.ArgumentException: An OnPermissionRequest handler is required when creating a session. For example, to allow all permissions, use CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll }); There was no public way for consumers to fix it from the outside — `ConfigureCopilotClient` only exposes `CopilotClientOptions`, not the per-session `SessionConfig` that owns the permission handler. What changed ------------ - `SquadAgent` now switches to the `AsAIAgent(client, sessionConfig, ...)` overload and constructs a `SessionConfig` with: - `OnPermissionRequest = PermissionHandler.ApproveAll` (sensible default for a host-process Squad adapter; the host already chose to instantiate Squad and is responsible for sandboxing). - `WorkingDirectory` defaulting to the resolved `Cwd` / `SquadFolderPath`. - `SystemMessage = new SystemMessageConfig { Content = Instructions }` when `SquadAgentOptions.Instructions` is set. - `SquadAgentOptions.ConfigureSession: Action?` is new — runs after Squad applies its defaults so a consumer can swap in a stricter permission handler, pin the model, restrict tools, etc. Tests ----- - New `SquadAgentSessionConfigTests` (4 cases) exercising the new surface: `ConfigureSession` is settable, runs against the live SessionConfig, can replace the permission handler, and can set `AvailableTools`. - All existing 43 tests still pass per TFM (141 total across net8/9/10). Verified -------- - The end-to-end consumer smoke test in `C:\Users\tamirdresher\source\repos\squad-agents-ai-consume-test` previously had to fall back to the raw SDK because of the missing handler. With this change, `SquadAgent.CreateSessionAsync()` returns successfully and `RunAsync` drives real 3-turn conversations against `.squad/`-init'd teams. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Squad.Agents.AI/SquadAgent.cs | 24 +++++- src/Squad.Agents.AI/SquadAgentOptions.cs | 25 ++++++ .../SquadAgentSessionConfigTests.cs | 85 +++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 test/Squad.Agents.AI.Tests/SquadAgentSessionConfigTests.cs diff --git a/src/Squad.Agents.AI/SquadAgent.cs b/src/Squad.Agents.AI/SquadAgent.cs index b88c1c551..2c5911c0a 100644 --- a/src/Squad.Agents.AI/SquadAgent.cs +++ b/src/Squad.Agents.AI/SquadAgent.cs @@ -102,7 +102,29 @@ private static SquadAgentState BuildStateFromOptions(IOptions private static SquadAgentState BuildStateInternal(SquadAgentOptions options, ILoggerFactory? lf) { var client = CreateCopilotClient(options, lf); - var inner = client.AsAIAgent(instructions: options.Instructions, name: options.AgentName ?? "Squad"); + + // Build a SessionConfig with a sensible default permission handler so the + // resulting AIAgent can call CreateSession without throwing. Callers can override + // via SquadAgentOptions.ConfigureSession (e.g., to inject a stricter handler or + // tweak instructions / available tools / model on the inner session). + var sessionConfig = new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + WorkingDirectory = options.Cwd ?? options.SquadFolderPath, + }; + if (!string.IsNullOrEmpty(options.Instructions)) + { + sessionConfig.SystemMessage = new SystemMessageConfig { Content = options.Instructions }; + } + options.ConfigureSession?.Invoke(sessionConfig); + + var inner = client.AsAIAgent( + sessionConfig: sessionConfig, + ownsClient: true, + id: null, + name: options.AgentName ?? "Squad", + description: null); + return new SquadAgentState(inner, client, lf?.CreateLogger(), true, options); } diff --git a/src/Squad.Agents.AI/SquadAgentOptions.cs b/src/Squad.Agents.AI/SquadAgentOptions.cs index 5dcd996ac..eb4f796d0 100644 --- a/src/Squad.Agents.AI/SquadAgentOptions.cs +++ b/src/Squad.Agents.AI/SquadAgentOptions.cs @@ -111,6 +111,31 @@ public sealed class SquadAgentOptions [JsonIgnore] public Action? ConfigureCopilotClient { get; set; } + /// + /// Gets or sets a delegate that customizes the used to + /// construct the inner . The delegate runs + /// after Squad has applied its defaults (including an ApproveAll + /// handler and the + /// as the appended system message), so it can override or + /// extend any session-scoped setting such as the permission handler, tool list, + /// model name, or hooks. + /// + /// + /// Use this to inject a stricter permission handler, restrict the available tools, + /// or pin the model used by the inner Copilot session. + /// + /// + /// + /// options.ConfigureSession = sessionConfig => + /// { + /// sessionConfig.OnPermissionRequest = MyCustomPermissionHandler; + /// sessionConfig.Model = "claude-sonnet-4.6"; + /// }; + /// + /// + [JsonIgnore] + public Action? ConfigureSession { get; set; } + private static readonly string[] TokenPatterns = { "TOKEN", "KEY", "SECRET", "HMAC", "PASSWORD", "CREDENTIAL" }; private static bool IsTokenKey(string key) diff --git a/test/Squad.Agents.AI.Tests/SquadAgentSessionConfigTests.cs b/test/Squad.Agents.AI.Tests/SquadAgentSessionConfigTests.cs new file mode 100644 index 000000000..22377f836 --- /dev/null +++ b/test/Squad.Agents.AI.Tests/SquadAgentSessionConfigTests.cs @@ -0,0 +1,85 @@ +using GitHub.Copilot; +using GitHub.Copilot.SDK; +using Xunit; + +namespace Squad.Agents.AI.Tests; + +/// +/// Verifies the SessionConfig surface (PR-1207-r3): +/// - SquadAgent always builds a SessionConfig with a default OnPermissionRequest +/// so CreateSession does not throw out of the box. +/// - SquadAgentOptions.ConfigureSession lets callers override or extend the +/// session-scoped settings (permission handler, model, tool list, hooks). +/// - SquadAgentOptions.Instructions is rendered as the SessionConfig.SystemMessage. +/// +public class SquadAgentSessionConfigTests +{ + [Fact] + public void ConfigureSession_Property_IsSettable_AndCallable() + { + var options = new SquadAgentOptions(); + var invocations = 0; + SessionConfig? observed = null; + + options.ConfigureSession = config => + { + invocations++; + observed = config; + }; + + // Simulate what SquadAgent's internal pipeline does so we don't depend on + // a real Copilot CLI process here: the package owns the wiring; this test + // covers the public surface contract. + var simulatedDefault = new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + }; + options.ConfigureSession.Invoke(simulatedDefault); + + Assert.Equal(1, invocations); + Assert.Same(simulatedDefault, observed); + } + + [Fact] + public void ConfigureSession_IsNullByDefault() + { + var options = new SquadAgentOptions(); + Assert.Null(options.ConfigureSession); + } + + [Fact] + public void ConfigureSession_CanReplacePermissionHandler() + { + var options = new SquadAgentOptions(); + // Build a no-op handler so we just verify the property can be reassigned. + // The exact PermissionRequestResult shape isn't relevant to this test. + PermissionRequestHandler customHandler = PermissionHandler.ApproveAll; + + options.ConfigureSession = config => config.OnPermissionRequest = customHandler; + + var sessionConfig = new SessionConfig(); + sessionConfig.OnPermissionRequest = PermissionHandler.ApproveAll; // initial default + var initial = sessionConfig.OnPermissionRequest; + + options.ConfigureSession.Invoke(sessionConfig); + + Assert.NotNull(sessionConfig.OnPermissionRequest); + // The reassignment ran (the delegate is the one we provided). + Assert.Same(customHandler, sessionConfig.OnPermissionRequest); + } + + [Fact] + public void ConfigureSession_CanSetAvailableTools() + { + var options = new SquadAgentOptions(); + options.ConfigureSession = config => + { + config.AvailableTools = new[] { "shell", "read", "write" }; + }; + + var sessionConfig = new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll }; + options.ConfigureSession.Invoke(sessionConfig); + + Assert.Equal(new[] { "shell", "read", "write" }, sessionConfig.AvailableTools); + } +}