- VS Code Version: 1.138.0 (commit
7debcd0e2acdea1c52de81bf9ee1620444407dda)
- OS Version: Linux x64
- Does this issue occur when all extensions are disabled?: Yes (the agent host and the bundled Copilot CLI are built in)
Summary
When the OS proxy is configured through a PAC script and no explicit proxy settings or proxy environment
variables are present, the agent host resolves the proxy for a single URL — api.githubcopilot.com,
or whatever resolveApiEndpoint() returns — and writes that one result into the Copilot CLI subprocess as
HTTP_PROXY/HTTPS_PROXY. Every HTTP request that subprocess makes is then forced through a proxy chosen
only for an external endpoint, including MCP requests to hosts the PAC script explicitly maps to
DIRECT.
Relationship to #326185 / #333614 — this is the remaining case, not a duplicate
I can confirm #333614 works as intended on 1.138.0. On the same machine, same day:
1.136.1 Proxy configuration: HTTP_PROXY=..., HTTPS_PROXY=..., NO_PROXY=(not set)
1.138.0 Proxy configuration: HTTP_PROXY=..., HTTPS_PROXY=..., NO_PROXY=localhost,127.0.0.1,*.corp.example,...
http.noProxy is now forwarded, and _readNoProxy() additionally falls back to an inherited
no_proxy/NO_PROXY. That fixed our symptom once we set http.noProxy explicitly.
What remains is the path #333614 deliberately left in place — "OS/PAC discovery remains the final
fallback". On that path there is no bypass list to forward, because PAC does not expose one (see
below), so a single collapsed proxy value is still applied process-wide. A clean machine with only a PAC
and no explicit settings therefore still fails on 1.138.0.
Why PAC makes this incorrect by construction
PAC has no bypass list. FindProxyForURL(url, host) answers per host, and the answer may depend on
dnsResolve(), myIpAddress() or arbitrary JavaScript. Collapsing one host's answer into a process-wide
value discards the function's entire purpose — and there is no list that could be forwarded as NO_PROXY
instead.
An anonymised PAC equivalent to the one in our environment:
function isLocalDestination(host, hostIP) {
return isPlainHostName(host) ||
isInNet(hostIP, "10.0.0.0", "255.0.0.0") ||
isInNet(hostIP, "172.16.0.0", "255.240.0.0") ||
isInNet(hostIP, "192.168.0.0", "255.255.0.0") ||
shExpMatch(host, "*.corp.example");
}
function FindProxyForURL(url, host) {
if (isLocalDestination(host, dnsResolve(host))) return "DIRECT";
return "PROXY proxy-external.corp.example:3128";
}
What it returns for the two hosts that matter:
| Target |
PAC answer |
api.githubcopilot.com |
PROXY proxy-external.corp.example:3128 |
internal-mcp.corp.example |
DIRECT — matches *.corp.example, and its address is in 10.0.0.0/8 |
The subprocess receives HTTP_PROXY=http://proxy-external.corp.example:3128 and sends the internal
request there. That proxy has no route into the internal network, so the request times out. The PAC's
DIRECT answer for the internal host is never queried on this code path.
Steps to Reproduce
All of the following must hold — on 1.138.0 any one of them is enough to mask the problem:
- OS proxy in PAC / auto mode, where the PAC returns
DIRECT for an internal host and a proxy for
api.githubcopilot.com.
- The client IP must not match any blanket-
DIRECT rule inside the PAC.
http.proxy unset — otherwise _readConfiguredProxy() short-circuits the PAC path.
http.noProxy unset, and no inherited no_proxy/NO_PROXY — otherwise _readNoProxy() masks it.
- No inherited
HTTP_PROXY/HTTPS_PROXY/ALL_PROXY (any case) — otherwise the agent host leaves proxy
configuration to the environment.
- Register a streamable-HTTP MCP server pointing at the internal host in
.mcp.json, then open an Agent
window session in that workspace.
Actual behaviour
First line of the Copilot CLI log (~/.copilot/logs/*.log) on every start:
[INFO] Proxy configuration: HTTP_PROXY=http://proxy-external.corp.example:3128,
HTTPS_PROXY=http://proxy-external.corp.example:3128, NO_PROXY=(not set)
The MCP connection then fails:
[ERROR] Failed to start MCP client for <server>: failed to initialize MCP client:
Send message error Transport [rmcp::transport::worker::WorkerTransport<
rmcp::transport::streamable_http_client::StreamableHttpClientWorker<...>>]
error: Client error: error sending request for url
(https://internal-mcp.corp.example/mcp/), when send initialize request
The VS Code log confirms the injection:
[info] [Copilot] Resolved CAPI proxy and forwarded HTTP_PROXY/HTTPS_PROXY to Copilot SDK
/proc/<cli-pid>/environ shows HTTP_PROXY/HTTPS_PROXY present, while the VS Code main process has
no proxy variables at all — confirming these values are injected by VS Code rather than inherited
from the desktop session.
Expected behaviour
The request to the internal host should follow the PAC's DIRECT answer for that host.
Evidence: three-way comparison
Same machine, same endpoint, curl only, isolating the proxy variables:
HTTP_PROXY |
NO_PROXY |
Result |
proxy-external.corp.example:3128 |
unset |
timeout (curl 28, 20 s) — reproduces the failure |
proxy-external.corp.example:3128 |
*.corp.example |
200, direct connection |
| an internal-capable proxy |
unset |
200, via proxy |
Row 1 is exactly the environment the agent host constructs on the PAC path. Rows 2 and 3 are the two
independent ways out, which is what isolates the cause: the failure is not "a proxy is in the way", it is
"a proxy chosen for a different host was applied to this one".
Root cause (code as of 1.138.0)
src/vs/platform/agentHost/node/copilot/copilotAgent.ts:
async _resolveProxyForSdk(e = process.env) {
let t = this._readConfiguredProxy();
if (t) return t; // http.proxy wins
if (!this._isSystemProxyEnabled()) return;
if (lD.some(o => e[o])) { /* leave it to the environment */ return }
let r = e.VSCODE_AGENT_HOST_CAPI_URL_OVERRIDE || O5; // O5 = https://api.githubcopilot.com
if (this._githubCredentials.token) { /* resolveApiEndpoint() may replace r */ }
return await this._proxyResolver.resolveProxy(r); // ← resolved for ONE url
}
_createCopilotCliEnvironment() {
let e = this._readConfiguredProxy() ?? (this._isSystemProxyEnabled() ? this._resolvedProxy : void 0);
...
if (e) { for (let a of j5) o[a] = e; ... } // ← j5 = ["HTTP_PROXY","HTTPS_PROXY"], applied globally
t && (o.NO_PROXY = t); // added by #333614; undefined on the PAC path
}
The intent looks narrow — the log message says "to Copilot SDK" — but the mechanism is a process-wide
environment variable, so the effective scope is every HTTP client in that subprocess. That includes the
MCP client, which is Rust reqwest and reads only environment variables; it cannot see VS Code settings.
#324209 (milestone 1.128.0) introduced this injection.
Suggested fix
In decreasing order of fidelity:
- Pass the PAC URL / auto-detect configuration to the subprocess and let its HTTP client resolve per
request, preserving PAC semantics.
- Evaluate the PAC for the hosts the subprocess is actually configured to reach (e.g. MCP server hosts
from .mcp.json) and forward the ones resolving to DIRECT as NO_PROXY.
- Do not derive a process-wide proxy from PAC resolution at all — scope the CAPI proxy to the SDK's CAPI
client, leaving other clients to the OS configuration.
7debcd0e2acdea1c52de81bf9ee1620444407dda)Summary
When the OS proxy is configured through a PAC script and no explicit proxy settings or proxy environment
variables are present, the agent host resolves the proxy for a single URL —
api.githubcopilot.com,or whatever
resolveApiEndpoint()returns — and writes that one result into the Copilot CLI subprocess asHTTP_PROXY/HTTPS_PROXY. Every HTTP request that subprocess makes is then forced through a proxy chosenonly for an external endpoint, including MCP requests to hosts the PAC script explicitly maps to
DIRECT.Relationship to #326185 / #333614 — this is the remaining case, not a duplicate
I can confirm #333614 works as intended on 1.138.0. On the same machine, same day:
http.noProxyis now forwarded, and_readNoProxy()additionally falls back to an inheritedno_proxy/NO_PROXY. That fixed our symptom once we sethttp.noProxyexplicitly.What remains is the path #333614 deliberately left in place — "OS/PAC discovery remains the final
fallback". On that path there is no bypass list to forward, because PAC does not expose one (see
below), so a single collapsed proxy value is still applied process-wide. A clean machine with only a PAC
and no explicit settings therefore still fails on 1.138.0.
Why PAC makes this incorrect by construction
PAC has no bypass list.
FindProxyForURL(url, host)answers per host, and the answer may depend ondnsResolve(),myIpAddress()or arbitrary JavaScript. Collapsing one host's answer into a process-widevalue discards the function's entire purpose — and there is no list that could be forwarded as
NO_PROXYinstead.
An anonymised PAC equivalent to the one in our environment:
What it returns for the two hosts that matter:
api.githubcopilot.comPROXY proxy-external.corp.example:3128internal-mcp.corp.exampleDIRECT— matches*.corp.example, and its address is in10.0.0.0/8The subprocess receives
HTTP_PROXY=http://proxy-external.corp.example:3128and sends the internalrequest there. That proxy has no route into the internal network, so the request times out. The PAC's
DIRECTanswer for the internal host is never queried on this code path.Steps to Reproduce
All of the following must hold — on 1.138.0 any one of them is enough to mask the problem:
DIRECTfor an internal host and a proxy forapi.githubcopilot.com.DIRECTrule inside the PAC.http.proxyunset — otherwise_readConfiguredProxy()short-circuits the PAC path.http.noProxyunset, and no inheritedno_proxy/NO_PROXY— otherwise_readNoProxy()masks it.HTTP_PROXY/HTTPS_PROXY/ALL_PROXY(any case) — otherwise the agent host leaves proxyconfiguration to the environment.
.mcp.json, then open an Agentwindow session in that workspace.
Actual behaviour
First line of the Copilot CLI log (
~/.copilot/logs/*.log) on every start:The MCP connection then fails:
The VS Code log confirms the injection:
/proc/<cli-pid>/environshowsHTTP_PROXY/HTTPS_PROXYpresent, while the VS Code main process hasno proxy variables at all — confirming these values are injected by VS Code rather than inherited
from the desktop session.
Expected behaviour
The request to the internal host should follow the PAC's
DIRECTanswer for that host.Evidence: three-way comparison
Same machine, same endpoint,
curlonly, isolating the proxy variables:HTTP_PROXYNO_PROXYproxy-external.corp.example:3128proxy-external.corp.example:3128*.corp.exampleRow 1 is exactly the environment the agent host constructs on the PAC path. Rows 2 and 3 are the two
independent ways out, which is what isolates the cause: the failure is not "a proxy is in the way", it is
"a proxy chosen for a different host was applied to this one".
Root cause (code as of 1.138.0)
src/vs/platform/agentHost/node/copilot/copilotAgent.ts:The intent looks narrow — the log message says "to Copilot SDK" — but the mechanism is a process-wide
environment variable, so the effective scope is every HTTP client in that subprocess. That includes the
MCP client, which is Rust
reqwestand reads only environment variables; it cannot see VS Code settings.#324209(milestone 1.128.0) introduced this injection.Suggested fix
In decreasing order of fidelity:
request, preserving PAC semantics.
from
.mcp.json) and forward the ones resolving toDIRECTasNO_PROXY.client, leaving other clients to the OS configuration.