@theia/ai-chat 1.71.0 → 1.72.0-next.11

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.
@@ -18,22 +18,30 @@ import { expect } from 'chai';
18
18
  import * as sinon from 'sinon';
19
19
  import { Container } from '@theia/core/shared/inversify';
20
20
  import { ToolConfirmationManager } from './chat-tool-preference-bindings';
21
- import { ToolConfirmationMode } from '../common/chat-tool-preferences';
21
+ import {
22
+ DEFAULT_TOOL_CONFIRMATION_PREFERENCE,
23
+ TOOL_CONFIRMATION_PREFERENCE,
24
+ ToolConfirmationMode
25
+ } from '../common/chat-tool-preferences';
22
26
  import { ToolRequest } from '@theia/ai-core';
23
27
  import { PreferenceService } from '@theia/core/lib/common/preferences';
24
28
  import { TrustAwarePreferenceReader } from '@theia/ai-core/lib/browser/trust-aware-preference-reader';
25
29
 
30
+ interface InspectResult<T> {
31
+ defaultValue?: T;
32
+ globalValue?: T;
33
+ workspaceValue?: T;
34
+ }
35
+
26
36
  describe('ToolConfirmationManager', () => {
27
37
  let manager: ToolConfirmationManager;
28
38
  let preferenceServiceMock: sinon.SinonStubbedInstance<PreferenceService>;
29
39
  let trustAwareReaderMock: sinon.SinonStubbedInstance<TrustAwarePreferenceReader>;
30
- let storedPreferences: { [toolId: string]: ToolConfirmationMode };
40
+ let storedPerToolPreferences: { [toolId: string]: ToolConfirmationMode };
41
+ let storedDefaultMode: ToolConfirmationMode | undefined;
31
42
  let trusted: boolean;
32
- let inspectResult: {
33
- defaultValue?: { [toolId: string]: ToolConfirmationMode };
34
- globalValue?: { [toolId: string]: ToolConfirmationMode };
35
- workspaceValue?: { [toolId: string]: ToolConfirmationMode };
36
- } | undefined;
43
+ let perToolInspectResult: InspectResult<{ [toolId: string]: ToolConfirmationMode }> | undefined;
44
+ let defaultInspectResult: InspectResult<ToolConfirmationMode> | undefined;
37
45
 
38
46
  const createToolRequest = (id: string, confirmAlwaysAllow?: boolean | string): ToolRequest => ({
39
47
  id,
@@ -44,26 +52,49 @@ describe('ToolConfirmationManager', () => {
44
52
  });
45
53
 
46
54
  beforeEach(() => {
47
- storedPreferences = {};
55
+ storedPerToolPreferences = {};
56
+ storedDefaultMode = undefined;
48
57
  trusted = true;
49
- inspectResult = undefined;
58
+ perToolInspectResult = undefined;
59
+ defaultInspectResult = undefined;
50
60
 
51
61
  preferenceServiceMock = {
52
- updateValue: sinon.stub().callsFake((_key: string, value: { [toolId: string]: ToolConfirmationMode }) => {
53
- storedPreferences = value;
62
+ updateValue: sinon.stub().callsFake((key: string, value: unknown) => {
63
+ if (key === TOOL_CONFIRMATION_PREFERENCE) {
64
+ storedPerToolPreferences = value as { [toolId: string]: ToolConfirmationMode };
65
+ } else if (key === DEFAULT_TOOL_CONFIRMATION_PREFERENCE) {
66
+ storedDefaultMode = value as ToolConfirmationMode;
67
+ }
54
68
  return Promise.resolve();
55
69
  }),
56
- inspect: sinon.stub().callsFake(() => inspectResult)
70
+ inspect: sinon.stub().callsFake((name: string) => {
71
+ if (name === TOOL_CONFIRMATION_PREFERENCE) {
72
+ return perToolInspectResult;
73
+ }
74
+ if (name === DEFAULT_TOOL_CONFIRMATION_PREFERENCE) {
75
+ return defaultInspectResult;
76
+ }
77
+ return undefined;
78
+ })
57
79
  } as unknown as sinon.SinonStubbedInstance<PreferenceService>;
58
80
 
59
81
  trustAwareReaderMock = {
60
- get: sinon.stub().callsFake(<T>(_name: string, fallback?: T): T | undefined => {
61
- if (trusted) {
62
- return (storedPreferences as unknown as T) ?? fallback;
82
+ get: sinon.stub().callsFake(<T>(name: string, fallback?: T): T | undefined => {
83
+ if (name === TOOL_CONFIRMATION_PREFERENCE) {
84
+ if (trusted) {
85
+ return (storedPerToolPreferences as unknown as T) ?? fallback;
86
+ }
87
+ const value = perToolInspectResult?.globalValue ?? perToolInspectResult?.defaultValue;
88
+ return ((value as unknown as T) ?? fallback);
89
+ }
90
+ if (name === DEFAULT_TOOL_CONFIRMATION_PREFERENCE) {
91
+ if (trusted) {
92
+ return (storedDefaultMode as unknown as T) ?? fallback;
93
+ }
94
+ const value = defaultInspectResult?.globalValue ?? defaultInspectResult?.defaultValue;
95
+ return ((value as unknown as T) ?? fallback);
63
96
  }
64
- const insp = inspectResult;
65
- const value = insp?.globalValue ?? insp?.defaultValue;
66
- return ((value as unknown as T) ?? fallback);
97
+ return fallback;
67
98
  })
68
99
  } as unknown as sinon.SinonStubbedInstance<TrustAwarePreferenceReader>;
69
100
 
@@ -74,81 +105,115 @@ describe('ToolConfirmationManager', () => {
74
105
  manager = container.get(ToolConfirmationManager);
75
106
  });
76
107
 
108
+ describe('getDefaultConfirmationMode', () => {
109
+ it('returns CONFIRM when nothing is set', () => {
110
+ expect(manager.getDefaultConfirmationMode()).to.equal(ToolConfirmationMode.CONFIRM);
111
+ });
112
+
113
+ it('returns the value stored in the user preference', () => {
114
+ storedDefaultMode = ToolConfirmationMode.ALWAYS_ALLOW;
115
+ expect(manager.getDefaultConfirmationMode()).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
116
+ });
117
+
118
+ it('falls back to the schema-level default when no user value is set', () => {
119
+ defaultInspectResult = { defaultValue: ToolConfirmationMode.DISABLED };
120
+ expect(manager.getDefaultConfirmationMode()).to.equal(ToolConfirmationMode.DISABLED);
121
+ });
122
+ });
123
+
124
+ describe('setDefaultConfirmationMode', () => {
125
+ it('persists the new default through the preference service', () => {
126
+ manager.setDefaultConfirmationMode(ToolConfirmationMode.ALWAYS_ALLOW);
127
+ expect(preferenceServiceMock.updateValue.calledOnce).to.be.true;
128
+ expect(preferenceServiceMock.updateValue.firstCall.args[0]).to.equal(DEFAULT_TOOL_CONFIRMATION_PREFERENCE);
129
+ expect(preferenceServiceMock.updateValue.firstCall.args[1]).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
130
+ expect(storedDefaultMode).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
131
+ });
132
+ });
133
+
77
134
  describe('getConfirmationMode', () => {
78
- it('should return ALWAYS_ALLOW for regular tools by default', () => {
135
+ it('returns CONFIRM for regular tools by default', () => {
79
136
  const mode = manager.getConfirmationMode('regularTool', 'chat-1');
80
- expect(mode).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
137
+ expect(mode).to.equal(ToolConfirmationMode.CONFIRM);
81
138
  });
82
139
 
83
- it('should return CONFIRM for confirmAlwaysAllow tools by default', () => {
140
+ it('returns CONFIRM for confirmAlwaysAllow tools by default', () => {
84
141
  const toolRequest = createToolRequest('dangerousTool', true);
85
142
  const mode = manager.getConfirmationMode('dangerousTool', 'chat-1', toolRequest);
86
143
  expect(mode).to.equal(ToolConfirmationMode.CONFIRM);
87
144
  });
88
145
 
89
- it('should return tool-specific preference when set', () => {
90
- storedPreferences['myTool'] = ToolConfirmationMode.DISABLED;
146
+ it('returns the global default for regular tools when set', () => {
147
+ storedDefaultMode = ToolConfirmationMode.ALWAYS_ALLOW;
148
+ const mode = manager.getConfirmationMode('regularTool', 'chat-1');
149
+ expect(mode).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
150
+ });
151
+
152
+ it('returns tool-specific preference when set', () => {
153
+ storedPerToolPreferences['myTool'] = ToolConfirmationMode.DISABLED;
91
154
  const mode = manager.getConfirmationMode('myTool', 'chat-1');
92
155
  expect(mode).to.equal(ToolConfirmationMode.DISABLED);
93
156
  });
94
157
 
95
- it('should return session override when set', () => {
158
+ it('returns session override when set', () => {
96
159
  manager.setSessionConfirmationMode('myTool', ToolConfirmationMode.ALWAYS_ALLOW, 'chat-1');
97
160
  const mode = manager.getConfirmationMode('myTool', 'chat-1');
98
161
  expect(mode).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
99
162
  });
100
163
 
101
- it('should not inherit global ALWAYS_ALLOW for confirmAlwaysAllow tools', () => {
102
- storedPreferences['*'] = ToolConfirmationMode.ALWAYS_ALLOW;
164
+ it('does not inherit global ALWAYS_ALLOW for confirmAlwaysAllow tools', () => {
165
+ storedDefaultMode = ToolConfirmationMode.ALWAYS_ALLOW;
103
166
  const toolRequest = createToolRequest('dangerousTool', true);
104
167
  const mode = manager.getConfirmationMode('dangerousTool', 'chat-1', toolRequest);
105
168
  expect(mode).to.equal(ToolConfirmationMode.CONFIRM);
106
169
  });
107
170
 
108
- it('should inherit global DISABLED for confirmAlwaysAllow tools', () => {
109
- storedPreferences['*'] = ToolConfirmationMode.DISABLED;
171
+ it('inherits global DISABLED for confirmAlwaysAllow tools', () => {
172
+ storedDefaultMode = ToolConfirmationMode.DISABLED;
110
173
  const toolRequest = createToolRequest('dangerousTool', true);
111
174
  const mode = manager.getConfirmationMode('dangerousTool', 'chat-1', toolRequest);
112
175
  expect(mode).to.equal(ToolConfirmationMode.DISABLED);
113
176
  });
177
+
178
+ it('respects an explicit per-tool ALWAYS_ALLOW for confirmAlwaysAllow tools', () => {
179
+ storedPerToolPreferences['dangerousTool'] = ToolConfirmationMode.ALWAYS_ALLOW;
180
+ const toolRequest = createToolRequest('dangerousTool', true);
181
+ const mode = manager.getConfirmationMode('dangerousTool', 'chat-1', toolRequest);
182
+ expect(mode).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
183
+ });
114
184
  });
115
185
 
116
186
  describe('workspace trust', () => {
117
- it('ignores workspace {"*": "always_allow"} when workspace is untrusted', () => {
187
+ it('ignores a workspace-scoped default of ALWAYS_ALLOW when workspace is untrusted', () => {
118
188
  trusted = false;
119
- // Simulate the effective (workspace-merged) preference containing always_allow,
120
- // while the user/global scope is not set and the schema default prescribes
121
- // CONFIRM. If the workspace override were honoured the mode would be
122
- // ALWAYS_ALLOW; because trust filters the workspace scope out, the default
123
- // (CONFIRM) wins, which demonstrably proves the override was dropped.
124
- storedPreferences['*'] = ToolConfirmationMode.ALWAYS_ALLOW;
125
- inspectResult = {
126
- defaultValue: { '*': ToolConfirmationMode.CONFIRM },
127
- workspaceValue: { '*': ToolConfirmationMode.ALWAYS_ALLOW }
189
+ // Simulate the effective (workspace-merged) default being ALWAYS_ALLOW while
190
+ // the user/global scope is unset and the schema default is CONFIRM. With trust
191
+ // disabled the workspace value must be dropped, so CONFIRM wins.
192
+ storedDefaultMode = ToolConfirmationMode.ALWAYS_ALLOW;
193
+ defaultInspectResult = {
194
+ defaultValue: ToolConfirmationMode.CONFIRM,
195
+ workspaceValue: ToolConfirmationMode.ALWAYS_ALLOW
128
196
  };
129
197
 
130
198
  const mode = manager.getConfirmationMode('someTool', 'chat-1');
131
199
  expect(mode).to.equal(ToolConfirmationMode.CONFIRM);
132
200
  });
133
201
 
134
- it('blocks confirmAlwaysAllow bypass via workspace {"*": "always_allow"} when untrusted', () => {
202
+ it('blocks confirmAlwaysAllow bypass via workspace ALWAYS_ALLOW default when untrusted', () => {
135
203
  trusted = false;
136
- storedPreferences['*'] = ToolConfirmationMode.ALWAYS_ALLOW;
137
- inspectResult = {
138
- defaultValue: {},
139
- workspaceValue: { '*': ToolConfirmationMode.ALWAYS_ALLOW }
204
+ storedDefaultMode = ToolConfirmationMode.ALWAYS_ALLOW;
205
+ defaultInspectResult = {
206
+ workspaceValue: ToolConfirmationMode.ALWAYS_ALLOW
140
207
  };
141
208
 
142
209
  const toolRequest = createToolRequest('dangerousTool', true);
143
210
  const mode = manager.getConfirmationMode('dangerousTool', 'chat-1', toolRequest);
144
- // Without the workspace value the map is empty, so the default for a
145
- // confirmAlwaysAllow tool (CONFIRM) is used.
146
211
  expect(mode).to.equal(ToolConfirmationMode.CONFIRM);
147
212
  });
148
213
 
149
- it('honours workspace {"*": "always_allow"} when workspace is trusted', () => {
214
+ it('honours a workspace-scoped ALWAYS_ALLOW default when workspace is trusted', () => {
150
215
  trusted = true;
151
- storedPreferences['*'] = ToolConfirmationMode.ALWAYS_ALLOW;
216
+ storedDefaultMode = ToolConfirmationMode.ALWAYS_ALLOW;
152
217
 
153
218
  const mode = manager.getConfirmationMode('regularTool', 'chat-1');
154
219
  expect(mode).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
@@ -156,139 +221,164 @@ describe('ToolConfirmationManager', () => {
156
221
  });
157
222
 
158
223
  describe('setConfirmationMode', () => {
159
- it('should persist ALWAYS_ALLOW for regular tools when different from default', () => {
160
- storedPreferences['*'] = ToolConfirmationMode.CONFIRM;
224
+ it('persists ALWAYS_ALLOW for a regular tool when default is CONFIRM', () => {
161
225
  manager.setConfirmationMode('regularTool', ToolConfirmationMode.ALWAYS_ALLOW);
162
226
  expect(preferenceServiceMock.updateValue.calledOnce).to.be.true;
163
- expect(storedPreferences['regularTool']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
227
+ expect(storedPerToolPreferences['regularTool']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
164
228
  });
165
229
 
166
- it('should persist ALWAYS_ALLOW for confirmAlwaysAllow tools', () => {
230
+ it('persists ALWAYS_ALLOW for confirmAlwaysAllow tools', () => {
167
231
  const toolRequest = createToolRequest('dangerousTool', true);
168
232
  manager.setConfirmationMode('dangerousTool', ToolConfirmationMode.ALWAYS_ALLOW, toolRequest);
169
233
  expect(preferenceServiceMock.updateValue.calledOnce).to.be.true;
170
- expect(storedPreferences['dangerousTool']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
234
+ expect(storedPerToolPreferences['dangerousTool']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
171
235
  });
172
236
 
173
- it('should not persist ALWAYS_ALLOW for regular tools when it matches default', () => {
237
+ it('does not persist when mode matches the global default', () => {
238
+ storedDefaultMode = ToolConfirmationMode.ALWAYS_ALLOW;
174
239
  manager.setConfirmationMode('regularTool', ToolConfirmationMode.ALWAYS_ALLOW);
175
240
  expect(preferenceServiceMock.updateValue.called).to.be.false;
176
241
  });
177
242
 
178
- it('should not persist when mode matches the global preference default', () => {
179
- inspectResult = {
180
- defaultValue: { '*': ToolConfirmationMode.CONFIRM }
181
- };
243
+ it('does not persist CONFIRM for a regular tool when default is CONFIRM', () => {
182
244
  manager.setConfirmationMode('regularTool', ToolConfirmationMode.CONFIRM);
183
245
  expect(preferenceServiceMock.updateValue.called).to.be.false;
184
246
  });
185
247
 
186
- it('should persist when mode differs from the global preference default', () => {
187
- inspectResult = {
188
- defaultValue: { '*': ToolConfirmationMode.CONFIRM }
189
- };
248
+ it('persists DISABLED when default is CONFIRM', () => {
190
249
  manager.setConfirmationMode('regularTool', ToolConfirmationMode.DISABLED);
191
250
  expect(preferenceServiceMock.updateValue.calledOnce).to.be.true;
192
- expect(storedPreferences['regularTool']).to.equal(ToolConfirmationMode.DISABLED);
251
+ expect(storedPerToolPreferences['regularTool']).to.equal(ToolConfirmationMode.DISABLED);
193
252
  });
194
253
 
195
- it('should not persist when mode matches the tool-specific preference default', () => {
196
- inspectResult = {
254
+ it('does not persist when mode matches a tool-specific schema default', () => {
255
+ perToolInspectResult = {
197
256
  defaultValue: { 'myTool': ToolConfirmationMode.DISABLED }
198
257
  };
199
258
  manager.setConfirmationMode('myTool', ToolConfirmationMode.DISABLED);
200
259
  expect(preferenceServiceMock.updateValue.called).to.be.false;
201
260
  });
202
261
 
203
- it('should remove entry when mode matches the tool-specific preference default and entry exists', () => {
204
- inspectResult = {
262
+ it('removes an existing entry when mode matches the tool-specific schema default', () => {
263
+ perToolInspectResult = {
205
264
  defaultValue: { 'myTool': ToolConfirmationMode.DISABLED }
206
265
  };
207
- storedPreferences['myTool'] = ToolConfirmationMode.ALWAYS_ALLOW;
266
+ storedPerToolPreferences['myTool'] = ToolConfirmationMode.ALWAYS_ALLOW;
208
267
  manager.setConfirmationMode('myTool', ToolConfirmationMode.DISABLED);
209
268
  expect(preferenceServiceMock.updateValue.calledOnce).to.be.true;
210
- expect(storedPreferences['myTool']).to.be.undefined;
269
+ expect(storedPerToolPreferences['myTool']).to.be.undefined;
211
270
  });
212
271
 
213
- it('should not persist CONFIRM for confirmAlwaysAllow tool when global preference default is CONFIRM', () => {
214
- inspectResult = {
215
- defaultValue: { '*': ToolConfirmationMode.CONFIRM }
216
- };
272
+ it('does not persist CONFIRM for confirmAlwaysAllow tools (matches effective default)', () => {
217
273
  const toolRequest = createToolRequest('dangerousTool', true);
218
274
  manager.setConfirmationMode('dangerousTool', ToolConfirmationMode.CONFIRM, toolRequest);
219
275
  expect(preferenceServiceMock.updateValue.called).to.be.false;
220
276
  });
221
277
 
222
- it('should persist ALWAYS_ALLOW for confirmAlwaysAllow tool when global preference default is CONFIRM', () => {
223
- inspectResult = {
224
- defaultValue: { '*': ToolConfirmationMode.CONFIRM }
225
- };
278
+ it('persists ALWAYS_ALLOW for a confirmAlwaysAllow tool when global default is ALWAYS_ALLOW', () => {
279
+ // The effective default for confirmAlwaysAllow tools is CONFIRM even when
280
+ // the global default is ALWAYS_ALLOW, so ALWAYS_ALLOW must still be persisted.
281
+ storedDefaultMode = ToolConfirmationMode.ALWAYS_ALLOW;
226
282
  const toolRequest = createToolRequest('dangerousTool', true);
227
283
  manager.setConfirmationMode('dangerousTool', ToolConfirmationMode.ALWAYS_ALLOW, toolRequest);
228
284
  expect(preferenceServiceMock.updateValue.calledOnce).to.be.true;
229
- expect(storedPreferences['dangerousTool']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
285
+ expect(storedPerToolPreferences['dangerousTool']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
230
286
  });
231
287
 
232
- it('should remove entry when setting mode that matches effective default', () => {
233
- storedPreferences['regularTool'] = ToolConfirmationMode.CONFIRM;
234
- manager.setConfirmationMode('regularTool', ToolConfirmationMode.ALWAYS_ALLOW);
288
+ it('removes an existing entry when setting CONFIRM for a confirmAlwaysAllow tool', () => {
289
+ const toolRequest = createToolRequest('dangerousTool', true);
290
+ storedPerToolPreferences['dangerousTool'] = ToolConfirmationMode.ALWAYS_ALLOW;
291
+ manager.setConfirmationMode('dangerousTool', ToolConfirmationMode.CONFIRM, toolRequest);
235
292
  expect(preferenceServiceMock.updateValue.calledOnce).to.be.true;
236
- expect(storedPreferences['regularTool']).to.be.undefined;
293
+ expect(storedPerToolPreferences['dangerousTool']).to.be.undefined;
237
294
  });
238
295
 
239
- it('should persist DISABLED for any tool', () => {
296
+ it('persists DISABLED for any tool when default is CONFIRM', () => {
240
297
  manager.setConfirmationMode('anyTool', ToolConfirmationMode.DISABLED);
241
298
  expect(preferenceServiceMock.updateValue.calledOnce).to.be.true;
242
- expect(storedPreferences['anyTool']).to.equal(ToolConfirmationMode.DISABLED);
243
- });
244
-
245
- it('should remove entry when setting CONFIRM for confirmAlwaysAllow tools (matches effective default)', () => {
246
- const toolRequest = createToolRequest('dangerousTool', true);
247
- storedPreferences['dangerousTool'] = ToolConfirmationMode.ALWAYS_ALLOW;
248
- manager.setConfirmationMode('dangerousTool', ToolConfirmationMode.CONFIRM, toolRequest);
249
- expect(preferenceServiceMock.updateValue.calledOnce).to.be.true;
250
- expect(storedPreferences['dangerousTool']).to.be.undefined;
299
+ expect(storedPerToolPreferences['anyTool']).to.equal(ToolConfirmationMode.DISABLED);
251
300
  });
252
301
  });
253
302
 
254
303
  describe('setSessionConfirmationMode', () => {
255
- it('should set session override for specific chat', () => {
304
+ it('sets a session override for a specific chat', () => {
256
305
  manager.setSessionConfirmationMode('myTool', ToolConfirmationMode.ALWAYS_ALLOW, 'chat-1');
257
306
  expect(manager.getConfirmationMode('myTool', 'chat-1')).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
258
- expect(manager.getConfirmationMode('myTool', 'chat-2')).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
307
+ expect(manager.getConfirmationMode('myTool', 'chat-2')).to.equal(ToolConfirmationMode.CONFIRM);
259
308
  });
260
309
 
261
- it('should prioritize session override over persisted preference', () => {
262
- storedPreferences['myTool'] = ToolConfirmationMode.DISABLED;
310
+ it('prioritizes session override over persisted preference', () => {
311
+ storedPerToolPreferences['myTool'] = ToolConfirmationMode.DISABLED;
263
312
  manager.setSessionConfirmationMode('myTool', ToolConfirmationMode.ALWAYS_ALLOW, 'chat-1');
264
313
  expect(manager.getConfirmationMode('myTool', 'chat-1')).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
265
314
  });
266
315
  });
267
316
 
268
317
  describe('clearSessionOverrides', () => {
269
- it('should clear overrides for specific chat', () => {
318
+ it('clears overrides for a specific chat', () => {
270
319
  manager.setSessionConfirmationMode('myTool', ToolConfirmationMode.ALWAYS_ALLOW, 'chat-1');
271
320
  manager.setSessionConfirmationMode('myTool', ToolConfirmationMode.DISABLED, 'chat-2');
272
321
 
273
322
  manager.clearSessionOverrides('chat-1');
274
323
 
275
- expect(manager.getConfirmationMode('myTool', 'chat-1')).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
324
+ expect(manager.getConfirmationMode('myTool', 'chat-1')).to.equal(ToolConfirmationMode.CONFIRM);
276
325
  expect(manager.getConfirmationMode('myTool', 'chat-2')).to.equal(ToolConfirmationMode.DISABLED);
277
326
  });
278
327
 
279
- it('should clear all overrides when no chatId provided', () => {
328
+ it('clears all overrides when no chatId is given', () => {
280
329
  manager.setSessionConfirmationMode('myTool', ToolConfirmationMode.ALWAYS_ALLOW, 'chat-1');
281
330
  manager.setSessionConfirmationMode('myTool', ToolConfirmationMode.DISABLED, 'chat-2');
282
331
 
283
332
  manager.clearSessionOverrides();
284
333
 
285
- expect(manager.getConfirmationMode('myTool', 'chat-1')).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
286
- expect(manager.getConfirmationMode('myTool', 'chat-2')).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
334
+ expect(manager.getConfirmationMode('myTool', 'chat-1')).to.equal(ToolConfirmationMode.CONFIRM);
335
+ expect(manager.getConfirmationMode('myTool', 'chat-2')).to.equal(ToolConfirmationMode.CONFIRM);
336
+ });
337
+ });
338
+
339
+ describe('getAllConfirmationSettings', () => {
340
+ it('returns the per-tool record from the trust-aware reader', () => {
341
+ storedPerToolPreferences['toolA'] = ToolConfirmationMode.ALWAYS_ALLOW;
342
+ storedPerToolPreferences['toolB'] = ToolConfirmationMode.DISABLED;
343
+
344
+ const settings = manager.getAllConfirmationSettings();
345
+
346
+ expect(settings).to.deep.equal({
347
+ toolA: ToolConfirmationMode.ALWAYS_ALLOW,
348
+ toolB: ToolConfirmationMode.DISABLED
349
+ });
350
+ });
351
+
352
+ it('returns an empty record when no tool-specific entries are configured', () => {
353
+ const settings = manager.getAllConfirmationSettings();
354
+ expect(settings).to.deep.equal({});
355
+ });
356
+ });
357
+
358
+ describe('resetAllConfirmationModeSettings', () => {
359
+ it('clears all per-tool entries', () => {
360
+ storedPerToolPreferences['toolA'] = ToolConfirmationMode.ALWAYS_ALLOW;
361
+ storedPerToolPreferences['toolB'] = ToolConfirmationMode.DISABLED;
362
+
363
+ manager.resetAllConfirmationModeSettings();
364
+
365
+ expect(preferenceServiceMock.updateValue.calledOnce).to.be.true;
366
+ expect(preferenceServiceMock.updateValue.firstCall.args[0]).to.equal(TOOL_CONFIRMATION_PREFERENCE);
367
+ expect(preferenceServiceMock.updateValue.firstCall.args[1]).to.deep.equal({});
368
+ });
369
+
370
+ it('does not modify the default-confirmation preference', () => {
371
+ storedDefaultMode = ToolConfirmationMode.ALWAYS_ALLOW;
372
+ storedPerToolPreferences['toolA'] = ToolConfirmationMode.DISABLED;
373
+
374
+ manager.resetAllConfirmationModeSettings();
375
+
376
+ expect(storedDefaultMode).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
287
377
  });
288
378
  });
289
379
 
290
380
  describe('confirmAlwaysAllow tools - "Always Approve" workflow', () => {
291
- it('should persist "Always Allow" for confirmAlwaysAllow tools', () => {
381
+ it('persists "Always Allow" for confirmAlwaysAllow tools', () => {
292
382
  const toolRequest = createToolRequest('shellExecute', 'This tool has full system access.');
293
383
 
294
384
  let mode = manager.getConfirmationMode('shellExecute', 'chat-1', toolRequest);
@@ -297,19 +387,19 @@ describe('ToolConfirmationManager', () => {
297
387
  manager.setConfirmationMode('shellExecute', ToolConfirmationMode.ALWAYS_ALLOW, toolRequest);
298
388
 
299
389
  expect(preferenceServiceMock.updateValue.calledOnce).to.be.true;
300
- expect(storedPreferences['shellExecute']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
390
+ expect(storedPerToolPreferences['shellExecute']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
301
391
 
302
392
  mode = manager.getConfirmationMode('shellExecute', 'chat-1', toolRequest);
303
393
  expect(mode).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
304
394
  });
305
395
 
306
- it('should persist "Disabled" for confirmAlwaysAllow tools', () => {
396
+ it('persists "Disabled" for confirmAlwaysAllow tools', () => {
307
397
  const toolRequest = createToolRequest('shellExecute', true);
308
398
 
309
399
  manager.setConfirmationMode('shellExecute', ToolConfirmationMode.DISABLED, toolRequest);
310
400
 
311
401
  expect(preferenceServiceMock.updateValue.calledOnce).to.be.true;
312
- expect(storedPreferences['shellExecute']).to.equal(ToolConfirmationMode.DISABLED);
402
+ expect(storedPerToolPreferences['shellExecute']).to.equal(ToolConfirmationMode.DISABLED);
313
403
 
314
404
  const mode = manager.getConfirmationMode('shellExecute', 'chat-1', toolRequest);
315
405
  expect(mode).to.equal(ToolConfirmationMode.DISABLED);
@@ -18,7 +18,11 @@ import { injectable, inject } from '@theia/core/shared/inversify';
18
18
  import {
19
19
  PreferenceService,
20
20
  } from '@theia/core/lib/common/preferences';
21
- import { ToolConfirmationMode, TOOL_CONFIRMATION_PREFERENCE } from '../common/chat-tool-preferences';
21
+ import {
22
+ ToolConfirmationMode,
23
+ TOOL_CONFIRMATION_PREFERENCE,
24
+ DEFAULT_TOOL_CONFIRMATION_PREFERENCE
25
+ } from '../common/chat-tool-preferences';
22
26
  import { ToolRequest } from '@theia/ai-core';
23
27
  import { TrustAwarePreferenceReader } from '@theia/ai-core/lib/browser/trust-aware-preference-reader';
24
28
 
@@ -36,12 +40,33 @@ export class ToolConfirmationManager {
36
40
  // In-memory session overrides (not persisted), per chat
37
41
  protected sessionOverrides: Map<string, Map<string, ToolConfirmationMode>> = new Map();
38
42
 
43
+ /**
44
+ * Get the global default confirmation mode (used when no tool-specific entry exists).
45
+ *
46
+ * Read through the trust-aware reader so that an untrusted workspace cannot override
47
+ * the default to a more permissive value.
48
+ */
49
+ getDefaultConfirmationMode(): ToolConfirmationMode {
50
+ const value = this.trustAwareReader.get<ToolConfirmationMode>(DEFAULT_TOOL_CONFIRMATION_PREFERENCE);
51
+ return value ?? this.getDefaultPreferenceSchemaDefault();
52
+ }
53
+
54
+ /**
55
+ * Set the global default confirmation mode.
56
+ *
57
+ * Returns the promise produced by the underlying preference update so callers can
58
+ * `await` completion and react to errors (e.g. show a notification on failure).
59
+ */
60
+ setDefaultConfirmationMode(mode: ToolConfirmationMode): Promise<void> {
61
+ return this.preferenceService.updateValue(DEFAULT_TOOL_CONFIRMATION_PREFERENCE, mode);
62
+ }
63
+
39
64
  /**
40
65
  * Get the confirmation mode for a specific tool, considering session overrides first (per chat).
41
66
  *
42
67
  * For tools with `confirmAlwaysAllow` flag:
43
- * - They default to CONFIRM mode instead of ALWAYS_ALLOW
44
- * - They don't inherit global ALWAYS_ALLOW from the '*' preference
68
+ * - They default to CONFIRM mode instead of inheriting ALWAYS_ALLOW from the global default.
69
+ * - Tool-specific preference entries are still respected (informed user consent).
45
70
  *
46
71
  * @param toolId - The tool identifier
47
72
  * @param chatId - The chat session identifier
@@ -55,21 +80,15 @@ export class ToolConfirmationManager {
55
80
  const toolConfirmation = this.trustAwareReader.get<Record<string, ToolConfirmationMode>>(
56
81
  TOOL_CONFIRMATION_PREFERENCE, {}
57
82
  ) ?? {};
58
- if (toolConfirmation[toolId]) {
83
+ if (toolId in toolConfirmation) {
59
84
  return toolConfirmation[toolId];
60
85
  }
61
- if (toolConfirmation['*']) {
62
- // For confirmAlwaysAllow tools, don't inherit global ALWAYS_ALLOW
63
- if (toolRequest?.confirmAlwaysAllow && toolConfirmation['*'] === ToolConfirmationMode.ALWAYS_ALLOW) {
64
- return ToolConfirmationMode.CONFIRM;
65
- }
66
- return toolConfirmation['*'];
86
+ const defaultMode = this.getDefaultConfirmationMode();
87
+ // For confirmAlwaysAllow tools, don't inherit a global ALWAYS_ALLOW default
88
+ if (toolRequest?.confirmAlwaysAllow && defaultMode === ToolConfirmationMode.ALWAYS_ALLOW) {
89
+ return ToolConfirmationMode.CONFIRM;
67
90
  }
68
-
69
- // Default: ALWAYS_ALLOW for normal tools, CONFIRM for confirmAlwaysAllow tools
70
- return toolRequest?.confirmAlwaysAllow
71
- ? ToolConfirmationMode.CONFIRM
72
- : ToolConfirmationMode.ALWAYS_ALLOW;
91
+ return defaultMode;
73
92
  }
74
93
 
75
94
  /**
@@ -79,30 +98,20 @@ export class ToolConfirmationManager {
79
98
  * @param mode - The confirmation mode to set
80
99
  * @param toolRequest - Optional ToolRequest to check for confirmAlwaysAllow flag
81
100
  */
82
- setConfirmationMode(toolId: string, mode: ToolConfirmationMode, toolRequest?: ToolRequest): void {
83
- const defaultPref = this.preferenceService.inspect(TOOL_CONFIRMATION_PREFERENCE)?.defaultValue as {
84
- [toolId: string]: ToolConfirmationMode;
85
- } || {};
101
+ setConfirmationMode(toolId: string, mode: ToolConfirmationMode, toolRequest?: ToolRequest): Promise<void> {
86
102
  const current = this.trustAwareReader.get<Record<string, ToolConfirmationMode>>(
87
103
  TOOL_CONFIRMATION_PREFERENCE, {}
88
104
  ) ?? {};
89
- let starMode = current['*'];
90
- if (starMode === undefined) {
91
- starMode = defaultPref['*'] ?? ToolConfirmationMode.ALWAYS_ALLOW;
92
- }
93
- // For confirmAlwaysAllow tools, the effective default is CONFIRM, not ALWAYS_ALLOW
94
- const effectiveDefault = (toolRequest?.confirmAlwaysAllow && starMode === ToolConfirmationMode.ALWAYS_ALLOW)
95
- ? ToolConfirmationMode.CONFIRM
96
- : defaultPref[toolId] ?? starMode;
105
+ const effectiveDefault = this.computeEffectiveDefaultForTool(toolId, toolRequest);
97
106
  if (mode === effectiveDefault) {
98
107
  if (toolId in current) {
99
108
  const { [toolId]: _, ...rest } = current;
100
- this.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, rest);
109
+ return this.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, rest);
101
110
  }
102
- } else {
103
- const updated = { ...current, [toolId]: mode };
104
- this.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, updated);
111
+ return Promise.resolve();
105
112
  }
113
+ const updated = { ...current, [toolId]: mode };
114
+ return this.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, updated);
106
115
  }
107
116
 
108
117
  /**
@@ -137,14 +146,37 @@ export class ToolConfirmationManager {
137
146
  ) ?? {};
138
147
  }
139
148
 
140
- resetAllConfirmationModeSettings(): void {
141
- const current = this.trustAwareReader.get<Record<string, ToolConfirmationMode>>(
142
- TOOL_CONFIRMATION_PREFERENCE, {}
143
- ) ?? {};
144
- if ('*' in current) {
145
- this.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, { '*': current['*'] });
146
- } else {
147
- this.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, {});
149
+ resetAllConfirmationModeSettings(): Promise<void> {
150
+ return this.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, {});
151
+ }
152
+
153
+ /**
154
+ * Compute the effective default for a given tool, taking the schema-level default,
155
+ * any product-shipped per-tool default, and the confirmAlwaysAllow flag into account.
156
+ */
157
+ protected computeEffectiveDefaultForTool(toolId: string, toolRequest?: ToolRequest): ToolConfirmationMode {
158
+ const perToolDefaults = this.preferenceService.inspect(TOOL_CONFIRMATION_PREFERENCE)?.defaultValue as
159
+ | { [toolId: string]: ToolConfirmationMode }
160
+ | undefined;
161
+ const perToolDefault = perToolDefaults?.[toolId];
162
+ if (perToolDefault) {
163
+ return perToolDefault;
148
164
  }
165
+ const globalDefault = this.getDefaultConfirmationMode();
166
+ if (toolRequest?.confirmAlwaysAllow && globalDefault === ToolConfirmationMode.ALWAYS_ALLOW) {
167
+ return ToolConfirmationMode.CONFIRM;
168
+ }
169
+ return globalDefault;
170
+ }
171
+
172
+ /**
173
+ * Read the schema-level default for the default-confirmation preference.
174
+ * Falls back to CONFIRM if the preference service has not registered the schema yet.
175
+ */
176
+ protected getDefaultPreferenceSchemaDefault(): ToolConfirmationMode {
177
+ const schemaDefault = this.preferenceService.inspect(DEFAULT_TOOL_CONFIRMATION_PREFERENCE)?.defaultValue as
178
+ | ToolConfirmationMode
179
+ | undefined;
180
+ return schemaDefault ?? ToolConfirmationMode.CONFIRM;
149
181
  }
150
182
  }