@google/gemini-cli-a2a-server 0.63.0-preview.0 → 0.64.0-nightly.20261001.gc6bccb7ec
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a2a-server.mjs +2114 -1379
- package/dist/src/config/config.js +13 -9
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +139 -5
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/config/settings.d.ts +2813 -11
- package/dist/src/config/settings.js +660 -51
- package/dist/src/config/settings.js.map +1 -1
- package/dist/src/config/settings.test.js +213 -10
- package/dist/src/config/settings.test.js.map +1 -1
- package/dist/src/http/app.js +1 -1
- package/dist/src/http/app.js.map +1 -1
- package/dist/src/http/app.test.js +36 -0
- package/dist/src/http/app.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -5,64 +5,646 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from 'node:fs';
|
|
7
7
|
import * as path from 'node:path';
|
|
8
|
+
import { z } from 'zod';
|
|
8
9
|
import { debugLogger, GEMINI_DIR, getErrorMessage, homedir, checkPathTrust, isHeadlessMode, } from '@google/gemini-cli-core';
|
|
9
10
|
import stripJsonComments from 'strip-json-comments';
|
|
10
11
|
export const USER_SETTINGS_DIR = path.join(homedir(), GEMINI_DIR);
|
|
11
12
|
export const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json');
|
|
13
|
+
export var MergeStrategy;
|
|
14
|
+
(function (MergeStrategy) {
|
|
15
|
+
MergeStrategy["REPLACE"] = "replace";
|
|
16
|
+
MergeStrategy["CONCAT"] = "concat";
|
|
17
|
+
MergeStrategy["UNION"] = "union";
|
|
18
|
+
MergeStrategy["SHALLOW_MERGE"] = "shallow_merge";
|
|
19
|
+
})(MergeStrategy || (MergeStrategy = {}));
|
|
20
|
+
const MIGRATION_MAP = {
|
|
21
|
+
accessibility: 'ui.accessibility',
|
|
22
|
+
allowedTools: 'tools.allowed',
|
|
23
|
+
allowMCPServers: 'mcp.allowed',
|
|
24
|
+
autoAccept: 'tools.autoAccept',
|
|
25
|
+
autoConfigureMaxOldSpaceSize: 'advanced.autoConfigureMemory',
|
|
26
|
+
bugCommand: 'advanced.bugCommand',
|
|
27
|
+
chatCompression: 'model.compressionThreshold',
|
|
28
|
+
checkpointing: 'general.checkpointing',
|
|
29
|
+
coreTools: 'tools.core',
|
|
30
|
+
contextFileName: 'context.fileName',
|
|
31
|
+
customThemes: 'ui.customThemes',
|
|
32
|
+
customWittyPhrases: 'ui.customWittyPhrases',
|
|
33
|
+
debugKeystrokeLogging: 'general.debugKeystrokeLogging',
|
|
34
|
+
disableAutoUpdate: 'general.disableAutoUpdate',
|
|
35
|
+
disableUpdateNag: 'general.disableUpdateNag',
|
|
36
|
+
dnsResolutionOrder: 'advanced.dnsResolutionOrder',
|
|
37
|
+
enableHooks: 'tools.enableHooks',
|
|
38
|
+
enablePromptCompletion: 'general.enablePromptCompletion',
|
|
39
|
+
enforcedAuthType: 'security.auth.enforcedType',
|
|
40
|
+
excludeTools: 'tools.exclude',
|
|
41
|
+
excludeMCPServers: 'mcp.excluded',
|
|
42
|
+
excludedProjectEnvVars: 'advanced.excludedEnvVars',
|
|
43
|
+
experimentalSkills: 'experimental.skills',
|
|
44
|
+
extensionManagement: 'experimental.extensionManagement',
|
|
45
|
+
extensions: 'extensions',
|
|
46
|
+
fileFiltering: 'context.fileFiltering',
|
|
47
|
+
folderTrustFeature: 'security.folderTrust.featureEnabled',
|
|
48
|
+
folderTrust: 'security.folderTrust.enabled',
|
|
49
|
+
hasSeenIdeIntegrationNudge: 'ide.hasSeenNudge',
|
|
50
|
+
hideWindowTitle: 'ui.hideWindowTitle',
|
|
51
|
+
showStatusInTitle: 'ui.showStatusInTitle',
|
|
52
|
+
hideTips: 'ui.hideTips',
|
|
53
|
+
hideBanner: 'ui.hideBanner',
|
|
54
|
+
hideFooter: 'ui.hideFooter',
|
|
55
|
+
hideCWD: 'ui.footer.hideCWD',
|
|
56
|
+
hideSandboxStatus: 'ui.footer.hideSandboxStatus',
|
|
57
|
+
hideModelInfo: 'ui.footer.hideModelInfo',
|
|
58
|
+
hideContextSummary: 'ui.hideContextSummary',
|
|
59
|
+
showMemoryUsage: 'ui.showMemoryUsage',
|
|
60
|
+
showLineNumbers: 'ui.showLineNumbers',
|
|
61
|
+
showCitations: 'ui.showCitations',
|
|
62
|
+
ideMode: 'ide.enabled',
|
|
63
|
+
includeDirectories: 'context.includeDirectories',
|
|
64
|
+
loadMemoryFromIncludeDirectories: 'context.loadFromIncludeDirectories',
|
|
65
|
+
logLevel: 'logging.level',
|
|
66
|
+
maxSessionTurns: 'model.maxSessionTurns',
|
|
67
|
+
mcpServers: 'mcpServers',
|
|
68
|
+
mcpServerCommand: 'mcp.serverCommand',
|
|
69
|
+
memoryImportFormat: 'context.importFormat',
|
|
70
|
+
memoryDiscoveryMaxDirs: 'context.discoveryMaxDirs',
|
|
71
|
+
model: 'model.name',
|
|
72
|
+
preferredEditor: 'general.preferredEditor',
|
|
73
|
+
retryFetchErrors: 'general.retryFetchErrors',
|
|
74
|
+
sandbox: 'tools.sandbox',
|
|
75
|
+
selectedAuthType: 'security.auth.selectedType',
|
|
76
|
+
enableInteractiveShell: 'tools.shell.enableInteractiveShell',
|
|
77
|
+
shellPager: 'tools.shell.pager',
|
|
78
|
+
shellShowColor: 'tools.shell.showColor',
|
|
79
|
+
shellInactivityTimeout: 'tools.shell.inactivityTimeout',
|
|
80
|
+
skipNextSpeakerCheck: 'model.skipNextSpeakerCheck',
|
|
81
|
+
summarizeToolOutput: 'model.summarizeToolOutput',
|
|
82
|
+
telemetry: 'telemetry',
|
|
83
|
+
theme: 'ui.theme',
|
|
84
|
+
toolDiscoveryCommand: 'tools.discoveryCommand',
|
|
85
|
+
toolCallCommand: 'tools.callCommand',
|
|
86
|
+
usageStatisticsEnabled: 'privacy.usageStatisticsEnabled',
|
|
87
|
+
useExternalAuth: 'security.auth.useExternal',
|
|
88
|
+
useRipgrep: 'tools.useRipgrep',
|
|
89
|
+
vimMode: 'general.vimMode',
|
|
90
|
+
};
|
|
91
|
+
const KNOWN_V2_CONTAINERS = new Set(Object.values(MIGRATION_MAP).map((p) => p.split('.')[0]));
|
|
92
|
+
const LEGACY_V1_ONLY_KEYS = new Set([
|
|
93
|
+
...Object.keys(MIGRATION_MAP).filter((k) => k !== MIGRATION_MAP[k] && !KNOWN_V2_CONTAINERS.has(k)),
|
|
94
|
+
'telemetryDisabled',
|
|
95
|
+
]);
|
|
96
|
+
const MERGE_STRATEGIES = {
|
|
97
|
+
mcpServers: MergeStrategy.SHALLOW_MERGE,
|
|
98
|
+
policyPaths: MergeStrategy.UNION,
|
|
99
|
+
adminPolicyPaths: MergeStrategy.UNION,
|
|
100
|
+
};
|
|
101
|
+
export function getMergeStrategyForPath(pathSegments) {
|
|
102
|
+
return MERGE_STRATEGIES[pathSegments.join('.')];
|
|
103
|
+
}
|
|
104
|
+
function isPlainObject(item) {
|
|
105
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
const proto = Object.getPrototypeOf(item);
|
|
109
|
+
return proto === null || proto === Object.prototype;
|
|
110
|
+
}
|
|
111
|
+
function isUnsafeKey(key) {
|
|
112
|
+
return key === '__proto__' || key === 'constructor' || key === 'prototype';
|
|
113
|
+
}
|
|
114
|
+
function mergeRecursively(target, source, getStrategy, currentPath = []) {
|
|
115
|
+
for (const key of Object.keys(source)) {
|
|
116
|
+
if (isUnsafeKey(key)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const srcValue = source[key];
|
|
120
|
+
if (srcValue === undefined) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const newPath = [...currentPath, key];
|
|
124
|
+
const objValue = target[key];
|
|
125
|
+
const mergeStrategy = getStrategy(newPath);
|
|
126
|
+
if (mergeStrategy === MergeStrategy.SHALLOW_MERGE && objValue && srcValue) {
|
|
127
|
+
const obj1 = isPlainObject(objValue) ? objValue : {};
|
|
128
|
+
const obj2 = isPlainObject(srcValue) ? srcValue : {};
|
|
129
|
+
const mergedShallow = {};
|
|
130
|
+
for (const [k, v] of Object.entries(obj1)) {
|
|
131
|
+
if (!isUnsafeKey(k))
|
|
132
|
+
mergedShallow[k] = v;
|
|
133
|
+
}
|
|
134
|
+
for (const [k, v] of Object.entries(obj2)) {
|
|
135
|
+
if (!isUnsafeKey(k))
|
|
136
|
+
mergedShallow[k] = v;
|
|
137
|
+
}
|
|
138
|
+
target[key] = mergedShallow;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (Array.isArray(objValue)) {
|
|
142
|
+
const srcArray = Array.isArray(srcValue) ? srcValue : [srcValue];
|
|
143
|
+
if (mergeStrategy === MergeStrategy.CONCAT) {
|
|
144
|
+
target[key] = objValue.concat(srcArray);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (mergeStrategy === MergeStrategy.UNION) {
|
|
148
|
+
target[key] = [...new Set(objValue.concat(srcArray))];
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (isPlainObject(objValue) && isPlainObject(srcValue)) {
|
|
153
|
+
mergeRecursively(objValue, srcValue, getStrategy, newPath);
|
|
154
|
+
}
|
|
155
|
+
else if (isPlainObject(srcValue)) {
|
|
156
|
+
const nestedTarget = {};
|
|
157
|
+
target[key] = nestedTarget;
|
|
158
|
+
mergeRecursively(nestedTarget, srcValue, getStrategy, newPath);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
target[key] = srcValue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return target;
|
|
165
|
+
}
|
|
12
166
|
/**
|
|
13
|
-
*
|
|
14
|
-
* Project settings override user settings if the workspace is trusted.
|
|
15
|
-
*
|
|
16
|
-
* How is it different to gemini-cli/cli: Returns already merged settings rather
|
|
17
|
-
* than `LoadedSettings` (unnecessary since we are not modifying users
|
|
18
|
-
* settings.json).
|
|
167
|
+
* Deeply merges configuration objects while protecting against prototype pollution.
|
|
19
168
|
*/
|
|
20
|
-
export function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
169
|
+
export function customDeepMerge(first, ...rest) {
|
|
170
|
+
const getStrategy = typeof first === 'function' ? first : getMergeStrategyForPath;
|
|
171
|
+
const sources = typeof first === 'function' ? rest : first ? [first, ...rest] : rest;
|
|
172
|
+
const result = {};
|
|
173
|
+
for (const source of sources) {
|
|
174
|
+
if (source && isPlainObject(source)) {
|
|
175
|
+
mergeRecursively(result, source, getStrategy);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
function setNestedProperty(obj, dotPath, value) {
|
|
181
|
+
const keys = dotPath.split('.');
|
|
182
|
+
const lastKey = keys.pop();
|
|
183
|
+
if (!lastKey || isUnsafeKey(lastKey))
|
|
184
|
+
return;
|
|
185
|
+
let current = obj;
|
|
186
|
+
for (const key of keys) {
|
|
187
|
+
if (isUnsafeKey(key))
|
|
188
|
+
return;
|
|
189
|
+
const existing = current[key];
|
|
190
|
+
if (existing === undefined) {
|
|
191
|
+
const nextObj = {};
|
|
192
|
+
current[key] = nextObj;
|
|
193
|
+
current = nextObj;
|
|
194
|
+
}
|
|
195
|
+
else if (isPlainObject(existing)) {
|
|
196
|
+
current = existing;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
current[lastKey] = value;
|
|
203
|
+
}
|
|
204
|
+
function getNestedProperty(obj, dotPath) {
|
|
205
|
+
const keys = dotPath.split('.');
|
|
206
|
+
let current = obj;
|
|
207
|
+
for (const key of keys) {
|
|
208
|
+
if (isUnsafeKey(key) || !isPlainObject(current) || !(key in current)) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
current = current[key];
|
|
212
|
+
}
|
|
213
|
+
return current;
|
|
214
|
+
}
|
|
215
|
+
function migrateInvertedBoolean(container, oldKey, newKey) {
|
|
216
|
+
const oldVal = container[oldKey];
|
|
217
|
+
const newVal = container[newKey];
|
|
218
|
+
if (typeof oldVal === 'boolean') {
|
|
219
|
+
if (typeof newVal !== 'boolean') {
|
|
220
|
+
container[newKey] = !oldVal;
|
|
221
|
+
}
|
|
222
|
+
delete container[oldKey];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Intercepts a V1 (flat) or hybrid settings object in memory and converts
|
|
227
|
+
* legacy/deprecated keys into the V2 hierarchical structure.
|
|
228
|
+
*/
|
|
229
|
+
export function migrateDeprecatedSettings(rawSettings) {
|
|
230
|
+
if (!isPlainObject(rawSettings)) {
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
const v2Settings = {};
|
|
234
|
+
const remainingKeys = new Set(Object.keys(rawSettings).filter((k) => !isUnsafeKey(k)));
|
|
235
|
+
// Normalize boolean telemetry: true/false -> { enabled: true/false }
|
|
236
|
+
const rawTelemetry = rawSettings['telemetry'];
|
|
237
|
+
if (typeof rawTelemetry === 'boolean') {
|
|
238
|
+
setNestedProperty(v2Settings, 'telemetry.enabled', rawTelemetry);
|
|
239
|
+
remainingKeys.delete('telemetry');
|
|
240
|
+
}
|
|
241
|
+
// Handle legacy telemetryDisabled -> telemetry.enabled = !telemetryDisabled
|
|
242
|
+
const telemetryDisabledVal = rawSettings['telemetryDisabled'];
|
|
243
|
+
if (typeof telemetryDisabledVal === 'boolean') {
|
|
244
|
+
if (getNestedProperty(v2Settings, 'telemetry.enabled') === undefined) {
|
|
245
|
+
setNestedProperty(v2Settings, 'telemetry.enabled', !telemetryDisabledVal);
|
|
246
|
+
}
|
|
247
|
+
remainingKeys.delete('telemetryDisabled');
|
|
248
|
+
}
|
|
249
|
+
for (const [oldKey, newPath] of Object.entries(MIGRATION_MAP)) {
|
|
250
|
+
// Skip keys where oldKey === newPath so V2 top-level containers are merged in the second pass
|
|
251
|
+
if (oldKey === newPath) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (remainingKeys.has(oldKey)) {
|
|
255
|
+
let val = rawSettings[oldKey];
|
|
256
|
+
// If a key is both a V1 key and a V2 container (e.g., 'model') and holds an object,
|
|
257
|
+
// preserve it as a V2 container so it is deep-merged in the carry-over pass.
|
|
258
|
+
if (KNOWN_V2_CONTAINERS.has(oldKey) && isPlainObject(val)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
// Normalize legacy boolean checkpointing: true -> { enabled: true }
|
|
262
|
+
if (oldKey === 'checkpointing' && typeof val === 'boolean') {
|
|
263
|
+
val = { enabled: val };
|
|
264
|
+
}
|
|
265
|
+
// Normalize legacy object chatCompression: { contextPercentageThreshold: n } -> n
|
|
266
|
+
if (oldKey === 'chatCompression' && isPlainObject(val)) {
|
|
267
|
+
const threshold = val['contextPercentageThreshold'];
|
|
268
|
+
if (typeof threshold === 'number') {
|
|
269
|
+
val = threshold;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
setNestedProperty(v2Settings, newPath, val);
|
|
273
|
+
remainingKeys.delete(oldKey);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Carry over and deep-merge any remaining V2 containers or custom keys (V2 takes precedence over V1)
|
|
277
|
+
for (const key of remainingKeys) {
|
|
278
|
+
const newVal = rawSettings[key];
|
|
279
|
+
if (newVal === undefined)
|
|
280
|
+
continue;
|
|
281
|
+
const existingVal = v2Settings[key];
|
|
282
|
+
if (isPlainObject(existingVal) && isPlainObject(newVal)) {
|
|
283
|
+
v2Settings[key] = customDeepMerge((subPath) => getMergeStrategyForPath([key, ...subPath]), existingVal, newVal);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
v2Settings[key] = newVal;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Migrate deprecated nested boolean/property settings within V2 containers
|
|
290
|
+
const generalObj = v2Settings['general'];
|
|
291
|
+
if (isPlainObject(generalObj)) {
|
|
292
|
+
const checkpointingVal = generalObj['checkpointing'];
|
|
293
|
+
if (typeof checkpointingVal === 'boolean') {
|
|
294
|
+
generalObj['checkpointing'] = { enabled: checkpointingVal };
|
|
295
|
+
}
|
|
296
|
+
migrateInvertedBoolean(generalObj, 'disableAutoUpdate', 'enableAutoUpdate');
|
|
297
|
+
migrateInvertedBoolean(generalObj, 'disableUpdateNag', 'enableAutoUpdateNotification');
|
|
298
|
+
}
|
|
299
|
+
const uiObj = v2Settings['ui'];
|
|
300
|
+
if (isPlainObject(uiObj)) {
|
|
301
|
+
const accessibilityObj = uiObj['accessibility'];
|
|
302
|
+
if (isPlainObject(accessibilityObj)) {
|
|
303
|
+
migrateInvertedBoolean(accessibilityObj, 'disableLoadingPhrases', 'enableLoadingPhrases');
|
|
304
|
+
const enableLP = accessibilityObj['enableLoadingPhrases'];
|
|
305
|
+
if (typeof enableLP === 'boolean' &&
|
|
306
|
+
uiObj['loadingPhrases'] === undefined) {
|
|
307
|
+
if (!enableLP) {
|
|
308
|
+
uiObj['loadingPhrases'] = 'off';
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const contextObj = v2Settings['context'];
|
|
314
|
+
if (isPlainObject(contextObj)) {
|
|
315
|
+
const fileFilteringObj = contextObj['fileFiltering'];
|
|
316
|
+
if (isPlainObject(fileFilteringObj)) {
|
|
317
|
+
migrateInvertedBoolean(fileFilteringObj, 'disableFuzzySearch', 'enableFuzzySearch');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const toolsObj = v2Settings['tools'];
|
|
321
|
+
if (isPlainObject(toolsObj)) {
|
|
322
|
+
const approvalModeVal = toolsObj['approvalMode'];
|
|
323
|
+
if (approvalModeVal !== undefined) {
|
|
324
|
+
const targetGeneral = isPlainObject(v2Settings['general'])
|
|
325
|
+
? v2Settings['general']
|
|
326
|
+
: {};
|
|
327
|
+
if (targetGeneral['defaultApprovalMode'] === undefined) {
|
|
328
|
+
targetGeneral['defaultApprovalMode'] = approvalModeVal;
|
|
329
|
+
}
|
|
330
|
+
v2Settings['general'] = targetGeneral;
|
|
331
|
+
delete toolsObj['approvalMode'];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return v2Settings;
|
|
335
|
+
}
|
|
336
|
+
const booleanPreprocess = z.preprocess((val) => {
|
|
337
|
+
if (typeof val === 'string') {
|
|
338
|
+
const lower = val.toLowerCase();
|
|
339
|
+
if (lower === 'true')
|
|
340
|
+
return true;
|
|
341
|
+
if (lower === 'false')
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
return val;
|
|
345
|
+
}, z.boolean());
|
|
346
|
+
const numberPreprocess = z.preprocess((val) => {
|
|
347
|
+
if (typeof val === 'string' && val.trim() !== '') {
|
|
348
|
+
const num = Number(val);
|
|
349
|
+
if (!Number.isNaN(num))
|
|
350
|
+
return num;
|
|
351
|
+
}
|
|
352
|
+
return val;
|
|
353
|
+
}, z.number());
|
|
354
|
+
export const settingsZodSchema = z
|
|
355
|
+
.object({
|
|
356
|
+
mcpServers: z
|
|
357
|
+
.record(z.string(), z.record(z.string(), z.unknown()))
|
|
358
|
+
.optional(),
|
|
359
|
+
mcp: z
|
|
360
|
+
.object({
|
|
361
|
+
serverCommand: z.string().optional(),
|
|
362
|
+
allowed: z.array(z.string()).optional(),
|
|
363
|
+
excluded: z.array(z.string()).optional(),
|
|
364
|
+
})
|
|
365
|
+
.passthrough()
|
|
366
|
+
.optional(),
|
|
367
|
+
tools: z
|
|
368
|
+
.object({
|
|
369
|
+
allowed: z.array(z.string()).optional(),
|
|
370
|
+
exclude: z.array(z.string()).optional(),
|
|
371
|
+
core: z.array(z.string()).optional(),
|
|
372
|
+
autoAccept: booleanPreprocess.optional(),
|
|
373
|
+
sandbox: z
|
|
374
|
+
.union([
|
|
375
|
+
booleanPreprocess,
|
|
376
|
+
z.string(),
|
|
377
|
+
z.record(z.string(), z.unknown()),
|
|
378
|
+
])
|
|
379
|
+
.optional(),
|
|
380
|
+
enableHooks: booleanPreprocess.optional(),
|
|
381
|
+
discoveryCommand: z.string().optional(),
|
|
382
|
+
callCommand: z.string().optional(),
|
|
383
|
+
useRipgrep: booleanPreprocess.optional(),
|
|
384
|
+
shell: z
|
|
385
|
+
.object({
|
|
386
|
+
enableInteractiveShell: booleanPreprocess.optional(),
|
|
387
|
+
pager: z.string().optional(),
|
|
388
|
+
showColor: booleanPreprocess.optional(),
|
|
389
|
+
inactivityTimeout: numberPreprocess.optional(),
|
|
390
|
+
})
|
|
391
|
+
.passthrough()
|
|
392
|
+
.optional(),
|
|
393
|
+
})
|
|
394
|
+
.passthrough()
|
|
395
|
+
.optional(),
|
|
396
|
+
telemetry: z
|
|
397
|
+
.union([
|
|
398
|
+
booleanPreprocess,
|
|
399
|
+
z
|
|
400
|
+
.object({
|
|
401
|
+
enabled: booleanPreprocess.optional(),
|
|
402
|
+
target: z.string().optional(),
|
|
403
|
+
otlpEndpoint: z.string().optional(),
|
|
404
|
+
otlpProtocol: z.string().optional(),
|
|
405
|
+
logPrompts: booleanPreprocess.optional(),
|
|
406
|
+
outfile: z.string().optional(),
|
|
407
|
+
useCollector: booleanPreprocess.optional(),
|
|
408
|
+
})
|
|
409
|
+
.passthrough(),
|
|
410
|
+
])
|
|
411
|
+
.optional(),
|
|
412
|
+
logging: z
|
|
413
|
+
.object({
|
|
414
|
+
level: z.string().optional(),
|
|
415
|
+
})
|
|
416
|
+
.passthrough()
|
|
417
|
+
.optional(),
|
|
418
|
+
ui: z
|
|
419
|
+
.object({
|
|
420
|
+
theme: z.string().optional(),
|
|
421
|
+
showMemoryUsage: booleanPreprocess.optional(),
|
|
422
|
+
showLineNumbers: booleanPreprocess.optional(),
|
|
423
|
+
showCitations: booleanPreprocess.optional(),
|
|
424
|
+
showStatusInTitle: booleanPreprocess.optional(),
|
|
425
|
+
hideWindowTitle: booleanPreprocess.optional(),
|
|
426
|
+
hideTips: booleanPreprocess.optional(),
|
|
427
|
+
hideBanner: booleanPreprocess.optional(),
|
|
428
|
+
hideFooter: booleanPreprocess.optional(),
|
|
429
|
+
hideContextSummary: booleanPreprocess.optional(),
|
|
430
|
+
loadingPhrases: z.string().optional(),
|
|
431
|
+
customThemes: z.record(z.string(), z.unknown()).optional(),
|
|
432
|
+
customWittyPhrases: z.array(z.string()).optional(),
|
|
433
|
+
footer: z
|
|
434
|
+
.object({
|
|
435
|
+
hideCWD: booleanPreprocess.optional(),
|
|
436
|
+
hideSandboxStatus: booleanPreprocess.optional(),
|
|
437
|
+
hideModelInfo: booleanPreprocess.optional(),
|
|
438
|
+
})
|
|
439
|
+
.passthrough()
|
|
440
|
+
.optional(),
|
|
441
|
+
accessibility: z
|
|
442
|
+
.object({
|
|
443
|
+
enableLoadingPhrases: booleanPreprocess.optional(),
|
|
444
|
+
screenReader: booleanPreprocess.optional(),
|
|
445
|
+
})
|
|
446
|
+
.passthrough()
|
|
447
|
+
.optional(),
|
|
448
|
+
})
|
|
449
|
+
.passthrough()
|
|
450
|
+
.optional(),
|
|
451
|
+
general: z
|
|
452
|
+
.object({
|
|
453
|
+
previewFeatures: booleanPreprocess.optional(),
|
|
454
|
+
vimMode: booleanPreprocess.optional(),
|
|
455
|
+
preferredEditor: z.string().optional(),
|
|
456
|
+
checkpointing: z
|
|
457
|
+
.object({
|
|
458
|
+
enabled: booleanPreprocess.optional(),
|
|
459
|
+
})
|
|
460
|
+
.passthrough()
|
|
461
|
+
.optional(),
|
|
462
|
+
enableAutoUpdate: booleanPreprocess.optional(),
|
|
463
|
+
enableAutoUpdateNotification: booleanPreprocess.optional(),
|
|
464
|
+
defaultApprovalMode: z.string().optional(),
|
|
465
|
+
debugKeystrokeLogging: booleanPreprocess.optional(),
|
|
466
|
+
enablePromptCompletion: booleanPreprocess.optional(),
|
|
467
|
+
retryFetchErrors: booleanPreprocess.optional(),
|
|
468
|
+
plan: z
|
|
469
|
+
.object({
|
|
470
|
+
enabled: booleanPreprocess.optional(),
|
|
471
|
+
})
|
|
472
|
+
.passthrough()
|
|
473
|
+
.optional(),
|
|
474
|
+
})
|
|
475
|
+
.passthrough()
|
|
476
|
+
.optional(),
|
|
477
|
+
context: z
|
|
478
|
+
.object({
|
|
479
|
+
fileName: z.union([z.string(), z.array(z.string())]).optional(),
|
|
480
|
+
includeDirectories: z.array(z.string()).optional(),
|
|
481
|
+
loadFromIncludeDirectories: booleanPreprocess.optional(),
|
|
482
|
+
importFormat: z.string().optional(),
|
|
483
|
+
discoveryMaxDirs: numberPreprocess.optional(),
|
|
484
|
+
fileFiltering: z
|
|
485
|
+
.object({
|
|
486
|
+
respectGitIgnore: booleanPreprocess.optional(),
|
|
487
|
+
respectGeminiIgnore: booleanPreprocess.optional(),
|
|
488
|
+
enableRecursiveFileSearch: booleanPreprocess.optional(),
|
|
489
|
+
enableFuzzySearch: booleanPreprocess.optional(),
|
|
490
|
+
customIgnoreFilePaths: z.array(z.string()).optional(),
|
|
491
|
+
})
|
|
492
|
+
.passthrough()
|
|
493
|
+
.optional(),
|
|
494
|
+
})
|
|
495
|
+
.passthrough()
|
|
496
|
+
.optional(),
|
|
497
|
+
security: z
|
|
498
|
+
.object({
|
|
499
|
+
folderTrust: z
|
|
500
|
+
.object({
|
|
501
|
+
enabled: booleanPreprocess.optional(),
|
|
502
|
+
featureEnabled: booleanPreprocess.optional(),
|
|
503
|
+
})
|
|
504
|
+
.passthrough()
|
|
505
|
+
.optional(),
|
|
506
|
+
auth: z
|
|
507
|
+
.object({
|
|
508
|
+
selectedType: z.string().optional(),
|
|
509
|
+
enforcedType: z.string().optional(),
|
|
510
|
+
useExternal: booleanPreprocess.optional(),
|
|
511
|
+
})
|
|
512
|
+
.passthrough()
|
|
513
|
+
.optional(),
|
|
514
|
+
})
|
|
515
|
+
.passthrough()
|
|
516
|
+
.optional(),
|
|
517
|
+
model: z
|
|
518
|
+
.object({
|
|
519
|
+
name: z.string().optional(),
|
|
520
|
+
maxSessionTurns: numberPreprocess.optional(),
|
|
521
|
+
compressionThreshold: numberPreprocess.optional(),
|
|
522
|
+
skipNextSpeakerCheck: booleanPreprocess.optional(),
|
|
523
|
+
summarizeToolOutput: z.record(z.string(), z.unknown()).optional(),
|
|
524
|
+
})
|
|
525
|
+
.passthrough()
|
|
526
|
+
.optional(),
|
|
527
|
+
ide: z
|
|
528
|
+
.object({
|
|
529
|
+
enabled: booleanPreprocess.optional(),
|
|
530
|
+
hasSeenNudge: booleanPreprocess.optional(),
|
|
531
|
+
})
|
|
532
|
+
.passthrough()
|
|
533
|
+
.optional(),
|
|
534
|
+
privacy: z
|
|
535
|
+
.object({
|
|
536
|
+
usageStatisticsEnabled: booleanPreprocess.optional(),
|
|
537
|
+
})
|
|
538
|
+
.passthrough()
|
|
539
|
+
.optional(),
|
|
540
|
+
advanced: z
|
|
541
|
+
.object({
|
|
542
|
+
autoConfigureMemory: booleanPreprocess.optional(),
|
|
543
|
+
bugCommand: z.record(z.string(), z.unknown()).optional(),
|
|
544
|
+
dnsResolutionOrder: z.string().optional(),
|
|
545
|
+
excludedEnvVars: z.array(z.string()).optional(),
|
|
546
|
+
ignoreLocalEnv: booleanPreprocess.optional(),
|
|
547
|
+
})
|
|
548
|
+
.passthrough()
|
|
549
|
+
.optional(),
|
|
550
|
+
experimental: z
|
|
551
|
+
.object({
|
|
552
|
+
enableAgents: booleanPreprocess.optional(),
|
|
553
|
+
skills: booleanPreprocess.optional(),
|
|
554
|
+
extensionManagement: booleanPreprocess.optional(),
|
|
555
|
+
worktrees: booleanPreprocess.optional(),
|
|
556
|
+
})
|
|
557
|
+
.passthrough()
|
|
558
|
+
.optional(),
|
|
559
|
+
extensions: z.record(z.string(), z.unknown()).optional(),
|
|
560
|
+
agents: z.record(z.string(), z.unknown()).optional(),
|
|
561
|
+
policyPaths: z.array(z.string()).optional(),
|
|
562
|
+
adminPolicyPaths: z.array(z.string()).optional(),
|
|
563
|
+
})
|
|
564
|
+
.passthrough()
|
|
565
|
+
.superRefine((obj, ctx) => {
|
|
566
|
+
for (const key of Object.keys(obj)) {
|
|
567
|
+
if (LEGACY_V1_ONLY_KEYS.has(key)) {
|
|
568
|
+
ctx.addIssue({
|
|
569
|
+
code: z.ZodIssueCode.custom,
|
|
570
|
+
path: [key],
|
|
571
|
+
message: `Unmigrated V1 setting '${key}' is not valid at the root of V2 settings.`,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
export function validateSettings(data) {
|
|
577
|
+
const result = settingsZodSchema.safeParse(data);
|
|
578
|
+
if (result.success) {
|
|
579
|
+
return {
|
|
580
|
+
success: true,
|
|
28
581
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
29
|
-
|
|
30
|
-
|
|
582
|
+
data: result.data,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
success: false,
|
|
587
|
+
error: result.error,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function loadAndMigrateFile(filePath, settingsErrors) {
|
|
591
|
+
try {
|
|
592
|
+
if (fs.existsSync(filePath)) {
|
|
593
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
594
|
+
const parsed = JSON.parse(stripJsonComments(content));
|
|
595
|
+
if (!isPlainObject(parsed)) {
|
|
596
|
+
settingsErrors.push({
|
|
597
|
+
message: 'Settings file is not a valid JSON object.',
|
|
598
|
+
path: filePath,
|
|
599
|
+
});
|
|
600
|
+
return {};
|
|
601
|
+
}
|
|
602
|
+
const resolved = resolveEnvVarsInObject(parsed);
|
|
603
|
+
const migrated = migrateDeprecatedSettings(resolved);
|
|
604
|
+
const validation = validateSettings(migrated);
|
|
605
|
+
if (!validation.success && validation.error) {
|
|
606
|
+
settingsErrors.push({
|
|
607
|
+
message: validation.error.message,
|
|
608
|
+
path: filePath,
|
|
609
|
+
});
|
|
610
|
+
return migrated;
|
|
611
|
+
}
|
|
612
|
+
return validation.data ?? migrated;
|
|
31
613
|
}
|
|
32
614
|
}
|
|
33
615
|
catch (error) {
|
|
34
616
|
settingsErrors.push({
|
|
35
617
|
message: getErrorMessage(error),
|
|
36
|
-
path:
|
|
618
|
+
path: filePath,
|
|
37
619
|
});
|
|
38
620
|
}
|
|
621
|
+
return {};
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Loads settings from user and workspace directories.
|
|
625
|
+
* Project settings override user settings if the workspace is trusted.
|
|
626
|
+
*
|
|
627
|
+
* How is it different to gemini-cli/cli: Returns already merged settings rather
|
|
628
|
+
* than `LoadedSettings` (unnecessary since we are not modifying users
|
|
629
|
+
* settings.json).
|
|
630
|
+
*/
|
|
631
|
+
export function loadSettings(workspaceDir, isTrustedOverride) {
|
|
632
|
+
const settingsErrors = [];
|
|
633
|
+
const userSettings = loadAndMigrateFile(USER_SETTINGS_PATH, settingsErrors);
|
|
39
634
|
let isTrusted = isTrustedOverride;
|
|
40
635
|
if (isTrusted === undefined) {
|
|
636
|
+
const isFolderTrustEnabled = userSettings.security?.folderTrust?.enabled ?? true;
|
|
41
637
|
const { isTrusted: trustResult } = checkPathTrust({
|
|
42
638
|
path: workspaceDir,
|
|
43
|
-
isFolderTrustEnabled
|
|
639
|
+
isFolderTrustEnabled,
|
|
44
640
|
isHeadless: isHeadlessMode(),
|
|
45
641
|
});
|
|
46
642
|
isTrusted = trustResult ?? false;
|
|
47
643
|
}
|
|
48
644
|
const workspaceSettingsPath = path.join(workspaceDir, GEMINI_DIR, 'settings.json');
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (fs.existsSync(workspaceSettingsPath)) {
|
|
53
|
-
const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
|
|
54
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
55
|
-
const parsedWorkspaceSettings = JSON.parse(stripJsonComments(projectContent));
|
|
56
|
-
workspaceSettings = resolveEnvVarsInObject(parsedWorkspaceSettings);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
settingsErrors.push({
|
|
61
|
-
message: getErrorMessage(error),
|
|
62
|
-
path: workspaceSettingsPath,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
645
|
+
const workspaceSettings = isTrusted
|
|
646
|
+
? loadAndMigrateFile(workspaceSettingsPath, settingsErrors)
|
|
647
|
+
: {};
|
|
66
648
|
if (settingsErrors.length > 0) {
|
|
67
649
|
debugLogger.error('Errors loading settings:');
|
|
68
650
|
for (const error of settingsErrors) {
|
|
@@ -70,12 +652,11 @@ export function loadSettings(workspaceDir, isTrustedOverride) {
|
|
|
70
652
|
debugLogger.error(` Message: ${error.message}`);
|
|
71
653
|
}
|
|
72
654
|
}
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
};
|
|
655
|
+
const mergedSettings = customDeepMerge(getMergeStrategyForPath,
|
|
656
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
657
|
+
userSettings,
|
|
658
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
659
|
+
workspaceSettings);
|
|
79
660
|
// Security: ensure policyPaths and adminPolicyPaths are only loaded from trusted, user-level
|
|
80
661
|
// configuration and cannot be overridden by workspace-level settings, even if the
|
|
81
662
|
// workspace is trusted.
|
|
@@ -83,18 +664,27 @@ export function loadSettings(workspaceDir, isTrustedOverride) {
|
|
|
83
664
|
mergedSettings.adminPolicyPaths = userSettings.adminPolicyPaths;
|
|
84
665
|
return mergedSettings;
|
|
85
666
|
}
|
|
86
|
-
function resolveEnvVarsInString(value) {
|
|
87
|
-
const envVarRegex = /\$(?:(\w+)|{([^}]
|
|
88
|
-
return value.replace(envVarRegex, (match, varName1, varName2) => {
|
|
667
|
+
export function resolveEnvVarsInString(value) {
|
|
668
|
+
const envVarRegex = /\$(?:(\w+)|{([^}]+?)(?::-([^}]*))?})/g;
|
|
669
|
+
return value.replace(envVarRegex, (match, varName1, varName2, defaultValue) => {
|
|
89
670
|
const varName = varName1 || varName2;
|
|
671
|
+
if (!varName) {
|
|
672
|
+
return match;
|
|
673
|
+
}
|
|
90
674
|
const envValue = process?.env?.[varName];
|
|
91
675
|
if (typeof envValue === 'string') {
|
|
92
676
|
return envValue;
|
|
93
677
|
}
|
|
678
|
+
if (defaultValue !== undefined) {
|
|
679
|
+
return defaultValue;
|
|
680
|
+
}
|
|
94
681
|
return match;
|
|
95
682
|
});
|
|
96
683
|
}
|
|
97
|
-
function resolveEnvVarsInObject(obj) {
|
|
684
|
+
export function resolveEnvVarsInObject(obj) {
|
|
685
|
+
return resolveEnvVarsInObjectInternal(obj, new WeakSet());
|
|
686
|
+
}
|
|
687
|
+
function resolveEnvVarsInObjectInternal(obj, visited) {
|
|
98
688
|
if (obj === null ||
|
|
99
689
|
obj === undefined ||
|
|
100
690
|
typeof obj === 'boolean' ||
|
|
@@ -106,16 +696,35 @@ function resolveEnvVarsInObject(obj) {
|
|
|
106
696
|
return resolveEnvVarsInString(obj);
|
|
107
697
|
}
|
|
108
698
|
if (Array.isArray(obj)) {
|
|
109
|
-
|
|
110
|
-
|
|
699
|
+
if (visited.has(obj)) {
|
|
700
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
701
|
+
return [...obj];
|
|
702
|
+
}
|
|
703
|
+
visited.add(obj);
|
|
704
|
+
const mapped = obj.map((item) => resolveEnvVarsInObjectInternal(item, visited));
|
|
705
|
+
visited.delete(obj);
|
|
706
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
707
|
+
return mapped;
|
|
111
708
|
}
|
|
112
709
|
if (typeof obj === 'object') {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
710
|
+
if (!isPlainObject(obj)) {
|
|
711
|
+
return obj;
|
|
712
|
+
}
|
|
713
|
+
const objRef = obj;
|
|
714
|
+
if (visited.has(objRef)) {
|
|
715
|
+
return { ...obj };
|
|
716
|
+
}
|
|
717
|
+
visited.add(objRef);
|
|
718
|
+
const newObj = {};
|
|
719
|
+
const sourceRecord = obj;
|
|
720
|
+
for (const key of Object.keys(sourceRecord)) {
|
|
721
|
+
if (isUnsafeKey(key)) {
|
|
722
|
+
continue;
|
|
117
723
|
}
|
|
724
|
+
newObj[key] = resolveEnvVarsInObjectInternal(sourceRecord[key], visited);
|
|
118
725
|
}
|
|
726
|
+
visited.delete(objRef);
|
|
727
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
119
728
|
return newObj;
|
|
120
729
|
}
|
|
121
730
|
return obj;
|