Path: blob/main/src/vs/platform/agentHost/node/agentConfigurationService.ts
13394 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as fs from 'fs';6import { Emitter, Event } from '../../../base/common/event.js';7import { Disposable } from '../../../base/common/lifecycle.js';8import { dirname } from '../../../base/common/path.js';9import { hasKey } from '../../../base/common/types.js';10import { URI } from '../../../base/common/uri.js';11import { createDecorator } from '../../instantiation/common/instantiation.js';12import { ILogService } from '../../log/common/log.js';13import { AgentHostConfigKey, agentHostCustomizationConfigSchema, defaultAgentHostCustomizationConfigValues } from '../common/agentHostCustomizationConfig.js';14import type { ISchema, SchemaDefinition, SchemaValue } from '../common/agentHostSchema.js';15import { ProtocolError } from '../common/state/sessionProtocol.js';16import { ActionType } from '../common/state/sessionActions.js';17import { parseSubagentSessionUri, type URI as ProtocolURI } from '../common/state/sessionState.js';18import { AgentHostStateManager } from './agentHostStateManager.js';1920export const IAgentConfigurationService = createDecorator<IAgentConfigurationService>('agentConfigurationService');2122/**23* Cohesive read/write surface for agent-host configuration.24*25* All platform-layer consumers (tool auto-approval, side effects, future26* host-config editors) should read and mutate config values through this27* service rather than reaching into raw session state. The service owns28* the `session → parent session → host` inheritance chain so that29* host-level defaults, subagent inheritance, and per-session overrides30* compose the same way everywhere.31*32* Reads go through a caller-supplied {@link ISchema}: each raw value is33* validated against the property's schema before being returned, so a34* malformed value in one layer transparently falls back to the next.35*/36export interface IAgentConfigurationService {37readonly _serviceBrand: undefined;3839/**40* Fires whenever a {@link ActionType.RootConfigChanged} action is41* processed by the state manager, signalling that callers should42* re-read any root config values they depend on.43*/44readonly onDidRootConfigChange: Event<void>;4546/**47* Returns the effective value of `key` for `session`, walking the48* `session → parent session → host` chain and returning the first49* layer that provides a value which validates against50* `schema.definition[key]`. Layers that provide a malformed value51* are logged and skipped. Returns `undefined` when no layer provides52* a valid value.53*/54getEffectiveValue<D extends SchemaDefinition, K extends keyof D & string>(55session: ProtocolURI,56schema: ISchema<D>,57key: K,58): SchemaValue<D[K]> | undefined;5960/**61* Returns the effective working directory for a session, falling back62* to the parent (subagent) session's working directory when the63* session itself does not have one set. The host layer does not carry64* a working directory.65*/66getEffectiveWorkingDirectory(session: ProtocolURI): string | undefined;6768/**69* Merges a partial config patch into a session's values via a70* {@link ActionType.SessionConfigChanged} action. Keys not present in71* `patch` are left untouched. The patch is applied atomically through72* the state manager's reducer.73*/74updateSessionConfig(session: ProtocolURI, patch: Record<string, unknown>): void;7576/**77* Returns the host-level value for `key`, validating it against78* `schema.definition[key]`. Invalid persisted values are logged and treated79* as missing.80*/81getRootValue<D extends SchemaDefinition, K extends keyof D & string>(82schema: ISchema<D>,83key: K,84): SchemaValue<D[K]> | undefined;8586/**87* Merges a partial config patch into the host-level value bag and persists88* the updated values for future agent-host lifetimes.89*/90updateRootConfig(patch: Record<string, unknown>, replace?: boolean): void;9192/**93* Persists the current host-level value bag without mutating it.94*/95persistRootConfig(): void;96}9798export class AgentConfigurationService extends Disposable implements IAgentConfigurationService {99declare readonly _serviceBrand: undefined;100private _rootConfigWrite = Promise.resolve();101102private readonly _onDidRootConfigChange = this._register(new Emitter<void>());103readonly onDidRootConfigChange: Event<void> = this._onDidRootConfigChange.event;104105constructor(106private readonly _stateManager: AgentHostStateManager,107@ILogService private readonly _logService: ILogService,108private readonly _rootConfigResource?: URI,109) {110super();111// Merge our customization schema/values into the existing root config112// (which already carries platform properties like permissions) rather113// than replacing it.114const existing = this._stateManager.rootState.config;115const ownSchema = agentHostCustomizationConfigSchema.toProtocol();116this._stateManager.rootState.config = {117schema: {118type: 'object',119properties: { ...existing?.schema.properties, ...ownSchema.properties },120},121values: { ...existing?.values, ...this._loadPersistedRootConfig() },122};123124this._register(this._stateManager.onDidEmitEnvelope(envelope => {125if (envelope.action.type === ActionType.RootConfigChanged) {126this._onDidRootConfigChange.fire();127}128}));129}130131getEffectiveValue<D extends SchemaDefinition, K extends keyof D & string>(132session: ProtocolURI,133schema: ISchema<D>,134key: K,135): SchemaValue<D[K]> | undefined {136for (const values of this._effectiveChain(session)) {137const raw = values[key];138if (raw === undefined) {139continue;140}141try {142schema.assertValid(key, raw);143return raw;144} catch (err) {145const reason = err instanceof ProtocolError ? err.message : String(err);146this._logService.warn(`[AgentConfigurationService] Value for '${key}' on ${session} failed schema validation, falling back: ${reason}`);147}148}149return undefined;150}151152getEffectiveWorkingDirectory(session: ProtocolURI): string | undefined {153const own = this._stateManager.getSessionState(session)?.summary.workingDirectory;154if (own !== undefined) {155return own;156}157const parentInfo = parseSubagentSessionUri(session);158if (parentInfo) {159return this._stateManager.getSessionState(parentInfo.parentSession)?.summary.workingDirectory;160}161return undefined;162}163164updateSessionConfig(session: ProtocolURI, patch: Record<string, unknown>): void {165this._stateManager.dispatchServerAction({166type: ActionType.SessionConfigChanged,167session,168config: patch,169});170}171172getRootValue<D extends SchemaDefinition, K extends keyof D & string>(173schema: ISchema<D>,174key: K,175): SchemaValue<D[K]> | undefined {176const root = this._stateManager.rootState.config?.values;177const raw = root?.[key];178if (raw === undefined) {179return undefined;180}181try {182schema.assertValid(key, raw);183return raw;184} catch (err) {185const reason = err instanceof ProtocolError ? err.message : String(err);186this._logService.warn(`[AgentConfigurationService] Host value for '${key}' failed schema validation, ignoring: ${reason}`);187return undefined;188}189}190191updateRootConfig(patch: Record<string, unknown>, replace = false): void {192this._stateManager.dispatchServerAction({193type: ActionType.RootConfigChanged,194config: patch,195replace,196});197this.persistRootConfig();198}199200persistRootConfig(): void {201if (!this._rootConfigResource) {202return;203}204205const values = this._stateManager.rootState.config?.values ?? { [AgentHostConfigKey.Customizations]: [] };206const content = JSON.stringify(values, undefined, '\t');207const resource = this._rootConfigResource;208209this._rootConfigWrite = this._rootConfigWrite210.catch(err => {211this._logService.warn('[AgentConfigurationService] Previous host config write failed', err);212})213.then(async () => {214await fs.promises.mkdir(dirname(resource.fsPath), { recursive: true });215await fs.promises.writeFile(resource.fsPath, `${content}\n`, 'utf8');216})217.catch(err => {218this._logService.error(`[AgentConfigurationService] Failed to persist host config to ${resource.fsPath}`, err);219});220}221222/**223* Yields the raw value bags that contribute to the effective config224* for `session`, in precedence order: session, parent subagent225* session (if any), host.226*/227private *_effectiveChain(session: ProtocolURI): Iterable<Record<string, unknown>> {228const own = this._stateManager.getSessionState(session)?.config?.values;229if (own) {230yield own;231}232const parentInfo = parseSubagentSessionUri(session);233if (parentInfo) {234const parent = this._stateManager.getSessionState(parentInfo.parentSession)?.config?.values;235if (parent) {236yield parent;237}238}239const host = this._stateManager.rootState.config?.values;240if (host) {241yield host;242}243}244245private _loadPersistedRootConfig(): Record<string, unknown> {246const defaults = defaultAgentHostCustomizationConfigValues;247if (!this._rootConfigResource) {248return { ...defaults };249}250251try {252const raw = fs.readFileSync(this._rootConfigResource.fsPath, 'utf8');253const parsed = JSON.parse(raw) as Record<string, unknown>;254return agentHostCustomizationConfigSchema.validateOrDefault(parsed, defaults);255} catch (err) {256const code = err && typeof err === 'object' && hasKey(err, { code: true }) ? String(err.code) : undefined;257if (code !== 'ENOENT') {258this._logService.warn(`[AgentConfigurationService] Failed to read host config from ${this._rootConfigResource.fsPath}: ${err instanceof Error ? err.message : String(err)}`);259}260return { ...defaults };261}262}263}264265266