Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/skills/policy-and-managed-settings/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ General rules:
- New Copilot enterprise controls should target the shared managed-settings/SDK model.
- The VS Code settings-to-managed-settings bridge is a compatibility path for legacy
settings only. Do not add a new VS Code setting in order to bridge it; define new
runtime-owned controls directly in the managed-settings/SDK contract. A temporary,
false-by-default compatibility gate for the bridge itself is allowed; it is not a
runtime control and must not become a template for new mapped settings.
runtime-owned controls directly in the managed-settings/SDK contract. The bridge itself
is unconditional; do not reintroduce a compatibility gate for it.
- Run `npm run export-policy-data` for every VS Code or extension policy change. Never
edit `build/lib/policies/policyData.jsonc` manually.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ There are two related but distinct bounded migrations:

Neither path is open to newly designed settings or controls. Put new runtime-owned
controls directly in the shared managed-settings schema and SDK contract.
The legacy-setting bridge may have a false-by-default experimental gate for staged
adoption; that gate controls the compatibility mechanism itself and is not a mapped
runtime control.
The legacy-setting bridge is unconditional; its former staged-adoption gate has been
retired now that the bridge is the default enforcement path.

## Legacy permission policy rules

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public SDK contract, without introducing or translating a VS Code setting.

Bridge invariants:

- the bridge is guarded by its own false-by-default experimental compatibility setting;
- the bridge is unconditional; it is not guarded by an opt-in setting, and the former `chat.agentHost.copilot.mapLegacySettingsToManagedSettings` gate has been removed from the settings registry;
- add mappings only for legacy settings that already exist; never create a new setting
for this bridge;
- mappings select one VS Code setting and use a callback typed against the host-owned managed
Expand Down
14 changes: 2 additions & 12 deletions src/vs/platform/agentHost/common/agentHostManagedSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ export interface IAgentHostManagedSettingsPermissions {
ask?: string[];
}

export const AgentHostMapLegacySettingsToManagedSettingsSettingId = 'chat.agentHost.copilot.mapLegacySettingsToManagedSettings';

