Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/node/protocol/toolApproval.integrationTest.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import assert from 'assert';
7
import type { IResponsePartAction } from '../../../common/state/sessionActions.js';
8
import { ResponsePartKind, type MarkdownResponsePart } from '../../../common/state/sessionState.js';
9
import {
10
createAndSubscribeSession,
11
dispatchTurnStarted,
12
getActionEnvelope,
13
IServerHandle,
14
isActionNotification,
15
startServer,
16
TestProtocolClient,
17
} from './testHelpers.js';
18
19
suite('Protocol WebSocket — Permissions & Auto-Approve', function () {
20
21
let server: IServerHandle;
22
let client: TestProtocolClient;
23
24
suiteSetup(async function () {
25
this.timeout(15_000);
26
server = await startServer();
27
});
28
29
suiteTeardown(function () {
30
server.process.kill();
31
});
32
33
setup(async function () {
34
this.timeout(10_000);
35
client = new TestProtocolClient(server.port);
36
await client.connect();
37
});
38
39
teardown(function () {
40
client.close();
41
});
42
43
// ---- Manual permission flow ------------------------------------------------
44
45
test('permission request → resolve → response', async function () {
46
this.timeout(10_000);
47
48
const sessionUri = await createAndSubscribeSession(client, 'test-permission');
49
dispatchTurnStarted(client, sessionUri, 'turn-perm', 'permission', 1);
50
51
// The mock agent fires tool_start + tool_ready instead of permission_request
52
await client.waitForNotification(n => isActionNotification(n, 'session/toolCallStart'));
53
await client.waitForNotification(n => isActionNotification(n, 'session/toolCallReady'));
54
55
// Confirm the tool call
56
client.notify('dispatchAction', {
57
clientSeq: 2,
58
action: {
59
type: 'session/toolCallConfirmed',
60
session: sessionUri,
61
turnId: 'turn-perm',
62
toolCallId: 'tc-perm-1',
63
approved: true,
64
},
65
});
66
67
const responsePart = await client.waitForNotification(n => isActionNotification(n, 'session/responsePart'));
68
const responsePartAction = getActionEnvelope(responsePart).action as IResponsePartAction;
69
assert.strictEqual(responsePartAction.part.kind, ResponsePartKind.Markdown);
70
assert.strictEqual((responsePartAction.part as MarkdownResponsePart).content, 'Allowed.');
71
72
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
73
});
74
75
// ---- Edit auto-approve patterns -------------------------------------------
76
77
test('auto-approves write to regular file (no pending confirmation)', async function () {
78
this.timeout(10_000);
79
80
const sessionUri = await createAndSubscribeSession(client, 'test-autoapprove', 'file:///workspace');
81
client.clearReceived();
82
83
// Start a turn that triggers a write permission request for a regular .ts file
84
dispatchTurnStarted(client, sessionUri, 'turn-autoapprove', 'write-file', 1);
85
86
// The write should be auto-approved — we should see tool_start, tool_complete, and turn_complete
87
// but NOT a pending-confirmation toolCallReady (one without `confirmed`).
88
await client.waitForNotification(n => isActionNotification(n, 'session/toolCallStart'));
89
await client.waitForNotification(n => isActionNotification(n, 'session/toolCallComplete'));
90
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
91
92
// Verify no pending-confirmation toolCallReady was received
93
const pendingConfirmNotifs = client.receivedNotifications(n => {
94
if (!isActionNotification(n, 'session/toolCallReady')) {
95
return false;
96
}
97
const action = getActionEnvelope(n).action as { confirmed?: string };
98
return !action.confirmed;
99
});
100
assert.strictEqual(pendingConfirmNotifs.length, 0, 'should not have received pending-confirmation toolCallReady for auto-approved write');
101
});
102
103
test('blocks write to .env file (requires manual confirmation)', async function () {
104
this.timeout(10_000);
105
106
const sessionUri = await createAndSubscribeSession(client, 'test-autoapprove-deny', 'file:///workspace');
107
client.clearReceived();
108
109
// Start a turn that tries to write .env (blocked by default patterns)
110
dispatchTurnStarted(client, sessionUri, 'turn-deny', 'write-env', 1);
111
112
// The .env write should NOT be auto-approved — we should see toolCallReady (pending confirmation)
113
await client.waitForNotification(n => isActionNotification(n, 'session/toolCallStart'));
114
await client.waitForNotification(n => isActionNotification(n, 'session/toolCallReady'));
115
116
// Confirm it manually to let the turn complete
117
client.notify('dispatchAction', {
118
clientSeq: 2,
119
action: {
120
type: 'session/toolCallConfirmed',
121
session: sessionUri,
122
turnId: 'turn-deny',
123
toolCallId: 'tc-write-env-1',
124
approved: true,
125
confirmed: 'user-action',
126
},
127
});
128
129
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
130
});
131
132
// ---- Shell auto-approve ---------------------------------------------------
133
134
test('auto-approves allowed shell command (no pending confirmation)', async function () {
135
this.timeout(10_000);
136
137
const sessionUri = await createAndSubscribeSession(client, 'test-shell-approve');
138
client.clearReceived();
139
140
// Start a turn that triggers a shell permission request for "ls -la" (allowed command)
141
dispatchTurnStarted(client, sessionUri, 'turn-shell-approve', 'run-safe-command', 1);
142
143
// The shell command should be auto-approved — we should see tool_start, tool_complete, and turn_complete
144
// but NOT a pending-confirmation toolCallReady.
145
await client.waitForNotification(n => isActionNotification(n, 'session/toolCallStart'));
146
await client.waitForNotification(n => isActionNotification(n, 'session/toolCallComplete'));
147
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
148
149
// Verify no pending-confirmation toolCallReady was received
150
const pendingConfirmNotifs = client.receivedNotifications(n => {
151
if (!isActionNotification(n, 'session/toolCallReady')) {
152
return false;
153
}
154
const action = getActionEnvelope(n).action as { confirmed?: string };
155
return !action.confirmed;
156
});
157
assert.strictEqual(pendingConfirmNotifs.length, 0, 'should not have received pending-confirmation toolCallReady for allowed shell command');
158
});
159
160
test('blocks denied shell command (requires manual confirmation)', async function () {
161
this.timeout(10_000);
162
163
const sessionUri = await createAndSubscribeSession(client, 'test-shell-deny');
164
client.clearReceived();
165
166
// Start a turn that triggers a shell permission request for "rm -rf /" (denied command)
167
dispatchTurnStarted(client, sessionUri, 'turn-shell-deny', 'run-dangerous-command', 1);
168
169
// The denied command should NOT be auto-approved — we should see toolCallReady (pending confirmation)
170
await client.waitForNotification(n => isActionNotification(n, 'session/toolCallStart'));
171
await client.waitForNotification(n => isActionNotification(n, 'session/toolCallReady'));
172
173
// Confirm it manually to let the turn complete
174
client.notify('dispatchAction', {
175
clientSeq: 2,
176
action: {
177
type: 'session/toolCallConfirmed',
178
session: sessionUri,
179
turnId: 'turn-shell-deny',
180
toolCallId: 'tc-shell-deny-1',
181
approved: true,
182
confirmed: 'user-action',
183
},
184
});
185
186
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
187
});
188
});
189
190