Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/common/agentHostSchema.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 { localize } from '../../../nls.js';
7
import { SessionConfigKey } from './sessionConfigKeys.js';
8
import type { SessionConfigPropertySchema, SessionConfigSchema } from './state/protocol/commands.js';
9
import { JsonRpcErrorCodes, ProtocolError } from './state/sessionProtocol.js';
10
11
// ---- Schema builder --------------------------------------------------------
12
13
/**
14
* A schema property with a phantom TypeScript type and a precomputed
15
* runtime validator.
16
*
17
* The `<T>` type parameter is the developer's assertion about the
18
* property's runtime shape; the validator derived from `protocol`
19
* (`type`, `enum`, `items`, `properties`, `required`) enforces it at
20
* runtime.
21
*/
22
export interface ISchemaProperty<T> {
23
readonly protocol: SessionConfigPropertySchema;
24
/**
25
* Returns `true` iff `value` conforms to {@link protocol}. Narrows
26
* the type to `T` for callers. The boolean form is preferred for
27
* control flow; use {@link assertValid} when you want a descriptive
28
* error for the offending path.
29
*/
30
validate(value: unknown): value is T;
31
/**
32
* Throws a {@link ProtocolError} with `JsonRpcErrorCodes.InvalidParams`
33
* describing the offending path (e.g. `'permissions.allow[2]'`) when
34
* `value` does not conform to {@link protocol}. Otherwise returns and
35
* narrows the type to `T`.
36
*
37
* @param path Dotted path prefix to embed in error messages. Defaults
38
* to empty (the value itself).
39
*/
40
assertValid(value: unknown, path?: string): asserts value is T;
41
}
42
43
/**
44
* Defines a strongly-typed schema property whose runtime validator is
45
* derived from the supplied JSON-schema descriptor.
46
*/
47
export function schemaProperty<T>(protocol: SessionConfigPropertySchema): ISchemaProperty<T> {
48
const assertFn = buildAssert(protocol);
49
const assertValid = (value: unknown, path: string = ''): asserts value is T => assertFn(value, path);
50
const validate = (value: unknown): value is T => {
51
try {
52
assertFn(value, '');
53
return true;
54
} catch {
55
return false;
56
}
57
};
58
return { protocol, validate, assertValid };
59
}
60
61
// eslint-disable-next-line @typescript-eslint/no-explicit-any
62
export type SchemaDefinition = Record<string, ISchemaProperty<any>>;
63
64
export type SchemaValue<P> = P extends ISchemaProperty<infer T> ? T : never;
65
66
export type SchemaValues<D extends SchemaDefinition> = {
67
[K in keyof D]?: SchemaValue<D[K]>;
68
};
69
70
/**
71
* A bundle of named schema properties plus helpers for serializing to the
72
* protocol shape, validating a values bag at write sites, and validating
73
* a single key at read sites.
74
*/
75
export interface ISchema<D extends SchemaDefinition> {
76
readonly definition: D;
77
/** Returns the protocol-serializable schema for this bundle. */
78
toProtocol(): SessionConfigSchema;
79
/**
80
* Validates each known key in `values` against its schema and returns
81
* a new plain record. Throws a {@link ProtocolError} with a path like
82
* `'permissions.allow[2]'` when any supplied value fails validation.
83
* Unknown keys are passed through untouched for forward-compatibility.
84
*/
85
values(values: SchemaValues<D>): Record<string, unknown>;
86
/**
87
* Returns `true` iff `value` validates against the schema for `key`.
88
* Unknown keys return `false`.
89
*/
90
validate<K extends keyof D & string>(key: K, value: unknown): value is SchemaValue<D[K]>;
91
/**
92
* Throws a {@link ProtocolError} describing the offending path when
93
* `value` does not validate against the schema for `key`, or when
94
* `key` is not defined in the schema.
95
*/
96
assertValid<K extends keyof D & string>(key: K, value: unknown): asserts value is SchemaValue<D[K]>;
97
/**
98
* Returns a fully-typed values bag by validating each key of the
99
* schema against `values` and falling back to the default when
100
* the incoming value is missing or fails validation.
101
*
102
* Semantics: for every key declared in the schema `definition`:
103
* - if `values[key]` validates, it is kept;
104
* - else if `key` is present in `defaults`, the default is used;
105
* - else the key is omitted from the result.
106
*
107
* This means callers MAY supply defaults for only a subset of the
108
* schema — keys not present in `defaults` are simply left unset
109
* when the incoming value is missing or invalid. This is useful
110
* when some properties (e.g. per-session `permissions`) should be
111
* inherited from a higher scope rather than materialized on every
112
* new session.
113
*
114
* Intended for sanitizing untrusted input at protocol boundaries
115
* (e.g. `resolveSessionConfig`). Keys that fail validation are
116
* silently replaced with their default or dropped; use
117
* {@link values} or {@link assertValid} when you want a descriptive
118
* {@link ProtocolError} instead.
119
*/
120
validateOrDefault<T extends Partial<{ [K in keyof D]: SchemaValue<D[K]> }>>(values: { [K in keyof T]?: unknown } | undefined, defaults: T): T;
121
}
122
123
export function createSchema<D extends SchemaDefinition>(definition: D): ISchema<D> {
124
return {
125
definition,
126
toProtocol(): SessionConfigSchema {
127
const properties: Record<string, SessionConfigPropertySchema> = {};
128
for (const key of Object.keys(definition)) {
129
properties[key] = definition[key].protocol;
130
}
131
return { type: 'object', properties };
132
},
133
values(values) {
134
const raw = values as Record<string, unknown>;
135
for (const key of Object.keys(definition)) {
136
const value = raw[key];
137
if (value === undefined) {
138
continue;
139
}
140
// Local with explicit annotation so TypeScript accepts the
141
// assertion-signature call (per TS4104).
142
const prop: ISchemaProperty<unknown> = definition[key];
143
prop.assertValid(value, key);
144
}
145
return { ...raw };
146
},
147
validate<K extends keyof D & string>(key: K, value: unknown): value is SchemaValue<D[K]> {
148
const prop = definition[key];
149
return prop ? prop.validate(value) : false;
150
},
151
assertValid<K extends keyof D & string>(key: K, value: unknown): asserts value is SchemaValue<D[K]> {
152
const prop: ISchemaProperty<unknown> | undefined = definition[key];
153
if (!prop) {
154
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, `Unknown schema key '${key}'`);
155
}
156
// Re-bind post-narrowing to keep the call target explicitly typed
157
// (required for assertion-signature calls, TS4104).
158
const narrowed: ISchemaProperty<unknown> = prop;
159
narrowed.assertValid(value, key);
160
},
161
validateOrDefault<T extends Partial<{ [K in keyof D]: SchemaValue<D[K]> }>>(values: { [K in keyof T]?: unknown } | undefined, defaults: T): T {
162
const result: Record<string, unknown> = {};
163
const raw: { [K in keyof T]?: unknown } = values ?? {};
164
for (const key of Object.keys(definition)) {
165
const prop = definition[key];
166
const candidate = raw[key];
167
if (candidate !== undefined && prop.validate(candidate)) {
168
result[key] = candidate;
169
} else if (Object.prototype.hasOwnProperty.call(defaults, key)) {
170
result[key] = (defaults as Record<string, unknown>)[key];
171
}
172
// else: key not in defaults and incoming value missing/invalid
173
// → leave unset so higher-scope defaults can fill in.
174
}
175
return result as T;
176
},
177
};
178
}
179
180
// ---- Validator derivation --------------------------------------------------
181
182
/**
183
* A validator that throws a {@link ProtocolError} annotated with the
184
* offending path when `value` does not conform, or returns normally
185
* when it does.
186
*/
187
type AssertValidator = (value: unknown, path: string) => void;
188
189
function buildAssert(schema: SessionConfigPropertySchema): AssertValidator {
190
if (schema.type === 'object' && schema.properties) {
191
const propAsserts: Record<string, AssertValidator> = {};
192
for (const key of Object.keys(schema.properties)) {
193
propAsserts[key] = buildAssert(schema.properties[key] as SessionConfigPropertySchema);
194
}
195
const required = new Set(schema.required ?? []);
196
return (value, path) => {
197
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
198
throw invalidParams(path, 'object', value);
199
}
200
const obj = value as Record<string, unknown>;
201
for (const key of Object.keys(propAsserts)) {
202
const childPath = joinPath(path, key);
203
if (obj[key] === undefined) {
204
if (required.has(key)) {
205
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, `Missing required property at '${childPath}'`);
206
}
207
continue;
208
}
209
propAsserts[key](obj[key], childPath);
210
}
211
};
212
}
213
if (schema.type === 'array' && schema.items) {
214
const itemAssert = buildAssert(schema.items as SessionConfigPropertySchema);
215
return (value, path) => {
216
if (!Array.isArray(value)) {
217
throw invalidParams(path, 'array', value);
218
}
219
for (let i = 0; i < value.length; i++) {
220
itemAssert(value[i], `${path}[${i}]`);
221
}
222
};
223
}
224
return buildPrimitiveAssert(schema);
225
}
226
227
function buildPrimitiveAssert(schema: SessionConfigPropertySchema): AssertValidator {
228
const enumDynamic = schema.enumDynamic === true;
229
return (value, path) => {
230
switch (schema.type) {
231
case 'string': if (typeof value !== 'string') { throw invalidParams(path, 'string', value); } break;
232
case 'number': if (typeof value !== 'number') { throw invalidParams(path, 'number', value); } break;
233
case 'boolean': if (typeof value !== 'boolean') { throw invalidParams(path, 'boolean', value); } break;
234
case 'array': if (!Array.isArray(value)) { throw invalidParams(path, 'array', value); } break;
235
case 'object': if (typeof value !== 'object' || value === null || Array.isArray(value)) { throw invalidParams(path, 'object', value); } break;
236
}
237
if (schema.enum && !enumDynamic && !schema.enum.includes(value as string)) {
238
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, `Invalid value at '${path || '<root>'}': ${safeStringify(value)} is not one of [${schema.enum.map(v => JSON.stringify(v)).join(', ')}]`);
239
}
240
};
241
}
242
243
function invalidParams(path: string, expected: string, value: unknown): ProtocolError {
244
return new ProtocolError(JsonRpcErrorCodes.InvalidParams, `Invalid value at '${path || '<root>'}': expected ${expected}, got ${safeStringify(value)}`);
245
}
246
247
function joinPath(parent: string, key: string): string {
248
return parent ? `${parent}.${key}` : key;
249
}
250
251
function safeStringify(value: unknown): string {
252
try {
253
return JSON.stringify(value);
254
} catch {
255
return String(value);
256
}
257
}
258
259
// ---- Platform-owned schema -------------------------------------------------
260
261
export type AutoApproveLevel = 'default' | 'autoApprove' | 'autopilot';
262
263
export type SessionMode = 'interactive' | 'plan';
264
265
export interface IPermissionsValue {
266
readonly allow: readonly string[];
267
readonly deny: readonly string[];
268
}
269
270
const permissionsProperty = schemaProperty<IPermissionsValue>({
271
type: 'object',
272
title: localize('agentHost.sessionConfig.permissions', "Permissions"),
273
description: localize('agentHost.sessionConfig.permissionsDescription', "Per-tool session permissions. Updated automatically when approving a tool \"in this Session\"."),
274
properties: {
275
allow: {
276
type: 'array',
277
title: localize('agentHost.sessionConfig.permissions.allow', "Allowed tools"),
278
items: {
279
type: 'string',
280
title: localize('agentHost.sessionConfig.permissions.toolName', "Tool name"),
281
},
282
},
283
deny: {
284
type: 'array',
285
title: localize('agentHost.sessionConfig.permissions.deny', "Denied tools"),
286
items: {
287
type: 'string',
288
title: localize('agentHost.sessionConfig.permissions.toolName', "Tool name"),
289
},
290
},
291
},
292
default: { allow: [], deny: [] },
293
sessionMutable: true,
294
});
295
296
/**
297
* Session-config properties owned by the platform itself — i.e. consumed
298
* by the agent host rather than by any particular agent.
299
*
300
* Agents extend this schema by spreading `platformSessionSchema.definition`
301
* into their own {@link createSchema} call together with any
302
* provider-specific properties.
303
*/
304
export const platformSessionSchema = createSchema({
305
[SessionConfigKey.AutoApprove]: schemaProperty<AutoApproveLevel>({
306
type: 'string',
307
title: localize('agentHost.sessionConfig.autoApprove', "Approvals"),
308
description: localize('agentHost.sessionConfig.autoApproveDescription', "Tool approval behavior for this session"),
309
enum: ['default', 'autoApprove', 'autopilot'],
310
enumLabels: [
311
localize('agentHost.sessionConfig.autoApprove.default', "Default Approvals"),
312
localize('agentHost.sessionConfig.autoApprove.bypass', "Bypass Approvals"),
313
localize('agentHost.sessionConfig.autoApprove.autopilot', "Autopilot (Preview)"),
314
],
315
enumDescriptions: [
316
localize('agentHost.sessionConfig.autoApprove.defaultDescription', "Copilot uses your configured settings"),
317
localize('agentHost.sessionConfig.autoApprove.bypassDescription', "All tool calls are auto-approved"),
318
localize('agentHost.sessionConfig.autoApprove.autopilotDescription', "Autonomously iterates from start to finish"),
319
],
320
default: 'default',
321
sessionMutable: true,
322
}),
323
[SessionConfigKey.Permissions]: permissionsProperty,
324
[SessionConfigKey.Mode]: schemaProperty<SessionMode>({
325
type: 'string',
326
title: localize('agentHost.sessionConfig.mode', "Agent Mode"),
327
description: localize('agentHost.sessionConfig.modeDescription', "How the agent should approach this turn"),
328
enum: ['interactive', 'plan'],
329
enumLabels: [
330
localize('agentHost.sessionConfig.mode.interactive', "Interactive"),
331
localize('agentHost.sessionConfig.mode.plan', "Plan"),
332
],
333
enumDescriptions: [
334
localize('agentHost.sessionConfig.mode.interactiveDescription', "Ask for input and approval for each action"),
335
localize('agentHost.sessionConfig.mode.planDescription', "Generate a plan first, then choose how to execute it"),
336
],
337
default: 'interactive',
338
sessionMutable: true,
339
}),
340
});
341
342
/**
343
* Root (agent host) config properties owned by the platform itself.
344
*
345
* Root config acts as the baseline that applies to every session:
346
*
347
* - {@link SessionConfigKey.Permissions} — host-wide allow/deny lists
348
* unioned with each session's own permissions when evaluating tool
349
* auto-approval. See `SessionPermissionManager` for the evaluation
350
* rules.
351
*/
352
export const platformRootSchema = createSchema({
353
[SessionConfigKey.Permissions]: permissionsProperty,
354
});
355
356