Skip to content

Commit e220ca3

Browse files
joshspicerCopilot
andcommitted
agentHost: bridge managed permission settings
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0858530 commit e220ca3

23 files changed

Lines changed: 1001 additions & 122 deletions

src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts

Lines changed: 105 additions & 41 deletions
Large diffs are not rendered by default.

src/vs/platform/agentHost/browser/remoteAgentHostServiceImpl.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,16 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo
358358
this._onDidChangeConnections.fire();
359359
}
360360
}));
361+
store.add(protocolClient.onDidChangeConnectionState(state => {
362+
if (this._entries.get(address) !== connEntry || state !== AgentHostClientState.Incompatible) {
363+
return;
364+
}
365+
connEntry.connected = false;
366+
connEntry.status = protocolClient.connectionError
367+
? RemoteAgentHostConnectionStatus.fromConnectError(protocolClient.connectionError, [PROTOCOL_VERSION]) ?? RemoteAgentHostConnectionStatus.disconnected
368+
: RemoteAgentHostConnectionStatus.disconnected;
369+
this._onDidChangeConnections.fire();
370+
}));
361371

362372
const config = getEntryTypeConfig(entry.connection.type);
363373
if (config.store !== 'runtime') {
@@ -568,8 +578,15 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo
568578
entry.status = RemoteAgentHostConnectionStatus.connected;
569579
this._onDidChangeConnections.fire();
570580
break;
571-
case AgentHostClientState.Connecting:
572581
case AgentHostClientState.Incompatible:
582+
entry.connected = false;
583+
entry.status = client.connectionError
584+
? RemoteAgentHostConnectionStatus.fromConnectError(client.connectionError, [PROTOCOL_VERSION]) ?? RemoteAgentHostConnectionStatus.disconnected
585+
: RemoteAgentHostConnectionStatus.disconnected;
586+
this._reconnectAttempts.delete(address);
587+
this._onDidChangeConnections.fire();
588+
break;
589+
case AgentHostClientState.Connecting:
573590
case AgentHostClientState.Closed:
574591
break;
575592
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { IConfigurationService, IConfigurationValue } from '../../configuration/common/configuration.js';
7+
import { GLOBAL_AUTO_APPROVE_SETTING_ID, IManagedPermissions, MANAGED_PERMISSION_TERMINAL_ASK_RULE, TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID } from './agentHostSchema.js';
8+
9+
export interface IManagedPermissionsSettingMapping {
10+
readonly settingId: string;
11+
readonly transform: (value: unknown) => IManagedPermissions | undefined;
12+
}
13+
14+
export function createManagedPermissionsSettingMapping<T>(settingId: string, transform: (value: T | undefined) => IManagedPermissions | undefined): IManagedPermissionsSettingMapping {
15+
return { settingId, transform: value => transform(value as T | undefined) };
16+
}
17+
18+
export const managedPermissionsSettingMappings: readonly IManagedPermissionsSettingMapping[] = [
19+
createManagedPermissionsSettingMapping<boolean>(GLOBAL_AUTO_APPROVE_SETTING_ID, value => value === false ? { disableBypassPermissionsMode: 'disable' } : undefined),
20+
createManagedPermissionsSettingMapping<boolean>(TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID, value => value === false ? { ask: [MANAGED_PERMISSION_TERMINAL_ASK_RULE] } : undefined),
21+
];
22+
23+
function getExplicitValue<T>(inspection: IConfigurationValue<T>): T | undefined {
24+
const hasConfiguredValue = inspection.policyValue !== undefined
25+
|| inspection.memoryValue !== undefined
26+
|| inspection.workspaceFolderValue !== undefined
27+
|| inspection.workspaceValue !== undefined
28+
|| inspection.userRemoteValue !== undefined
29+
|| inspection.userLocalValue !== undefined
30+
|| inspection.userValue !== undefined
31+
|| inspection.applicationValue !== undefined;
32+
return hasConfiguredValue ? inspection.value : undefined;
33+
}
34+
35+
export function resolveManagedPermissions(configurationService: IConfigurationService): IManagedPermissions | undefined {
36+
let disableBypassPermissionsMode: 'disable' | undefined;
37+
let askForShell = false;
38+
for (const entry of managedPermissionsSettingMappings) {
39+
const contribution = entry.transform(getExplicitValue(configurationService.inspect(entry.settingId)));
40+
disableBypassPermissionsMode ??= contribution?.disableBypassPermissionsMode;
41+
askForShell ||= contribution?.ask?.includes(MANAGED_PERMISSION_TERMINAL_ASK_RULE) === true;
42+
}
43+
return disableBypassPermissionsMode || askForShell ? {
44+
...(disableBypassPermissionsMode ? { disableBypassPermissionsMode } : {}),
45+
...(askForShell ? { ask: [MANAGED_PERMISSION_TERMINAL_ASK_RULE] as const } : {}),
46+
} : undefined;
47+
}

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,52 @@ const permissionsProperty = schemaProperty<IPermissionsValue>({
297297
sessionMutable: true,
298298
});
299299

300+
/** Managed runtime restrictions synthesized from effective VS Code settings. */
301+
export interface IManagedPermissions {
302+
readonly disableBypassPermissionsMode?: 'disable';
303+
/** Canonical all-shell prompt rule; active runtime rule policy defaults other governed kinds to ask. */
304+
readonly ask?: readonly ['Shell'];
305+
}
306+
307+
export const MANAGED_PERMISSION_TERMINAL_ASK_RULE = 'Shell';
308+
309+
/**
310+
* Treat the empty object used as the merge-safe root-config clear sentinel as
311+
* no managed policy.
312+
*/
313+
export function normalizeManagedPermissions(permissions: IManagedPermissions | undefined): IManagedPermissions | undefined {
314+
const disableBypassPermissionsMode = permissions?.disableBypassPermissionsMode === 'disable';
315+
const askForShell = permissions?.ask?.includes(MANAGED_PERMISSION_TERMINAL_ASK_RULE) === true;
316+
return disableBypassPermissionsMode || askForShell ? {
317+
...(disableBypassPermissionsMode ? { disableBypassPermissionsMode: 'disable' as const } : {}),
318+
...(askForShell ? { ask: [MANAGED_PERMISSION_TERMINAL_ASK_RULE] as const } : {}),
319+
} : undefined;
320+
}
321+
322+
const managedPermissionsProperty = schemaProperty<IManagedPermissions>({
323+
type: 'object',
324+
title: localize('agentHost.config.managedPermissions.title', "Managed Permissions"),
325+
description: localize('agentHost.config.managedPermissions.description', "Permission restrictions derived from effective VS Code settings and forwarded to the runtime as `managedSettings.permissions` at session startup."),
326+
properties: {
327+
disableBypassPermissionsMode: {
328+
type: 'string',
329+
title: localize('agentHost.config.managedPermissions.disableBypass', "Disable bypass permissions mode"),
330+
enum: ['disable'],
331+
},
332+
ask: {
333+
type: 'array',
334+
title: localize('agentHost.config.managedPermissions.ask', "Required permission prompts"),
335+
items: {
336+
type: 'string',
337+
title: localize('agentHost.config.managedPermissions.rule', "Permission rule"),
338+
enum: [MANAGED_PERMISSION_TERMINAL_ASK_RULE],
339+
},
340+
},
341+
},
342+
// No default: `{}` is the wire-level clear sentinel and is normalized to
343+
// `undefined` before SDK launch, so `managedSettings` is omitted.
344+
});
345+
300346
/**
301347
* Session-config properties owned by the platform itself — i.e. consumed
302348
* by the agent host rather than by any particular agent.
@@ -433,6 +479,24 @@ export const TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID = 'chat.tools.terminal.ena
433479
*/
434480
export const AgentHostGlobalAutoApproveEnabledConfigKey = 'globalAutoApproveEnabled';
435481

482+
/**
483+
* The VS Code setting ID for global auto approve. Defined here so renderer-side
484+
* agent-host clients can forward it without importing from `workbench/contrib/chat`.
485+
*/
486+
export const GLOBAL_AUTO_APPROVE_SETTING_ID = 'chat.tools.global.autoApprove';
487+
488+
/**
489+
* Root config key forwarded from the renderer holding the {@link IManagedPermissions}
490+
* object. Synthesized by VS Code from the effective values of
491+
* `chat.tools.global.autoApprove` and
492+
* `chat.tools.terminal.enableAutoApprove`, and forwarded to the runtime as
493+
* `managedSettings.permissions` at SDK session startup. Absent when no policy applies.
494+
*/
495+
export const AgentHostManagedPermissionsConfigKey = 'managedPermissions';
496+
497+
/** Marker written to diagnostic logs instead of enterprise-managed permission rules. */
498+
export const AgentHostManagedPermissionsLogRedaction = '<redacted>';
499+
436500
/**
437501
* Root config key forwarded from the renderer when VS Code's `chat.autoReply`
438502
* setting changes. When `true`, the agent host auto-answers `ask_user`
@@ -714,6 +778,7 @@ export const platformRootSchema = createSchema({
714778
description: localize('agentHost.config.globalAutoApproveEnabled.description', "Whether VS Code's global auto-approve setting is enabled. When `true`, every tool call is auto-approved, equivalent to a session using Allow all."),
715779
default: false,
716780
}),
781+
[AgentHostManagedPermissionsConfigKey]: managedPermissionsProperty,
717782
[AgentHostAutoReplyEnabledConfigKey]: schemaProperty<boolean>({
718783
type: 'boolean',
719784
title: localize('agentHost.config.autoReplyEnabled.title', "Auto Reply"),

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2125,6 +2125,9 @@ export interface IAgentService {
21252125
*/
21262126
dispatchAction(channel: string, action: SessionAction | ChatAction | TerminalAction | ClientChangesetAction | ClientAnnotationsAction | IRootConfigChangedAction, clientId: string, clientSeq: number, clientContext?: IAgentHostClientTelemetryContext): void;
21272127

2128+
/** Remove the enterprise-managed permission contribution owned by a disconnected client. */
2129+
removeClientManagedPermissions(clientId: string): void;
2130+
21282131
/**
21292132
* List the contents of a directory on the agent host's filesystem.
21302133
* Used by the client to drive a remote folder picker before session creation.

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { joinPath } from '../../../base/common/resources.js';
1010
import { isUriComponents, URI, UriComponents } from '../../../base/common/uri.js';
1111
import { IFileService, IFileStatWithMetadata } from '../../files/common/files.js';
1212
import { ILogService } from '../../log/common/log.js';
13+
import { AgentHostManagedPermissionsConfigKey, AgentHostManagedPermissionsLogRedaction } from './agentHostSchema.js';
1314

1415
export type AhpLogDirection = 'c2s' | 's2c';
1516

@@ -240,7 +241,10 @@ function stringifyAhpLogEntryTruncated(value: unknown, maxStringLength: number):
240241
* {@link URI.revive}. This avoids the expensive deep-clone tree walk that
241242
* would otherwise be required to find every URI in a message payload.
242243
*/
243-
function _ahpReplacer(this: unknown, _key: string, value: unknown): unknown {
244+
function _ahpReplacer(this: unknown, key: string, value: unknown): unknown {
245+
if (key === AgentHostManagedPermissionsConfigKey) {
246+
return AgentHostManagedPermissionsLogRedaction;
247+
}
244248
if (
245249
value
246250
&& typeof value === 'object'

0 commit comments

Comments
 (0)