Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/node/agentConfigurationService.ts
13394 views
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 * 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 type { ISchema, SchemaDefinition, SchemaValue } from '../common/agentHostSchema.js';
16
import { ProtocolError } from '../common/state/sessionProtocol.js';
17
import { ActionType } from '../common/state/sessionActions.js';
18
import { parseSubagentSessionUri, type URI as ProtocolURI } from '../common/state/sessionState.js';
19
import { AgentHostStateManager } from './agentHostStateManager.js';
20
21
export const IAgentConfigurationService = createDecorator<IAgentConfigurationService>('agentConfigurationService');
22
23
/**
24
* Cohesive read/write surface for agent-host configuration.
25
*
26
* All platform-layer consumers (tool auto-approval, side effects, future
27
* host-config editors) should read and mutate config values through this
28
* service rather than reaching into raw session state. The service owns
29
* the `session → parent session → host` inheritance chain so that
30
* host-level defaults, subagent inheritance, and per-session overrides
31
* compose the same way everywhere.
32
*
33
* Reads go through a caller-supplied {@link ISchema}: each raw value is
34
* validated against the property's schema before being returned, so a
35
* malformed value in one layer transparently falls back to the next.
36
*/
37
export interface IAgentConfigurationService {
38
readonly _serviceBrand: undefined;
39
40
/**
41
* Fires whenever a {@link ActionType.RootConfigChanged} action is
42
* processed by the state manager, signalling that callers should
43
* re-read any root config values they depend on.
44
*/
45
readonly onDidRootConfigChange: Event<void>;
46
47
/**
48
* Returns the effective value of `key` for `session`, walking the
49
* `session → parent session → host` chain and returning the first
50
* layer that provides a value which validates against
51
* `schema.definition[key]`. Layers that provide a malformed value
52
* are logged and skipped. Returns `undefined` when no layer provides
53
* a valid value.
54
*/
55
getEffectiveValue<D extends SchemaDefinition, K extends keyof D & string>(
56
session: ProtocolURI,
57
schema: ISchema<D>,
58
key: K,
59
): SchemaValue<D[K]> | undefined;
60
61
/**
62
* Returns the effective working directory for a session, falling back
63
* to the parent (subagent) session's working directory when the
64
* session itself does not have one set. The host layer does not carry
65
* a working directory.
66
*/
67
getEffectiveWorkingDirectory(session: ProtocolURI): string | undefined;
68
69
/**
70
* Merges a partial config patch into a session's values via a
71
* {@link ActionType.SessionConfigChanged} action. Keys not present in
72
* `patch` are left untouched. The patch is applied atomically through
73
* the state manager's reducer.
74
*/
75
updateSessionConfig(session: ProtocolURI, patch: Record<string, unknown>): void;
76
77
/**
78
* Returns the host-level value for `key`, validating it against
79
* `schema.definition[key]`. Invalid persisted values are logged and treated
80
* as missing.
81
*/
82
getRootValue<D extends SchemaDefinition, K extends keyof D & string>(
83
schema: ISchema<D>,
84
key: K,
85
): SchemaValue<D[K]> | undefined;
86
87
/**
88
* Merges a partial config patch into the host-level value bag and persists
89
* the updated values for future agent-host lifetimes.
90
*/
91
updateRootConfig(patch: Record<string, unknown>, replace?: boolean): void;
92
93
/**
94
* Persists the current host-level value bag without mutating it.
95
*/
96
persistRootConfig(): void;
97
}
98
99
export class AgentConfigurationService extends Disposable implements IAgentConfigurationService {
100
declare readonly _serviceBrand: undefined;
101
private _rootConfigWrite = Promise.resolve();
102
103
private readonly _onDidRootConfigChange = this._register(new Emitter<void>());
104
readonly onDidRootConfigChange: Event<void> = this._onDidRootConfigChange.event;
105
106
constructor(
107
private readonly _stateManager: AgentHostStateManager,
108
@ILogService private readonly _logService: ILogService,
109
private readonly _rootConfigResource?: URI,
110
) {
111
super();
112
// Merge our customization schema/values into the existing root config
113
// (which already carries platform properties like permissions) rather
114
// than replacing it.
115
const existing = this._stateManager.rootState.config;
116
const ownSchema = agentHostCustomizationConfigSchema.toProtocol();
117
this._stateManager.rootState.config = {
118
schema: {
119
type: 'object',
120
properties: { ...existing?.schema.properties, ...ownSchema.properties },
121
},
122
values: { ...existing?.values, ...this._loadPersistedRootConfig() },
123
};
124
125
this._register(this._stateManager.onDidEmitEnvelope(envelope => {
126
if (envelope.action.type === ActionType.RootConfigChanged) {
127
this._onDidRootConfigChange.fire();
128
}
129
}));
130
}
131
132
getEffectiveValue<D extends SchemaDefinition, K extends keyof D & string>(
133
session: ProtocolURI,
134
schema: ISchema<D>,
135
key: K,
136
): SchemaValue<D[K]> | undefined {
137
for (const values of this._effectiveChain(session)) {
138
const raw = values[key];
139
if (raw === undefined) {
140
continue;
141
}
142
try {
143
schema.assertValid(key, raw);
144
return raw;
145
} catch (err) {
146
const reason = err instanceof ProtocolError ? err.message : String(err);
147
this._logService.warn(`[AgentConfigurationService] Value for '${key}' on ${session} failed schema validation, falling back: ${reason}`);
148
}
149
}
150
return undefined;
151
}
152
153
getEffectiveWorkingDirectory(session: ProtocolURI): string | undefined {
154
const own = this._stateManager.getSessionState(session)?.summary.workingDirectory;
155
if (own !== undefined) {
156
return own;
157
}
158
const parentInfo = parseSubagentSessionUri(session);
159
if (parentInfo) {
160
return this._stateManager.getSessionState(parentInfo.parentSession)?.summary.workingDirectory;
161
}
162
return undefined;
163
}
164
165
updateSessionConfig(session: ProtocolURI, patch: Record<string, unknown>): void {
166
this._stateManager.dispatchServerAction({
167
type: ActionType.SessionConfigChanged,
168
session,
169
config: patch,
170
});
171
}
172
173
getRootValue<D extends SchemaDefinition, K extends keyof D & string>(
174
schema: ISchema<D>,
175
key: K,
176
): SchemaValue<D[K]> | undefined {
177
const root = this._stateManager.rootState.config?.values;
178
const raw = root?.[key];
179
if (raw === undefined) {
180
return undefined;
181
}
182
try {
183
schema.assertValid(key, raw);
184
return raw;
185
} catch (err) {
186
const reason = err instanceof ProtocolError ? err.message : String(err);
187
this._logService.warn(`[AgentConfigurationService] Host value for '${key}' failed schema validation, ignoring: ${reason}`);
188
return undefined;
189
}
190
}
191
192
updateRootConfig(patch: Record<string, unknown>, replace = false): void {
193
this._stateManager.dispatchServerAction({
194
type: ActionType.RootConfigChanged,
195
config: patch,
196
replace,
197
});
198
this.persistRootConfig();
199
}
200
201
persistRootConfig(): void {
202
if (!this._rootConfigResource) {
203
return;
204
}
205
206
const values = this._stateManager.rootState.config?.values ?? { [AgentHostConfigKey.Customizations]: [] };
207
const content = JSON.stringify(values, undefined, '\t');
208
const resource = this._rootConfigResource;
209
210
this._rootConfigWrite = this._rootConfigWrite
211
.catch(err => {
212
this._logService.warn('[AgentConfigurationService] Previous host config write failed', err);
213
})
214
.then(async () => {
215
await fs.promises.mkdir(dirname(resource.fsPath), { recursive: true });
216
await fs.promises.writeFile(resource.fsPath, `${content}\n`, 'utf8');
217
})
218
.catch(err => {
219
this._logService.error(`[AgentConfigurationService] Failed to persist host config to ${resource.fsPath}`, err);
220
});
221
}
222
223
/**
224
* Yields the raw value bags that contribute to the effective config
225
* for `session`, in precedence order: session, parent subagent
226
* session (if any), host.
227
*/
228
private *_effectiveChain(session: ProtocolURI): Iterable<Record<string, unknown>> {
229
const own = this._stateManager.getSessionState(session)?.config?.values;
230
if (own) {
231
yield own;
232
}
233
const parentInfo = parseSubagentSessionUri(session);
234
if (parentInfo) {
235
const parent = this._stateManager.getSessionState(parentInfo.parentSession)?.config?.values;
236
if (parent) {
237
yield parent;
238
}
239
}
240
const host = this._stateManager.rootState.config?.values;
241
if (host) {
242
yield host;
243
}
244
}
245
246
private _loadPersistedRootConfig(): Record<string, unknown> {
247
const defaults = defaultAgentHostCustomizationConfigValues;
248
if (!this._rootConfigResource) {
249
return { ...defaults };
250
}
251
252
try {
253
const raw = fs.readFileSync(this._rootConfigResource.fsPath, 'utf8');
254
const parsed = JSON.parse(raw) as Record<string, unknown>;
255
return agentHostCustomizationConfigSchema.validateOrDefault(parsed, defaults);
256
} catch (err) {
257
const code = err && typeof err === 'object' && hasKey(err, { code: true }) ? String(err.code) : undefined;
258
if (code !== 'ENOENT') {
259
this._logService.warn(`[AgentConfigurationService] Failed to read host config from ${this._rootConfigResource.fsPath}: ${err instanceof Error ? err.message : String(err)}`);
260
}
261
return { ...defaults };
262
}
263
}
264
}
265
266