File size: 7,644 Bytes
c44600e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig, poll } from './test-helper.js';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
import { env } from 'node:process';
import stripAnsi from 'strip-ansi';

// Browser agent Chrome DevTools MCP connection is flaky in Docker sandbox.
// See: https://github.com/google-gemini/gemini-cli/issues/24382
const isDockerSandbox = env['GEMINI_SANDBOX'] === 'docker';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const chromeAvailable = (() => {
  try {
    if (process.platform === 'darwin') {
      execSync(
        'test -d "/Applications/Google Chrome.app"  || test -d "/Applications/Chromium.app"',
        {
          stdio: 'ignore',
        },
      );
    } else if (process.platform === 'linux') {
      execSync(
        'which google-chrome || which chromium-browser || which chromium',
        { stdio: 'ignore' },
      );
    } else if (process.platform === 'win32') {
      const chromePaths = [
        'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
        'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
        `${process.env['LOCALAPPDATA'] ?? ''}\\Google\\Chrome\\Application\\chrome.exe`,
      ];
      const found = chromePaths.some((p) => existsSync(p));
      if (!found) {
        execSync('where chrome || where chromium', { stdio: 'ignore' });
      }
    } else {
      return false;
    }
    return true;
  } catch {
    return false;
  }
})();

describe.skipIf(!chromeAvailable)('browser-policy', () => {
  let rig: TestRig;

  beforeEach(() => {
    rig = new TestRig();
  });

  afterEach(async () => {
    await rig.cleanup();
  });

  it.skipIf(isDockerSandbox)(
    'should skip confirmation when "Allow all server tools for this session" is chosen',
    async () => {
      rig.setup('browser-policy-skip-confirmation', {
        fakeResponsesPath: join(__dirname, 'browser-policy.responses'),
        settings: {
          agents: {
            overrides: {
              browser_agent: {
                enabled: true,
              },
            },
            browser: {
              headless: true,
              sessionMode: 'isolated',
              allowedDomains: ['example.com'],
            },
          },
        },
      });

      // Manually trust the folder to avoid the dialog and enable option 3
      const geminiDir = join(rig.homeDir!, '.gemini');
      mkdirSync(geminiDir, { recursive: true });

      // Write to trustedFolders.json
      const trustedFoldersPath = join(geminiDir, 'trustedFolders.json');
      const trustedFolders = {
        [rig.testDir!]: 'TRUST_FOLDER',
      };
      writeFileSync(
        trustedFoldersPath,
        JSON.stringify(trustedFolders, null, 2),
      );

      // Force confirmation for browser agent.
      // NOTE: We don't force confirm browser tools here because "Allow all server tools"
      // adds a rule with ALWAYS_ALLOW_PRIORITY (3.9x) which would be overshadowed by
      // a rule in the user tier (4.x) like the one from this TOML.
      // By removing the explicit mcp rule, the first MCP tool will still prompt
      // due to default approvalMode = 'default', and then "Allow all" will correctly
      // bypass subsequent tools.
      const policyFile = join(rig.testDir!, 'force-confirm.toml');
      writeFileSync(
        policyFile,
        `
[[rule]]
name = "Force confirm browser_agent"
toolName = "invoke_agent"
argsPattern = "\\"agent_name\\":\\\\s*\\"browser_agent\\""
decision = "ask_user"
priority = 200
`,
      );

      // Update settings.json in both project and home directories to point to the policy file
      for (const baseDir of [rig.testDir!, rig.homeDir!]) {
        const settingsPath = join(baseDir, '.gemini', 'settings.json');
        if (existsSync(settingsPath)) {
          const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
          settings.policyPaths = [policyFile];
          // Ensure folder trust is enabled
          settings.security = settings.security || {};
          settings.security.folderTrust = settings.security.folderTrust || {};
          settings.security.folderTrust.enabled = true;
          writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
        }
      }

      const run = await rig.runInteractive({
        approvalMode: 'default',
        env: {
          GEMINI_CLI_INTEGRATION_TEST: 'true',
        },
      });

      await run.sendKeys(
        'Open https://example.com and check if there is a heading\r',
      );
      await run.sendKeys('\r');

      // Handle confirmations.
      // 1. Initial browser_agent delegation (likely only 3 options, so use option 1: Allow once)
      await poll(
        () => stripAnsi(run.output).toLowerCase().includes('action required'),
        60000,
        1000,
      );
      await run.sendKeys('1\r');
      await new Promise((r) => setTimeout(r, 2000));

      // Handle privacy notice
      await poll(
        () => stripAnsi(run.output).toLowerCase().includes('privacy notice'),
        5000,
        100,
      );
      await run.sendKeys('1\r');
      await new Promise((r) => setTimeout(r, 5000));

      // new_page (MCP tool, should have 4 options, use option 3: Allow all server tools)
      await poll(
        () => {
          const stripped = stripAnsi(run.output).toLowerCase();
          return (
            stripped.includes('new_page') &&
            stripped.includes('allow all server tools for this session')
          );
        },
        60000,
        1000,
      );

      // Select "Allow all server tools for this session" (option 3)
      await run.sendKeys('3\r');

      // Wait for the browser agent to finish (success or failure)
      await poll(
        () => {
          const stripped = stripAnsi(run.output).toLowerCase();
          return (
            stripped.includes('completed successfully') ||
            stripped.includes('agent error')
          );
        },
        120000,
        1000,
      );

      const output = stripAnsi(run.output).toLowerCase();

      expect(output).toContain('browser_agent');
      // The test validates that "Allow all server tools" skips subsequent
      // tool confirmations — the browser agent may still fail due to
      // Chrome/MCP issues in CI, which is acceptable for this policy test.
      expect(
        output.includes('completed successfully') ||
          output.includes('agent error'),
      ).toBe(true);
    },
  );

  it('should show the visible warning when browser agent starts in existing session mode', async () => {
    rig.setup('browser-session-warning', {
      fakeResponsesPath: join(__dirname, 'browser-agent.cleanup.responses'),
      settings: {
        general: {
          enableAutoUpdateNotification: false,
        },
        agents: {
          overrides: {
            browser_agent: {
              enabled: true,
            },
          },
          browser: {
            sessionMode: 'existing',
            headless: true,
          },
        },
      },
    });

    const stdout = await rig.runCommand(['Open https://example.com'], {
      env: {
        GEMINI_API_KEY: 'fake-key',
        GEMINI_TELEMETRY_DISABLED: 'true',
        DEV: 'true',
      },
    });

    expect(stdout).toContain('saved logins will be visible');
  });
});