Skip to content

policy: gate Copilot on a fresh managed-settings fetch - #331878

Closed
joshspicer wants to merge 1 commit into
mainfrom
agents/vscode-internalbacklog-issue-8825-planning
Closed

policy: gate Copilot on a fresh managed-settings fetch#331878
joshspicer wants to merge 1 commit into
mainfrom
agents/vscode-internalbacklog-issue-8825-planning

Conversation

@joshspicer

@joshspicer joshspicer commented Aug 20, 2026

Copy link
Copy Markdown
Member

Makes forceRemoteSettingsRefresh a genuine fail-closed startup gate: when an enterprise sets it, VS Code fetches managed settings fresh before enabling Copilot agent functionality, and blocks rather than falling back to cached policy when that fetch fails.

Tracking: microsoft/vscode-internalbacklog#8825

Note

This replaces the two previous commits on this branch (a contract-only managedSettingsFreshness.ts state machine). That approach designed a general contract before it had a consumer; this is the behaviour itself, built on the pattern already in the codebase.

Approach

VS Code already has a working fail-closed gate of exactly this shape — the HTTP 466 "client update required" path. This copies it rather than inventing a parallel mechanism:

466 gate This gate
managedSettingsCompatibilityError on IDefaultAccountService managedSettingsRefreshBlocked
onDidChangeManagedSettingsCompatibilityError onDidChangeManagedSettingsRefreshBlocked
consumed in AccountPolicyGateContribution.updatePolicyGateState() same method, same context key, same setForceHidden

Sign-in stays reachable throughout, so a user can always recover.

shouldForceRemoteSettingsRefresh() (dead code since 661f18f — it had no production caller) becomes resolveForceRemoteSettingsRefresh(), which additionally consults the file channel and reports the winning channel. It returns undefined when no channel sets the key, so an explicit managed false is distinguishable from an absent value.

Behaviour worth reviewing

A fresh 404 satisfies the requirement. It means "no policy file configured". Blocking on it would permanently lock out every org that has no policy file.

The requirement outlives the policy cache. The cache is honoured for one hour and a failed fetch returns managedSettings: undefined — dropping the restrictions and the flag itself. A naive sticky gate would silently reopen after an hour offline, so the requirement is persisted separately under defaultAccount.managedSettingsRefreshRequired.

Scope follows the delivery channel. The server delivers the control per organization, so it is recorded per account — one account's response must never speak for another's. Native MDM and the on-disk file are machine-wide and read live on every evaluation, so precedence is applied when the requirement is read (which is what lets a native MDM false override a server true). A machine-wide local hint covers only the startup window in which those channels are still loading.

Because the local channels are read live rather than behind an authenticated fetch, a signed-out machine carrying an MDM- or file-delivered flag is gated on first launch — one of the gaps called out during planning.

Migration. The flag already lives in users' cached server bags today, so a requirement recorded by a previous build is recovered from there on first run; without it, every flagged org would get one ungated offline launch on upgrade.

Retries are throttled (60s), because a refresh fires on every window focus and an offline client would otherwise issue a request per alt-tab. Skipping a retry inside the window never reopens the gate.

Behaviour change to confirm ⚠️

The managed-settings fetch is now restricted to the evaluated account's sessions. request() otherwise falls through to the next session on 401/404, which is a cross-account fallback: another organization could answer for your account, and its settings were merged and cached under your account.

