mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-08 16:11:58 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cad496650b | |||
| 38de78045d | |||
| dce25e4229 | |||
| 59bf8cbb45 |
@@ -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) -
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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`**:
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Generated
+9
-9
@@ -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
@@ -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,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",
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,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',
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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,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,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,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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user