src/vs/platform/agentHost/node/agentConfigurationService.ts

379 LOC · 366 covered · 13 uncovered · 73 ranges · 3039 concepts · 34 introducers · 1442 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- agentConfigurationService.ts ×16
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 * as fs from 'fs';
7 > import { Emitter, Event } from '../../../base/common/event.js';
8 > import { Disposable } from '../../../base/common/lifecycle.js';
9 > import { dirname } from '../../../base/common/path.js';
10 > import { hasKey } from '../../../base/common/types.js';
11 > import { URI } from '../../../base/common/uri.js';
12 > import { createDecorator } from '../../instantiation/common/instantiation.js';
13 > import { ILogService } from '../../log/common/log.js';
14 > import { AgentHostConfigKey, agentHostCustomizationConfigSchema, defaultAgentHostCustomizationConfigValues } from '../common/agentHostCustomizationConfig.js';
15 > import { getAgentCustomizationSettingsEntries, getProviderBackedRootConfigKeys, withAgentCustomizationSettings, type IAgentCustomizationSettingsRegistration } from '../common/agentCustomizationSettings.js';
16 > import { copilotCliConfigSchema } from '../common/copilotCliConfig.js';
17 > import { sandboxConfigSchema } from '../common/sandboxConfigSchema.js';
18 > import type { ISchema, SchemaDefinition, SchemaValue } from '../common/agentHostSchema.js';
19 > import { ProtocolError } from '../common/state/sessionProtocol.js';
20 > import { ActionType } from '../common/state/sessionActions.js';
21 > import { parseSubagentSessionUri, ROOT_STATE_URI, type URI as ProtocolURI } from '../common/state/sessionState.js';
22 > import { AgentSession } from '../common/agentService.js';
23 > import { AgentHostStateManager } from './agentHostStateManager.js';
24 > import type { WorktreeIsolation } from './shared/worktreeIsolation.js';
25 >
26 > export const IAgentConfigurationService = createDecorator<IAgentConfigurationService>('agentConfigurationService');
27 >
28 > export interface IAgentSessionConfigurationChangeEvent {
29 > readonly session: ProtocolURI;
30 > readonly config: Record<string, unknown>;
31 > }
32 >
33 > /**
34 > * Cohesive read/write surface for agent-host configuration.
35 > *
36 > * All platform-layer consumers (tool auto-approval, side effects, future
37 > * host-config editors) should read and mutate config values through this
38 > * service rather than reaching into raw session state. The service owns
39 > * the `session → parent session → host` inheritance chain so that
40 > * host-level defaults, subagent inheritance, and per-session overrides
41 > * compose the same way everywhere.
42 > *
43 > * Reads go through a caller-supplied {@link ISchema}: each raw value is
44 > * validated against the property's schema before being returned, so a
45 > * malformed value in one layer transparently falls back to the next.
46 > */
47 > export interface IAgentConfigurationService {
48 > readonly _serviceBrand: undefined;
49 >
50 > /**
51 > * Fires whenever a {@link ActionType.RootConfigChanged} action is
52 > * processed by the state manager, signalling that callers should
53 > * re-read any root config values they depend on.
54 > */
55 > readonly onDidRootConfigChange: Event<void>;
56 >
57 > /** Fires whenever a session configuration change is processed. */
58 > readonly onDidSessionConfigChange: Event<IAgentSessionConfigurationChangeEvent>;
59 >
60 > /**
61 > * Returns the effective value of `key` for `session`, walking the
62 > * `session → parent session → host` chain and returning the first
63 > * layer that provides a value which validates against
64 > * `schema.definition[key]`. Layers that provide a malformed value
65 > * are logged and skipped. Returns `undefined` when no layer provides
66 > * a valid value.
67 > */
68 > getEffectiveValue<D extends SchemaDefinition, K extends keyof D & string>(
69 > session: ProtocolURI,
70 > schema: ISchema<D>,
71 > key: K,
72 > ): SchemaValue<D[K]> | undefined;
73 >
74 > /**
75 > * Returns the effective working directory for a session, falling back
76 > * to the parent (subagent) session's working directory when the
77 > * session itself does not have one set. The host layer does not carry
78 > * a working directory.
79 > */
80 > getEffectiveWorkingDirectory(session: ProtocolURI): string | undefined;
81 >
82 > /**
83 > * Whether a fresh worktree-isolation session's worktree has not yet been
84 > * created. Agents consult this to defer prewarming (and any other eager
85 > * materialization) until the host resolves the worktree on the first send.
86 > */
87 > isWorkingDirectoryPending(session: ProtocolURI): boolean;
88 >
89 > /** Resolves a persisted working directory, repairing a removed worktree when possible. */
90 > resolveWorkingDirectoryForResume(session: ProtocolURI, workingDirectory: URI): Promise<URI>;
91 >
92 > /**
93 > * Merges a partial config patch into a session's values via a
94 > * {@link ActionType.SessionConfigChanged} action. Keys not present in
95 > * `patch` are left untouched. The patch is applied atomically through
96 > * the state manager's reducer.
97 > */
98 > updateSessionConfig(session: ProtocolURI, patch: Record<string, unknown>): void;
99 >
100 > /**
101 > * Returns the merged config values currently stored on `session`.
102 > *
103 > * Reflects the live state managed by the reducer: every
104 > * {@link ActionType.SessionConfigChanged} action mutates these values
105 > * before this method returns. Callers materializing a provisional session
106 > * use this to read the user's latest selections without subscribing to
107 > * the action stream themselves.
108 > */
109 > getSessionConfigValues(session: ProtocolURI): Record<string, unknown> | undefined;
110 >
111 > /**
112 > * Returns the host-level value for `key`, validating it against
113 > * `schema.definition[key]`. Invalid persisted values are logged and treated
114 > * as missing.
115 > */
116 > getRootValue<D extends SchemaDefinition, K extends keyof D & string>(
117 > schema: ISchema<D>,
118 > key: K,
119 > ): SchemaValue<D[K]> | undefined;
120 >
121 > /**
122 > * Merges a partial config patch into the host-level value bag and persists
123 > * the updated values for future agent-host lifetimes.
124 > */
125 > updateRootConfig(patch: Record<string, unknown>, replace?: boolean): void;
126 >
127 > /**
128 > * Persists the current host-level value bag without mutating it.
129 > */
130 > persistRootConfig(): void;
131 >
132 > /**
133 > * Resolves once any in-flight root-config write has settled.
134 > */
135 > whenIdle(): Promise<void>;
136 >
137 > registerProviderConfiguration?(registration: IAgentCustomizationSettingsRegistration): void;
138 > getRootConfigValues?(): Readonly<Record<string, unknown>>;
139 > }
140 >
141 > export class AgentConfigurationService extends Disposable implements IAgentConfigurationService {
142 > declare readonly _serviceBrand: undefined;
143 > private _rootConfigWrite = Promise.resolve();
144 >
145 > private readonly _onDidRootConfigChange = this._register(new Emitter<void>());
146 > readonly onDidRootConfigChange: Event<void> = this._onDidRootConfigChange.event;
147 > private readonly _onDidSessionConfigChange = this._register(new Emitter<IAgentSessionConfigurationChangeEvent>());
148 > readonly onDidSessionConfigChange: Event<IAgentSessionConfigurationChangeEvent> = this._onDidSessionConfigChange.event;
149 >
150 > /**
151 > * Host-owned worktree isolation controller. Injected after construction (via
152 > * {@link setWorktreeIsolation}) because it only becomes available once the
153 > * branch-name generator has been wired, which happens after this service is
154 > * built. Consulted by {@link isWorkingDirectoryPending}, which degrades to
155 > * folder behavior while it is unset (tests, early startup).
156 > */
157 > private _worktree: WorktreeIsolation | undefined;
158 >
159 > setWorktreeIsolation(worktree: WorktreeIsolation): void {
160 > this._worktree = worktree;
161 > }
162 >
163 > constructor(
164 > private readonly _stateManager: AgentHostStateManager, agentConfigurationService.ts ×5
165 > @ILogService private readonly _logService: ILogService,
166 > private readonly _rootConfigResource?: URI,
167 > providerConfigurations: readonly IAgentCustomizationSettingsRegistration[] = [],
168 > ) {
169 > super();
170 > // Merge our customization schema/values into the existing root config
171 > // (which already carries platform properties like permissions) rather
172 > // than replacing it.
173 > const existing = this._stateManager.rootState.config;
174 > const ownSchema = agentHostCustomizationConfigSchema.toProtocol();
175 > const sandboxSchema = sandboxConfigSchema.toProtocol();
176 > const copilotCliSchema = copilotCliConfigSchema.toProtocol();
177 > this._stateManager.rootState.config = {
178 > schema: {
179 > type: 'object',
180 > properties: { ...existing?.schema.properties, ...ownSchema.properties, ...sandboxSchema.properties, ...copilotCliSchema.properties },
181 > },
182 > values: { ...existing?.values, ...this._loadPersistedRootConfig() },
183 > };
184 > for (const registration of providerConfigurations) {
185 > this.registerProviderConfiguration(registration); agentConfigurationService.ts ×1
186 > }
188 > this._register(this._stateManager.onDidEmitEnvelope(envelope => {
189 > if (envelope.action.type === ActionType.RootConfigChanged) { agentConfigurationService.ts ×2
190 > this._onDidRootConfigChange.fire(); agentConfigurationService.ts ×1
191 > } else if (envelope.action.type === ActionType.SessionConfigChanged) { agentConfigurationService.ts ×2
192 > this._onDidSessionConfigChange.fire({ reducer.ts ×2
193 > session: envelope.channel,
194 > config: envelope.action.config,
195 > });
196 > }
198 > }
200 > getEffectiveValue<D extends SchemaDefinition, K extends keyof D & string>(
201 > session: ProtocolURI, agentConfigurationService.ts ×6
202 > schema: ISchema<D>,
203 > key: K,
204 > ): SchemaValue<D[K]> | undefined {
205 > for (const values of this._effectiveChain(session)) {
206 > const raw = values[key];
207 > if (raw === undefined) {
209 > }
211 > schema.assertValid(key, raw);
212 > return raw;
213 > } catch (err) {
214 > const reason = err instanceof ProtocolError ? err.message : String(err); agentConfigurationService.ts ×1
215 > this._logService.warn(`[AgentConfigurationService] Value for '${key}' on ${session} failed schema validation, falling back: ${reason}`);
216 > }
218 > return undefined; agentConfigurationService.ts ×2
221 > getEffectiveWorkingDirectory(session: ProtocolURI): string | undefined {
222 > const own = this._stateManager.getSessionState(session)?.workingDirectories?.[0]; agentConfigurationService.ts ×2
223 > if (own !== undefined) {
225 > }
226 > const parentInfo = parseSubagentSessionUri(session); agentConfigurationService.ts ×1
227 > if (parentInfo) {
228 > return this._stateManager.getSessionState(parentInfo.parentSession.toString())?.workingDirectories?.[0]; agentConfigurationService.ts ×1
229 > }
230 > return undefined; agentConfigurationService.ts ×1
233 > isWorkingDirectoryPending(session: ProtocolURI): boolean {
234 > return this._worktree?.isWorkingDirectoryPending(AgentSession.id(session)) ?? false; worktreeIsolation.ts ×2
235 > }
237 > async resolveWorkingDirectoryForResume(session: ProtocolURI, workingDirectory: URI): Promise<URI> {
238 > return this._worktree?.resolveWorkingDirectoryForResume(URI.parse(session), AgentSession.id(session), workingDirectory) ?? workingDirectory; copilotAgent.ts ×2
239 > }
241 > updateSessionConfig(session: ProtocolURI, patch: Record<string, unknown>): void {
242 > this._stateManager.dispatchServerAction(session, { agentConfigurationService.ts ×1
243 > type: ActionType.SessionConfigChanged,
244 > config: patch,
245 > });
246 > }
248 > getSessionConfigValues(session: ProtocolURI): Record<string, unknown> | undefined {
249 > return this._stateManager.getSessionState(session)?.config?.values; agentConfigurationService.ts ×1
250 > }
252 > getRootValue<D extends SchemaDefinition, K extends keyof D & string>(
253 > schema: ISchema<D>, agentConfigurationService.ts ×2
254 > key: K,
255 > ): SchemaValue<D[K]> | undefined {
256 > const root = this._stateManager.rootState.config?.values;
257 > const raw = root?.[key];
258 > if (raw === undefined) {
259 > return undefined; agentConfigurationService.ts ×1
260 > }
262 > schema.assertValid(key, raw);
263 > return raw;
264 > } catch (err) {
265 const reason = err instanceof ProtocolError ? err.message : String(err);
266 this._logService.warn(`[AgentConfigurationService] Host value for '${key}' failed schema validation, ignoring: ${reason}`);
267 return undefined;
268 }
271 > updateRootConfig(patch: Record<string, unknown>, replace = false): void {
272 > this._stateManager.dispatchServerAction(ROOT_STATE_URI, { agentConfigurationService.ts ×1
273 > type: ActionType.RootConfigChanged,
274 > config: patch,
275 > replace,
276 > });
277 > this.persistRootConfig();
278 > }
280 > persistRootConfig(): void {
281 > if (!this._rootConfigResource) { agentConfigurationService.ts ×3
283 > }
285 > const values = { ...(this._stateManager.rootState.config?.values ?? { [AgentHostConfigKey.Customizations]: [] }) }; agentConfigurationService.ts ×3
286 > for (const key of getProviderBackedRootConfigKeys(this._stateManager.rootState)) {
287 > delete values[key]; agentConfigurationService.ts ×1
288 > }
289 > const content = JSON.stringify(values, undefined, '\t'); agentConfigurationService.ts ×7
290 > const resource = this._rootConfigResource;
291 >
292 > this._rootConfigWrite = this._rootConfigWrite
293 > .catch(err => {
294 this._logService.warn('[AgentConfigurationService] Previous host config write failed', err);
296 > .then(async () => {
297 > await fs.promises.mkdir(dirname(resource.fsPath), { recursive: true });
298 > await fs.promises.writeFile(resource.fsPath, `${content}\n`, 'utf8');
299 > })
300 > .catch(err => {
301 this._logService.error(`[AgentConfigurationService] Failed to persist host config to ${resource.fsPath}`, err);
305 > async whenIdle(): Promise<void> {
306 > await this._rootConfigWrite; agentConfigurationService.ts ×7
307 > }
309 > registerProviderConfiguration(registration: IAgentCustomizationSettingsRegistration): void {
310 > const config = this._stateManager.rootState.config; agentConfigurationService.ts ×2
311 > if (!config) {
312 return;
313 }
314 > Object.assign(config.schema.properties, registration.properties); agentConfigurationService.ts ×2
315 > for (const [key, property] of Object.entries(registration.properties)) {
316 > if (config.values[key] === undefined && property.default !== undefined) {
317 > config.values[key] = property.default;
318 > }
319 > }
320 > const registrations = getAgentCustomizationSettingsEntries(this._stateManager.rootState).filter(entry => entry.provider !== registration.provider);
321 > this._stateManager.rootState._meta = withAgentCustomizationSettings(this._stateManager.rootState, [...registrations, {
322 > provider: registration.provider,
323 > title: registration.title,
324 > description: registration.description,
325 > settings: registration.settings,
326 > configurationFile: registration.configurationFile,
327 > }]);
328 > }
330 > getRootConfigValues(): Readonly<Record<string, unknown>> {
331 return this._stateManager.rootState.config?.values ?? {};
332 }
334 > /**
335 > * Yields the raw value bags that contribute to the effective config
336 > * for `session`, in precedence order: session, parent subagent
337 > * session (if any), host.
338 > */
339 > private *_effectiveChain(session: ProtocolURI): Iterable<Record<string, unknown>> {
340 > const own = this._stateManager.getSessionState(session)?.config?.values; agentConfigurationService.ts ×6
341 > if (own) {
344 > const parentInfo = parseSubagentSessionUri(session); agentConfigurationService.ts ×1
345 > if (parentInfo) {
346 > const parent = this._stateManager.getSessionState(parentInfo.parentSession.toString())?.config?.values; agentConfigurationService.ts ×2
347 > if (parent) {
348 > yield parent;
349 }
351 > const host = this._stateManager.rootState.config?.values; agentConfigurationService.ts ×2
357 > private _loadPersistedRootConfig(): Record<string, unknown> {
358 > const defaults = defaultAgentHostCustomizationConfigValues; agentConfigurationService.ts ×5
359 > if (!this._rootConfigResource) {
360 > return { ...defaults };
361 > }
363 > try {
364 > const raw = fs.readFileSync(this._rootConfigResource.fsPath, 'utf8');
365 > const parsed = JSON.parse(raw) as Record<string, unknown>;
366 > return {
367 > ...agentHostCustomizationConfigSchema.validateOrDefault(parsed, defaults),
368 > ...sandboxConfigSchema.validateOrDefault(parsed, {}),
369 > ...copilotCliConfigSchema.validateOrDefault(parsed, {}),
370 > };
371 > } catch (err) {
372 > const code = err && typeof err === 'object' && hasKey(err, { code: true }) ? String(err.code) : undefined;
373 > if (code !== 'ENOENT') {
374 this._logService.warn(`[AgentConfigurationService] Failed to read host config from ${this._rootConfigResource.fsPath}: ${err instanceof Error ? err.message : String(err)}`);
375 }
376 > return { ...defaults }; agentConfigurationService.ts ×7
377 > }