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); + } +}