Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion src/Squad.Agents.AI/SquadAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,29 @@ private static SquadAgentState BuildStateFromOptions(IOptions<SquadAgentOptions>
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<SquadAgent>(), true, options);
}

Expand Down
25 changes: 25 additions & 0 deletions src/Squad.Agents.AI/SquadAgentOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,31 @@ public sealed class SquadAgentOptions
[JsonIgnore]
public Action<CopilotClientOptions>? ConfigureCopilotClient { get; set; }

/// <summary>
/// Gets or sets a delegate that customizes the <see cref="SessionConfig"/> used to
/// construct the inner <see cref="Microsoft.Agents.AI.AIAgent"/>. The delegate runs
/// after Squad has applied its defaults (including an <c>ApproveAll</c>
/// <see cref="SessionConfig.OnPermissionRequest"/> handler and the
/// <see cref="Instructions"/> 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.
Comment on lines +117 to +121
/// </summary>
/// <remarks>
/// <para>Use this to inject a stricter permission handler, restrict the available tools,
/// or pin the model used by the inner Copilot session.</para>
/// </remarks>
/// <example>
/// <code>
/// options.ConfigureSession = sessionConfig =>
/// {
/// sessionConfig.OnPermissionRequest = MyCustomPermissionHandler;
/// sessionConfig.Model = "claude-sonnet-4.6";
/// };
/// </code>
/// </example>
[JsonIgnore]
public Action<SessionConfig>? ConfigureSession { get; set; }

private static readonly string[] TokenPatterns = { "TOKEN", "KEY", "SECRET", "HMAC", "PASSWORD", "CREDENTIAL" };

private static bool IsTokenKey(string key)
Expand Down
85 changes: 85 additions & 0 deletions test/Squad.Agents.AI.Tests/SquadAgentSessionConfigTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using GitHub.Copilot;
using GitHub.Copilot.SDK;
using Xunit;

namespace Squad.Agents.AI.Tests;

/// <summary>
/// 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.
/// </summary>
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);
Comment on lines +30 to +40
}

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