/**
* Which configuration layers may drive a mapping.
*
Expand Down Expand Up @@ -276,7 +274,6 @@ const managedPermissionsSettings: readonly IManagedPermissionsSettingMapping[] =
];

export const managedPermissionsConfigurationIds = [
AgentHostMapLegacySettingsToManagedSettingsSettingId,
...managedPermissionsSettings.flatMap(mapping => [mapping.settingId, ...mapping.additionalSettingIds ?? []]),
];

Expand All @@ -301,17 +298,10 @@ function isStringArrayOrUndefined(value: unknown): boolean {
* Combines every mapping's contribution into the single document sent to the
* host, deduplicating rules that more than one setting produced.
*
* Contributing any rule at all makes the runtime's managed policy "active",
* which causes unmatched shell, read, write, URL and factory requests to require
* approval. That is broader than any individual mapping intends, but it errs
* toward prompting, and the alternative — an `allow` list — resolves to
* auto-approval. See the module comment.
* Client-injected managed permissions are non-activating, so these rules bind
* without forcing unmatched requests to prompt. See the module comment.
*/
export function resolveManagedSettingsPermissions(configurationService: IConfigurationService): IAgentHostManagedSettingsPermissions {
if (getGlobalConfigurationValue<boolean>(configurationService, AgentHostMapLegacySettingsToManagedSettingsSettingId) !== true) {
return {};
}

const deny = new Set<string>();
const ask = new Set<string>();
let disableBypassPermissionsMode: 'disable' | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import assert from 'assert';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
import type { IConfigurationService, IConfigurationValue } from '../../../configuration/common/configuration.js';
import { AgentHostMapLegacySettingsToManagedSettingsSettingId, resolveManagedSettingsPermissions } from '../../common/agentHostManagedSettings.js';
import { resolveManagedSettingsPermissions } from '../../common/agentHostManagedSettings.js';
import { AgentNetworkDomainSettingId } from '../../../networkFilter/common/settings.js';
import { ELIGIBLE_FOR_AUTO_APPROVAL_SETTING_ID, GLOBAL_AUTO_APPROVE_SETTING_ID, TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID, TERMINAL_AUTO_APPROVE_SETTING_ID } from '../../common/agentHostSchema.js';

Expand All @@ -22,7 +22,6 @@ suite('AgentHostManagedSettings', () => {

test('combines restrictive contributions from explicitly configured global values', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[GLOBAL_AUTO_APPROVE_SETTING_ID]: { defaultValue: false, policyValue: false },
[TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID]: { defaultValue: true, userValue: false },
});
Expand All @@ -35,7 +34,6 @@ suite('AgentHostManagedSettings', () => {

test('respects global precedence and ignores defaults and workspace values', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[GLOBAL_AUTO_APPROVE_SETTING_ID]: { defaultValue: false, userValue: false, policyValue: true },
[TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID]: { defaultValue: false, workspaceValue: false, workspaceFolderValue: false },
});
Expand All @@ -45,11 +43,9 @@ suite('AgentHostManagedSettings', () => {

test('does not promote user or application preferences to managed bypass restrictions', () => {
const userConfigurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[GLOBAL_AUTO_APPROVE_SETTING_ID]: { defaultValue: false, userValue: false },
});
const applicationConfigurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[GLOBAL_AUTO_APPROVE_SETTING_ID]: { defaultValue: false, applicationValue: false },
});

Expand All @@ -59,27 +55,50 @@ suite('AgentHostManagedSettings', () => {
], [{}, {}]);
});

test('does not map legacy settings while the compatibility bridge is disabled', () => {
test('maps legacy settings without any opt-in present', () => {
Comment thread
joshspicer marked this conversation as resolved.
Comment thread
joshspicer marked this conversation as resolved.
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false },
[GLOBAL_AUTO_APPROVE_SETTING_ID]: { defaultValue: false, userValue: false },
[TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID]: { defaultValue: true, userValue: false },
});

assert.deepStrictEqual(resolveManagedSettingsPermissions(configurationService), {});
assert.deepStrictEqual(resolveManagedSettingsPermissions(configurationService), {
ask: ['Shell'],
});
});

test('returns an empty contribution after explicit restrictions are removed', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, applicationValue: true },
});
test('ignores a stale entry for the removed opt-in, whatever its value', () => {
// The gating setting was removed; a leftover settings.json entry must not
// switch off a restriction an administrator configured.
const removedOptIn = 'chat.agentHost.copilot.mapLegacySettingsToManagedSettings';
const restricted = {
[AgentNetworkDomainSettingId.NetworkFilter]: { defaultValue: false, policyValue: true },
[AgentNetworkDomainSettingId.AllowedNetworkDomains]: { defaultValue: [] },
[AgentNetworkDomainSettingId.DeniedNetworkDomains]: { defaultValue: [], policyValue: ['evil.example'] },
};

assert.deepStrictEqual([
resolveManagedSettingsPermissions(createConfigurationService({
...restricted,
[removedOptIn]: { userValue: false },
})),
resolveManagedSettingsPermissions(createConfigurationService({
...restricted,
[removedOptIn]: { userValue: true },
})),
], [
{ deny: ['Domain(evil.example)'] },
{ deny: ['Domain(evil.example)'] },
]);
});

test('returns an empty contribution when no legacy setting is restricted', () => {
const configurationService = createConfigurationService({});

assert.deepStrictEqual(resolveManagedSettingsPermissions(configurationService), {});
});

