Skip to content

Commit 8d85ef1

Browse files
joshspicerCopilot
andcommitted
agentHost: graduate the legacy-settings managed-permissions bridge
The bridge from legacy VS Code settings to the Copilot SDK's per-session managedSettings.permissions shipped behind the experimental opt-in chat.agentHost.copilot.mapLegacySettingsToManagedSettings because the runtime treated any managed rule from any source as activating the global managed policy, so one narrow restriction forced unmatched shell/read/write/URL/factory requests to prompt. github/copilot-agent-runtime#16249 makes client/session-injected managed permissions non-activating, which removes that broadening. The bundled runtime VS Code spawns contains the fix, so the bridge becomes the default enforcement path. The setting is deprecated and its value is now ignored rather than honored as an opt-out: the mappings only ever contribute administrator-configured restrictions, so honoring an explicit false would let a user switch off an enterprise policy. Refs #332011 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6834b15 commit 8d85ef1

3 files changed

Lines changed: 44 additions & 35 deletions

File tree

src/vs/platform/agentHost/common/agentHostManagedSettings.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ export interface IAgentHostManagedSettingsPermissions {
5353
ask?: string[];
5454
}
5555

56+
/**
57+
* Deprecated opt-in that used to gate this bridge. The mapping is now the
58+
* default enforcement path, so the configured value is ignored: honoring an
59+
* explicit `false` would let a user switch off restrictions an administrator
60+
* configured through the legacy settings this bridge reads.
61+
*/
5662
export const AgentHostMapLegacySettingsToManagedSettingsSettingId = 'chat.agentHost.copilot.mapLegacySettingsToManagedSettings';
5763

