@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.
- package/lib/browser/chat-tool-preference-bindings.d.ts +28 -4
- package/lib/browser/chat-tool-preference-bindings.d.ts.map +1 -1
- package/lib/browser/chat-tool-preference-bindings.js +55 -32
- package/lib/browser/chat-tool-preference-bindings.js.map +1 -1
- package/lib/browser/chat-tool-preference-bindings.spec.js +168 -98
- package/lib/browser/chat-tool-preference-bindings.spec.js.map +1 -1
- package/lib/common/chat-tool-preferences.d.ts +2 -0
- package/lib/common/chat-tool-preferences.d.ts.map +1 -1
- package/lib/common/chat-tool-preferences.js +26 -9
- package/lib/common/chat-tool-preferences.js.map +1 -1
- package/package.json +9 -9
- package/src/browser/chat-tool-preference-bindings.spec.ts +194 -104
- package/src/browser/chat-tool-preference-bindings.ts +71 -39
- package/src/common/chat-tool-preferences.ts +29 -8
|
@@ -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 {
|
|
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
|
|
40
|
+
let storedPerToolPreferences: { [toolId: string]: ToolConfirmationMode };
|
|
41
|
+
let storedDefaultMode: ToolConfirmationMode | undefined;
|
|
31
42
|
let trusted: boolean;
|
|
32
|
-
let
|
|
33
|
-
|
|
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
|
-
|
|
55
|
+
storedPerToolPreferences = {};
|
|
56
|
+
storedDefaultMode = undefined;
|
|
48
57
|
trusted = true;
|
|
49
|
-
|
|
58
|
+
perToolInspectResult = undefined;
|
|
59
|
+
defaultInspectResult = undefined;
|
|
50
60
|
|
|
51
61
|
preferenceServiceMock = {
|
|
52
|
-
updateValue: sinon.stub().callsFake((
|
|
53
|
-
|
|
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(() =>
|
|
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>(
|
|
61
|
-
if (
|
|
62
|
-
|
|
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
|
-
|
|
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('
|
|
135
|
+
it('returns CONFIRM for regular tools by default', () => {
|
|
79
136
|
const mode = manager.getConfirmationMode('regularTool', 'chat-1');
|
|
80
|
-
expect(mode).to.equal(ToolConfirmationMode.
|
|
137
|
+
expect(mode).to.equal(ToolConfirmationMode.CONFIRM);
|
|
81
138
|
});
|
|
82
139
|
|
|
83
|
-
it('
|
|
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('
|
|
90
|
-
|
|
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('
|
|
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('
|
|
102
|
-
|
|
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('
|
|
109
|
-
|
|
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
|
|
187
|
+
it('ignores a workspace-scoped default of ALWAYS_ALLOW when workspace is untrusted', () => {
|
|
118
188
|
trusted = false;
|
|
119
|
-
// Simulate the effective (workspace-merged)
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
202
|
+
it('blocks confirmAlwaysAllow bypass via workspace ALWAYS_ALLOW default when untrusted', () => {
|
|
135
203
|
trusted = false;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
214
|
+
it('honours a workspace-scoped ALWAYS_ALLOW default when workspace is trusted', () => {
|
|
150
215
|
trusted = true;
|
|
151
|
-
|
|
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('
|
|
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(
|
|
227
|
+
expect(storedPerToolPreferences['regularTool']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
|
|
164
228
|
});
|
|
165
229
|
|
|
166
|
-
it('
|
|
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(
|
|
234
|
+
expect(storedPerToolPreferences['dangerousTool']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
|
|
171
235
|
});
|
|
172
236
|
|
|
173
|
-
it('
|
|
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('
|
|
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('
|
|
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(
|
|
251
|
+
expect(storedPerToolPreferences['regularTool']).to.equal(ToolConfirmationMode.DISABLED);
|
|
193
252
|
});
|
|
194
253
|
|
|
195
|
-
it('
|
|
196
|
-
|
|
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('
|
|
204
|
-
|
|
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
|
-
|
|
266
|
+
storedPerToolPreferences['myTool'] = ToolConfirmationMode.ALWAYS_ALLOW;
|
|
208
267
|
manager.setConfirmationMode('myTool', ToolConfirmationMode.DISABLED);
|
|
209
268
|
expect(preferenceServiceMock.updateValue.calledOnce).to.be.true;
|
|
210
|
-
expect(
|
|
269
|
+
expect(storedPerToolPreferences['myTool']).to.be.undefined;
|
|
211
270
|
});
|
|
212
271
|
|
|
213
|
-
it('
|
|
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('
|
|
223
|
-
|
|
224
|
-
|
|
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(
|
|
285
|
+
expect(storedPerToolPreferences['dangerousTool']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
|
|
230
286
|
});
|
|
231
287
|
|
|
232
|
-
it('
|
|
233
|
-
|
|
234
|
-
|
|
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(
|
|
293
|
+
expect(storedPerToolPreferences['dangerousTool']).to.be.undefined;
|
|
237
294
|
});
|
|
238
295
|
|
|
239
|
-
it('
|
|
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(
|
|
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('
|
|
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.
|
|
307
|
+
expect(manager.getConfirmationMode('myTool', 'chat-2')).to.equal(ToolConfirmationMode.CONFIRM);
|
|
259
308
|
});
|
|
260
309
|
|
|
261
|
-
it('
|
|
262
|
-
|
|
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('
|
|
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.
|
|
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('
|
|
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.
|
|
286
|
-
expect(manager.getConfirmationMode('myTool', 'chat-2')).to.equal(ToolConfirmationMode.
|
|
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('
|
|
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(
|
|
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('
|
|
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(
|
|
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 {
|
|
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
|
-
* -
|
|
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
|
|
83
|
+
if (toolId in toolConfirmation) {
|
|
59
84
|
return toolConfirmation[toolId];
|
|
60
85
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
}
|