mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-07 16:14:56 +01:00
* policy: add managed-settings freshness contract
Groundwork for making `forceRemoteSettingsRefresh` a real fail-closed
startup gate (microsoft/vscode-internalbacklog#8825). Contract only — no
behavior change, and nothing gates on freshness yet.
Adds `managedSettingsFreshness.ts`, declaring the state machine shared by
the fetch path, the policy gate and Policy Diagnostics so those consumers
cannot drift: `NotRequired` / `Pending` / `Satisfied` / `Blocked`, the
failure categories every inability-to-refresh maps to, and scoping by
account + provider + endpoint so satisfaction is never transferable
across accounts or GHE hosts.
Replaces `shouldForceRemoteSettingsRefresh` with
`resolveForceRemoteSettingsRefresh`, which resolves through
`pickManagedSettings` instead of re-implementing precedence. Two fixes
fall out: the file channel now participates (the old helper read only
native MDM and server, silently ignoring managed-file delivery), and an
explicit managed `false` is now distinguishable from an absent value,
which a later change needs in order to know when the requirement may be
cleared.
The old helper had no production caller — it was left orphaned when
661f18fdeb reworked the managed-settings fetch — so this is inert.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* policy: enforce freshness invariants in the type
Address PR feedback: `IManagedSettingsFreshness` was a bag of optional
fields, so a consumer could construct `Blocked` with no failure,
`Satisfied` with no scope, or attach `httpStatus`/`retryAfter` to states
where they mean nothing — leaving the fetch, gate and diagnostics
consumers free to drift despite the type.
Models it as a discriminated union instead, so each active state requires
the fields its contract defines. `Blocked` is itself a union keyed on the
failure category, so a status code is required for an HTTP error, a
backoff deadline for rate limiting, and neither is accepted elsewhere.
`source` is now the shared `ManagedSettingsChannel` rather than `string`,
and is required on the effective states, which also encodes that it is
never `'none'` once a channel has supplied the control.
Adds `@ts-expect-error` coverage for the three rejected shapes: the
directives fail the build if any shape becomes constructible again.
`isSameManagedSettingsFreshnessScope` is now a private helper with
required arguments — the union guarantees a scope is present, so its
undefined-tolerance was unreachable, and nothing outside this module
used it.
Also trims two over-long comments flagged in review.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chat: fail closed on forced managed settings refresh
Require a fresh managed-settings response before enabling AI features when forceRemoteSettingsRefresh is effective. Preserve recovery through sign-in and retry, expose diagnostics, and cover native, server, file, failure, scope, and sign-out behavior.
Related to microsoft/vscode-internalbacklog#8825.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chat: tighten managed settings recovery UX
Re-render the Agents window when freshness failure details change and preserve startup notification deferral.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chat: address managed settings review feedback
Scope cached server controls before precedence, avoid expired rate-limit poll loops, and align update-required recovery guidance across workbench and Agents window UI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: await explicit managed settings recovery refresh
Classic web initialization intentionally skips the default-account fetch. Exercise the explicit refresh path before asserting the no-token fail-closed state so the browser suite observes the same lifecycle it is validating.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: provide product name in policy overlay fixture
Ensure managed-settings messages render Code - OSS instead of an undefined product label in component screenshots.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: add managed settings failure modes
Let the mock policy server return HTTP errors, malformed JSON, immediate disconnects, or no response until client timeout through presets, the GUI, and the control API.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chat: tighten forced managed settings recovery
Improve forced-refresh progress and blocked-state UX, bound automatic retries after failures, preserve the ungoverned cache path, and simplify mock policy failure controls.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chat: close managed settings dialog on retry
Start the explicit managed-settings refresh without making the dialog wait for the network request to complete.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chat: separate managed settings requirement copy
Place the organization requirement and fetch failure remediation in separate paragraphs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chat: address managed settings review feedback
Preserve cached and blocked freshness state, report failed manual syncs, retain pending mock-server edits, and include attempted scope in diagnostics.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chat: reduce managed settings freshness implementation
Trim redundant contract commentary and tests, simplify refresh resolution, deduplicate failure transitions, and keep no-flag tests independent from retry bypass behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
312 lines
9.8 KiB
TypeScript
312 lines
9.8 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
/**
|
|
* Shared definition of the Copilot "policy" endpoints that
|
|
* `DefaultAccountService` (src/vs/workbench/services/accounts/browser/defaultAccount.ts)
|
|
* calls, together with sample response bodies. This is the single source of
|
|
* truth shared by the Node server (route table + default state) and the browser
|
|
* GUI (endpoint tabs + preset dropdown), exported UMD-style so it loads in both
|
|
* environments without a build step.
|
|
*
|
|
* For the default (github.com) provider these URLs are read verbatim from
|
|
* `product.json` -> `defaultChatAgent.<productKey>`, so pointing all of them at
|
|
* a local server via `product.overrides.json` lets a dev exercise the whole
|
|
* policy pipeline offline. The same paths are also served under a system proxy
|
|
* rule, which is how a stable/Insiders build or the CLI reaches this server.
|
|
*
|
|
* Endpoints not marked `mockedByDefault` start in passthrough: the server
|
|
* forwards them to the real API so a blanket proxy rule stays safe.
|
|
*
|
|
* NOTE: The server uses `module.stripTypeScriptTypes()` to serve this file to
|
|
* the browser as plain JavaScript — no build step is needed.
|
|
*/
|
|
|
|
export interface EndpointPreset {
|
|
id: string;
|
|
label: string;
|
|
description: string;
|
|
status?: number;
|
|
body: unknown;
|
|
}
|
|
|
|
export type EndpointResponseMode = 'json' | 'malformed-json' | 'disconnect' | 'timeout';
|
|
|
|
export interface EndpointDef {
|
|
/** Stable id used by the API + GUI. */
|
|
id: string;
|
|
/** Human label for the GUI tab. */
|
|
label: string;
|
|
/** URL path the server serves / Code OSS calls. */
|
|
path: string;
|
|
/** Key under product.json `defaultChatAgent`. */
|
|
productKey: string;
|
|
/** One-line summary for the GUI. */
|
|
description: string;
|
|
/**
|
|
* Whether this endpoint is mocked when the server starts. Everything else
|
|
* is proxied to the real API, so a blanket proxy rule stays safe: only the
|
|
* endpoints you deliberately turn on get faked.
|
|
*/
|
|
mockedByDefault?: boolean;
|
|
/** Validate 2xx bodies against the managed-settings JSON schema. */
|
|
schema?: boolean;
|
|
/** First preset is used as the default body. */
|
|
presets: EndpointPreset[];
|
|
}
|
|
|
|
/* eslint-disable-next-line no-var -- UMD global for browser <script> context */
|
|
declare var MOCK_POLICY_ENDPOINTS: EndpointDef[];
|
|
|
|
(function (root: Record<string, unknown> | undefined, factory: () => EndpointDef[]) {
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory();
|
|
} else if (root) {
|
|
root.MOCK_POLICY_ENDPOINTS = factory();
|
|
}
|
|
})(typeof self !== 'undefined' ? self as unknown as Record<string, unknown> : undefined, function (): EndpointDef[] {
|
|
|
|
const endpoints: EndpointDef[] = [
|
|
{
|
|
id: 'managedSettings',
|
|
label: 'Managed Settings',
|
|
path: '/copilot_internal/managed_settings',
|
|
productKey: 'managedSettingsUrl',
|
|
description: 'Enterprise copilot settings from .github/copilot/settings.json. An empty object means no policy file is present.',
|
|
mockedByDefault: true,
|
|
schema: true,
|
|
presets: [
|
|
{
|
|
id: 'empty',
|
|
label: 'Empty (no policy file)',
|
|
description: 'An empty object is a successful "no enterprise policy file present" response.',
|
|
status: 200,
|
|
body: {}
|
|
},
|
|
{
|
|
id: 'disable-bypass-permissions',
|
|
label: 'Disable bypass permissions',
|
|
description: 'Blocks all escalation to bypass-permissions ("allow-all"/"yolo") mode, including auto-approval.',
|
|
status: 200,
|
|
body: {
|
|
permissions: {
|
|
disableBypassPermissionsMode: 'disable'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 'allow-auto-only',
|
|
label: 'Allow auto-approval only',
|
|
description: 'Blocks full allow-all bypass but still permits advisory auto-approval (LLM safety recommendations with normal prompt paths).',
|
|
status: 200,
|
|
body: {
|
|
permissions: {
|
|
disableBypassPermissionsMode: 'allow-auto-only'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 'deny-dangerous-commands',
|
|
label: 'Deny dangerous shell/file operations',
|
|
description: 'Blocks specific shell commands, workspace-scoped file writes, and a domain outright. A single leading slash means the workspace root in the managed permission syntax.',
|
|
status: 200,
|
|
body: {
|
|
permissions: {
|
|
deny: [
|
|
'Shell(rm -rf *)',
|
|
'Shell(curl *)',
|
|
'Write(/.github/workflows/**)',
|
|
'Domain(evil.example.com)'
|
|
]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 'workspace-scoped-paths',
|
|
label: 'Workspace-scoped paths',
|
|
description: 'Demonstrates paths relative to the workspace root: /src/** and /test/** match only inside the workspace, while /package.json targets that workspace file.',
|
|
status: 200,
|
|
body: {
|
|
permissions: {
|
|
ask: [
|
|
'Write(/src/**)',
|
|
'Write(/test/**)'
|
|
],
|
|
deny: [
|
|
'Write(/package.json)'
|
|
]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 'ask-before-publish',
|
|
label: 'Ask before publishing or deploying',
|
|
description: 'Requires human approval for package publish/deploy commands and writes anywhere under the user home directory, including workspaces located there. It does not cover paths outside the home directory.',
|
|
status: 200,
|
|
body: {
|
|
permissions: {
|
|
ask: [
|
|
'Shell(npm publish *)',
|
|
'Shell(git push *)',
|
|
'Write(~/**)'
|
|
]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 'lockdown-allowlist',
|
|
label: 'Lockdown: allow only an approved set',
|
|
description: 'Intersects with any other managed allow list, so only requests every managed source admits run without prompting. Combine with deny/ask for defense in depth.',
|
|
status: 200,
|
|
body: {
|
|
permissions: {
|
|
disableBypassPermissionsMode: 'disable',
|
|
allow: [
|
|
'Read(**)',
|
|
'Shell(git status)',
|
|
'Shell(git diff *)',
|
|
'Domain(github.com)',
|
|
'Domain(*.githubusercontent.com)'
|
|
],
|
|
deny: [
|
|
'Write(/.github/workflows/**)',
|
|
'Write(~/.ssh/**)'
|
|
]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 'model-auto',
|
|
label: 'Model: auto',
|
|
description: 'Sets the managed model to auto.',
|
|
status: 200,
|
|
body: {
|
|
model: 'auto'
|
|
}
|
|
},
|
|
{
|
|
id: 'extra-known-marketplaces',
|
|
label: 'Extra known marketplaces',
|
|
description: 'Adds marketplaces with managed auto-update settings.',
|
|
status: 200,
|
|
body: {
|
|
extraKnownMarketplaces: {
|
|
'vscode-team-kit': {
|
|
source: {
|
|
source: 'github',
|
|
repo: 'microsoft/vscode-team-kit'
|
|
},
|
|
autoUpdate: true
|
|
},
|
|
'awesome-copilot': {
|
|
source: {
|
|
source: 'github',
|
|
repo: 'github/awesome-copilot',
|
|
ref: 'marketplace'
|
|
},
|
|
autoUpdate: false
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 'customization-lockdown',
|
|
label: 'Customization lockdown',
|
|
description: 'Allows only managed plugins, MCP servers, and hooks, and forces a remote settings refresh.',
|
|
status: 200,
|
|
body: {
|
|
strictPluginOnlyCustomization: true,
|
|
allowManagedMcpServersOnly: true,
|
|
allowManagedHooksOnly: true,
|
|
forceRemoteSettingsRefresh: true
|
|
}
|
|
},
|
|
{
|
|
id: 'not-configured',
|
|
label: 'Not configured (404)',
|
|
description: 'No server-managed policy is configured.',
|
|
status: 404,
|
|
body: {}
|
|
},
|
|
{
|
|
id: 'update-required',
|
|
label: 'Client update required (466)',
|
|
description: 'Rejects the client because it cannot enforce the effective managed settings.',
|
|
status: 466,
|
|
body: {
|
|
error_code: 'client_update_required',
|
|
client_id: 'vscode',
|
|
client_version: '1.132.0',
|
|
minimum_client_version: '1.133.0'
|
|
}
|
|
},
|
|
{
|
|
id: 'server-error',
|
|
label: 'Server error (500)',
|
|
description: 'Returns an HTTP 500 response to exercise the fail-closed HTTP error path.',
|
|
status: 500,
|
|
body: { error: 'mock_managed_settings_failure' }
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'entitlements',
|
|
label: 'Entitlements',
|
|
path: '/copilot_internal/user',
|
|
productKey: 'entitlementUrl',
|
|
description: 'Gates the entire flow — token and managed settings are only fetched when chat_enabled is true.',
|
|
presets: [
|
|
{
|
|
id: 'enterprise-enabled',
|
|
label: 'Enterprise, chat enabled',
|
|
description: 'Chat enabled with cloud session storage; the common dev case.',
|
|
body: {
|
|
access_type_sku: 'copilot_enterprise_seat',
|
|
chat_enabled: true,
|
|
assigned_date: '2024-01-01T00:00:00Z',
|
|
can_signup_for_limited: false,
|
|
copilot_plan: 'enterprise',
|
|
organization_login_list: ['contoso'],
|
|
analytics_tracking_id: 'dev-analytics-id',
|
|
cloud_session_storage_enabled: true
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'token',
|
|
label: 'Token',
|
|
path: '/copilot_internal/v2/token',
|
|
productKey: 'tokenEntitlementUrl',
|
|
description: 'Token string carries policy flags as key=value pairs separated by semicolons. Flags: agent_mode, editor_preview_features, mcp, sn, fcv1.',
|
|
presets: [
|
|
{
|
|
id: 'all-enabled',
|
|
label: 'All features enabled',
|
|
description: 'agent_mode=1, editor_preview_features=1, mcp=1.',
|
|
body: { token: 'agent_mode=1;editor_preview_features=1;mcp=1;sn=dev;fcv1=dev:devsignature' }
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'mcpRegistry',
|
|
label: 'MCP Registry',
|
|
path: '/copilot/mcp_registry',
|
|
productKey: 'mcpRegistryDataUrl',
|
|
description: 'Only fetched when the token has mcp=1. Returns the enterprise MCP registry URL and access level.',
|
|
presets: [
|
|
{
|
|
id: 'registry-only',
|
|
label: 'Registry only',
|
|
description: 'Restrict MCP servers to the enterprise registry.',
|
|
body: { mcp_registries: [{ url: 'https://mcp.contoso.example/registry', registry_access: 'registry_only' }] }
|
|
}
|
|
]
|
|
}
|
|
];
|
|
|
|
return endpoints;
|
|
});
|