fix: always use system copilot cli

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot CLI
2026-03-21 10:29:51 +01:00
co-authored by Copilot
parent 3e1165fa3a
commit 8f9eb99f3f
4 changed files with 30 additions and 60 deletions
@@ -8,6 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="GitHub.Copilot.SDK" Version="0.2.0" />
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-rc4" /> <PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-rc4" />
<PackageReference Include="Microsoft.Agents.AI.GitHub.Copilot" Version="1.0.0-preview.260311.1" /> <PackageReference Include="Microsoft.Agents.AI.GitHub.Copilot" Version="1.0.0-preview.260311.1" />
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-rc4" /> <PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-rc4" />
@@ -1,4 +1,3 @@
using System.Runtime.InteropServices;
using GitHub.Copilot.SDK; using GitHub.Copilot.SDK;
namespace Kopaya.AgentHost.Services; namespace Kopaya.AgentHost.Services;
@@ -8,62 +7,34 @@ internal static class CopilotCliPathResolver
private const string CopilotCommandName = "copilot"; private const string CopilotCommandName = "copilot";
private const string DefaultWindowsPathExtensions = ".COM;.EXE;.BAT;.CMD"; private const string DefaultWindowsPathExtensions = ".COM;.EXE;.BAT;.CMD";
public static CopilotClientOptions? CreateClientOptions() public static CopilotClientOptions CreateClientOptions()
{ {
CopilotCliResolution resolution = Resolve( string? cliPath = Resolve(
Environment.ProcessPath,
Environment.GetEnvironmentVariable("PATH"), Environment.GetEnvironmentVariable("PATH"),
Environment.GetEnvironmentVariable("PATHEXT"), Environment.GetEnvironmentVariable("PATHEXT"),
RuntimeInformation.IsOSPlatform(OSPlatform.Windows), OperatingSystem.IsWindows(),
File.Exists); File.Exists);
if (!resolution.ShouldOverrideCliPath) if (string.IsNullOrWhiteSpace(cliPath))
{
return null;
}
if (string.IsNullOrWhiteSpace(resolution.CliPath))
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
"Development sidecar could not find the system-installed 'copilot' command on PATH. Install the GitHub Copilot CLI or provide an explicit CliPath."); "Kopaya requires the system-installed 'copilot' command on PATH. Install the GitHub Copilot CLI and ensure it is available in the current environment.");
} }
return new CopilotClientOptions return new CopilotClientOptions
{ {
CliPath = resolution.CliPath, CliPath = cliPath,
}; };
} }
internal static CopilotCliResolution Resolve( internal static string? Resolve(
string? processPath,
string? pathValue, string? pathValue,
string? pathExtValue, string? pathExtValue,
bool isWindows, bool isWindows,
Func<string, bool> fileExists) Func<string, bool> fileExists)
{ {
ArgumentNullException.ThrowIfNull(fileExists); ArgumentNullException.ThrowIfNull(fileExists);
return ResolveCliPath(pathValue, pathExtValue, isWindows, fileExists);
if (!IsDevelopmentHost(processPath))
{
return default;
}
return new CopilotCliResolution(
ShouldOverrideCliPath: true,
CliPath: ResolveCliPath(pathValue, pathExtValue, isWindows, fileExists));
}
private static bool IsDevelopmentHost(string? processPath)
{
if (string.IsNullOrWhiteSpace(processPath))
{
return false;
}
return string.Equals(
Path.GetFileNameWithoutExtension(processPath),
"dotnet",
StringComparison.OrdinalIgnoreCase);
} }
private static string? ResolveCliPath( private static string? ResolveCliPath(
@@ -128,5 +99,3 @@ internal static class CopilotCliPathResolver
} }
} }
} }
internal readonly record struct CopilotCliResolution(bool ShouldOverrideCliPath, string? CliPath);
@@ -170,11 +170,11 @@ public sealed class CopilotWorkflowRunner
{ {
List<IAsyncDisposable> disposables = []; List<IAsyncDisposable> disposables = [];
List<AIAgent> agents = []; List<AIAgent> agents = [];
CopilotClientOptions? clientOptions = CopilotCliPathResolver.CreateClientOptions(); CopilotClientOptions clientOptions = CopilotCliPathResolver.CreateClientOptions();
foreach (PatternAgentDefinitionDto definition in pattern.Agents) foreach (PatternAgentDefinitionDto definition in pattern.Agents)
{ {
CopilotClient client = clientOptions is null ? new() : new(clientOptions); CopilotClient client = new(clientOptions);
await client.StartAsync(cancellationToken).ConfigureAwait(false); await client.StartAsync(cancellationToken).ConfigureAwait(false);
SessionConfig sessionConfig = new() SessionConfig sessionConfig = new()
@@ -263,7 +263,7 @@ public sealed class CopilotWorkflowRunner
{ {
return Task.FromResult(new PermissionRequestResult return Task.FromResult(new PermissionRequestResult
{ {
Kind = "approved", Kind = PermissionRequestResultKind.Approved,
}); });
} }
} }
@@ -5,7 +5,7 @@ namespace Kopaya.AgentHost.Tests;
public sealed class CopilotCliPathResolverTests public sealed class CopilotCliPathResolverTests
{ {
[Fact] [Fact]
public void Resolve_UsesCopilotFromPathDuringDevelopment() public void Resolve_UsesCopilotFromPath()
{ {
string copilotDirectory = @"C:\tools\copilot"; string copilotDirectory = @"C:\tools\copilot";
HashSet<string> existingFiles = new(StringComparer.OrdinalIgnoreCase) HashSet<string> existingFiles = new(StringComparer.OrdinalIgnoreCase)
@@ -13,42 +13,42 @@ public sealed class CopilotCliPathResolverTests
Path.Combine(copilotDirectory, "copilot.exe"), Path.Combine(copilotDirectory, "copilot.exe"),
}; };
CopilotCliResolution resolution = CopilotCliPathResolver.Resolve( string? cliPath = CopilotCliPathResolver.Resolve(
processPath: @"C:\Program Files\dotnet\dotnet.exe",
pathValue: $"C:\\other;\"{copilotDirectory}\"", pathValue: $"C:\\other;\"{copilotDirectory}\"",
pathExtValue: ".COM;.EXE;.BAT;.CMD", pathExtValue: ".COM;.EXE;.BAT;.CMD",
isWindows: true, isWindows: true,
fileExists: existingFiles.Contains); fileExists: existingFiles.Contains);
Assert.True(resolution.ShouldOverrideCliPath); Assert.Equal(Path.Combine(copilotDirectory, "copilot.exe"), cliPath, ignoreCase: true);
Assert.Equal(Path.Combine(copilotDirectory, "copilot.exe"), resolution.CliPath, ignoreCase: true);
} }
[Fact] [Fact]
public void Resolve_LeavesPackagedRuntimeOnBundledCli() public void Resolve_UsesDefaultWindowsExtensionsWhenPathExtMissing()
{ {
CopilotCliResolution resolution = CopilotCliPathResolver.Resolve( string copilotDirectory = @"C:\tools\copilot";
processPath: @"C:\Program Files\Kopaya\Kopaya.AgentHost.exe", HashSet<string> existingFiles = new(StringComparer.OrdinalIgnoreCase)
pathValue: @"C:\tools", {
pathExtValue: ".COM;.EXE;.BAT;.CMD", Path.Combine(copilotDirectory, "copilot.cmd"),
};
string? cliPath = CopilotCliPathResolver.Resolve(
pathValue: $"C:\\other;\"{copilotDirectory}\"",
pathExtValue: null,
isWindows: true, isWindows: true,
fileExists: _ => true); fileExists: existingFiles.Contains);
Assert.False(resolution.ShouldOverrideCliPath); Assert.Equal(Path.Combine(copilotDirectory, "copilot.cmd"), cliPath, ignoreCase: true);
Assert.Null(resolution.CliPath);
} }
[Fact] [Fact]
public void Resolve_ReportsMissingCopilotWhenDevelopmentPathDoesNotContainIt() public void Resolve_ReturnsNullWhenPathDoesNotContainCopilot()
{ {
CopilotCliResolution resolution = CopilotCliPathResolver.Resolve( string? cliPath = CopilotCliPathResolver.Resolve(
processPath: @"C:\Program Files\dotnet\dotnet.exe",
pathValue: @"C:\tools;C:\other", pathValue: @"C:\tools;C:\other",
pathExtValue: ".COM;.EXE;.BAT;.CMD", pathExtValue: ".COM;.EXE;.BAT;.CMD",
isWindows: true, isWindows: true,
fileExists: _ => false); fileExists: _ => false);
Assert.True(resolution.ShouldOverrideCliPath); Assert.Null(cliPath);
Assert.Null(resolution.CliPath);
} }
} }