5864
/**
@@ -241,7 +247,6 @@ const managedPermissionsSettings: readonly IManagedPermissionsSettingMapping[] =
241247
];
242248

243249
export const managedPermissionsConfigurationIds = [
244-
AgentHostMapLegacySettingsToManagedSettingsSettingId,
245250
...managedPermissionsSettings.flatMap(mapping => [mapping.settingId, ...mapping.additionalSettingIds ?? []]),
246251
];
247252

@@ -266,17 +271,15 @@ function isStringArrayOrUndefined(value: unknown): boolean {
266271
* Combines every mapping's contribution into the single document sent to the
267272
* host, deduplicating rules that more than one setting produced.
268273
*
269-
* Contributing any rule at all makes the runtime's managed policy "active",
270-
* which causes unmatched shell, read, write, URL and factory requests to require
271-
* approval. That is broader than any individual mapping intends, but it errs
272-
* toward prompting, and the alternative — an `allow` list — resolves to
273-
* auto-approval. See the module comment.
274+
* The rules contributed here bind without broadening anything else: the runtime
275+
* treats client-injected managed permissions as non-activating, so a lone `deny`
276+
* or `ask` rule is enforced while unmatched shell, read, write, URL and factory
277+
* requests keep falling through to the host's normal approval flow. External
278+
* server and MDM layers still activate the blanket "unmatched requests must be
279+
* approved" lockdown, and a managed `allow` list remains something this bridge
280+
* must not contribute — see the module comment.
274281
*/
275282
export function resolveManagedSettingsPermissions(configurationService: IConfigurationService): IAgentHostManagedSettingsPermissions {
276-
if (getGlobalConfigurationValue<boolean>(configurationService, AgentHostMapLegacySettingsToManagedSettingsSettingId) !== true) {
277-
return {};
278-
}
279-
280283
const deny = new Set<string>();
281284
const ask = new Set<string>();
282285
let disableBypassPermissionsMode: 'disable' | undefined;

src/vs/platform/agentHost/test/common/agentHostManagedSettings.test.ts

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ suite('AgentHostManagedSettings', () => {
2222

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

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

4644
test('does not promote user or application preferences to managed bypass restrictions', () => {
4745
const userConfigurationService = createConfigurationService({
48-
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
4946
[GLOBAL_AUTO_APPROVE_SETTING_ID]: { defaultValue: false, userValue: false },
5047
});
5148
const applicationConfigurationService = createConfigurationService({
52-
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
5349
[GLOBAL_AUTO_APPROVE_SETTING_ID]: { defaultValue: false, applicationValue: false },
5450
});
5551

@@ -59,27 +55,47 @@ suite('AgentHostManagedSettings', () => {
5955
], [{}, {}]);
6056
});
6157

62-
test('does not map legacy settings while the compatibility bridge is disabled', () => {
58+
test('maps legacy settings without any opt-in present', () => {
6359
const configurationService = createConfigurationService({
64-
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false },
6560
[GLOBAL_AUTO_APPROVE_SETTING_ID]: { defaultValue: false, userValue: false },
6661
[TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID]: { defaultValue: true, userValue: false },
6762
});
6863

69-
assert.deepStrictEqual(resolveManagedSettingsPermissions(configurationService), {});
64+
assert.deepStrictEqual(resolveManagedSettingsPermissions(configurationService), {
65+
ask: ['Shell'],
66+
});
7067
});
7168

72-
test('returns an empty contribution after explicit restrictions are removed', () => {
73-
const configurationService = createConfigurationService({
74-
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, applicationValue: true },
75-
});
69+
test('ignores the deprecated opt-in, so it cannot switch off a mapped restriction', () => {
70+
const restricted = {
71+
[AgentNetworkDomainSettingId.NetworkFilter]: { defaultValue: false, policyValue: true },
72+
[AgentNetworkDomainSettingId.AllowedNetworkDomains]: { defaultValue: [] },
73+
[AgentNetworkDomainSettingId.DeniedNetworkDomains]: { defaultValue: [], policyValue: ['evil.example'] },
74+
};
75+
76+
assert.deepStrictEqual([
77+
resolveManagedSettingsPermissions(createConfigurationService({
78+
...restricted,
79+
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: true, userValue: false },
80+
})),
81+
resolveManagedSettingsPermissions(createConfigurationService({
82+
...restricted,
83+
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: true, userValue: true },
84+
})),
85+
], [
86+
{ deny: ['Domain(evil.example)'] },
87+
{ deny: ['Domain(evil.example)'] },
88+
]);
89+
});
90+
91+
test('returns an empty contribution when no legacy setting is restricted', () => {
92+
const configurationService = createConfigurationService({});
7693

7794
assert.deepStrictEqual(resolveManagedSettingsPermissions(configurationService), {});
7895
});
7996

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

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

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

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

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

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

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

171181
test('requires approval for explicitly denied terminal commands', () => {
172182
const configurationService = createConfigurationService({
173-
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
174183
[TERMINAL_AUTO_APPROVE_SETTING_ID]: {
175184
defaultValue: {},
176185
policyValue: { rm: false, 'git push': false, npm: true },
@@ -184,7 +193,6 @@ suite('AgentHostManagedSettings', () => {
184193

185194
test('skips terminal denials the SDK shell grammar cannot express', () => {
186195
const configurationService = createConfigurationService({
187-
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
188196
[TERMINAL_AUTO_APPROVE_SETTING_ID]: {
189197
defaultValue: {},
190198
policyValue: {
@@ -202,7 +210,6 @@ suite('AgentHostManagedSettings', () => {
202210

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

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

229235
test('treats a long-form sub-command denial like a bare false', () => {
230236
const configurationService = createConfigurationService({
231-
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: { defaultValue: false, userValue: true },
232237
[TERMINAL_AUTO_APPROVE_SETTING_ID]: {
233238
defaultValue: {},
234239
policyValue: { rm: { approve: false }, ls: { approve: true } },

src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,9 +1614,10 @@ configurationRegistry.registerConfiguration({
16141614
[AgentHostMapLegacySettingsToManagedSettingsSettingId]: {
16151615
type: 'boolean',
16161616
markdownDescription: nls.localize('chat.agentHost.copilot.mapLegacySettingsToManagedSettings', "When enabled, maps supported legacy VS Code settings to equivalent Copilot SDK managed settings for local Agent Host sessions. Only restrictions are mapped, and only from globally-scoped values — workspace and folder values are ignored. Applies to local sessions using the Copilot agent; remote hosts and other agents are unaffected. This compatibility bridge is temporary and is not used for new settings."),
1617-
default: false,
1617+
markdownDeprecationMessage: nls.localize('chat.agentHost.copilot.mapLegacySettingsToManagedSettings.deprecated', "This setting is no longer used. Supported legacy settings are always mapped to Copilot SDK managed settings, so this value is ignored."),
1618+
default: true,
16181619
scope: ConfigurationScope.APPLICATION_MACHINE,
1619-
tags: ['experimental', 'advanced'],
1620+
tags: ['advanced'],
16201621
},
16211622
[AgentHostOpus48PromptEnabledSettingId]: {
16221623
type: 'boolean',

0 commit comments

Comments
 (0)