This fixes a permanent-lockout path (MDM sets the flag → server 404s for account A → account B always answers first → A's satisfying 404 is discarded and A is gated forever). But it is not bit-identical for unflagged users: in a multi-account setup with an expired primary token, VS Code today fetches managed settings with a different account's token and applies that org's policy to you. I judged "more correct" beats "bit-identical" here, but it deserves an explicit ack.

Inertness

The feature is inert when the flag isn't set — that's essentially all users — and this is asserted, not assumed:

  • no fetch is blocked when the refresh flag is not set anywhere — an unflagged org is never gated, even with the endpoint down
  • a failed fetch never escalates from another account cached settings — the policy cache is never consulted for gate decisions, so an unflagged user cannot be blocked by a polluted cache

Scope

Not included, deliberately:

  • No HTTP header work. The request already sends cache-control: no-cache; the transport is fetch and disableCache: true is already passed.
  • No schema validation. The Copilot runtime owns the managed-settings schema; duplicating it here is forbidden by .github/skills/policy-and-managed-settings.
  • No telemetry or diagnostics until the behaviour itself is settled.
  • Renderer-side gating only. Remote/SSH/tunnel Agent Hosts are not gated: the managed-settings channel to them intentionally does not exist (remoteAgentHostProtocolClient.ts sends {} to non-local hosts).

Validation

  • 28 tests in defaultAccount.test.ts and 2 in accountPolicyGateContribution.test.ts, covering first launch, cached flag, MDM/file-delivered flag, explicit managed false, successful refresh, each failure class, account switching, migration and retry throttling
  • 178 passing across the affected policy/accounts/agentHost suites
  • npm run typecheck-client, npm run valid-layers-check, eslint — all clean

Tests were run directly under mocha: this environment can't build the native modules the Electron harness needs (npm install fails on an authenticated NuGet feed with a 401, unrelated to this change), so please let CI confirm.

This change went through seven rounds of adversarial review, which surfaced eleven fail-open paths in a gate whose only job is to fail closed — several only becoming visible once an earlier fix was in place. The tests named above encode each of them.

Copilot AI balanced review requested due to automatic review settings August 20, 2026 22:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the shared contract for fail-closed managed-settings freshness without enabling enforcement.

Changes:

  • Defines freshness states, failure categories, and account/provider/endpoint scoping.
  • Resolves refresh policy across native MDM, server, and file channels.
  • Adds unit coverage for resolution and freshness helpers.
Show a summary per file
File Description
managedSettingsFreshness.ts Defines the freshness contract and helpers.
copilotManagedSettings.ts Adds channel-aware refresh-control resolution.
managedSettingsFreshness.test.ts Tests freshness states and scope matching.
copilotManagedSettings.test.ts Tests channel precedence and malformed values.

Review details

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 4/4 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread src/vs/platform/policy/common/managedSettingsFreshness.ts Outdated
Comment thread src/vs/platform/policy/test/common/managedSettingsFreshness.test.ts Outdated
Comment thread src/vs/platform/policy/common/copilotManagedSettings.ts Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid PR. Tests cover the important paths.

When an enterprise sets `forceRemoteSettingsRefresh`, VS Code must fetch
managed settings fresh before enabling Copilot agent functionality, and
block rather than fall back to cached policy when that fetch fails.

Mirrors the existing HTTP 466 "client update required" fail-closed path:
`DefaultAccountProvider` publishes `managedSettingsRefreshBlocked`, and
`AccountPolicyGateContribution` ORs it into the same context key and
`setForceHidden` call the 466 gate already drives. Sign-in stays
reachable so a user can recover.

Notable behaviours:

- A fresh 404 means "no policy file configured" and satisfies the
  requirement. Blocking on it would lock out every org without a policy
  file.
- The requirement is persisted outside the policy cache, which expires
  hourly and is dropped on a failed fetch — otherwise the gate would
  silently reopen after an hour offline.
- The server channel is recorded per account, since the server delivers
  the control per organization. Native MDM and the on-disk file are
  machine-wide and read live, so precedence is applied at read time and
  a signed-out machine carrying a local flag is still gated.
- The managed-settings fetch is restricted to the evaluated account's
  sessions. `request` otherwise falls through to the next session on
  401/404, letting another organization answer for — and have its
  settings cached under — this account.
- Cache-bypassing retries are throttled, as a refresh fires on every
  window focus.

Inert when the flag is unset, which is covered by tests.

Refs microsoft/vscode-internalbacklog#8825

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@joshspicer
joshspicer force-pushed the agents/vscode-internalbacklog-issue-8825-planning branch from 3995c58 to 8bfeec4 Compare August 24, 2026 18:04
@joshspicer joshspicer changed the title policy: add managed-settings freshness contract policy: gate Copilot on a fresh managed-settings fetch Aug 24, 2026
@joshspicer joshspicer closed this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants