fix: shell-launch copilot on windows

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot CLI
2026-03-21 10:44:00 +01:00
co-authored by Copilot
parent 2fbb5dc5a5
commit 56c81fe053
4 changed files with 144 additions and 3 deletions
@@ -1,3 +1,4 @@
using System.Collections;
using GitHub.Copilot.SDK;
namespace Kopaya.AgentHost.Services;
@@ -6,6 +7,7 @@ internal static class CopilotCliPathResolver
{
private const string CopilotCommandName = "copilot";
private const string DefaultWindowsPathExtensions = ".COM;.EXE;.BAT;.CMD";
private static readonly string[] BlockedCliEnvironmentPrefixes = ["BUN_", "COPILOT_", "ELECTRON_", "NODE_", "NPM_"];
public static CopilotClientOptions CreateClientOptions()
{
@@ -21,9 +23,16 @@ internal static class CopilotCliPathResolver
"Kopaya requires the system-installed 'copilot' command on PATH. Install the GitHub Copilot CLI and ensure it is available in the current environment.");
}
CopilotCliLaunch launch = ResolveCliLaunch(
cliPath,
OperatingSystem.IsWindows(),
Environment.GetEnvironmentVariable("ComSpec"));
return new CopilotClientOptions
{
CliPath = cliPath,
CliPath = launch.Path,
CliArgs = launch.Args,
Environment = ResolveCliEnvironment(GetCurrentEnvironmentVariables()),
};
}
@@ -37,6 +46,50 @@ internal static class CopilotCliPathResolver
return ResolveCliPath(pathValue, pathExtValue, isWindows, fileExists);
}
internal static IReadOnlyDictionary<string, string> ResolveCliEnvironment(
IEnumerable<KeyValuePair<string, string?>> environmentVariables)
{
ArgumentNullException.ThrowIfNull(environmentVariables);
Dictionary<string, string> sanitizedEnvironment = new(StringComparer.OrdinalIgnoreCase);
foreach (KeyValuePair<string, string?> entry in environmentVariables)
{
if (string.IsNullOrWhiteSpace(entry.Key) || entry.Value is null)
{
continue;
}
string normalizedKey = entry.Key.ToUpperInvariant();
if (BlockedCliEnvironmentPrefixes.Any(prefix => normalizedKey.StartsWith(prefix, StringComparison.Ordinal)))
{
continue;
}
sanitizedEnvironment[entry.Key] = entry.Value;
}
return sanitizedEnvironment;
}
internal static CopilotCliLaunch ResolveCliLaunch(string cliPath, bool isWindows, string? commandProcessorPath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(cliPath);
if (!isWindows)
{
return new CopilotCliLaunch(cliPath, []);
}
string launchPath = string.IsNullOrWhiteSpace(commandProcessorPath)
? "cmd.exe"
: commandProcessorPath;
return new CopilotCliLaunch(
launchPath,
["/d", "/s", "/c", CopilotCommandName]);
}
private static string? ResolveCliPath(
string? pathValue,
string? pathExtValue,
@@ -98,4 +151,15 @@ internal static class CopilotCliPathResolver
}
}
}
private static IEnumerable<KeyValuePair<string, string?>> GetCurrentEnvironmentVariables()
{
return Environment.GetEnvironmentVariables()
.Cast<DictionaryEntry>()
.Select(entry => new KeyValuePair<string, string?>(
entry.Key?.ToString() ?? string.Empty,
entry.Value?.ToString()));
}
}
internal sealed record CopilotCliLaunch(string Path, string[] Args);
@@ -51,4 +51,75 @@ public sealed class CopilotCliPathResolverTests
Assert.Null(cliPath);
}
[Fact]
public void ResolveCliEnvironment_RemovesRuntimeSpecificPrefixes()
{
IReadOnlyDictionary<string, string> environment = CopilotCliPathResolver.ResolveCliEnvironment(
[
new KeyValuePair<string, string?>("PATH", @"C:\tools"),
new KeyValuePair<string, string?>("APPDATA", @"C:\Users\mail\AppData\Roaming"),
new KeyValuePair<string, string?>("COPILOT_CLI", "1"),
new KeyValuePair<string, string?>("NODE_OPTIONS", "--no-warnings"),
new KeyValuePair<string, string?>("electron_run_as_node", "1"),
new KeyValuePair<string, string?>("BUN_FAKE_FLAG", "1"),
new KeyValuePair<string, string?>("npm_config_user_agent", "bun/1.3.6"),
]);
Assert.Equal(
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["PATH"] = @"C:\tools",
["APPDATA"] = @"C:\Users\mail\AppData\Roaming",
},
environment);
}
[Fact]
public void ResolveCliEnvironment_PreservesUnrelatedVariables()
{
IReadOnlyDictionary<string, string> environment = CopilotCliPathResolver.ResolveCliEnvironment(
[
new KeyValuePair<string, string?>("PATH", @"C:\tools"),
new KeyValuePair<string, string?>("HOME", @"C:\Users\mail"),
new KeyValuePair<string, string?>("HTTPS_PROXY", "http://proxy.local:8080"),
new KeyValuePair<string, string?>("FORCE_COLOR", "1"),
]);
Assert.Equal(
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["PATH"] = @"C:\tools",
["HOME"] = @"C:\Users\mail",
["HTTPS_PROXY"] = "http://proxy.local:8080",
["FORCE_COLOR"] = "1",
},
environment);
}
[Fact]
public void ResolveCliLaunch_UsesCommandProcessorWrapperOnWindows()
{
CopilotCliLaunch launch = CopilotCliPathResolver.ResolveCliLaunch(
cliPath: @"C:\Tools With Spaces\copilot.exe",
isWindows: true,
commandProcessorPath: @"C:\Windows\System32\cmd.exe");
Assert.Equal(@"C:\Windows\System32\cmd.exe", launch.Path);
Assert.Equal(
["/d", "/s", "/c", "copilot"],
launch.Args);
}
[Fact]
public void ResolveCliLaunch_UsesCliDirectlyOutsideWindows()
{
CopilotCliLaunch launch = CopilotCliPathResolver.ResolveCliLaunch(
cliPath: "/usr/local/bin/copilot",
isWindows: false,
commandProcessorPath: null);
Assert.Equal("/usr/local/bin/copilot", launch.Path);
Assert.Empty(launch.Args);
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
const blockedEnvironmentKeys = new Set(['ELECTRON_RUN_AS_NODE', 'NODE_OPTIONS']);
const blockedEnvironmentPrefixes = ['BUN_', 'COPILOT_', 'ELECTRON_', 'NODE_', 'NPM_'];
export function createSidecarEnvironment(baseEnvironment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const sanitizedEnvironment: NodeJS.ProcessEnv = {};
@@ -6,7 +6,7 @@ export function createSidecarEnvironment(baseEnvironment: NodeJS.ProcessEnv): No
for (const [name, value] of Object.entries(baseEnvironment)) {
const normalizedName = name.toUpperCase();
if (normalizedName.startsWith('COPILOT_') || blockedEnvironmentKeys.has(normalizedName)) {
if (blockedEnvironmentPrefixes.some((prefix) => normalizedName.startsWith(prefix))) {
continue;
}
+6
View File
@@ -7,13 +7,17 @@ describe('createSidecarEnvironment', () => {
expect(
createSidecarEnvironment({
PATH: 'C:\\tools',
APPDATA: 'C:\\Users\\mail\\AppData\\Roaming',
COPILOT_CLI: '1',
COPILOT_LOADER_PID: '1234',
copilot_run_app: '1',
bun_install: 'C:\\Users\\mail\\.bun',
NODE_OPTIONS: '--no-warnings',
electron_run_as_node: '1',
npm_config_user_agent: 'bun/1.3.6',
}),
).toEqual({
APPDATA: 'C:\\Users\\mail\\AppData\\Roaming',
PATH: 'C:\\tools',
});
});
@@ -24,11 +28,13 @@ describe('createSidecarEnvironment', () => {
PATH: 'C:\\tools',
HOME: 'C:\\Users\\mail',
FORCE_COLOR: '1',
HTTPS_PROXY: 'http://proxy.local:8080',
}),
).toEqual({
PATH: 'C:\\tools',
HOME: 'C:\\Users\\mail',
FORCE_COLOR: '1',
HTTPS_PROXY: 'http://proxy.local:8080',
});
});
});