test('deduplicates a rule that more than one entry produces', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[AgentNetworkDomainSettingId.NetworkFilter]: { defaultValue: false, policyValue: true },
[AgentNetworkDomainSettingId.AllowedNetworkDomains]: { defaultValue: [] },
// Three spellings of the same host, which all normalize to one rule.
Expand All @@ -96,7 +115,6 @@ suite('AgentHostManagedSettings', () => {

test('reduces denied domains to the host the network filter matches on', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[AgentNetworkDomainSettingId.NetworkFilter]: { defaultValue: false, policyValue: true },
[AgentNetworkDomainSettingId.AllowedNetworkDomains]: { defaultValue: [] },
[AgentNetworkDomainSettingId.DeniedNetworkDomains]: {
Expand All @@ -112,7 +130,6 @@ suite('AgentHostManagedSettings', () => {

test('denies configured domains while the network filter is on', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[AgentNetworkDomainSettingId.NetworkFilter]: { defaultValue: false, policyValue: true },
[AgentNetworkDomainSettingId.DeniedNetworkDomains]: { defaultValue: [], policyValue: ['evil.com', '*.tracker.example'] },
[AgentNetworkDomainSettingId.AllowedNetworkDomains]: { defaultValue: [], policyValue: ['github.com'] },
Expand All @@ -125,7 +142,6 @@ suite('AgentHostManagedSettings', () => {

test('denies every domain when the filter is on and neither list is configured', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[AgentNetworkDomainSettingId.NetworkFilter]: { defaultValue: false, policyValue: true },
[AgentNetworkDomainSettingId.DeniedNetworkDomains]: { defaultValue: [] },
[AgentNetworkDomainSettingId.AllowedNetworkDomains]: { defaultValue: [] },
Expand All @@ -136,7 +152,6 @@ suite('AgentHostManagedSettings', () => {

test('contributes nothing from domain lists while the network filter is off', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[AgentNetworkDomainSettingId.NetworkFilter]: { defaultValue: false },
[AgentNetworkDomainSettingId.DeniedNetworkDomains]: { defaultValue: [], policyValue: ['evil.com'] },
});
Expand All @@ -146,7 +161,6 @@ suite('AgentHostManagedSettings', () => {

test('skips denied domain patterns the SDK cannot express', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[AgentNetworkDomainSettingId.NetworkFilter]: { defaultValue: false, policyValue: true },
[AgentNetworkDomainSettingId.DeniedNetworkDomains]: { defaultValue: [], policyValue: ['$(evil)', 'ok.example'] },
[AgentNetworkDomainSettingId.AllowedNetworkDomains]: { defaultValue: [] },
Expand All @@ -159,7 +173,6 @@ suite('AgentHostManagedSettings', () => {

test('maps a bare wildcard denial onto the all-domains family rule', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[AgentNetworkDomainSettingId.NetworkFilter]: { defaultValue: false, policyValue: true },
[AgentNetworkDomainSettingId.DeniedNetworkDomains]: { defaultValue: [], policyValue: ['*'] },
[AgentNetworkDomainSettingId.AllowedNetworkDomains]: { defaultValue: [] },
Expand All @@ -170,7 +183,6 @@ suite('AgentHostManagedSettings', () => {

test('requires approval for explicitly denied terminal commands', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[TERMINAL_AUTO_APPROVE_SETTING_ID]: {
defaultValue: {},
policyValue: { rm: false, 'git push': false, npm: true },
Expand All @@ -184,7 +196,6 @@ suite('AgentHostManagedSettings', () => {

test('skips terminal denials the SDK shell grammar cannot express', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[TERMINAL_AUTO_APPROVE_SETTING_ID]: {
defaultValue: {},
policyValue: {
Expand All @@ -202,7 +213,6 @@ suite('AgentHostManagedSettings', () => {

test('keeps an absolute command path that VS Code treats as a literal', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
// Starts and ends with `/` but the trailing segment is not a flag list,
// so the auto-approver reads it as a path rather than a regular expression.
[TERMINAL_AUTO_APPROVE_SETTING_ID]: { defaultValue: {}, policyValue: { '/usr/bin/rm': false } },
Expand All @@ -215,7 +225,6 @@ suite('AgentHostManagedSettings', () => {

test('skips a wildcard command key rather than broadening it', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
// `*` is a literal in VS Code but a command-boundary wildcard in the SDK,
// so bridging this would require approval for every git command.
[TERMINAL_AUTO_APPROVE_SETTING_ID]: { defaultValue: {}, policyValue: { 'git *': false, 'rm': false } },
Expand All @@ -228,7 +237,6 @@ suite('AgentHostManagedSettings', () => {

test('treats a long-form sub-command denial like a bare false', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[TERMINAL_AUTO_APPROVE_SETTING_ID]: {
defaultValue: {},
policyValue: { rm: { approve: false }, ls: { approve: true } },
Expand All @@ -242,7 +250,6 @@ suite('AgentHostManagedSettings', () => {

test('locks the bypass mode when a tool is marked ineligible for auto-approval', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[ELIGIBLE_FOR_AUTO_APPROVAL_SETTING_ID]: { defaultValue: {}, policyValue: { fetch: false } },
});

Expand All @@ -253,7 +260,6 @@ suite('AgentHostManagedSettings', () => {

test('contributes nothing when every tool is left eligible for auto-approval', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
// Only `true` entries: the policy re-affirms the default and removes nothing.
[ELIGIBLE_FOR_AUTO_APPROVAL_SETTING_ID]: { defaultValue: {}, policyValue: { fetch: true, runTask: true } },
});
Expand All @@ -263,7 +269,6 @@ suite('AgentHostManagedSettings', () => {

test('contributes nothing from the default empty per-tool auto-approval map', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[ELIGIBLE_FOR_AUTO_APPROVAL_SETTING_ID]: { defaultValue: {}, policyValue: {} },
});

Expand All @@ -272,15 +277,12 @@ suite('AgentHostManagedSettings', () => {

test('does not promote user, application, or workspace per-tool auto-approval values', () => {
const userConfigurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[ELIGIBLE_FOR_AUTO_APPROVAL_SETTING_ID]: { defaultValue: {}, userValue: { fetch: false } },
});
const applicationConfigurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[ELIGIBLE_FOR_AUTO_APPROVAL_SETTING_ID]: { defaultValue: {}, applicationValue: { fetch: false } },
});
const workspaceConfigurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[ELIGIBLE_FOR_AUTO_APPROVAL_SETTING_ID]: { defaultValue: {}, workspaceValue: { fetch: false } },
});

Expand All @@ -293,7 +295,6 @@ suite('AgentHostManagedSettings', () => {

test('clears the bypass lock after the per-tool auto-approval policy is removed', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[ELIGIBLE_FOR_AUTO_APPROVAL_SETTING_ID]: { defaultValue: {} },
});

Expand All @@ -302,7 +303,6 @@ suite('AgentHostManagedSettings', () => {

test('ignores a malformed per-tool auto-approval value without throwing', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[ELIGIBLE_FOR_AUTO_APPROVAL_SETTING_ID]: { defaultValue: {}, policyValue: ['fetch'] },
});

Expand All @@ -311,7 +311,6 @@ suite('AgentHostManagedSettings', () => {

test('fails closed and locks the bypass mode for a non-boolean per-tool auto-approval entry', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[ELIGIBLE_FOR_AUTO_APPROVAL_SETTING_ID]: { defaultValue: {}, policyValue: { fetch: 'no' } },
});

Expand All @@ -322,7 +321,6 @@ suite('AgentHostManagedSettings', () => {

test('does not duplicate the bypass lock across the global and per-tool auto-approval policies', () => {
const configurationService = createConfigurationService({
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
[GLOBAL_AUTO_APPROVE_SETTING_ID]: { defaultValue: false, policyValue: false },
[ELIGIBLE_FOR_AUTO_APPROVAL_SETTING_ID]: { defaultValue: {}, policyValue: { fetch: false } },
});
Expand Down
Loading
Loading