Managed-settings interim fail-closed uses an empty allow list ([[]]) and permanently drops user MCP servers that register inside the window — reproduces via the desktop app, on an account with no managed policy
- Dominant language
- Shell
- Stars
- 11.2k
- Forks
- 1.9k
- Avg merge
- 14h 16m
- Merged PRs (30d)
- 6
Description
### Describe the bug
While the CLI resolves managed settings, it installs an **interim "deny everything" MCP policy**
(`managedAllowedMcpServerLists: [[]]` — one list, containing nothing). Any user-configured MCP server
that happens to register during that window is rejected with:
```
[ERROR] Skipping MCP server "": not permitted by enterprise managed allow list
```
Two things make this a real bug rather than a benign transient:
1. **It fires even when there is no managed policy at all.** In my logs the resolution completes in
5–65 ms with `source=none`, `serverFetchFailed=false`, and
`[managedSettings] applied: no bypass restriction in force (managed policy absent)`. Nothing failed,
and there is no enterprise policy on this account. The deny is purely pre-emptive.
2. **Servers denied during that window are never restored.** The "policy relaxed" restore loop replays
only servers captured in `policyRemovedConfigs`, and the startup deny paths never write there. Those
servers stay dead for the entire life of the session.
Net effect: opening a session while any session is resolving managed settings silently produces a
session with **no user MCP servers**, permanently. Built-in servers are exempt (the trusted-first-party
check keeps entries whose `source === "builtin"`), so it presents as "my servers are broken" rather than
"a policy was applied".
### Important scope note
**The defect is in the CLI, but it is only reachable when the CLI is driven by a client that sets
`enableManagedSettings: true`.** The GitHub Copilot desktop app sets it on every `session.create` and
`session.resume`. A CLI launched by hand does not, so **this cannot be reproduced from a bare terminal
invocation** — please see the warning at the top of the repro steps before trying.
This is provable from both sides of the interface:
- **CLI side** (`app.js`, 1.0.79-5): 21 occurrences of `enableManagedSettings`, and **not one assigns it
`true`**. Both `enableManagedSettings=!0` and `enableManagedSettings:!0` are absent from the entire
bundle. The field initialises to `!1` and is thereafter only read from the client payload
(`this.enableManagedSettings = n.enableManagedSettings ?? !1`). Every managed-settings entry point is
gated on it, including `refreshManagedSettings()`, which opens with
`if(!this.enableManagedSettings)return;`.
- **App side** (`copilot-sdk/index.js`, app 1.1.6): passes
`enableManagedSettings: config.enableManagedSettings` into both `session.create` and `session.resume`.
That is why the same binary produces 257 deny events under the app and 0 when launched directly.
### Impact
- Sessions silently lose all user MCP tooling. There is no `tools_changed` notification, no banner, and
no error surfaced in the UI — only an `[ERROR]` line in a process log the user has no reason to read.
- The message names a cause (`enterprise managed allow list`) that **does not exist on the account**,
sending users to hunt for a nonexistent policy. At least one upstream reporter (#4392) read it as a
genuine corporate whitelist.
- Agents and skills that depend on those servers fail in ways that look like the servers are
misconfigured, so users edit working configuration trying to fix it.
- MCP child processes are spawned and then discarded, so the startup cost is paid for no benefit.
## Affected Version
**Reproduced on CLI payload 1.0.79-5**, driven by desktop app 1.1.6 (`commit=a9706b2`); also observed
under app 1.1.5 (`f459745`). Windows 11 x64.
The mechanism is present in every payload from **1.0.72** onward. Marker counts taken directly from each
shipped `app.js`:
| CLI payload | `managedAllowedMcpServerLists:[[]]` | host-wide fan-out to active sessions | `managedSettingsResolutionInFlight` |
|---|---|---|---|
| **1.0.71** | **absent** | **0** | **0** |
| 1.0.72 | present | 0 | 0 |
| 1.0.73 | present | 0 | 0 |
| **1.0.78-2** | present | **1** | **4** |
| 1.0.79-5 | present | 1 | 4 |
| 1.0.79-9 | present | 1 | 4 |
- **1.0.72** introduced the empty-allow-list interim state, behind a narrow session-local guard.
- **1.0.78-2** made it routine: it added fan-out to every active session and widened the trigger guard at
the same time (diff in Additional context).
- **1.0.71 does not contain the mechanism at all** and is the basis of the workaround.
Install timeline on this machine, from `~/.copilot/logs/github-app.*.log`:
```
2026-08-04T07:42:36Z Detected Copilot CLI version at startup version=Copilot binary version: 1.0.78-2 source=Some(Bundled)
2026-08-07T15:03:37Z Detected Copilot CLI version at startup version=Copilot binary version: 1.0.79-5 source=Some(Bundled)
```
**The desktop app version is a precondition, not the cause.** `copilot-sdk/index.js` is byte-identical
between app 1.1.5 and 1.1.6 (SHA-256 `648CD26BA299…`, 449,504 bytes in both), and the app has passed
`enableManagedSettings` since 1.1.0 — so nothing changed on the app side across the window in which the
symptom appeared.
> Note on reading versions: `copilot.exe --version` self-resolves to the newest *installed* payload and
> will misreport which payload is actually running. Use `--no-auto-update --version`, or inspect the
> loaded module path of a live process.
### Affected version
CLI payload 1.0.79-5+. See the bottom of description for details on affected version
### Steps to reproduce the behavior
> ⚠️ **A hand-launched CLI will not reproduce this, and the clean log it produces is not evidence the
> build is fine.** Running `copilot --server --stdio --no-auto-update` directly produced **0**
> `[managedSettings]` lines and **0** deny events, while the app-spawned *same binary* in the same hour
> produced **823** and **257** respectively. The reason is in the scope note above: such a process is
> never sent `enableManagedSettings: true`, so it never enters the managed-settings path at all — and
> that holds **even if you authenticate it**. Reproduce through the desktop app.
1. Install the desktop app (1.1.5 or 1.1.6) with a CLI payload of **1.0.78-2 or later**.
2. Sign in with an account that has **no** enterprise managed policy. Confirm with the process log — a
clean resolution on such an account looks like this:
```
2026-08-09T15:19:40.754Z [INFO] [managedSettings] self-fetch starting for account https://github.com/
2026-08-09T15:19:40.755Z [INFO] [managedSettings] device MDM: no policy present on this device
2026-08-09T15:19:40.915Z [INFO] [managedSettings] server policy: none for this account (404/empty) from https://github.com
2026-08-09T15:19:40.916Z [INFO] [managedSettings] effective policy resolved: source=none, bypassDisabled=false, serverFetchFailed=false
2026-08-09T15:19:40.916Z [INFO] [managedSettings] applied: no bypass restriction in force (managed policy absent)
```
3. Configure 2 or more user MCP servers in `~/.copilot/mcp-config.json`. Transport does not matter —
stdio, http and local all fail together. Do not rely on the built-in GitHub server to show the
failure; it is exempt.
4. **Open several app sessions in quick succession**, or open a new session while another is still
starting. Opening a single session already issues `session.create` and then `session.resume` roughly
2 s later, so one open produces two overlapping option updates; several opens make a collision
near-certain.
5. Search the CLI process logs for the denial:
```powershell
Select-String -Path "$env:USERPROFILE\.copilot\logs\process-*.log" `
-SimpleMatch 'not permitted by enterprise managed allow list' |
Where-Object { $_.Line -match '^\d{4}-\d{2}-\d{2}T' -and $_.Line.Length -lt 300 }
```
> **The `^\d{4}-…` anchor and the length filter are both necessary.** Conversation transcripts and
> fetched page text are written into these same logs, so an unanchored search matches quoted copies of
> the message and overcounts badly. This produced false positives twice during the investigation.
**Output — the denial, with the surrounding lifecycle.** From
`~/.copilot/logs/process-1786241789611-38652.log`. Note the ordering and the millisecond deltas: the
skips land **inside** the fetch, and the fetch **succeeds**:
```
2026-08-09T02:16:45.131Z [INFO] [managedSettings] self-fetch starting for account https://github.com/
2026-08-09T02:16:45.136Z [ERROR] Skipping MCP server "ado-skype": not permitted by enterprise managed allow list
2026-08-09T02:16:45.136Z [ERROR] Skipping MCP server "ado-domoreexp": not permitted by enterprise managed allow list
2026-08-09T02:16:45.136Z [ERROR] Skipping MCP server "mcp_windbg": not permitted by enterprise managed allow list
2026-08-09T02:16:45.136Z [ERROR] Skipping MCP server "obsidian-vault-coding-brain": not permitted by enterprise managed allow list
2026-08-09T02:16:45.196Z [INFO] [managedSettings] self-fetch complete for https://github.com/: bypassDisabled=false, serverFetchFailed=false, serverCacheFallback=false, deviceLoadFailed=false
2026-08-09T02:16:45.634Z [WARNING] Failed to refresh persisted MCP tools for mcp_windbg; continuing with cached tools: Error: MCP server "mcp_windbg" is not connected
```
- deny window here = **65 ms**; the servers were denied at **+5 ms**
- `serverFetchFailed=false` — the policy fetch worked
- the MCP child processes had **already been spawned**
(`MCP server stderr {"server_name":"ado-domoreexp", ...}` at `02:16:45.079`), so the work is done and
then thrown away
6. To see the window itself and every trigger, pull the whole lifecycle:
```powershell
Select-String -Path "$env:USERPROFILE\.copilot\logs\process-*.log" -SimpleMatch '[managedSettings]'
```
**Output — a tighter variant**, in which a server is denied **1 ms after the fetch already completed**,
showing the interim policy is not cleared synchronously with completion:
```
2026-08-09T02:18:14.600Z [INFO] [managedSettings] self-fetch starting for account https://github.com/
2026-08-09T02:18:14.605Z [ERROR] Skipping MCP server "ado-skype": not permitted by enterprise managed allow list
... (ado-domoreexp, mcp_windbg, obsidian-vault-coding-brain, same millisecond)
2026-08-09T02:18:14.619Z [INFO] [managedSettings] self-fetch complete for https://github.com/: ... serverFetchFailed=false ...
2026-08-09T02:18:14.620Z [ERROR] Skipping MCP server "obsidian-vault-coding-brain": not permitted by enterprise managed allow list
```
**Output — additional trigger sources**, each of which opens its own deny window:
```
2026-08-09T02:18:18.068Z [INFO] [managedSettings] self-fetch starting for account device-only
2026-08-09T02:18:18.076Z [INFO] [managedSettings] self-fetch starting for account https://github.com/
2026-08-09T02:18:18.079Z [INFO] [managedSettings] self-fetch result for device-only discarded: account changed while the fetch was in flight
```
**Reproduction rate.** Over the surviving log-retention window (2026-08-08T22:46Z – 2026-08-09T02:18Z),
during ordinary multi-session use: **42 skip records in 9 bursts across 2 CLI processes** —
`obsidian-vault-coding-brain` ×15, `ado-skype` ×9, `ado-domoreexp` ×9, `mcp_windbg` ×9. The built-in
GitHub server was never skipped. Not every session is affected; which sessions lose servers permanently
is decided by the timing rule described in Additional context.
### Expected behavior
On an account with **no** enterprise managed policy, all servers configured in `~/.copilot/mcp-config.json`
should start and remain available for the life of the session. Concretely:
1. **No deny while the policy is merely undetermined.** "We have not resolved the policy yet" should not
be encoded as "a policy exists and it permits nothing." With `source=none`, `serverFetchFailed=false`
and `managed policy absent`, zero servers should be filtered.
2. **If a pre-emptive deny is genuinely required, it must be reversible.** A server denied during the
interim window should be restored when the window closes, exactly as a server stopped by a real
policy tightening already is. Today the two cases behave differently and only one of them recovers.
3. **Registration should wait for the policy rather than race it.** The gate that exists for this purpose
should be in force during startup registration.
4. **A resolution that has not failed should not disturb sessions that are already running.** Servers
already connected should not be torn down for a routine re-resolution in another session.
5. **If servers are withheld, say so where the user will see it.** The failure is currently silent in the
UI, and the message that does exist blames an enterprise policy that is not present on the account.
### Additional context
### Where the code is
All references are to the shipped bundle:
```
%LOCALAPPDATA%\copilot\pkg\win32-x64\1.0.79-5\app.js (9,317,686 chars, 6,135 lines)
```
The file is bundled one module per line, so the relevant lines are 100–150 KB wide (line 2345 is 147,313
chars; line 2398 is 114,363). **A line number alone will not locate anything — use line:col, or the byte
offset.** Both are given; either can be re-derived by searching for the symbol name, which is unminified.
| # | What | Symbol | line:col | byte offset |
|---|---|---|---|---|
| 1 | Producer emits the empty allow list | `composedManagedMcpHostOptions` | 2398:44941 | 2147203 |
| 1a | The literal itself | `managedAllowedMcpServerLists:[[]]` | 2398:45016 | 2147278 |
| 2 | Consumer's `length > 0` test | `managedMcpPolicyPlan` | 2345:75295 | 1893804 |
| 3 | Tighten + relax, both halves | `enforceManagedMcpPolicy` | 2345:76770 | 1895279 |
| 3a | **The only `.set()` in the bundle** | `policyRemovedConfigs.set(` | 2345:77656 | 1896165 |
| 4 | Deny path A — bulk start loop | after `managedMcpPolicyBlockReasonForBulk` | 2345:79639 | 1898148 |
| 5 | Deny path B — first check | `startServerOnce` | 2345:93949 | 1912458 |
| 6 | Deny path C — second check | `startServerOnce` | 2345:94664 | 1913173 |
| 7 | The gate that opts itself out | `expectManagedMcpPolicy` | 2398:43337 | 2145599 |
| 8 | Host-wide fan-out | `beginManagedSettingsResolution` | 2398:42842 | 2145104 |
| 9 | The trigger | `updateOptions` | 2402:67610 | 2284349 |
| 10 | Client-supplied flag default | `enableManagedSettings ?? !1` | 2402:40113 | 2256852 |
To re-derive line:col from an offset (`[regex]::Matches` over a 9 MB string hangs — use `IndexOf`):
```powershell
$s = [IO.File]::ReadAllText("$env:LOCALAPPDATA\copilot\pkg\win32-x64\1.0.79-5\app.js")
$off = $s.IndexOf('policyRemovedConfigs.set(', [StringComparison]::Ordinal)
$line = 1; $i = $s.IndexOf("`n"); $last = 0
while ($i -ge 0 -and $i -lt $off) { $line++; $last = $i + 1; $i = $s.IndexOf("`n", $i + 1) }
"line $line col $($off - $last + 1) offset $off"
```
There is a fourth `mcpHostStateAddFiltered(...)` call at 2345:93083 / offset 1911592, inside
`applyConfigFilterToServer`. **It is not part of this defect** — it is the separate config-filter path
and logs `MCP server "X" filtered: `, not the allow-list message. Noted so the count is not
mistaken for four managed-policy deny sites; there are three.
### Root cause
#### 1. The producer emits "a policy exists and it allows nothing"
```js
composedManagedMcpHostOptions() {
if (this.managedMcpInterimFailClosed)
return { managedAllowedMcpServerLists: [[]] }; // <-- one list, containing nothing
let e = this.composeManagedMcpPolicy();
return {
managedAllowedMcpServerLists: e.allowedLists.length > 0 ? e.allowedLists : void 0,
managedDeniedMcpServers: e.denied.length > 0 ? e.denied : void 0
};
}
```
Note the asymmetry with the normal path directly below it: when there is genuinely no policy the normal
path returns `void 0`, but the interim path returns `[[]]`.
#### 2. The consumer's "is a policy configured?" test cannot tell `[[]]` from a real policy
```js
async managedMcpPolicyPlan(e, n) {
let r = this.managedAllowedMcpServerLists ?? [],
o = r.length > 0, // [[]].length === 1 -> true
s = (this.managedDeniedMcpServers?.length ?? 0) > 0;
return !o && !s
? { removeNames: [], logMessages: [] }
: await h.mcpHostManagedMcpPolicyActions(/* ... */);
}
```
`[[]]` is truthy for `length > 0`, so the native policy engine runs against an empty allow list and
removes everything not exempt. The user-visible string comes from `o.logMessages[0]`, produced by the
native addon — which is why grepping `app.js` for `not permitted by enterprise managed allow list` finds
nothing.
Exemption set — why built-ins survive and every user server dies together regardless of transport:
```js
isRegisteredServerTrustedFirstParty(e) {
return this.trustedDynamicStartServerNames.has(e)
|| this.trustedDefaultServerNames.includes(e)
|| this.trustedExternalServerNames.has(e);
}
// trustedDefaultServerNames keeps only entries whose source === "builtin"
```
#### 3. The bug's teeth: the startup deny paths are not restorable
`enforceManagedMcpPolicy` contains both halves of the policy transition. On tighten it stops the server
**and stashes its config**; on relax it replays **only** what was stashed:
```js
// tighten: stop + stash
try { await this.stopServer(l); } catch (d) { /* ... */ }
this.policyRemovedConfigs.set(l, c); // <-- the ONLY .set() in the file (2345:77656)
delete this.config.mcpServers[l];
// relax: restore
for (let [l, c] of [...this.policyRemovedConfigs])
if (!s.has(l)) {
if (this.config.mcpServers[l]) { this.policyRemovedConfigs.delete(l); continue; }
try { await this.startServer(l, c); } catch (d) { /* ... */ continue; }
this.config.mcpServers[l] && (this.policyRemovedConfigs.delete(l), a = !0);
}
```
But a server denied **while starting** never reaches that stash. `startServerOnce` checks the policy
twice and both times just marks the server filtered and returns:
```js
async startServerOnce(e, n, r, o, s = !1) {
let a = this.currentServerStopVersion(e);
if (!this.validateServerConfig(e, n)) return;
let l = await this.managedMcpPolicyBlockReason(e, n, s);
if (l) { h.mcpHostStateAddFiltered(this.hostStateHandle, e); this.logger.log(l); return; } // 2345:93949
/* ... gate plan, config filtering ... */
let g = await this.managedMcpPolicyBlockReason(e, d, s);
if (g || this.currentServerStopVersion(e) !== a) {
g && (h.mcpHostStateAddFiltered(this.hostStateHandle, e), this.logger.log(g)); return; // 2345:94664
}
this.config.mcpServers[e] = d; // only reached when NOT blocked
/* ... connect ... */
}
```
and the bulk path additionally **deletes the config outright**:
```js
let d = await this.managedMcpPolicyBlockReasonForBulk(l, c);
if (d) {
this.logger.log(d);
h.mcpHostStateAddFiltered(this.hostStateHandle, l);
delete this.config.mcpServers[l]; // <-- dropped, never stashed for restore (2345:79639)
return;
}
```
**There is exactly one `policyRemovedConfigs.set(...)` in the bundle, and none of the three
managed-policy deny paths is it.** Therefore:
| when the window opens | outcome |
|---|---|
| server **already registered** | stopped, stashed, **restored automatically** when the policy clears |
| server **registering right now** | filtered + config dropped, **never restored** — dead for the life of the session |
A fresh session registers its MCP servers at session start, which is exactly when the window opens, so
the common case is the non-restorable one. This single asymmetry is why the failure looks
non-deterministic, and why "it fixed itself after a minute" and "it was dead for the whole session" are
both accurate reports of the same defect.
Servers in the second row can still come back if a later full MCP client rebuild happens to re-register
them from the original config — the teardown/rebuild described in #4392. That is incidental rather than
the restore path, and it can be denied again: I measured 4 consecutive bursts on the same 4 servers
within 90 s.
#### 4. What opens the window, and why it opens so often
```js
beginManagedSettingsResolution() {
if (this.lastManagedSettingsLayers) {
this.managedSettingsResolutionInFlight = !0;
this.managedSettingsPolicyGeneration++;
for (let n of this.activeSessions.values())
n.beginManagedSettingsResolution({ permissions: !1 }); // fan-out to EVERY live session
}
}
```
`{permissions:false}` skips the permissions half, but the session-level handler still always calls
`beginInterimManagedMcpFailClosed()`. A session that joins while a resolution is in flight is
immediately fail-closed too:
```js
applyCurrentManagedSettingsPolicy(e, n, r) {
/* ... */ this.managedSettingsResolutionInFlight
? e.beginManagedSettingsResolution({ permissions: !1 })
: /* ... */
}
```
There is a gate intended to make MCP registration wait for the policy, but it opts itself out whenever
the host already exists, so registration proceeds against the interim `[[]]`:
```js
expectManagedMcpPolicy() {
if (this.managedMcpPolicyApplied || this.mcpHost || this.managedMcpPolicyGate) return;
/* ...create gate... */
}
// consumer:
this.managedMcpPolicyGate && await this.managedMcpPolicyGate.promise; // no gate -> awaits nothing
```
### The 1.0.78-2 trigger-guard change
```js
// 1.0.73 — narrow: authInfo present AND identity defined AND identity changed; session-local effect
if (e.authInfo !== void 0 && this.enableManagedSettings) {
let l = YX(this.authInfo);
l !== void 0 && l !== this.managedSettingsFetchIdentity && (
this.managedSettingsFetchIdentity = l,
this.beginInterimManagedMcpFailClosed(), // this session only
/* ... */);
}
// 1.0.78-2 and later — broad: key merely present, undefined identity no longer suppresses,
// plus a generation check; effect is now host-wide
if (this.enableManagedSettings && ("authInfo" in e || e.enableManagedSettings === !0)) {
let c = ene(this.authInfo);
(c !== this.managedSettingsFetchIdentity ||
this.managedSettingsAuthGeneration !== this.managedSettingsFetchGeneration) && (
this.managedSettingsFetchIdentity = c,
this.managedSettingsFetchGeneration = this.managedSettingsAuthGeneration,
this.beginManagedSettingsResolution(), // fan-out to ALL sessions
/* ... */);
}
```
Three widenings at once: `e.authInfo !== void 0` became `"authInfo" in e`; the `l !== void 0` guard was
dropped; and a generation disjunct was added. `managedSettingsAuthGeneration++` (new in 1.0.78-2, fired
on `auth_info_updated`) means each auth refresh arms the next options update to re-trigger the cycle.
The **hourly** `refreshManagedSettings()` timer does *not* call `beginManagedSettingsResolution()` — only
`updateOptions` does. So the window is tied to session churn, not to the timer.
### Suggested fixes (any one breaks the chain)
1. **Do not use `[[]]` as a sentinel.** Represent "policy undetermined" as a distinct flag, or make the
consumer test `r.some(list => list.length > 0)` instead of `r.length > 0`.
2. **Make the deny paths restorable** — have the `startServerOnce` and bulk deny paths record the config
into `policyRemovedConfigs` (or an equivalent) so the "policy relaxed" loop can replay them. This is
the smallest change that removes the permanent-loss case.
3. **Fix the gate** — drop `|| this.mcpHost` from `expectManagedMcpPolicy()` so registration waits for
the resolution instead of racing it.
4. **Do not fan out to already-running sessions**, or at minimum do not tear down servers that are
already connected for a resolution that has not failed.
### Workaround (verified)
Pin the CLI to **1.0.71**, which does not contain the mechanism at all:
```powershell
[Environment]::SetEnvironmentVariable(
'COPILOT_CLI_PATH',
"$env:LOCALAPPDATA\github-copilot-sdk\cli\1.0.71\copilot.exe",
'User')
# then fully exit and restart the app
```
Confirmed applied — note that the reported version string is unreliable here; the **path** is ground truth:
```
2026-08-09T05:01:55Z Detected Copilot CLI version at startup version=GitHub Copilot CLI 1.0.79-9
source=Some(Local) path="...\github-copilot-sdk\cli\1.0.71\copilot.exe"
bundled_version="1.0.79-5"
```
All 8 running `copilot.exe` processes load their payload from `pkg\win32-x64\1.0.71\`, and there have
been **zero** deny events in the ~11 h since, across continued multi-session use.
### What we could not determine
- **Whether the interim state is ever legitimately needed.** If the intent is to fail closed only when
the policy genuinely cannot be determined, then the `serverFetchFailed=false` / `managed policy absent`
case observed here should never have entered it at all.
- **Exact first occurrence.** `process-*.log` files rotate; the oldest surviving one starts
2026-08-08T22:24Z. Earlier deny events were observed during this investigation but those files are now
gone. The version matrix is the reliable dating, not the logs.
- **Why the perceived onset was 2026-08-08** when 1.0.78-2 shipped on 08-04. Best explanation: the
fan-out only bites when sessions overlap, and concurrent multi-session usage began on 08-08. **Not
proven** — offered as a hypothesis, not a finding.
### Related issues
- **#4378** — *"…registry policy fetch fails with 401/403 on GHEC data residency, silently blocking all
user-configured MCP servers"* — same symptom, different cause (fetch **fails**). Here the fetch
**succeeds** and there is no policy, so this is a distinct defect that #4378's fix would not cover.
- **#4349** — *"Managed settings policy fetch fails closed on valid enum value…"* — another fail-closed
input-handling variant. Also fetch-side.
- **#4392** — post-auth MCP client teardown/rebuild. Relevant twice over: it is the incidental mechanism
behind the observed silent recoveries, and its author also read the allow-list message as a real
corporate policy.
**Please do not close this as a duplicate of the fetch-failure family.** The discriminator is one field
in the resolved line: every filed variant has `serverFetchFailed=true`; this one has
`serverFetchFailed=false` with `source=none` / `managed policy absent`. Relatedly, the **v1.0.78 release
note** advertises a fail-closed → fail-open change, but that covers the *fetch-failure* path only. This
variant is untouched by it and in fact reproduces on 1.0.79-5, which postdates it.
A third-party report quoting `Skipping MCP server "umacs-build-bridge"` dated 2026-08-03 matches the
1.0.72/1.0.73 era, consistent with the mechanism landing in 1.0.72.
Contributor guide
Research direction
Start by tracing managed-settings handling in the shipped app.js, especially enableManagedSettings, managedAllowedMcpServerLists, and the restore paths; compare the client payload wiring in copilot-sdk/index.js. Use the process logs and ~/.copilot/mcp-config.json reproduction to verify the lifecycle. Done means user MCP servers remain available when policy resolution reports source=none, with appropriate coverage for the interim window and restoration behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100