Compare commits

..

4 Commits

Author SHA1 Message Date
Chris Williams cad496650b Fix up links in README 2026-03-20 14:29:30 -07:00
Chris Williams 38de78045d Add redirects 2026-03-20 14:25:44 -07:00
Chris Williams dce25e4229 Move docs into resources 2026-03-20 14:25:44 -07:00
Chris Williams 59bf8cbb45 Shift heavy install and authentication docs out of getting start and into resources 2026-03-20 14:25:40 -07:00
41 changed files with 185 additions and 488 deletions
+4 -4
View File
@@ -30,7 +30,7 @@ Learn all about Gemini CLI in our [documentation](https://geminicli.com/docs/).
## 📦 Installation
See
[Gemini CLI installation, execution, and releases](./docs/get-started/installation.md)
[Gemini CLI installation, execution, and releases](./docs/resources/installation.md)
for recommended system specifications and a detailed installation guide.
### Quick Install
@@ -209,7 +209,7 @@ gemini
```
For Google Workspace accounts and other authentication methods, see the
[authentication guide](./docs/get-started/authentication.md).
[authentication guide](./docs/resources/authentication.md).
## 🚀 Getting Started
@@ -280,8 +280,8 @@ gemini
- [**Quickstart Guide**](./docs/get-started/index.md) - Get up and running
quickly.
- [**Authentication Setup**](./docs/get-started/authentication.md) - Detailed
auth configuration.
- [**Authentication Setup**](./docs/resources/authentication.md) - Detailed auth
configuration.
- [**Configuration Guide**](./docs/reference/configuration.md) - Settings and
customization.
- [**Keyboard Shortcuts**](./docs/reference/keyboard-shortcuts.md) -
+1 -1
View File
@@ -508,7 +508,7 @@ other events. For more information, see the
You can enforce a specific authentication method for all users by setting the
`enforcedAuthType` in the system-level `settings.json` file. This prevents users
from choosing a different authentication method. See the
[Authentication docs](../get-started/authentication.md) for more details.
[Authentication docs](../resources/authentication.md) for more details.
**Example:** Enforce the use of Google login for all users.
-7
View File
@@ -101,13 +101,6 @@ they appear in the UI.
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
### Agents
| UI Label | Setting | Description | Default |
| ------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------- | ------- |
| Confirm Sensitive Actions | `agents.browser.confirmSensitiveActions` | Require manual confirmation for sensitive browser actions (e.g., fill_form, evaluate_script). | `false` |
| Block File Uploads | `agents.browser.blockFileUploads` | Hard-block file upload requests from the browser agent. | `false` |
### Context
| UI Label | Setting | Description | Default |
+1 -1
View File
@@ -24,7 +24,7 @@ project-specific behavior or create a customized persona.
You can set the environment variable temporarily in your shell, or persist it
via a `.gemini/.env` file. See
[Persisting Environment Variables](../get-started/authentication.md#persisting-environment-variables).
[Persisting Environment Variables](../resources/authentication.md#persisting-environment-variables).
- Use the project default path (`.gemini/system.md`):
- `GEMINI_SYSTEM_MD=true` or `GEMINI_SYSTEM_MD=1`
+3 -2
View File
@@ -24,7 +24,8 @@ Once Gemini CLI is installed, run Gemini CLI from your command line:
gemini
```
For more installation options, see [Gemini CLI Installation](./installation.md).
For more installation options, see
[Gemini CLI Installation](../resources/installation.md).
## Authenticate
@@ -46,7 +47,7 @@ cases, you can log in with your existing Google account:
Certain account types may require you to configure a Google Cloud project. For
more information, including other authentication methods, see
[Gemini CLI Authentication Setup](./authentication.md).
[Gemini CLI Authentication Setup](../resources/authentication.md).
## Configure
+2 -2
View File
@@ -470,5 +470,5 @@ console.error('Consolidating memories for session end...');
While project-level hooks are great for specific repositories, you can share
your hooks across multiple projects by packaging them as a
[Gemini CLI extension](../extensions/index.md). This provides version control,
easy distribution, and centralized management.
[Gemini CLI extension](https://www.google.com/search?q=../extensions/index.md).
This provides version control, easy distribution, and centralized management.
+7 -4
View File
@@ -10,15 +10,15 @@ context.
npm install -g @google/gemini-cli
```
For more installation options and authentication setup, see the full
[Installation](./resources/installation.md) and
[Authentication](./resources/authentication.md) guides.
## Get started
Jump in to Gemini CLI.
- **[Quickstart](./get-started/index.md):** Your first session with Gemini CLI.
- **[Installation](./get-started/installation.md):** How to install Gemini CLI
on your system.
- **[Authentication](./get-started/authentication.md):** Setup instructions for
personal and enterprise accounts.
- **[Examples](./get-started/examples.md):** Practical examples of Gemini CLI in
action.
- **[CLI cheatsheet](./cli/cli-reference.md):** A quick reference for common
@@ -116,6 +116,9 @@ Deep technical documentation and API specifications.
Support, release history, and legal information.
- **[FAQ](./resources/faq.md):** Answers to frequently asked questions.
- **[Installation](./resources/installation.md):** How to install Gemini CLI.
- **[Authentication](./resources/authentication.md):** Setup instructions for
personal and enterprise accounts.
- **[Quota and pricing](./resources/quota-and-pricing.md):** Limits and billing
details.
- **[Terms and privacy](./resources/tos-privacy.md):** Official notices and
+2
View File
@@ -11,8 +11,10 @@
"/docs/core/tools-api": "/docs/reference/tools",
"/docs/reference/tools-api": "/docs/reference/tools",
"/docs/faq": "/docs/resources/faq",
"/docs/get-started/authentication": "/docs/resources/authentication",
"/docs/get-started/configuration": "/docs/reference/configuration",
"/docs/get-started/configuration-v1": "/docs/reference/configuration",
"/docs/get-started/installation": "/docs/resources/installation",
"/docs/index": "/docs",
"/docs/quota-and-pricing": "/docs/resources/quota-and-pricing",
"/docs/tos-privacy": "/docs/resources/tos-privacy",
+3 -14
View File
@@ -1210,17 +1210,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Disable user input on browser window during automation.
- **Default:** `true`
- **`agents.browser.confirmSensitiveActions`** (boolean):
- **Description:** Require manual confirmation for sensitive browser actions
(e.g., fill_form, evaluate_script).
- **Default:** `false`
- **Requires restart:** Yes
- **`agents.browser.blockFileUploads`** (boolean):
- **Description:** Hard-block file upload requests from the browser agent.
- **Default:** `false`
- **Requires restart:** Yes
#### `context`
- **`context.fileName`** (string | string[]):
@@ -1914,8 +1903,8 @@ within your user's home folder.
Environment variables are a common way to configure applications, especially for
sensitive information like API keys or for settings that might change between
environments. For authentication setup, see the
[Authentication documentation](../get-started/authentication.md) which covers
all available authentication methods.
[Authentication documentation](../resources/authentication.md) which covers all
available authentication methods.
The CLI automatically loads environment variables from an `.env` file. The
loading order is:
@@ -1935,7 +1924,7 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- **`GEMINI_API_KEY`**:
- Your API key for the Gemini API.
- One of several available
[authentication methods](../get-started/authentication.md).
[authentication methods](../resources/authentication.md).
- Set this in your shell profile (e.g., `~/.bashrc`, `~/.zshrc`) or an `.env`
file.
- **`GEMINI_MODEL`**:
+9 -14
View File
@@ -262,8 +262,8 @@ Here is a breakdown of the fields available in a TOML policy rule:
# A unique name for the tool, or an array of names.
toolName = "run_shell_command"
# (Optional) The name of a subagent. If provided, the rule only applies to tool
# calls made by this specific subagent.
# (Optional) The name of a subagent. If provided, the rule only applies to tool calls
# made by this specific subagent.
subagent = "generalist"
# (Optional) The name of an MCP server. Can be combined with toolName
@@ -278,17 +278,14 @@ toolAnnotations = { readOnlyHint = true }
argsPattern = '"command":"(git|npm)'
# (Optional) A string or array of strings that a shell command must start with.
# This is syntactic sugar for `toolName = "run_shell_command"` and an
# `argsPattern`.
# This is syntactic sugar for `toolName = "run_shell_command"` and an `argsPattern`.
commandPrefix = "git"
# (Optional) A regex to match against the entire shell command.
# This is also syntactic sugar for `toolName = "run_shell_command"`.
# Note: This pattern is tested against the JSON representation of the arguments
# (e.g., `{"command":"<your_command>"}`). Because it prepends `"command":"`,
# it effectively matches from the start of the command.
# Anchors like `^` or `$` apply to the full JSON string,
# so `^` should usually be avoided here.
# Note: This pattern is tested against the JSON representation of the arguments (e.g., `{"command":"<your_command>"}`).
# Because it prepends `"command":"`, it effectively matches from the start of the command.
# Anchors like `^` or `$` apply to the full JSON string, so `^` should usually be avoided here.
# You cannot use commandPrefix and commandRegex in the same rule.
commandRegex = "git (commit|push)"
@@ -298,16 +295,14 @@ decision = "ask_user"
# The priority of the rule, from 0 to 999.
priority = 10
# (Optional) A custom message to display when a tool call is denied by this
# rule. This message is returned to the model and user,
# useful for explaining *why* it was denied.
# (Optional) A custom message to display when a tool call is denied by this rule.
# This message is returned to the model and user, useful for explaining *why* it was denied.
deny_message = "Deletion is permanent"
# (Optional) An array of approval modes where this rule is active.
modes = ["autoEdit"]
# (Optional) A boolean to restrict the rule to interactive (true) or
# non-interactive (false) environments.
# (Optional) A boolean to restrict the rule to interactive (true) or non-interactive (false) environments.
# If omitted, the rule applies to both.
interactive = true
```
+5 -5
View File
@@ -7,11 +7,6 @@
"items": [
{ "label": "Overview", "slug": "docs" },
{ "label": "Quickstart", "slug": "docs/get-started" },
{ "label": "Installation", "slug": "docs/get-started/installation" },
{
"label": "Authentication",
"slug": "docs/get-started/authentication"
},
{ "label": "Examples", "slug": "docs/get-started/examples" },
{ "label": "CLI cheatsheet", "slug": "docs/cli/cli-reference" },
{
@@ -220,6 +215,11 @@
"label": "Resources",
"items": [
{ "label": "FAQ", "slug": "docs/resources/faq" },
{ "label": "Installation", "slug": "docs/resources/installation" },
{
"label": "Authentication",
"slug": "docs/resources/authentication"
},
{
"label": "Quota and pricing",
"slug": "docs/resources/quota-and-pricing"
+2 -2
View File
@@ -12,7 +12,7 @@ import { appEvalTest } from './app-test-helper.js';
import { PolicyDecision } from '@google/gemini-cli-core';
describe('Model Steering Behavioral Evals', () => {
appEvalTest('USUALLY_PASSES', {
appEvalTest('ALWAYS_PASSES', {
name: 'Corrective Hint: Model switches task based on hint during tool turn',
configOverrides: {
excludeTools: ['run_shell_command', 'ls', 'google_web_search'],
@@ -52,7 +52,7 @@ describe('Model Steering Behavioral Evals', () => {
},
});
appEvalTest('USUALLY_PASSES', {
appEvalTest('ALWAYS_PASSES', {
name: 'Suggestive Hint: Model incorporates user guidance mid-stream',
configOverrides: {
excludeTools: ['run_shell_command', 'ls', 'google_web_search'],
+9 -9
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"workspaces": [
"packages/*"
],
@@ -17413,7 +17413,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
"@google-cloud/storage": "^7.16.0",
@@ -17528,7 +17528,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
@@ -17700,7 +17700,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
@@ -17966,7 +17966,7 @@
},
"packages/devtools": {
"name": "@google/gemini-cli-devtools",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"ws": "^8.16.0"
@@ -17981,7 +17981,7 @@
},
"packages/sdk": {
"name": "@google/gemini-cli-sdk",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -17998,7 +17998,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18015,7 +18015,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260321.fc03891a1"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -27,7 +27,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260321.fc03891a1"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
@@ -637,4 +637,64 @@ describe('ExtensionManager', () => {
);
});
});
describe('orphaned extension cleanup', () => {
it('should remove broken extension metadata on startup to allow re-installation', async () => {
const extName = 'orphaned-ext';
const sourceDir = path.join(tempHomeDir, 'valid-source');
fs.mkdirSync(sourceDir, { recursive: true });
fs.writeFileSync(
path.join(sourceDir, 'gemini-extension.json'),
JSON.stringify({ name: extName, version: '1.0.0' }),
);
// Link an extension successfully.
await extensionManager.loadExtensions();
await extensionManager.installOrUpdateExtension({
source: sourceDir,
type: 'link',
});
const destinationPath = path.join(userExtensionsDir, extName);
const metadataPath = path.join(
destinationPath,
'.gemini-extension-install.json',
);
expect(fs.existsSync(metadataPath)).toBe(true);
// Simulate metadata corruption (e.g., pointing to a non-existent source).
fs.writeFileSync(
metadataPath,
JSON.stringify({ source: '/NON_EXISTENT_PATH', type: 'link' }),
);
// Simulate CLI startup. The manager should detect the broken link
// and proactively delete the orphaned metadata directory.
const newManager = new ExtensionManager({
settings: createTestMergedSettings(),
workspaceDir: tempWorkspaceDir,
requestConsent: vi.fn().mockResolvedValue(true),
requestSetting: null,
integrityManager: mockIntegrityManager,
});
await newManager.loadExtensions();
// Verify the extension failed to load and was proactively cleaned up.
expect(newManager.getExtensions().some((e) => e.name === extName)).toBe(
false,
);
expect(fs.existsSync(destinationPath)).toBe(false);
// Verify the system is self-healed and allows re-linking to the valid source.
await newManager.installOrUpdateExtension({
source: sourceDir,
type: 'link',
});
expect(newManager.getExtensions().some((e) => e.name === extName)).toBe(
true,
);
});
});
});
+11 -4
View File
@@ -982,11 +982,18 @@ Would you like to attempt to install via "git clone" instead?`,
plan: config.plan,
};
} catch (e) {
debugLogger.error(
`Warning: Skipping extension in ${effectiveExtensionPath}: ${getErrorMessage(
e,
)}`,
const extName = path.basename(extensionDir);
debugLogger.warn(
`Warning: Removing broken extension ${extName}: ${getErrorMessage(e)}`,
);
try {
await fs.promises.rm(extensionDir, { recursive: true, force: true });
} catch (rmError) {
debugLogger.error(
`Failed to remove broken extension directory ${extensionDir}:`,
rmError,
);
}
return null;
}
}
+10 -18
View File
@@ -249,10 +249,8 @@ describe('extension tests', () => {
expect(extensions[0].name).toBe('test-extension');
});
it('should skip the extension if a context file path is outside the extension directory and log an error', async () => {
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
it('should log a warning and remove the extension if a context file path is outside the extension directory', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
createExtension({
extensionsDir: userExtensionsDir,
name: 'traversal-extension',
@@ -662,10 +660,8 @@ name = "yolo-checker"
expect(serverConfig.env!['MISSING_VAR_BRACES']).toBe('${ALSO_UNDEFINED}');
});
it('should skip an extension with invalid JSON config and log an error', async () => {
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
it('should remove an extension with invalid JSON config and log a warning', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
// Good extension
createExtension({
@@ -686,17 +682,15 @@ name = "yolo-checker"
expect(extensions[0].name).toBe('good-ext');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`,
`Warning: Removing broken extension bad-ext: Failed to load extension config from ${badConfigPath}`,
),
);
consoleSpy.mockRestore();
});
it('should skip an extension with missing "name" in config and log an error', async () => {
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
it('should remove an extension with missing "name" in config and log a warning', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
// Good extension
createExtension({
@@ -717,7 +711,7 @@ name = "yolo-checker"
expect(extensions[0].name).toBe('good-ext');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`,
`Warning: Removing broken extension bad-ext-no-name: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`,
),
);
@@ -743,10 +737,8 @@ name = "yolo-checker"
expect(extensions[0].mcpServers?.['test-server'].trust).toBeUndefined();
});
it('should log an error for invalid extension names during loading', async () => {
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
it('should log a warning for invalid extension names during loading', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
createExtension({
extensionsDir: userExtensionsDir,
name: 'bad_name',
-20
View File
@@ -1198,26 +1198,6 @@ const SETTINGS_SCHEMA = {
'Disable user input on browser window during automation.',
showInDialog: false,
},
confirmSensitiveActions: {
type: 'boolean',
label: 'Confirm Sensitive Actions',
category: 'Advanced',
requiresRestart: true,
default: false,
description:
'Require manual confirmation for sensitive browser actions (e.g., fill_form, evaluate_script).',
showInDialog: true,
},
blockFileUploads: {
type: 'boolean',
label: 'Block File Uploads',
category: 'Advanced',
requiresRestart: true,
default: false,
description:
'Hard-block file upload requests from the browser agent.',
showInDialog: true,
},
},
},
},
+16 -10
View File
@@ -376,14 +376,6 @@ export type RenderInstance = {
capturedOverflowActions: OverflowActions | undefined;
};
export type RenderWithProvidersInstance = RenderInstance & {
simulateClick: (
col: number,
row: number,
button?: 0 | 1 | 2,
) => Promise<void>;
};
const instances: InkInstance[] = [];
export const render = async (
@@ -626,7 +618,15 @@ export const renderWithProviders = async (
};
appState?: AppState;
} = {},
): Promise<RenderWithProvidersInstance> => {
): Promise<
RenderInstance & {
simulateClick: (
col: number,
row: number,
button?: 0 | 1 | 2,
) => Promise<void>;
}
> => {
const baseState: UIState = new Proxy(
{ ...baseMockUiState, ...providedUiState },
{
@@ -861,7 +861,13 @@ export async function renderHookWithProviders<Result, Props>(
const Wrapper = options.wrapper || (({ children }) => <>{children}</>);
let renderResult: RenderWithProvidersInstance;
let renderResult: RenderInstance & {
simulateClick: (
col: number,
row: number,
button?: 0 | 1 | 2,
) => Promise<void>;
};
await act(async () => {
renderResult = await renderWithProviders(
@@ -3249,9 +3249,8 @@ describe('useGeminiStream', () => {
),
);
// Reset fake timers to startTime because the asynchronous render lifecycle
// (via waitUntilReady) advances the mock clock while waiting for initial
// components to settle.
// Reset start time after hook render, because renderHook (async)
// advances fake timers by 50ms during its internal waitUntilReady() check.
vi.setSystemTime(startTime);
// Submit query
@@ -43,11 +43,10 @@ const CWD = '/test/project';
const GIT_LOGS_HEAD_PATH = path.join(CWD, '.git', 'logs', 'HEAD');
describe('useGitBranchName', () => {
let deferredSpawn: Array<{
let deferredSpawn: {
resolve: (val: { stdout: string; stderr: string }) => void;
reject: (err: Error) => void;
args: string[];
}> = [];
} | null = null;
beforeEach(() => {
vol.reset(); // Reset in-memory filesystem
@@ -55,11 +54,11 @@ describe('useGitBranchName', () => {
[GIT_LOGS_HEAD_PATH]: 'ref: refs/heads/main',
});
deferredSpawn = [];
deferredSpawn = null;
vi.mocked(mockSpawnAsync).mockImplementation(
(_command: string, args: string[]) =>
() =>
new Promise((resolve, reject) => {
deferredSpawn.push({ resolve, reject, args });
deferredSpawn = { resolve, reject };
}),
);
});
@@ -92,9 +91,7 @@ describe('useGitBranchName', () => {
expect(result.current).toBeUndefined();
await act(async () => {
const spawn = deferredSpawn.shift()!;
expect(spawn.args).toContain('--abbrev-ref');
spawn.resolve({ stdout: 'main\n', stderr: '' });
deferredSpawn?.resolve({ stdout: 'main\n', stderr: '' });
});
expect(result.current).toBe('main');
@@ -104,9 +101,7 @@ describe('useGitBranchName', () => {
const { result } = await renderGitBranchNameHook(CWD);
await act(async () => {
const spawn = deferredSpawn.shift()!;
expect(spawn.args).toContain('--abbrev-ref');
spawn.reject(new Error('Git error'));
deferredSpawn?.reject(new Error('Git error'));
});
expect(result.current).toBeUndefined();
@@ -116,16 +111,12 @@ describe('useGitBranchName', () => {
const { result } = await renderGitBranchNameHook(CWD);
await act(async () => {
const spawn = deferredSpawn.shift()!;
expect(spawn.args).toContain('--abbrev-ref');
spawn.resolve({ stdout: 'HEAD\n', stderr: '' });
deferredSpawn?.resolve({ stdout: 'HEAD\n', stderr: '' });
});
// It should now call spawnAsync again for the short hash
await act(async () => {
const spawn = deferredSpawn.shift()!;
expect(spawn.args).toContain('--short');
spawn.resolve({ stdout: 'a1b2c3d\n', stderr: '' });
deferredSpawn?.resolve({ stdout: 'a1b2c3d\n', stderr: '' });
});
expect(result.current).toBe('a1b2c3d');
@@ -135,15 +126,11 @@ describe('useGitBranchName', () => {
const { result } = await renderGitBranchNameHook(CWD);
await act(async () => {
const spawn = deferredSpawn.shift()!;
expect(spawn.args).toContain('--abbrev-ref');
spawn.resolve({ stdout: 'HEAD\n', stderr: '' });
deferredSpawn?.resolve({ stdout: 'HEAD\n', stderr: '' });
});
await act(async () => {
const spawn = deferredSpawn.shift()!;
expect(spawn.args).toContain('--short');
spawn.reject(new Error('Git error'));
deferredSpawn?.reject(new Error('Git error'));
});
expect(result.current).toBeUndefined();
@@ -156,9 +143,7 @@ describe('useGitBranchName', () => {
const { result } = await renderGitBranchNameHook(CWD);
await act(async () => {
const spawn = deferredSpawn.shift()!;
expect(spawn.args).toContain('--abbrev-ref');
spawn.resolve({ stdout: 'main\n', stderr: '' });
deferredSpawn?.resolve({ stdout: 'main\n', stderr: '' });
});
expect(result.current).toBe('main');
@@ -175,9 +160,7 @@ describe('useGitBranchName', () => {
// Resolving the new branch name fetch
await act(async () => {
const spawn = deferredSpawn.shift()!;
expect(spawn.args).toContain('--abbrev-ref');
spawn.resolve({ stdout: 'develop\n', stderr: '' });
deferredSpawn?.resolve({ stdout: 'develop\n', stderr: '' });
});
expect(result.current).toBe('develop');
@@ -190,9 +173,7 @@ describe('useGitBranchName', () => {
const { result } = await renderGitBranchNameHook(CWD);
await act(async () => {
const spawn = deferredSpawn.shift()!;
expect(spawn.args).toContain('--abbrev-ref');
spawn.resolve({ stdout: 'main\n', stderr: '' });
deferredSpawn?.resolve({ stdout: 'main\n', stderr: '' });
});
expect(result.current).toBe('main');
@@ -207,8 +188,8 @@ describe('useGitBranchName', () => {
fs.writeFileSync(GIT_LOGS_HEAD_PATH, 'ref: refs/heads/develop');
});
// spawnAsync should NOT have been called again for updating
expect(deferredSpawn.length).toBe(0);
// spawnAsync should NOT have been called again
expect(vi.mocked(mockSpawnAsync)).toHaveBeenCalledTimes(1);
expect(result.current).toBe('main');
});
@@ -222,9 +203,7 @@ describe('useGitBranchName', () => {
const { unmount } = await renderGitBranchNameHook(CWD);
await act(async () => {
const spawn = deferredSpawn.shift()!;
expect(spawn.args).toContain('--abbrev-ref');
spawn.resolve({ stdout: 'main\n', stderr: '' });
deferredSpawn?.resolve({ stdout: 'main\n', stderr: '' });
});
// Wait for watcher to be set up BEFORE unmounting
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-core",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"description": "Gemini CLI Core",
"license": "Apache-2.0",
"repository": {
@@ -11,10 +11,8 @@ import {
} from './browserAgentFactory.js';
import { injectAutomationOverlay } from './automationOverlay.js';
import { makeFakeConfig } from '../../test-utils/config.js';
import { PolicyDecision, PRIORITY_SUBAGENT_TOOL } from '../../policy/types.js';
import type { Config } from '../../config/config.js';
import type { MessageBus } from '../../confirmation-bus/message-bus.js';
import type { PolicyEngine } from '../../policy/policy-engine.js';
import type { BrowserManager } from './browserManager.js';
// Create mock browser manager
@@ -302,116 +300,6 @@ describe('browserAgentFactory', () => {
});
});
describe('Policy Registration', () => {
let mockPolicyEngine: {
addRule: ReturnType<typeof vi.fn>;
hasRuleForTool: ReturnType<typeof vi.fn>;
removeRulesForTool: ReturnType<typeof vi.fn>;
getRules: ReturnType<typeof vi.fn>;
};
beforeEach(() => {
mockPolicyEngine = {
addRule: vi.fn(),
hasRuleForTool: vi.fn().mockReturnValue(false),
removeRulesForTool: vi.fn(),
getRules: vi.fn().mockReturnValue([]),
};
vi.spyOn(mockConfig, 'getPolicyEngine').mockReturnValue(
mockPolicyEngine as unknown as PolicyEngine,
);
});
it('should register sensitive action rules', async () => {
mockConfig = makeFakeConfig({
agents: {
browser: {
confirmSensitiveActions: true,
},
},
});
vi.spyOn(mockConfig, 'getPolicyEngine').mockReturnValue(
mockPolicyEngine as unknown as PolicyEngine,
);
await createBrowserAgentDefinition(mockConfig, mockMessageBus);
expect(mockPolicyEngine.addRule).toHaveBeenCalledWith(
expect.objectContaining({
toolName: 'mcp_browser_agent_fill',
decision: PolicyDecision.ASK_USER,
priority: 999,
}),
);
expect(mockPolicyEngine.addRule).toHaveBeenCalledWith(
expect.objectContaining({
toolName: 'mcp_browser_agent_upload_file',
decision: PolicyDecision.ASK_USER,
priority: 999,
}),
);
expect(mockPolicyEngine.addRule).toHaveBeenCalledWith(
expect.objectContaining({
toolName: 'mcp_browser_agent_evaluate_script',
decision: PolicyDecision.ASK_USER,
priority: 999,
}),
);
});
it('should register fill rule even when confirmSensitiveActions is disabled', async () => {
await createBrowserAgentDefinition(mockConfig, mockMessageBus);
expect(mockPolicyEngine.addRule).toHaveBeenCalledWith(
expect.objectContaining({
toolName: 'mcp_browser_agent_fill',
}),
);
expect(mockPolicyEngine.addRule).not.toHaveBeenCalledWith(
expect.objectContaining({
toolName: 'mcp_browser_agent_upload_file',
}),
);
});
it('should register ALLOW rules for read-only tools', async () => {
mockBrowserManager.getDiscoveredTools.mockResolvedValue([
{ name: 'take_snapshot', description: 'Take snapshot' },
{ name: 'take_screenshot', description: 'Take screenshot' },
{ name: 'list_pages', description: 'list all pages' },
]);
await createBrowserAgentDefinition(mockConfig, mockMessageBus);
expect(mockPolicyEngine.addRule).toHaveBeenCalledWith(
expect.objectContaining({
toolName: 'mcp_browser_agent_take_snapshot',
decision: PolicyDecision.ALLOW,
priority: PRIORITY_SUBAGENT_TOOL,
}),
);
expect(mockPolicyEngine.addRule).toHaveBeenCalledWith(
expect.objectContaining({
toolName: 'mcp_browser_agent_take_screenshot',
decision: PolicyDecision.ALLOW,
priority: PRIORITY_SUBAGENT_TOOL,
}),
);
expect(mockPolicyEngine.addRule).toHaveBeenCalledWith(
expect.objectContaining({
toolName: 'mcp_browser_agent_list_pages',
decision: PolicyDecision.ALLOW,
priority: PRIORITY_SUBAGENT_TOOL,
}),
);
});
});
describe('cleanupBrowserAgent', () => {
it('should call close on browser manager', async () => {
await cleanupBrowserAgent(
@@ -21,8 +21,6 @@ import type { LocalAgentDefinition } from '../types.js';
import type { MessageBus } from '../../confirmation-bus/message-bus.js';
import type { AnyDeclarativeTool } from '../../tools/tools.js';
import { BrowserManager } from './browserManager.js';
import { BROWSER_AGENT_NAME } from './browserAgentDefinition.js';
import { MCP_TOOL_PREFIX } from '../../tools/mcp-tool.js';
import {
BrowserAgentDefinition,
type BrowserTaskResultSchema,
@@ -32,11 +30,6 @@ import { createAnalyzeScreenshotTool } from './analyzeScreenshot.js';
import { injectAutomationOverlay } from './automationOverlay.js';
import { injectInputBlocker } from './inputBlocker.js';
import { debugLogger } from '../../utils/debugLogger.js';
import {
PolicyDecision,
PRIORITY_SUBAGENT_TOOL,
type PolicyRule,
} from '../../policy/types.js';
/**
* Creates a browser agent definition with MCP tools configured.
@@ -93,79 +86,9 @@ export async function createBrowserAgentDefinition(
browserManager,
messageBus,
shouldDisableInput,
browserConfig.customConfig.blockFileUploads,
);
const availableToolNames = mcpTools.map((t) => t.name);
// Register high-priority policy rules for sensitive actions which is not
// able to be overwrite by YOLO mode.
const policyEngine = config.getPolicyEngine();
if (policyEngine) {
const existingRules = policyEngine.getRules();
const restrictedTools = ['fill', 'fill_form'];
// ASK_USER for upload_file and evaluate_script when sensitive action
// need confirmation.
if (browserConfig.customConfig.confirmSensitiveActions) {
restrictedTools.push('upload_file', 'evaluate_script');
}
for (const toolName of restrictedTools) {
const rule = generateAskUserRules(toolName);
if (!existingRules.some((r) => isRuleEqual(r, rule))) {
policyEngine.addRule(rule);
}
}
// Reduce noise for read-only tools in default mode
const readOnlyTools = [
'take_snapshot',
'take_screenshot',
'list_pages',
'list_network_requests',
];
for (const toolName of readOnlyTools) {
if (availableToolNames.includes(toolName)) {
const rule = generateAllowRules(toolName);
if (!existingRules.some((r) => isRuleEqual(r, rule))) {
policyEngine.addRule(rule);
}
}
}
}
function generateAskUserRules(toolName: string): PolicyRule {
return {
toolName: `${MCP_TOOL_PREFIX}${BROWSER_AGENT_NAME}_${toolName}`,
decision: PolicyDecision.ASK_USER,
priority: 999,
source: 'BrowserAgent (Sensitive Actions)',
mcpName: BROWSER_AGENT_NAME,
};
}
function generateAllowRules(toolName: string): PolicyRule {
return {
toolName: `${MCP_TOOL_PREFIX}${BROWSER_AGENT_NAME}_${toolName}`,
decision: PolicyDecision.ALLOW,
priority: PRIORITY_SUBAGENT_TOOL,
source: 'BrowserAgent (Read-Only)',
mcpName: BROWSER_AGENT_NAME,
};
}
// Check if policy rule the same in all the attributes that we care about
function isRuleEqual(rule1: PolicyRule, rule2: PolicyRule) {
return (
rule1.toolName === rule2.toolName &&
rule1.decision === rule2.decision &&
rule1.priority === rule2.priority &&
rule1.mcpName === rule2.mcpName
);
}
// Validate required semantic tools are available
const requiredSemanticTools = [
'click',
@@ -301,55 +301,4 @@ describe('mcpToolWrapper', () => {
expect(mockBrowserManager.callTool).toHaveBeenCalledTimes(3);
});
});
describe('Hard Block: upload_file', () => {
beforeEach(() => {
mockMcpTools.push({
name: 'upload_file',
description: 'Upload a file',
inputSchema: {
type: 'object',
properties: { path: { type: 'string' } },
},
});
});
it('should block upload_file when blockFileUploads is true', async () => {
const tools = await createMcpDeclarativeTools(
mockBrowserManager,
mockMessageBus,
false,
true, // blockFileUploads
);
const uploadTool = tools.find((t) => t.name === 'upload_file')!;
const invocation = uploadTool.build({ path: 'test.txt' });
const result = await invocation.execute(new AbortController().signal);
expect(result.error).toBeDefined();
expect(result.llmContent).toContain('File uploads are blocked');
expect(mockBrowserManager.callTool).not.toHaveBeenCalled();
});
it('should NOT block upload_file when blockFileUploads is false', async () => {
const tools = await createMcpDeclarativeTools(
mockBrowserManager,
mockMessageBus,
false,
false, // blockFileUploads
);
const uploadTool = tools.find((t) => t.name === 'upload_file')!;
const invocation = uploadTool.build({ path: 'test.txt' });
const result = await invocation.execute(new AbortController().signal);
expect(result.error).toBeUndefined();
expect(result.llmContent).toBe('Tool result');
expect(mockBrowserManager.callTool).toHaveBeenCalledWith(
'upload_file',
expect.anything(),
expect.anything(),
);
});
});
});
@@ -63,7 +63,6 @@ class McpToolInvocation extends BaseToolInvocation<
params: Record<string, unknown>,
messageBus: MessageBus,
private readonly shouldDisableInput: boolean,
private readonly blockFileUploads: boolean = false,
) {
super(
params,
@@ -115,16 +114,6 @@ class McpToolInvocation extends BaseToolInvocation<
async execute(signal: AbortSignal): Promise<ToolResult> {
try {
// Hard block for file uploads if configured
if (this.blockFileUploads && this.toolName === 'upload_file') {
const errorMsg = 'File uploads are blocked by configuration.';
return {
llmContent: `Error: ${errorMsg}`,
returnDisplay: `Error: ${errorMsg}`,
error: { message: errorMsg },
};
}
// Suspend the input blocker for interactive tools so
// chrome-devtools-mcp's interactability checks pass.
// Only toggles pointer-events CSS — no DOM change, no flicker.
@@ -208,7 +197,6 @@ class McpDeclarativeTool extends DeclarativeTool<
parameterSchema: unknown,
messageBus: MessageBus,
private readonly shouldDisableInput: boolean,
private readonly blockFileUploads: boolean = false,
) {
super(
name,
@@ -239,7 +227,6 @@ class McpDeclarativeTool extends DeclarativeTool<
params,
this.messageBus,
this.shouldDisableInput,
this.blockFileUploads,
);
}
}
@@ -262,7 +249,6 @@ export async function createMcpDeclarativeTools(
browserManager: BrowserManager,
messageBus: MessageBus,
shouldDisableInput: boolean = false,
blockFileUploads: boolean = false,
): Promise<McpDeclarativeTool[]> {
// Get dynamically discovered tools from the MCP server
const mcpTools = await browserManager.getDiscoveredTools();
@@ -286,7 +272,6 @@ export async function createMcpDeclarativeTools(
schema.parametersJsonSchema,
messageBus,
shouldDisableInput,
blockFileUploads,
);
});
@@ -66,10 +66,9 @@ export const CodebaseInvestigatorAgent = (
name: 'codebase_investigator',
kind: 'local',
displayName: 'Codebase Investigator Agent',
description:
`The specialized tool for codebase analysis, architectural mapping, and understanding system-wide dependencies. ` +
`Invoke this tool for tasks like vague requests, bug root-cause analysis, system refactoring, comprehensive feature implementation or to answer questions about the codebase that require investigation. ` +
`It returns a structured report with key file paths, symbols, and actionable architectural insights.`,
description: `The specialized tool for codebase analysis, architectural mapping, and understanding system-wide dependencies.
Invoke this tool for tasks like vague requests, bug root-cause analysis, system refactoring, comprehensive feature implementation or to answer questions about the codebase that require investigation.
It returns a structured report with key file paths, symbols, and actionable architectural insights.`,
inputConfig: {
inputSchema: {
type: 'object',
-6
View File
@@ -330,10 +330,6 @@ export interface BrowserAgentCustomConfig {
allowedDomains?: string[];
/** Disable user input on the browser window during automation. Default: true in non-headless mode */
disableUserInput?: boolean;
/** Whether to confirm sensitive actions (e.g., fill_form, evaluate_script). */
confirmSensitiveActions?: boolean;
/** Whether to block file uploads. */
blockFileUploads?: boolean;
}
/**
@@ -3139,8 +3135,6 @@ export class Config implements McpContext, AgentLoopContext {
visualModel: customConfig.visualModel,
allowedDomains: customConfig.allowedDomains,
disableUserInput: customConfig.disableUserInput,
confirmSensitiveActions: customConfig.confirmSensitiveActions,
blockFileUploads: customConfig.blockFileUploads,
},
};
}
@@ -696,14 +696,4 @@ describe('ide-connection-utils', () => {
); // Short-circuiting
});
});
describe('createProxyAwareFetch', () => {
it('should return a proxy-aware fetcher function', async () => {
const { createProxyAwareFetch } = await import(
'./ide-connection-utils.js'
);
const fetcher = await createProxyAwareFetch('127.0.0.1');
expect(typeof fetcher).toBe('function');
});
});
});
@@ -7,7 +7,7 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { EnvHttpProxyAgent, fetch as undiciFetch } from 'undici';
import { EnvHttpProxyAgent } from 'undici';
import { debugLogger } from '../utils/debugLogger.js';
import { isSubpath, resolveToRealPath } from '../utils/paths.js';
import { isNodeError } from '../utils/errors.js';
@@ -286,7 +286,12 @@ export async function createProxyAwareFetch(ideServerHost: string) {
const agent = new EnvHttpProxyAgent({
noProxy: [existingNoProxy, ideServerHost].filter(Boolean).join(','),
});
const undiciPromise = import('undici');
// Suppress unhandled rejection if the promise is not awaited immediately.
// If the import fails, the error will be thrown when awaiting undiciPromise below.
undiciPromise.catch(() => {});
return async (url: string | URL, init?: RequestInit): Promise<Response> => {
const { fetch: fetchFn } = await undiciPromise;
const fetchOptions: RequestInit & { dispatcher?: unknown } = {
...init,
dispatcher: agent,
@@ -294,7 +299,7 @@ export async function createProxyAwareFetch(ideServerHost: string) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const options = fetchOptions as unknown as import('undici').RequestInit;
try {
const response = await undiciFetch(url, options);
const response = await fetchFn(url, options);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return new Response(response.body as ReadableStream<unknown> | null, {
status: response.status,
@@ -160,11 +160,6 @@ describe('PolicyEngine', () => {
engine = new PolicyEngine({ rules });
// Match with unqualified name + serverName
expect((await engine.check({ name: 'tool' }, 'my-server')).decision).toBe(
PolicyDecision.ALLOW,
);
// Match with qualified name (standard)
expect(
(await engine.check({ name: 'mcp_my-server_tool' }, 'my-server'))
+1 -24
View File
@@ -30,8 +30,6 @@ import {
MCP_TOOL_PREFIX,
isMcpToolAnnotation,
parseMcpToolName,
formatMcpToolName,
isMcpToolName,
} from '../tools/mcp-tool.js';
function isWildcardPattern(name: string): boolean {
@@ -118,28 +116,7 @@ function ruleMatches(
return false;
}
} else if (toolCall.name !== rule.toolName) {
// If names don't match exactly, check for MCP short/full name mismatches
let mcpMatch = false;
if (serverName && toolCall.name) {
// Case 1: Rule uses short name + mcpName -> match FQN tool call
if (rule.mcpName && !isMcpToolName(rule.toolName)) {
if (
toolCall.name === formatMcpToolName(rule.mcpName, rule.toolName)
) {
mcpMatch = true;
}
}
// Case 2: Rule uses FQN -> match short tool call (qualified by serverName)
if (!mcpMatch && isMcpToolName(rule.toolName)) {
if (rule.toolName === formatMcpToolName(serverName, toolCall.name)) {
mcpMatch = true;
}
}
}
if (!mcpMatch) {
return false;
}
return false;
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-devtools",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"type": "module",
"main": "dist/src/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-sdk",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"description": "Gemini CLI SDK",
"license": "Apache-2.0",
"repository": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-test-utils",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "gemini-cli-vscode-ide-companion",
"displayName": "Gemini CLI Companion",
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
"version": "0.36.0-nightly.20260321.fc03891a1",
"version": "0.36.0-nightly.20260317.2f90b4653",
"publisher": "google",
"icon": "assets/icon.png",
"repository": {
-14
View File
@@ -2134,20 +2134,6 @@
"markdownDescription": "Disable user input on browser window during automation.\n\n- Category: `Advanced`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"confirmSensitiveActions": {
"title": "Confirm Sensitive Actions",
"description": "Require manual confirmation for sensitive browser actions (e.g., fill_form, evaluate_script).",
"markdownDescription": "Require manual confirmation for sensitive browser actions (e.g., fill_form, evaluate_script).\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"blockFileUploads": {
"title": "Block File Uploads",
"description": "Hard-block file upload requests from the browser agent.",
"markdownDescription": "Hard-block file upload requests from the browser agent.\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"type": "boolean"
}
},
"additionalProperties": false