Path: blob/main/src/vs/platform/agentHost/test/node/protocol/toolApproval.integrationTest.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import type { IResponsePartAction } from '../../../common/state/sessionActions.js';7import { ResponsePartKind, type MarkdownResponsePart } from '../../../common/state/sessionState.js';8import {9createAndSubscribeSession,10dispatchTurnStarted,11getActionEnvelope,12IServerHandle,13isActionNotification,14startServer,15TestProtocolClient,16} from './testHelpers.js';1718suite('Protocol WebSocket — Permissions & Auto-Approve', function () {1920let server: IServerHandle;21let client: TestProtocolClient;2223suiteSetup(async function () {24this.timeout(15_000);25server = await startServer();26});2728suiteTeardown(function () {29server.process.kill();30});3132setup(async function () {33this.timeout(10_000);34client = new TestProtocolClient(server.port);35await client.connect();36});3738teardown(function () {39client.close();40});4142// ---- Manual permission flow ------------------------------------------------4344test('permission request → resolve → response', async function () {45this.timeout(10_000);4647const sessionUri = await createAndSubscribeSession(client, 'test-permission');48dispatchTurnStarted(client, sessionUri, 'turn-perm', 'permission', 1);4950// The mock agent fires tool_start + tool_ready instead of permission_request51await client.waitForNotification(n => isActionNotification(n, 'session/toolCallStart'));52await client.waitForNotification(n => isActionNotification(n, 'session/toolCallReady'));5354// Confirm the tool call55client.notify('dispatchAction', {56clientSeq: 2,57action: {58type: 'session/toolCallConfirmed',59session: sessionUri,60turnId: 'turn-perm',61toolCallId: 'tc-perm-1',62approved: true,63},64});6566const responsePart = await client.waitForNotification(n => isActionNotification(n, 'session/responsePart'));67const responsePartAction = getActionEnvelope(responsePart).action as IResponsePartAction;68assert.strictEqual(responsePartAction.part.kind, ResponsePartKind.Markdown);69assert.strictEqual((responsePartAction.part as MarkdownResponsePart).content, 'Allowed.');7071await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));72});7374// ---- Edit auto-approve patterns -------------------------------------------7576test('auto-approves write to regular file (no pending confirmation)', async function () {77this.timeout(10_000);7879const sessionUri = await createAndSubscribeSession(client, 'test-autoapprove', 'file:///workspace');80client.clearReceived();8182// Start a turn that triggers a write permission request for a regular .ts file83dispatchTurnStarted(client, sessionUri, 'turn-autoapprove', 'write-file', 1);8485// The write should be auto-approved — we should see tool_start, tool_complete, and turn_complete86// but NOT a pending-confirmation toolCallReady (one without `confirmed`).87await client.waitForNotification(n => isActionNotification(n, 'session/toolCallStart'));88await client.waitForNotification(n => isActionNotification(n, 'session/toolCallComplete'));89await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));9091// Verify no pending-confirmation toolCallReady was received92const pendingConfirmNotifs = client.receivedNotifications(n => {93if (!isActionNotification(n, 'session/toolCallReady')) {94return false;95}96const action = getActionEnvelope(n).action as { confirmed?: string };97return !action.confirmed;98});99assert.strictEqual(pendingConfirmNotifs.length, 0, 'should not have received pending-confirmation toolCallReady for auto-approved write');100});101102test('blocks write to .env file (requires manual confirmation)', async function () {103this.timeout(10_000);104105const sessionUri = await createAndSubscribeSession(client, 'test-autoapprove-deny', 'file:///workspace');106client.clearReceived();107108// Start a turn that tries to write .env (blocked by default patterns)109dispatchTurnStarted(client, sessionUri, 'turn-deny', 'write-env', 1);110111// The .env write should NOT be auto-approved — we should see toolCallReady (pending confirmation)112await client.waitForNotification(n => isActionNotification(n, 'session/toolCallStart'));113await client.waitForNotification(n => isActionNotification(n, 'session/toolCallReady'));114115// Confirm it manually to let the turn complete116client.notify('dispatchAction', {117clientSeq: 2,118action: {119type: 'session/toolCallConfirmed',120session: sessionUri,121turnId: 'turn-deny',122toolCallId: 'tc-write-env-1',123approved: true,124confirmed: 'user-action',125},126});127128await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));129});130131// ---- Shell auto-approve ---------------------------------------------------132133test('auto-approves allowed shell command (no pending confirmation)', async function () {134this.timeout(10_000);135136const sessionUri = await createAndSubscribeSession(client, 'test-shell-approve');137client.clearReceived();138139// Start a turn that triggers a shell permission request for "ls -la" (allowed command)140dispatchTurnStarted(client, sessionUri, 'turn-shell-approve', 'run-safe-command', 1);141142// The shell command should be auto-approved — we should see tool_start, tool_complete, and turn_complete143// but NOT a pending-confirmation toolCallReady.144await client.waitForNotification(n => isActionNotification(n, 'session/toolCallStart'));145await client.waitForNotification(n => isActionNotification(n, 'session/toolCallComplete'));146await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));147148// Verify no pending-confirmation toolCallReady was received149const pendingConfirmNotifs = client.receivedNotifications(n => {150if (!isActionNotification(n, 'session/toolCallReady')) {151return false;152}153const action = getActionEnvelope(n).action as { confirmed?: string };154return !action.confirmed;155});156assert.strictEqual(pendingConfirmNotifs.length, 0, 'should not have received pending-confirmation toolCallReady for allowed shell command');157});158159test('blocks denied shell command (requires manual confirmation)', async function () {160this.timeout(10_000);161162const sessionUri = await createAndSubscribeSession(client, 'test-shell-deny');163client.clearReceived();164165// Start a turn that triggers a shell permission request for "rm -rf /" (denied command)166dispatchTurnStarted(client, sessionUri, 'turn-shell-deny', 'run-dangerous-command', 1);167168// The denied command should NOT be auto-approved — we should see toolCallReady (pending confirmation)169await client.waitForNotification(n => isActionNotification(n, 'session/toolCallStart'));170await client.waitForNotification(n => isActionNotification(n, 'session/toolCallReady'));171172// Confirm it manually to let the turn complete173client.notify('dispatchAction', {174clientSeq: 2,175action: {176type: 'session/toolCallConfirmed',177session: sessionUri,178turnId: 'turn-shell-deny',179toolCallId: 'tc-shell-deny-1',180approved: true,181confirmed: 'user-action',182},183});184185await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));186});187});188189190