mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-26 21:03:58 +02:00
fix: shell-launch copilot on windows
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections;
|
||||||
using GitHub.Copilot.SDK;
|
using GitHub.Copilot.SDK;
|
||||||
|
|
||||||
namespace Kopaya.AgentHost.Services;
|
namespace Kopaya.AgentHost.Services;
|
||||||
@@ -6,6 +7,7 @@ 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";
|
||||||
|
private static readonly string[] BlockedCliEnvironmentPrefixes = ["BUN_", "COPILOT_", "ELECTRON_", "NODE_", "NPM_"];
|
||||||
|
|
||||||
public static CopilotClientOptions CreateClientOptions()
|
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.");
|
"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
|
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);
|
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(
|
private static string? ResolveCliPath(
|
||||||
string? pathValue,
|
string? pathValue,
|
||||||
string? pathExtValue,
|
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);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
export function createSidecarEnvironment(baseEnvironment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||||
const sanitizedEnvironment: 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)) {
|
for (const [name, value] of Object.entries(baseEnvironment)) {
|
||||||
const normalizedName = name.toUpperCase();
|
const normalizedName = name.toUpperCase();
|
||||||
|
|
||||||
if (normalizedName.startsWith('COPILOT_') || blockedEnvironmentKeys.has(normalizedName)) {
|
if (blockedEnvironmentPrefixes.some((prefix) => normalizedName.startsWith(prefix))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ describe('createSidecarEnvironment', () => {
|
|||||||
expect(
|
expect(
|
||||||
createSidecarEnvironment({
|
createSidecarEnvironment({
|
||||||
PATH: 'C:\\tools',
|
PATH: 'C:\\tools',
|
||||||
|
APPDATA: 'C:\\Users\\mail\\AppData\\Roaming',
|
||||||
COPILOT_CLI: '1',
|
COPILOT_CLI: '1',
|
||||||
COPILOT_LOADER_PID: '1234',
|
COPILOT_LOADER_PID: '1234',
|
||||||
copilot_run_app: '1',
|
copilot_run_app: '1',
|
||||||
|
bun_install: 'C:\\Users\\mail\\.bun',
|
||||||
NODE_OPTIONS: '--no-warnings',
|
NODE_OPTIONS: '--no-warnings',
|
||||||
electron_run_as_node: '1',
|
electron_run_as_node: '1',
|
||||||
|
npm_config_user_agent: 'bun/1.3.6',
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
|
APPDATA: 'C:\\Users\\mail\\AppData\\Roaming',
|
||||||
PATH: 'C:\\tools',
|
PATH: 'C:\\tools',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -24,11 +28,13 @@ describe('createSidecarEnvironment', () => {
|
|||||||
PATH: 'C:\\tools',
|
PATH: 'C:\\tools',
|
||||||
HOME: 'C:\\Users\\mail',
|
HOME: 'C:\\Users\\mail',
|
||||||
FORCE_COLOR: '1',
|
FORCE_COLOR: '1',
|
||||||
|
HTTPS_PROXY: 'http://proxy.local:8080',
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
PATH: 'C:\\tools',
|
PATH: 'C:\\tools',
|
||||||
HOME: 'C:\\Users\\mail',
|
HOME: 'C:\\Users\\mail',
|
||||||
FORCE_COLOR: '1',
|
FORCE_COLOR: '1',
|
||||||
|
HTTPS_PROXY: 'http://proxy.local:8080',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user