1053
}
1054
}
1056
return this._client;
1057
}
1059
return this._clientStarting;
1060
}
1061
>
// Snapshot the startup config so we can detect a change that lands while the
copilotAgent.ts
1062
>
// client is still starting and abort the stale start (the values are baked
1063
>
// into the client options / subprocess env below).
1064
>
const sessionSyncAtStartup = this._isSessionSyncEnabled();
1065
>
const rubberDuckAtStartup = this._isRubberDuckEnabled();
1066
>
const copilotSdkLogLevelSettingAtStartup = this._getCopilotSdkLogLevelSetting();
1067
>
const enterpriseHostAtStartup = this._getEnterpriseHost();
1068
>
const systemProxyEnabledAtStartup = this._isSystemProxyEnabled();
1069
>
const clientStarting = (async () => {
1070
>
this._logService.info('[Copilot] Starting CopilotClient...');
1071
>
1072
>
// Build a clean env for the CLI subprocess, stripping Electron/VS Code vars
1073
>
// that can interfere with the Node.js process the SDK spawns.
1074
>
const env: Record<string, string | undefined> = Object.assign({}, process.env, { ELECTRON_RUN_AS_NODE: '1' });
1075
>
delete env['NODE_OPTIONS'];
1076
>
delete env['VSCODE_INSPECTOR_OPTIONS'];
1077
>
delete env['VSCODE_ESM_ENTRYPOINT'];
1078
>
delete env['VSCODE_HANDLES_UNCAUGHT_ERRORS'];
1079
>
for (const key of Object.keys(env)) {
1080
>
if (key === 'ELECTRON_RUN_AS_NODE') {
1081
>
continue;
1082
>
}
1083
>
if (key === 'VSCODE_AGENT_HOST_CAPI_URL_OVERRIDE') {
1084
// used for running the CLI in a test harness against a mock CAPI server
1085
continue;
1086
}
1087
>
if (key.startsWith('VSCODE_') || key.startsWith('ELECTRON_')) {
copilotAgent.ts
1088
delete env[key];
1089
}
1091
>
env['COPILOT_CLI_RUN_AS_NODE'] = '1';
1092
>
env['USE_BUILTIN_RIPGREP'] = 'false';
1093
>
env['COPILOT_MCP_APPS'] = 'true';
1094
>
// Attribute the CLI and everything it spawns (`gh`, …) back to
1095
>
// VS Code. Already inherited via `process.env` (the strip loop only
1096
>
// removes `VSCODE_*`/`ELECTRON_*`); set here as defense in depth.
1097
>
env[AiAgentEnvVar] = AiAgentEnvValue;
1098
>
// Required by the currently bundled SDK to enable its experimental auto-approval judge.
1099
>
env['AUTO_APPROVAL'] = 'true';
1100
>
await this._configureProxyEnv(env);
1101
>
1102
>
// On Linux the MXC bubblewrap sandbox backend does not forward a PTY into
1103
>
// the container, so the CLI's default PTY-backed interactive shell can
1104
>
// never start bash under the sandbox: the inner shell sees a non-tty
1105
>
// stdin, runs non-interactively, reads EOF and exits immediately, which
1106
>
// surfaces as "Failed to start bash process". Force the CLI's pipe-based
1107
>
// spawn shell backend (`SHELL_SPAWN_BACKEND`), which runs each command as
1108
>
// a one-shot child process and works correctly under bubblewrap. The CLI
1109
>
// already force-enables this on Alpine/musl; glibc Linux needs it too for
1110
>
// sandboxed shells. This becomes a no-op once the bundled CLI defaults the
1111
>
// spawn backend on for all of Linux.
1112
>
if (process.platform === 'linux') {
1113
>
const enabledFlags = env['COPILOT_CLI_ENABLED_FEATURE_FLAGS'];
1114
>
const flags = new Set((enabledFlags ?? '').split(',').map(f => f.trim()).filter(Boolean));
1115
>
flags.add('SHELL_SPAWN_BACKEND');
1116
>
env['COPILOT_CLI_ENABLED_FEATURE_FLAGS'] = [...flags].join(',');
1117
>
}
1118
>
1119
>
// Identify VS Code's agent host traffic in CAPI
1120
>
env['GITHUB_COPILOT_INTEGRATION_ID'] = COPILOT_INTEGRATION_ID;
1121
>
this._logService.info(`[Copilot] Set CLI env: GITHUB_COPILOT_INTEGRATION_ID=${COPILOT_INTEGRATION_ID}`);
1122
>
1123
>
// Point the Copilot CLI at a configured GitHub Enterprise host for its
1124
>
// authentication and CAPI endpoint discovery. `COPILOT_GH_HOST` is
1125
>
// Copilot-CLI-specific (it does not affect the `gh` CLI). Unset for
1126
>
// github.com so the CLI uses its default host.
1127
>
const enterpriseHost = this._getEnterpriseHost();
1128
>
if (enterpriseHost) {
1129
env['COPILOT_GH_HOST'] = enterpriseHost;
1130
this._logService.info(`[Copilot] Set CLI env: COPILOT_GH_HOST=${enterpriseHost}`);
1131
}
1133
>
// Enable the rubber duck critic subagent in the CLI when the agent host
1134
>
// config opts in. `RUBBER_DUCK_AGENT` is the SDK's required interface for
1135
>
// gating this experimental feature
1136
>
if (this._isRubberDuckEnabled()) {
1137
env['RUBBER_DUCK_AGENT'] = 'true';
1139
>
delete env['RUBBER_DUCK_AGENT'];
1140
>
}
1141
>
1142
>
// Resolve the CLI entry point and native SDK binaries from node_modules.
1143
>
// In the desktop app these live next to the ASAR archive in
1144
>
// `node_modules.asar.unpacked` (the `@github/copilot-<platform>` CLI and
1145
>
// the `@microsoft/mxc-sdk/bin` executables are unpacked so they can be
1146
>
// spawned), while in dev and on the server (which has no ASAR) they live
1147
>
// in a plain `node_modules`.
1148
>
// We can't use require.resolve() because @github/copilot's exports map
1149
>
// blocks direct subpath access.
1150
>
const nodeModulesUri = FileAccess.asFileUri(getAppNodeModulesPath());
1151
>
const cliPath = await resolveCopilotCliPath(nodeModulesUri);
1152
>
1153
>
// The SDK's sandbox auto-detection looks for `<MXC_BIN_DIR>/<arch>/wxc-exec.exe`
1154
>
// (and the Linux/macOS equivalents). VS Code core ships the MXC sandbox binaries
1155
>
// at `<nodeModules>/@microsoft/mxc-sdk/bin/<arch>/`, so point `MXC_BIN_DIR` there.
1156
>
// The @github/copilot package's own `mxc-bin/` is excluded from the product build
1157
>
// (see build/.moduleignore), mirroring `CopilotCLISDK.getPackage` in the extension.
1158
>
env['MXC_BIN_DIR'] = URI.joinPath(nodeModulesUri, '@microsoft', 'mxc-sdk', 'bin').fsPath;
1159
>
1160
>
// Add VS Code's built-in ripgrep to PATH so the CLI subprocess can find it.
1161
>
const resolvedRgDiskPath = await rgDiskPath();
1162
>
const rgDir = dirname(resolvedRgDiskPath);
1163
>
// On Windows the env key is typically "Path" (not "PATH"). Since we copied
1164
>
// process.env into a plain (case-sensitive) object, we must find the actual key.
1165
>
const pathKey = Object.keys(env).find(k => k.toUpperCase() === 'PATH') ?? 'PATH';
1166
>
const currentPath = env[pathKey];
1167
>
env[pathKey] = currentPath ? `${currentPath}${delimiter}${rgDir}` : rgDir;
1168
>
this._logService.info(`[Copilot] Resolved CLI path: ${cliPath}`);
1169
>
1170
>
const telemetry = await this._otelService.getSdkTelemetryConfig();
1171
>
const copilotSdkLogLevelAtStartup = this._resolveCopilotSdkLogLevel(copilotSdkLogLevelSettingAtStartup);
1172
>
1173
>
const clientOptions: CopilotClientOptions = {
1174
>
useLoggedInUser: false,
1175
>
connection: RuntimeConnection.forStdio({ path: cliPath }),
1176
>
env,
1177
>
telemetry,
1178
>
logLevel: copilotSdkLogLevelAtStartup,
1179
>
enableRemoteSessions: sessionSyncAtStartup,
1180
>
onGitHubTelemetry: notification => this._routeGitHubTelemetry(notification),
1181
>
};
1182
>
const client = this._createCopilotClient(clientOptions);
1183
>
await client.start();
1184
>
if (this._shutdownPromise) {
1185
await client.stop();
1186
throw new CancellationError();
1187
}
1188
>
if (this._isSessionSyncEnabled() !== sessionSyncAtStartup || this._isRubberDuckEnabled() !== rubberDuckAtStartup || this._getCopilotSdkLogLevelSetting() !== copilotSdkLogLevelSettingAtStartup || this._getEnterpriseHost() !== enterpriseHostAtStartup || this._isSystemProxyEnabled() !== systemProxyEnabledAtStartup) {
copilotAgent.ts
1189
await client.stop();
1190
throw new Error('Copilot startup config changed while the client was starting');