mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 08:10:57 -07:00
Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ea49d486b | |||
| 4d4fc80f25 | |||
| dfa56c37b4 | |||
| 447a854ad9 | |||
| b58d79c517 | |||
| daf3691841 | |||
| 517961b2eb | |||
| ec0161ad37 | |||
| cdf077da56 | |||
| 99e5164c82 | |||
| d0ebc81c28 | |||
| c67817f1a9 | |||
| c7d44e339b | |||
| 6055c47079 | |||
| 4c533b1249 | |||
| 4a3d9414ef | |||
| 0df9498674 | |||
| 8f391585ab | |||
| e7b6326cfa | |||
| 1a70fdd364 | |||
| b316fcc44d | |||
| d1dc4902fd | |||
| d3766875f8 | |||
| 28935d1e6b | |||
| 244a608186 | |||
| fc03891a11 | |||
| 974d29128f | |||
| 3382e0413e | |||
| 992c04e768 | |||
| fbb17ebf58 | |||
| e8fe43bd69 | |||
| 11ec4ac2f8 | |||
| 8eb419a47a | |||
| 4e80f01fda | |||
| 6c78eb7a39 | |||
| 86a3a913b5 | |||
| 05e4ea80ee | |||
| cca595971d | |||
| 26b9af1cdc | |||
| b459e1a108 | |||
| 62cb14fa52 | |||
| 7a65c1e91d | |||
| 5a3c7154df | |||
| b9c87c14a2 | |||
| 52250c162d | |||
| b52641de0d | |||
| 8615315711 | |||
| c9a336976b | |||
| 06a7873c51 | |||
| 0e66f545ca | |||
| 98d1bec99f | |||
| 08063d7b0a | |||
| 2ebcd48a4e | |||
| 36dbaa8462 | |||
| 4fc059beb5 | |||
| 46ec71bf0e | |||
| 33f630111f | |||
| b3ebab308e | |||
| 4e5dfd0cb7 | |||
| 524b1e39a5 | |||
| 32a123fc54 | |||
| 23264ced9a | |||
| 7de0616229 | |||
| 39d3b0e28c | |||
| 5acaacad96 | |||
| a921bcd9ef | |||
| 4249de5e23 | |||
| 536bbdf73f | |||
| 201efbd07f | |||
| 8ae078b4d7 | |||
| d84b173330 | |||
| 33d654e7d9 | |||
| 6eb100cc41 | |||
| dca1ef09cb | |||
| 616c944350 | |||
| c0f919310e | |||
| 92a09c8d8d | |||
| ecae62e25a | |||
| abb4583554 | |||
| fc7a269f0a | |||
| 9c85ef1fda | |||
| bd60ba9e68 | |||
| 2e466bd4cb | |||
| 55778583b9 | |||
| 661bd39362 | |||
| 04eb2ee9a3 | |||
| c6f6fb00c9 | |||
| 02eff4efb3 | |||
| db9eef50ef |
+12
-1
@@ -2,9 +2,20 @@
|
||||
"experimental": {
|
||||
"plan": true,
|
||||
"extensionReloading": true,
|
||||
"modelSteering": true
|
||||
"modelSteering": true,
|
||||
"memoryManager": true
|
||||
},
|
||||
"general": {
|
||||
"devtools": true
|
||||
},
|
||||
"agents": {
|
||||
"overrides": {
|
||||
"browser_agent": { "enabled": true }
|
||||
},
|
||||
"browser": {
|
||||
"headless": true,
|
||||
"sessionMode": "isolated",
|
||||
"allowedDomains": ["*.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,12 +71,44 @@ accessible.
|
||||
tables).
|
||||
- **Media:** Use lowercase hyphenated filenames. Provide descriptive alt text
|
||||
for all images.
|
||||
- **Details section:** Use the `<details>` tag to create a collapsible section.
|
||||
This is useful for supplementary or data-heavy information that isn't critical
|
||||
to the main flow.
|
||||
|
||||
Example:
|
||||
|
||||
<details>
|
||||
<summary>Title</summary>
|
||||
|
||||
- First entry
|
||||
- Second entry
|
||||
|
||||
</details>
|
||||
|
||||
- **Callouts**: Use GitHub-flavored markdown alerts to highlight important
|
||||
information. To ensure the formatting is preserved by `npm run format`, place
|
||||
an empty line, then the `<!-- prettier-ignore -->` comment directly before
|
||||
the callout block. The callout type (`[!TYPE]`) should be on the first line,
|
||||
followed by a newline, and then the content, with each subsequent line of
|
||||
content starting with `>`. Available types are `NOTE`, `TIP`, `IMPORTANT`,
|
||||
`WARNING`, and `CAUTION`.
|
||||
|
||||
Example:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is an example of a multi-line note that will be preserved
|
||||
> by Prettier.
|
||||
|
||||
### Structure
|
||||
- **BLUF:** Start with an introduction explaining what to expect.
|
||||
- **Experimental features:** If a feature is clearly noted as experimental,
|
||||
add the following note immediately after the introductory paragraph:
|
||||
`> **Note:** This is a preview feature currently under active development.`
|
||||
add the following note immediately after the introductory paragraph:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is an experimental feature currently under active development.
|
||||
|
||||
- **Headings:** Use hierarchical headings to support the user journey.
|
||||
- **Procedures:**
|
||||
- Introduce lists of steps with a complete sentence.
|
||||
@@ -85,8 +117,7 @@ add the following note immediately after the introductory paragraph:
|
||||
- Put conditions before instructions (e.g., "On the Settings page, click...").
|
||||
- Provide clear context for where the action takes place.
|
||||
- Indicate optional steps clearly (e.g., "Optional: ...").
|
||||
- **Elements:** Use bullet lists, tables, notes (`> **Note:**`), and warnings
|
||||
(`> **Warning:**`).
|
||||
- **Elements:** Use bullet lists, tables, details, and callouts.
|
||||
- **Avoid using a table of contents:** If a table of contents is present, remove
|
||||
it.
|
||||
- **Next steps:** Conclude with a "Next steps" section if applicable.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
packages/core/src/services/scripts/*.exe
|
||||
@@ -1,7 +1,9 @@
|
||||
name: 'Website issue'
|
||||
description: 'Report an issue with the Gemini CLI Website and Gemini CLI Extensions Gallery'
|
||||
title: 'GeminiCLI.com Feedback: [ISSUE]'
|
||||
labels:
|
||||
- 'area/extensions'
|
||||
- 'area/documentation'
|
||||
body:
|
||||
- type: 'markdown'
|
||||
attributes:
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
name: 'Evals: PR Guidance'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'packages/core/src/**/*.ts'
|
||||
- '!**/*.test.ts'
|
||||
- '!**/*.test.tsx'
|
||||
|
||||
permissions:
|
||||
pull-requests: 'write'
|
||||
contents: 'read'
|
||||
|
||||
jobs:
|
||||
provide-guidance:
|
||||
name: 'Model Steering Guidance'
|
||||
runs-on: 'ubuntu-latest'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Detect Steering Changes'
|
||||
id: 'detect'
|
||||
run: |
|
||||
STEERING_DETECTED=$(node scripts/changed_prompt.js --steering-only)
|
||||
echo "STEERING_DETECTED=$STEERING_DETECTED" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Analyze PR Content'
|
||||
if: "steps.detect.outputs.STEERING_DETECTED == 'true'"
|
||||
id: 'analysis'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
# Check for behavioral eval changes
|
||||
EVAL_CHANGES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep "^evals/" || true)
|
||||
if [ -z "$EVAL_CHANGES" ]; then
|
||||
echo "MISSING_EVALS=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Check if user is a maintainer (has write/admin access)
|
||||
USER_PERMISSION=$(gh api repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission --jq '.permission')
|
||||
if [[ "$USER_PERMISSION" == "admin" || "$USER_PERMISSION" == "write" ]]; then
|
||||
echo "IS_MAINTAINER=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: 'Post Guidance Comment'
|
||||
if: "steps.detect.outputs.STEERING_DETECTED == 'true'"
|
||||
uses: 'thollander/actions-comment-pull-request@65f9e5c9a1f2cd378bd74b2e057c9736982a8e74' # ratchet:thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
comment-tag: 'eval-guidance-bot'
|
||||
message: |
|
||||
### 🧠 Model Steering Guidance
|
||||
|
||||
This PR modifies files that affect the model's behavior (prompts, tools, or instructions).
|
||||
|
||||
${{ steps.analysis.outputs.MISSING_EVALS == 'true' && '- ⚠️ **Consider adding Evals:** No behavioral evaluations (`evals/*.eval.ts`) were added or updated in this PR. Consider adding a test case to verify the new behavior and prevent regressions.' || '' }}
|
||||
${{ steps.analysis.outputs.IS_MAINTAINER == 'true' && '- 🚀 **Maintainer Reminder:** Please ensure that these changes do not regress results on benchmark evals before merging.' || '' }}
|
||||
|
||||
---
|
||||
*This is an automated guidance message triggered by steering logic signatures.*
|
||||
@@ -61,6 +61,7 @@ jobs:
|
||||
GEMINI_MODEL: '${{ matrix.model }}'
|
||||
RUN_EVALS: "${{ github.event.inputs.run_all != 'false' }}"
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
VITEST_RETRY: 0
|
||||
run: |
|
||||
CMD="npm run test:all_evals"
|
||||
PATTERN="${TEST_NAME_PATTERN}"
|
||||
|
||||
@@ -106,6 +106,67 @@ organization.
|
||||
ensures users maintain final control over which permitted servers are actually
|
||||
active in their environment.
|
||||
|
||||
#### Required MCP Servers (preview)
|
||||
|
||||
**Default**: empty
|
||||
|
||||
Allows administrators to define MCP servers that are **always injected** into
|
||||
the user's environment. Unlike the allowlist (which filters user-configured
|
||||
servers), required servers are automatically added regardless of the user's
|
||||
local configuration.
|
||||
|
||||
**Required Servers Format:**
|
||||
|
||||
```json
|
||||
{
|
||||
"requiredMcpServers": {
|
||||
"corp-compliance-tool": {
|
||||
"url": "https://mcp.corp/compliance",
|
||||
"type": "http",
|
||||
"trust": true,
|
||||
"description": "Corporate compliance tool"
|
||||
},
|
||||
"internal-registry": {
|
||||
"url": "https://registry.corp/mcp",
|
||||
"type": "sse",
|
||||
"authProviderType": "google_credentials",
|
||||
"oauth": {
|
||||
"scopes": ["https://www.googleapis.com/auth/scope"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Supported Fields:**
|
||||
|
||||
- `url`: (Required) The full URL of the MCP server endpoint.
|
||||
- `type`: (Required) The connection type (`sse` or `http`).
|
||||
- `trust`: (Optional) If set to `true`, tool execution will not require user
|
||||
approval. Defaults to `true` for required servers.
|
||||
- `description`: (Optional) Human-readable description of the server.
|
||||
- `authProviderType`: (Optional) Authentication provider (`dynamic_discovery`,
|
||||
`google_credentials`, or `service_account_impersonation`).
|
||||
- `oauth`: (Optional) OAuth configuration including `scopes`, `clientId`, and
|
||||
`clientSecret`.
|
||||
- `targetAudience`: (Optional) OAuth target audience for service-to-service
|
||||
auth.
|
||||
- `targetServiceAccount`: (Optional) Service account email to impersonate.
|
||||
- `headers`: (Optional) Additional HTTP headers to send with requests.
|
||||
- `includeTools` / `excludeTools`: (Optional) Tool filtering lists.
|
||||
- `timeout`: (Optional) Timeout in milliseconds for MCP requests.
|
||||
|
||||
**Client Enforcement Logic:**
|
||||
|
||||
- Required servers are injected **after** allowlist filtering, so they are
|
||||
always available even if the allowlist is active.
|
||||
- If a required server has the **same name** as a locally configured server, the
|
||||
admin configuration **completely overrides** the local one.
|
||||
- Required servers only support remote transports (`sse`, `http`). Local
|
||||
execution fields (`command`, `args`, `env`, `cwd`) are not supported.
|
||||
- Required servers can coexist with allowlisted servers — both features work
|
||||
independently.
|
||||
|
||||
### Unmanaged Capabilities
|
||||
|
||||
**Enabled/Disabled** | Default: disabled
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Preview release: v0.35.0-preview.1
|
||||
# Preview release: v0.35.0-preview.2
|
||||
|
||||
Released: March 17, 2026
|
||||
Released: March 19, 2026
|
||||
|
||||
Our preview release includes the latest, new, and experimental features. This
|
||||
release may not be as stable as our [latest weekly release](latest.md).
|
||||
@@ -33,6 +33,10 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
## What's Changed
|
||||
|
||||
- fix(patch): cherry-pick 4e5dfd0 to release/v0.35.0-preview.1-pr-23074 to patch
|
||||
version v0.35.0-preview.1 and create version 0.35.0-preview.2 by
|
||||
@gemini-cli-robot in
|
||||
[#23134](https://github.com/google-gemini/gemini-cli/pull/23134)
|
||||
- feat(cli): customizable keyboard shortcuts by @scidomino in
|
||||
[#21945](https://github.com/google-gemini/gemini-cli/pull/21945)
|
||||
- feat(core): Thread `AgentLoopContext` through core. by @joshualitt in
|
||||
@@ -373,4 +377,4 @@ npm install -g @google/gemini-cli@preview
|
||||
[#22815](https://github.com/google-gemini/gemini-cli/pull/22815)
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.34.0-preview.4...v0.35.0-preview.1
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.34.0-preview.4...v0.35.0-preview.2
|
||||
|
||||
@@ -39,7 +39,9 @@ file in your project's temporary directory, typically located at
|
||||
The Checkpointing feature is disabled by default. To enable it, you need to edit
|
||||
your `settings.json` file.
|
||||
|
||||
> **Note:** The `--checkpointing` command-line flag was removed in version
|
||||
<!-- prettier-ignore -->
|
||||
> [!CAUTION]
|
||||
> The `--checkpointing` command-line flag was removed in version
|
||||
> 0.11.0. Checkpointing can now only be enabled through the `settings.json`
|
||||
> configuration file.
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ These commands are available within the interactive REPL.
|
||||
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. |
|
||||
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. Forces non-interactive mode. |
|
||||
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
|
||||
| `--worktree` | `-w` | string | - | Start Gemini in a new git worktree. If no name is provided, one is generated automatically. Requires `experimental.worktrees: true` in settings. |
|
||||
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
|
||||
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
|
||||
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
|
||||
|
||||
@@ -30,7 +30,9 @@ separator (`/` or `\`) being converted to a colon (`:`).
|
||||
- A file at `<project>/.gemini/commands/git/commit.toml` becomes the namespaced
|
||||
command `/git:commit`.
|
||||
|
||||
> [!TIP] After creating or modifying `.toml` command files, run
|
||||
<!-- prettier-ignore -->
|
||||
> [!TIP]
|
||||
> After creating or modifying `.toml` command files, run
|
||||
> `/commands reload` to pick up your changes without restarting the CLI.
|
||||
|
||||
## TOML file format (v1)
|
||||
@@ -177,10 +179,10 @@ ensure that only intended commands can be run.
|
||||
automatically shell-escaped (see
|
||||
[Context-Aware Injection](#1-context-aware-injection-with-args) above).
|
||||
3. **Robust parsing:** The parser correctly handles complex shell commands that
|
||||
include nested braces, such as JSON payloads. **Note:** The content inside
|
||||
`!{...}` must have balanced braces (`{` and `}`). If you need to execute a
|
||||
command containing unbalanced braces, consider wrapping it in an external
|
||||
script file and calling the script within the `!{...}` block.
|
||||
include nested braces, such as JSON payloads. The content inside `!{...}`
|
||||
must have balanced braces (`{` and `}`). If you need to execute a command
|
||||
containing unbalanced braces, consider wrapping it in an external script
|
||||
file and calling the script within the `!{...}` block.
|
||||
4. **Security check and confirmation:** The CLI performs a security check on
|
||||
the final, resolved command (after arguments are escaped and substituted). A
|
||||
dialog will appear showing the exact command(s) to be executed.
|
||||
|
||||
+15
-9
@@ -5,9 +5,11 @@ and managing Gemini CLI in an enterprise environment. By leveraging system-level
|
||||
settings, administrators can enforce security policies, manage tool access, and
|
||||
ensure a consistent experience for all users.
|
||||
|
||||
> **A note on security:** The patterns described in this document are intended
|
||||
> to help administrators create a more controlled and secure environment for
|
||||
> using Gemini CLI. However, they should not be considered a foolproof security
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> The patterns described in this document are intended to help
|
||||
> administrators create a more controlled and secure environment for using
|
||||
> Gemini CLI. However, they should not be considered a foolproof security
|
||||
> boundary. A determined user with sufficient privileges on their local machine
|
||||
> may still be able to circumvent these configurations. These measures are
|
||||
> designed to prevent accidental misuse and enforce corporate policy in a
|
||||
@@ -280,10 +282,12 @@ environment to a blocklist.
|
||||
}
|
||||
```
|
||||
|
||||
**Security note:** Blocklisting with `excludeTools` is less secure than
|
||||
allowlisting with `coreTools`, as it relies on blocking known-bad commands, and
|
||||
clever users may find ways to bypass simple string-based blocks. **Allowlisting
|
||||
is the recommended approach.**
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> Blocklisting with `excludeTools` is less secure than
|
||||
> allowlisting with `coreTools`, as it relies on blocking known-bad commands,
|
||||
> and clever users may find ways to bypass simple string-based blocks.
|
||||
> **Allowlisting is the recommended approach.**
|
||||
|
||||
### Disabling YOLO mode
|
||||
|
||||
@@ -494,8 +498,10 @@ other events. For more information, see the
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Ensure that `logPrompts` is set to `false` in an enterprise setting to
|
||||
avoid collecting potentially sensitive information from user prompts.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Ensure that `logPrompts` is set to `false` in an enterprise setting to
|
||||
> avoid collecting potentially sensitive information from user prompts.
|
||||
|
||||
## Authentication
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# Git Worktrees (experimental)
|
||||
|
||||
When working on multiple tasks at once, you can use Git worktrees to give each
|
||||
Gemini session its own copy of the codebase. Git worktrees create separate
|
||||
working directories that each have their own files and branch while sharing the
|
||||
same repository history. This prevents changes in one session from colliding
|
||||
with another.
|
||||
|
||||
Learn more about [session management](./session-management.md).
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is an experimental feature currently under active development. Your
|
||||
> feedback is invaluable as we refine this feature. If you have ideas,
|
||||
> suggestions, or encounter issues:
|
||||
>
|
||||
> - [Open an issue](https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml) on GitHub.
|
||||
> - Use the **/bug** command within Gemini CLI to file an issue.
|
||||
|
||||
Learn more in the official Git worktree
|
||||
[documentation](https://git-scm.com/docs/git-worktree).
|
||||
|
||||
## How to enable Git worktrees
|
||||
|
||||
Git worktrees are an experimental feature. You must enable them in your settings
|
||||
using the `/settings` command or by manually editing your `settings.json` file.
|
||||
|
||||
1. Use the `/settings` command.
|
||||
2. Search for and set **Enable Git Worktrees** to `true`.
|
||||
|
||||
Alternatively, add the following to your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"worktrees": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How to use Git worktrees
|
||||
|
||||
Use the `--worktree` (`-w`) flag to create an isolated worktree and start Gemini
|
||||
CLI in it.
|
||||
|
||||
- **Start with a specific name:** The value you pass becomes both the directory
|
||||
name (within `.gemini/worktrees/`) and the branch name.
|
||||
|
||||
```bash
|
||||
gemini --worktree feature-search
|
||||
```
|
||||
|
||||
- **Start with a random name:** If you omit the name, Gemini generates a random
|
||||
one automatically (for example, `worktree-a1b2c3d4`).
|
||||
|
||||
```bash
|
||||
gemini --worktree
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Remember to initialize your development environment in each new
|
||||
> worktree according to your project's setup. Depending on your stack, this
|
||||
> might include running dependency installation (`npm install`, `yarn`), setting
|
||||
> up virtual environments, or following your project's standard build process.
|
||||
|
||||
## How to exit a Git worktree session
|
||||
|
||||
When you exit a worktree session (using `/quit` or `Ctrl+C`), Gemini leaves the
|
||||
worktree intact so your work is not lost. This includes your uncommitted changes
|
||||
(modified files, staged changes, or untracked files) and any new commits you
|
||||
have made.
|
||||
|
||||
Gemini prioritizes a fast and safe exit: it **does not automatically delete**
|
||||
your worktree or branch. You are responsible for cleaning up your worktrees
|
||||
manually once you are finished with them.
|
||||
|
||||
When you exit, Gemini displays instructions on how to resume your work or how to
|
||||
manually remove the worktree if you no longer need it.
|
||||
|
||||
## Resuming work in a Git worktree
|
||||
|
||||
To resume a session in a worktree, navigate to the worktree directory and start
|
||||
Gemini CLI with the `--resume` flag and the session ID:
|
||||
|
||||
```bash
|
||||
cd .gemini/worktrees/feature-search
|
||||
gemini --resume <session_id>
|
||||
```
|
||||
|
||||
## Managing Git worktrees manually
|
||||
|
||||
For more control over worktree location and branch configuration, or to clean up
|
||||
a preserved worktree, you can use Git directly:
|
||||
|
||||
- **Clean up a preserved Git worktree:**
|
||||
```bash
|
||||
git worktree remove .gemini/worktrees/feature-search --force
|
||||
git branch -D worktree-feature-search
|
||||
```
|
||||
- **Create a Git worktree manually:**
|
||||
```bash
|
||||
git worktree add ../project-feature-search -b feature-search
|
||||
cd ../project-feature-search && gemini
|
||||
```
|
||||
|
||||
[Open an issue]: https://github.com/google-gemini/gemini-cli/issues
|
||||
@@ -4,9 +4,10 @@ Model steering lets you provide real-time guidance and feedback to Gemini CLI
|
||||
while it is actively executing a task. This lets you correct course, add missing
|
||||
context, or skip unnecessary steps without having to stop and restart the agent.
|
||||
|
||||
> **Note:** This is a preview feature under active development. Preview features
|
||||
> may only be available in the **Preview** channel or may need to be enabled
|
||||
> under `/settings`.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is an experimental feature currently under active development and
|
||||
> may need to be enabled under `/settings`.
|
||||
|
||||
Model steering is particularly useful during complex [Plan Mode](./plan-mode.md)
|
||||
workflows or long-running subagent executions where you want to ensure the agent
|
||||
|
||||
+3
-1
@@ -5,7 +5,9 @@ used by Gemini CLI, giving you more control over your results. Use **Pro**
|
||||
models for complex tasks and reasoning, **Flash** models for high speed results,
|
||||
or the (recommended) **Auto** setting to choose the best model for your tasks.
|
||||
|
||||
> **Note:** The `/model` command (and the `--model` flag) does not override the
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> The `/model` command (and the `--model` flag) does not override the
|
||||
> model used by sub-agents. Consequently, even when using the `/model` flag you
|
||||
> may see other models used in your model usage reports.
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ Gemini CLI can send system notifications to alert you when a session completes
|
||||
or when it needs your attention, such as when it's waiting for you to approve a
|
||||
tool call.
|
||||
|
||||
> **Note:** This is a preview feature currently under active development.
|
||||
> Preview features may be available on the **Preview** channel or may need to be
|
||||
> enabled under `/settings`.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is an experimental feature currently under active development and
|
||||
> may need to be enabled under `/settings`.
|
||||
|
||||
Notifications are particularly useful when running long-running tasks or using
|
||||
[Plan Mode](./plan-mode.md), letting you switch to other windows while Gemini
|
||||
|
||||
@@ -35,19 +35,17 @@ To launch Gemini CLI in Plan Mode once:
|
||||
To start Plan Mode while using Gemini CLI:
|
||||
|
||||
- **Keyboard shortcut:** Press `Shift+Tab` to cycle through approval modes
|
||||
(`Default` -> `Auto-Edit` -> `Plan`).
|
||||
|
||||
> **Note:** Plan Mode is automatically removed from the rotation when Gemini
|
||||
> CLI is actively processing or showing confirmation dialogs.
|
||||
(`Default` -> `Auto-Edit` -> `Plan`). Plan Mode is automatically removed from
|
||||
the rotation when Gemini CLI is actively processing or showing confirmation
|
||||
dialogs.
|
||||
|
||||
- **Command:** Type `/plan` in the input box.
|
||||
|
||||
- **Natural Language:** Ask Gemini CLI to "start a plan for...". Gemini CLI
|
||||
calls the
|
||||
[`enter_plan_mode`](../tools/planning.md#1-enter_plan_mode-enterplanmode) tool
|
||||
to switch modes.
|
||||
> **Note:** This tool is not available when Gemini CLI is in
|
||||
> [YOLO mode](../reference/configuration.md#command-line-arguments).
|
||||
to switch modes. This tool is not available when Gemini CLI is in
|
||||
[YOLO mode](../reference/configuration.md#command-line-arguments).
|
||||
|
||||
## How to use Plan Mode
|
||||
|
||||
@@ -407,7 +405,9 @@ To build a custom planning workflow, you can use:
|
||||
[custom plan directories](#custom-plan-directory-and-policies) and
|
||||
[custom policies](#custom-policies).
|
||||
|
||||
> **Note:** Use [Conductor] as a reference when building your own custom
|
||||
<!-- prettier-ignore -->
|
||||
> [!TIP]
|
||||
> Use [Conductor] as a reference when building your own custom
|
||||
> planning workflow.
|
||||
|
||||
By using Plan Mode as its execution environment, your custom methodology can
|
||||
|
||||
+24
-4
@@ -50,7 +50,25 @@ Cross-platform sandboxing with complete process isolation.
|
||||
**Note**: Requires building the sandbox image locally or using a published image
|
||||
from your organization's registry.
|
||||
|
||||
### 3. gVisor / runsc (Linux only)
|
||||
### 3. Windows Native Sandbox (Windows only)
|
||||
|
||||
... **Troubleshooting and Side Effects:**
|
||||
|
||||
The Windows Native sandbox uses the `icacls` command to set a "Low Mandatory
|
||||
Level" on files and directories it needs to write to.
|
||||
|
||||
- **Persistence**: These integrity level changes are persistent on the
|
||||
filesystem. Even after the sandbox session ends, files created or modified by
|
||||
the sandbox will retain their "Low" integrity level.
|
||||
- **Manual Reset**: If you need to reset the integrity level of a file or
|
||||
directory, you can use:
|
||||
```powershell
|
||||
icacls "C:\path\to\dir" /setintegritylevel Medium
|
||||
```
|
||||
- **System Folders**: The sandbox manager automatically skips setting integrity
|
||||
levels on system folders (like `C:\Windows`) for safety.
|
||||
|
||||
### 4. gVisor / runsc (Linux only)
|
||||
|
||||
Strongest isolation available: runs containers inside a user-space kernel via
|
||||
[gVisor](https://github.com/google/gvisor). gVisor intercepts all container
|
||||
@@ -253,9 +271,11 @@ $env:SANDBOX_SET_UID_GID="false" # Disable UID/GID mapping
|
||||
DEBUG=1 gemini -s -p "debug command"
|
||||
```
|
||||
|
||||
**Note:** If you have `DEBUG=true` in a project's `.env` file, it won't affect
|
||||
gemini-cli due to automatic exclusion. Use `.gemini/.env` files for gemini-cli
|
||||
specific debug settings.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> If you have `DEBUG=true` in a project's `.env` file, it won't affect
|
||||
> gemini-cli due to automatic exclusion. Use `.gemini/.env` files for
|
||||
> gemini-cli specific debug settings.
|
||||
|
||||
### Inspect sandbox
|
||||
|
||||
|
||||
@@ -96,6 +96,12 @@ Compatibility aliases:
|
||||
- `/chat ...` works for the same commands.
|
||||
- `/resume checkpoints ...` also remains supported during migration.
|
||||
|
||||
## Parallel sessions with Git worktrees
|
||||
|
||||
When working on multiple tasks at once, you can use
|
||||
[Git worktrees](./git-worktrees.md) to give each Gemini session its own copy of
|
||||
the codebase. This prevents changes in one session from colliding with another.
|
||||
|
||||
## Managing sessions
|
||||
|
||||
You can list and delete sessions to keep your history organized and manage disk
|
||||
|
||||
+14
-1
@@ -11,7 +11,9 @@ locations:
|
||||
- **User settings**: `~/.gemini/settings.json`
|
||||
- **Workspace settings**: `your-project/.gemini/settings.json`
|
||||
|
||||
Note: Workspace settings override user settings.
|
||||
<!-- prettier-ignore -->
|
||||
> [!IMPORTANT]
|
||||
> Workspace settings override user settings.
|
||||
|
||||
## Settings reference
|
||||
|
||||
@@ -99,6 +101,13 @@ 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 |
|
||||
@@ -115,6 +124,8 @@ they appear in the UI.
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| -------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Sandbox Allowed Paths | `tools.sandboxAllowedPaths` | List of additional paths that the sandbox is allowed to access. | `[]` |
|
||||
| Sandbox Network Access | `tools.sandboxNetworkAccess` | Whether the sandbox is allowed to access the network. | `false` |
|
||||
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
|
||||
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
|
||||
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
|
||||
@@ -147,11 +158,13 @@ they appear in the UI.
|
||||
| UI Label | Setting | Description | Default |
|
||||
| -------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
|
||||
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
|
||||
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Plan | `experimental.plan` | Enable Plan Mode. | `true` |
|
||||
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
|
||||
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
|
||||
| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` |
|
||||
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |
|
||||
|
||||
### Skills
|
||||
|
||||
+4
-2
@@ -63,8 +63,10 @@ Use the `/skills` slash command to view and manage available expertise:
|
||||
- `/skills enable <name>`: Re-enables a disabled skill.
|
||||
- `/skills reload`: Refreshes the list of discovered skills from all tiers.
|
||||
|
||||
_Note: `/skills disable` and `/skills enable` default to the `user` scope. Use
|
||||
`--scope workspace` to manage workspace-specific settings._
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> `/skills disable` and `/skills enable` default to the `user` scope. Use
|
||||
> `--scope workspace` to manage workspace-specific settings.
|
||||
|
||||
### From the Terminal
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ core instructions will apply unless you include them yourself.
|
||||
This feature is intended for advanced users who need to enforce strict,
|
||||
project-specific behavior or create a customized persona.
|
||||
|
||||
> Tip: You can export the current default system prompt to a file first, review
|
||||
<!-- prettier-ignore -->
|
||||
> [!TIP]
|
||||
> You can export the current default system prompt to a file first, review
|
||||
> it, and then selectively modify or replace it (see
|
||||
> [“Export the default prompt”](#export-the-default-prompt-recommended)).
|
||||
|
||||
|
||||
+34
-3
@@ -125,9 +125,11 @@ You must complete several setup steps before enabling Google Cloud telemetry.
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** This setting requires **Direct export** (in-process exporters)
|
||||
> and cannot be used when `useCollector` is `true`. If both are enabled,
|
||||
> telemetry will be disabled.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This setting requires **Direct export** (in-process exporters)
|
||||
> and cannot be used when `useCollector` is `true`. If both are enabled,
|
||||
> telemetry will be disabled.
|
||||
|
||||
3. Ensure your account or service account has these IAM roles:
|
||||
- Cloud Trace Agent
|
||||
@@ -304,6 +306,7 @@ Emitted at startup with the CLI configuration.
|
||||
- `extension_ids` (string)
|
||||
- `extensions_count` (int)
|
||||
- `auth_type` (string)
|
||||
- `worktree_active` (boolean)
|
||||
- `github_workflow_name` (string, optional)
|
||||
- `github_repository_hash` (string, optional)
|
||||
- `github_event_name` (string, optional)
|
||||
@@ -901,6 +904,20 @@ Logs keychain availability checks.
|
||||
|
||||
- `available` (boolean)
|
||||
|
||||
##### `gemini_cli.startup_stats`
|
||||
|
||||
Logs detailed startup performance statistics.
|
||||
|
||||
<details>
|
||||
<summary>Attributes</summary>
|
||||
|
||||
- `phases` (json array of startup phases)
|
||||
- `os_platform` (string)
|
||||
- `os_release` (string)
|
||||
- `is_docker` (boolean)
|
||||
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
||||
### Metrics
|
||||
@@ -917,6 +934,20 @@ Gemini CLI exports several custom metrics.
|
||||
|
||||
Incremented once per CLI startup.
|
||||
|
||||
##### Onboarding
|
||||
|
||||
Tracks onboarding flow from authentication to the user
|
||||
|
||||
- `gemini_cli.onboarding.start` (Counter, Int): Incremented when the
|
||||
authentication flow begins.
|
||||
|
||||
- `gemini_cli.onboarding.success` (Counter, Int): Incremented when the user
|
||||
onboarding flow completes successfully.
|
||||
<details>
|
||||
<summary>Attributes (Success)</summary>
|
||||
|
||||
- `user_tier` (string)
|
||||
|
||||
##### Tools
|
||||
|
||||
##### `gemini_cli.tool.call.count`
|
||||
|
||||
+12
-8
@@ -36,9 +36,11 @@ using the `/theme` command within Gemini CLI:
|
||||
preview or highlight as you select.
|
||||
4. Confirm your selection to apply the theme.
|
||||
|
||||
**Note:** If a theme is defined in your `settings.json` file (either by name or
|
||||
by a file path), you must remove the `"theme"` setting from the file before you
|
||||
can change the theme using the `/theme` command.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> If a theme is defined in your `settings.json` file (either by name or
|
||||
> by a file path), you must remove the `"theme"` setting from the file before
|
||||
> you can change the theme using the `/theme` command.
|
||||
|
||||
### Theme persistence
|
||||
|
||||
@@ -179,11 +181,13 @@ custom theme defined in `settings.json`.
|
||||
}
|
||||
```
|
||||
|
||||
**Security note:** For your safety, Gemini CLI will only load theme files that
|
||||
are located within your home directory. If you attempt to load a theme from
|
||||
outside your home directory, a warning will be displayed and the theme will not
|
||||
be loaded. This is to prevent loading potentially malicious theme files from
|
||||
untrusted sources.
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> For your safety, Gemini CLI will only load theme files that
|
||||
> are located within your home directory. If you attempt to load a theme from
|
||||
> outside your home directory, a warning will be displayed and the theme will
|
||||
> not be loaded. This is to prevent loading potentially malicious theme files
|
||||
> from untrusted sources.
|
||||
|
||||
### Example custom theme
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ create files, and control what Gemini CLI can see.
|
||||
## Prerequisites
|
||||
|
||||
- Gemini CLI installed and authenticated.
|
||||
- A project directory to work with (e.g., a git repository).
|
||||
- A project directory to work with (for example, a git repository).
|
||||
|
||||
## How to give the agent context (Reading files)
|
||||
## Providing context by reading files
|
||||
|
||||
Gemini CLI will generally try to read relevant files, sometimes prompting you
|
||||
for access (depending on your settings). To ensure that Gemini CLI uses a file,
|
||||
@@ -58,11 +58,13 @@ You know there's a `UserProfile` component, but you don't know where it lives.
|
||||
```
|
||||
|
||||
Gemini uses the `glob` or `list_directory` tools to search your project
|
||||
structure. It will return the specific path (e.g.,
|
||||
structure. It will return the specific path (for example,
|
||||
`src/components/UserProfile.tsx`), which you can then use with `@` in your next
|
||||
turn.
|
||||
|
||||
> **Tip:** You can also ask for lists of files, like "Show me all the TypeScript
|
||||
<!-- prettier-ignore -->
|
||||
> [!TIP]
|
||||
> You can also ask for lists of files, like "Show me all the TypeScript
|
||||
> configuration files in the root directory."
|
||||
|
||||
## How to modify code
|
||||
@@ -111,8 +113,8 @@ or, better yet, run your project's tests.
|
||||
`Run the tests for the UserProfile component.`
|
||||
```
|
||||
|
||||
Gemini CLI uses the `run_shell_command` tool to execute your test runner (e.g.,
|
||||
`npm test` or `jest`). This ensures the changes didn't break existing
|
||||
Gemini CLI uses the `run_shell_command` tool to execute your test runner (for
|
||||
example, `npm test` or `jest`). This ensures the changes didn't break existing
|
||||
functionality.
|
||||
|
||||
## Advanced: Controlling what Gemini sees
|
||||
|
||||
@@ -62,8 +62,10 @@ You tell Gemini about new servers by editing your `settings.json`.
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** The `command` is `docker`, and the rest are arguments passed to it.
|
||||
> We map the local environment variable into the container so your secret isn't
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> The `command` is `docker`, and the rest are arguments passed to it. We
|
||||
> map the local environment variable into the container so your secret isn't
|
||||
> hardcoded in the config file.
|
||||
|
||||
## How to verify the connection
|
||||
|
||||
@@ -11,8 +11,8 @@ persistent facts, and inspect the active context.
|
||||
|
||||
## Why manage context?
|
||||
|
||||
Out of the box, Gemini CLI is smart but generic. It doesn't know your preferred
|
||||
testing framework, your indentation style, or that you hate using `any` in
|
||||
Gemini CLI is powerful but general. It doesn't know your preferred testing
|
||||
framework, your indentation style, or your preference against `any` in
|
||||
TypeScript. Context management solves this by giving the agent persistent
|
||||
memory.
|
||||
|
||||
@@ -109,11 +109,11 @@ immediately. Force a reload with:
|
||||
|
||||
## Best practices
|
||||
|
||||
- **Keep it focused:** Don't dump your entire internal wiki into `GEMINI.md`.
|
||||
Keep instructions actionable and relevant to code generation.
|
||||
- **Keep it focused:** Avoid adding excessive content to `GEMINI.md`. Keep
|
||||
instructions actionable and relevant to code generation.
|
||||
- **Use negative constraints:** Explicitly telling the agent what _not_ to do
|
||||
(e.g., "Do not use class components") is often more effective than vague
|
||||
positive instructions.
|
||||
(for example, "Do not use class components") is often more effective than
|
||||
vague positive instructions.
|
||||
- **Review often:** Periodically check your `GEMINI.md` files to remove outdated
|
||||
rules.
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ structured environment with model steering's real-time feedback, you can guide
|
||||
Gemini CLI through the research and design phases to ensure the final
|
||||
implementation plan is exactly what you need.
|
||||
|
||||
> **Note:** This is a preview feature under active development. Preview features
|
||||
> may only be available in the **Preview** channel or may need to be enabled
|
||||
> under `/settings`.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is an experimental feature currently under active development and
|
||||
> may need to be enabled under `/settings`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ automate complex workflows, and manage background processes safely.
|
||||
## Prerequisites
|
||||
|
||||
- Gemini CLI installed and authenticated.
|
||||
- Basic familiarity with your system's shell (Bash, Zsh, PowerShell, etc.).
|
||||
- Basic familiarity with your system's shell (Bash, Zsh, PowerShell, and so on).
|
||||
|
||||
## How to run commands directly (`!`)
|
||||
|
||||
@@ -49,7 +49,7 @@ You want to run tests and fix any failures.
|
||||
6. Gemini uses `replace` to fix the bug.
|
||||
7. Gemini runs `npm test` again to verify the fix.
|
||||
|
||||
This loop turns Gemini into an autonomous engineer.
|
||||
This loop lets Gemini work autonomously.
|
||||
|
||||
## How to manage background processes
|
||||
|
||||
@@ -75,7 +75,7 @@ confirmation prompts) by streaming the output to you. However, for highly
|
||||
interactive tools (like `vim` or `top`), it's often better to run them yourself
|
||||
in a separate terminal window or use the `!` prefix.
|
||||
|
||||
## Safety first
|
||||
## Safety features
|
||||
|
||||
Giving an AI access to your shell is powerful but risky. Gemini CLI includes
|
||||
several safety layers.
|
||||
|
||||
@@ -10,7 +10,9 @@ agents in the following repositories:
|
||||
- [ADK Samples (Python)](https://github.com/google/adk-samples/tree/main/python)
|
||||
- [ADK Python Contributing Samples](https://github.com/google/adk-python/tree/main/contributing/samples)
|
||||
|
||||
> **Note: Remote subagents are currently an experimental feature.**
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Remote subagents are currently an experimental feature.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -82,7 +84,8 @@ Markdown file.
|
||||
---
|
||||
```
|
||||
|
||||
> **Note:** Mixed local and remote agents, or multiple local agents, are not
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE] Mixed local and remote agents, or multiple local agents, are not
|
||||
> supported in a single file; the list format is currently remote-only.
|
||||
|
||||
## Authentication
|
||||
@@ -362,5 +365,7 @@ Users can manage subagents using the following commands within the Gemini CLI:
|
||||
- `/agents enable <agent_name>`: Enables a specific subagent.
|
||||
- `/agents disable <agent_name>`: Disables a specific subagent.
|
||||
|
||||
> **Tip:** You can use the `@cli_help` agent within Gemini CLI for assistance
|
||||
<!-- prettier-ignore -->
|
||||
> [!TIP]
|
||||
> You can use the `@cli_help` agent within Gemini CLI for assistance
|
||||
> with configuring subagents.
|
||||
|
||||
+21
-13
@@ -5,16 +5,18 @@ session. They are designed to handle specific, complex tasks—like deep codebas
|
||||
analysis, documentation lookup, or domain-specific reasoning—without cluttering
|
||||
the main agent's context or toolset.
|
||||
|
||||
> **Note: Subagents are currently an experimental feature.**
|
||||
>
|
||||
> To use custom subagents, you must ensure they are enabled in your
|
||||
> `settings.json` (enabled by default):
|
||||
>
|
||||
> ```json
|
||||
> {
|
||||
> "experimental": { "enableAgents": true }
|
||||
> }
|
||||
> ```
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Subagents are currently an experimental feature.
|
||||
>
|
||||
To use custom subagents, you must ensure they are enabled in your
|
||||
`settings.json` (enabled by default):
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": { "enableAgents": true }
|
||||
}
|
||||
```
|
||||
|
||||
## What are subagents?
|
||||
|
||||
@@ -114,7 +116,9 @@ Gemini CLI comes with the following built-in subagents:
|
||||
the pricing table from this page," "Click the login button and enter my
|
||||
credentials."
|
||||
|
||||
> **Note:** This is a preview feature currently under active development.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is a preview feature currently under active development.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
@@ -217,7 +221,9 @@ captures a screenshot and sends it to the vision model for analysis. The model
|
||||
returns coordinates and element descriptions that the browser agent uses with
|
||||
the `click_at` tool for precise, coordinate-based interactions.
|
||||
|
||||
> **Note:** The visual agent requires API key or Vertex AI authentication. It is
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> The visual agent requires API key or Vertex AI authentication. It is
|
||||
> not available when using "Sign in with Google".
|
||||
|
||||
## Creating custom subagents
|
||||
@@ -405,7 +411,9 @@ that your subagent was called with a specific prompt and the given description.
|
||||
Gemini CLI can also delegate tasks to remote subagents using the Agent-to-Agent
|
||||
(A2A) protocol.
|
||||
|
||||
> **Note: Remote subagents are currently an experimental feature.**
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Remote subagents are currently an experimental feature.
|
||||
|
||||
See the [Remote Subagents documentation](remote-agents) for detailed
|
||||
configuration, authentication, and usage instructions.
|
||||
|
||||
@@ -23,7 +23,7 @@ Gemini CLI creates a copy of the extension during installation. You must run
|
||||
GitHub, you must have `git` installed on your machine.
|
||||
|
||||
```bash
|
||||
gemini extensions install <source> [--ref <ref>] [--auto-update] [--pre-release] [--consent]
|
||||
gemini extensions install <source> [--ref <ref>] [--auto-update] [--pre-release] [--consent] [--skip-settings]
|
||||
```
|
||||
|
||||
- `<source>`: The GitHub URL or local path of the extension.
|
||||
@@ -31,6 +31,7 @@ gemini extensions install <source> [--ref <ref>] [--auto-update] [--pre-release]
|
||||
- `--auto-update`: Enable automatic updates for this extension.
|
||||
- `--pre-release`: Enable installation of pre-release versions.
|
||||
- `--consent`: Acknowledge security risks and skip the confirmation prompt.
|
||||
- `--skip-settings`: Skip the configuration on install process.
|
||||
|
||||
### Uninstall an extension
|
||||
|
||||
@@ -234,7 +235,9 @@ skill definitions in a `skills/` directory. For example,
|
||||
|
||||
### Sub-agents
|
||||
|
||||
> **Note:** Sub-agents are a preview feature currently under active development.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Sub-agents are a preview feature currently under active development.
|
||||
|
||||
Provide [sub-agents](../core/subagents.md) that users can delegate tasks to. Add
|
||||
agent definition files (`.md`) to an `agents/` directory in your extension root.
|
||||
@@ -253,7 +256,9 @@ Rules contributed by extensions run in their own tier (tier 2), alongside
|
||||
workspace-defined policies. This tier has higher priority than the default rules
|
||||
but lower priority than user or admin policies.
|
||||
|
||||
> **Warning:** For security, Gemini CLI ignores any `allow` decisions or `yolo`
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> For security, Gemini CLI ignores any `allow` decisions or `yolo`
|
||||
> mode configurations in extension policies. This ensures that an extension
|
||||
> cannot automatically approve tool calls or bypass security measures without
|
||||
> your confirmation.
|
||||
|
||||
@@ -4,7 +4,9 @@ To use Gemini CLI, you'll need to authenticate with Google. This guide helps you
|
||||
quickly find the best way to sign in based on your account type and how you're
|
||||
using the CLI.
|
||||
|
||||
> **Note:** Looking for a high-level comparison of all available subscriptions?
|
||||
<!-- prettier-ignore -->
|
||||
> [!TIP]
|
||||
> Looking for a high-level comparison of all available subscriptions?
|
||||
> To compare features and find the right quota for your needs, see our
|
||||
> [Plans page](https://geminicli.com/plans/).
|
||||
|
||||
@@ -40,11 +42,11 @@ Select the authentication method that matches your situation in the table below:
|
||||
|
||||
If you run Gemini CLI on your local machine, the simplest authentication method
|
||||
is logging in with your Google account. This method requires a web browser on a
|
||||
machine that can communicate with the terminal running Gemini CLI (e.g., your
|
||||
local machine).
|
||||
machine that can communicate with the terminal running Gemini CLI (for example,
|
||||
your local machine).
|
||||
|
||||
> **Important:** If you are a **Google AI Pro** or **Google AI Ultra**
|
||||
> subscriber, use the Google account associated with your subscription.
|
||||
If you are a **Google AI Pro** or **Google AI Ultra** subscriber, use the Google
|
||||
account associated with your subscription.
|
||||
|
||||
To authenticate and use Gemini CLI:
|
||||
|
||||
@@ -107,7 +109,9 @@ To authenticate and use Gemini CLI with a Gemini API key:
|
||||
|
||||
4. Select **Use Gemini API key**.
|
||||
|
||||
> **Warning:** Treat API keys, especially for services like Gemini, as sensitive
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> Treat API keys, especially for services like Gemini, as sensitive
|
||||
> credentials. Protect them to prevent unauthorized access and potential misuse
|
||||
> of the service under your account.
|
||||
|
||||
@@ -130,7 +134,7 @@ For example:
|
||||
**macOS/Linux**
|
||||
|
||||
```bash
|
||||
# Replace with your project ID and desired location (e.g., us-central1)
|
||||
# Replace with your project ID and desired location (for example, us-central1)
|
||||
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||
export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"
|
||||
```
|
||||
@@ -138,7 +142,7 @@ export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"
|
||||
**Windows (PowerShell)**
|
||||
|
||||
```powershell
|
||||
# Replace with your project ID and desired location (e.g., us-central1)
|
||||
# Replace with your project ID and desired location (for example, us-central1)
|
||||
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||
$env:GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"
|
||||
```
|
||||
@@ -150,20 +154,20 @@ To make any Vertex AI environment variable settings persistent, see
|
||||
|
||||
Consider this authentication method if you have Google Cloud CLI installed.
|
||||
|
||||
> **Note:** If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you
|
||||
> must unset them to use ADC:
|
||||
>
|
||||
> **macOS/Linux**
|
||||
>
|
||||
> ```bash
|
||||
> unset GOOGLE_API_KEY GEMINI_API_KEY
|
||||
> ```
|
||||
>
|
||||
> **Windows (PowerShell)**
|
||||
>
|
||||
> ```powershell
|
||||
> Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
|
||||
> ```
|
||||
If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you must unset
|
||||
them to use ADC.
|
||||
|
||||
**macOS/Linux**
|
||||
|
||||
```bash
|
||||
unset GOOGLE_API_KEY GEMINI_API_KEY
|
||||
```
|
||||
|
||||
**Windows (PowerShell)**
|
||||
|
||||
```powershell
|
||||
Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
|
||||
```
|
||||
|
||||
1. Verify you have a Google Cloud project and Vertex AI API is enabled.
|
||||
|
||||
@@ -188,20 +192,20 @@ Consider this authentication method if you have Google Cloud CLI installed.
|
||||
Consider this method of authentication in non-interactive environments, CI/CD
|
||||
pipelines, or if your organization restricts user-based ADC or API key creation.
|
||||
|
||||
> **Note:** If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you
|
||||
> must unset them:
|
||||
>
|
||||
> **macOS/Linux**
|
||||
>
|
||||
> ```bash
|
||||
> unset GOOGLE_API_KEY GEMINI_API_KEY
|
||||
> ```
|
||||
>
|
||||
> **Windows (PowerShell)**
|
||||
>
|
||||
> ```powershell
|
||||
> Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
|
||||
> ```
|
||||
If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you must unset
|
||||
them:
|
||||
|
||||
**macOS/Linux**
|
||||
|
||||
```bash
|
||||
unset GOOGLE_API_KEY GEMINI_API_KEY
|
||||
```
|
||||
|
||||
**Windows (PowerShell)**
|
||||
|
||||
```powershell
|
||||
Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
|
||||
```
|
||||
|
||||
1. [Create a service account and key](https://cloud.google.com/iam/docs/keys-create-delete)
|
||||
and download the provided JSON file. Assign the "Vertex AI User" role to the
|
||||
@@ -233,8 +237,11 @@ pipelines, or if your organization restricts user-based ADC or API key creation.
|
||||
```
|
||||
|
||||
5. Select **Vertex AI**.
|
||||
> **Warning:** Protect your service account key file as it gives access to
|
||||
> your resources.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> Protect your service account key file as it gives access to
|
||||
> your resources.
|
||||
|
||||
#### C. Vertex AI - Google Cloud API key
|
||||
|
||||
@@ -257,10 +264,9 @@ pipelines, or if your organization restricts user-based ADC or API key creation.
|
||||
$env:GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
|
||||
```
|
||||
|
||||
> **Note:** If you see errors like
|
||||
> `"API keys are not supported by this API..."`, your organization might
|
||||
> restrict API key usage for this service. Try the other Vertex AI
|
||||
> authentication methods instead.
|
||||
If you see errors like `"API keys are not supported by this API..."`, your
|
||||
organization might restrict API key usage for this service. Try the other
|
||||
Vertex AI authentication methods instead.
|
||||
|
||||
3. [Configure your Google Cloud Project](#set-gcp).
|
||||
|
||||
@@ -274,7 +280,9 @@ pipelines, or if your organization restricts user-based ADC or API key creation.
|
||||
|
||||
## Set your Google Cloud project <a id="set-gcp"></a>
|
||||
|
||||
> **Important:** Most individual Google accounts (free and paid) don't require a
|
||||
<!-- prettier-ignore -->
|
||||
> [!IMPORTANT]
|
||||
> Most individual Google accounts (free and paid) don't require a
|
||||
> Google Cloud project for authentication.
|
||||
|
||||
When you sign in using your Google account, you may need to configure a Google
|
||||
@@ -325,29 +333,31 @@ persist them with the following methods:
|
||||
1. **Add your environment variables to your shell configuration file:** Append
|
||||
the environment variable commands to your shell's startup file.
|
||||
|
||||
**macOS/Linux** (e.g., `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
|
||||
**macOS/Linux** (for example, `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
|
||||
|
||||
```bash
|
||||
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
**Windows (PowerShell)** (e.g., `$PROFILE`):
|
||||
**Windows (PowerShell)** (for example, `$PROFILE`):
|
||||
|
||||
```powershell
|
||||
Add-Content -Path $PROFILE -Value '$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"'
|
||||
. $PROFILE
|
||||
```
|
||||
|
||||
> **Warning:** Be aware that when you export API keys or service account
|
||||
> paths in your shell configuration file, any process launched from that
|
||||
> shell can read them.
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> Be aware that when you export API keys or service account
|
||||
> paths in your shell configuration file, any process launched from that
|
||||
> shell can read them.
|
||||
|
||||
2. **Use a `.env` file:** Create a `.gemini/.env` file in your project
|
||||
directory or home directory. Gemini CLI automatically loads variables from
|
||||
the first `.env` file it finds, searching up from the current directory,
|
||||
then in your home directory's `.gemini/.env` (e.g., `~/.gemini/.env` or
|
||||
`%USERPROFILE%\.gemini\.env`).
|
||||
then in your home directory's `.gemini/.env` (for example, `~/.gemini/.env`
|
||||
or `%USERPROFILE%\.gemini\.env`).
|
||||
|
||||
Example for user-wide settings:
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ Gemini CLI helps you automate common engineering tasks by combining AI reasoning
|
||||
with local system tools. This document provides examples of how to use the CLI
|
||||
for file management, code analysis, and data transformation.
|
||||
|
||||
> **Note:** These examples demonstrate potential capabilities. Your actual
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> These examples demonstrate potential capabilities. Your actual
|
||||
> results can vary based on the model used and your project environment.
|
||||
|
||||
## Rename your photographs based on content
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
Gemini 3 Pro and Gemini 3 Flash are available on Gemini CLI for all users!
|
||||
|
||||
> **Note:** Gemini 3.1 Pro Preview is rolling out. To determine whether you have
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Gemini 3.1 Pro Preview is rolling out. To determine whether you have
|
||||
> access to Gemini 3.1, use the `/model` command and select **Manual**. If you
|
||||
> have access, you will see `gemini-3.1-pro-preview`.
|
||||
>
|
||||
@@ -25,7 +27,7 @@ Get started by upgrading Gemini CLI to the latest version:
|
||||
npm install -g @google/gemini-cli@latest
|
||||
```
|
||||
|
||||
After you’ve confirmed your version is 0.21.1 or later:
|
||||
If your version is 0.21.1 or later:
|
||||
|
||||
1. Run `/model`.
|
||||
2. Select **Auto (Gemini 3)**.
|
||||
@@ -39,7 +41,9 @@ When you encounter that limit, you’ll be given the option to switch to Gemini
|
||||
2.5 Pro, upgrade for higher limits, or stop. You’ll also be told when your usage
|
||||
limit resets and Gemini 3 Pro can be used again.
|
||||
|
||||
> **Note:** Looking to upgrade for higher limits? To compare subscription
|
||||
<!-- prettier-ignore -->
|
||||
> [!TIP]
|
||||
> Looking to upgrade for higher limits? To compare subscription
|
||||
> options and find the right quota for your needs, see our
|
||||
> [Plans page](https://geminicli.com/plans/).
|
||||
|
||||
@@ -52,7 +56,9 @@ There may be times when the Gemini 3 Pro model is overloaded. When that happens,
|
||||
Gemini CLI will ask you to decide whether you want to keep trying Gemini 3 Pro
|
||||
or fallback to Gemini 2.5 Pro.
|
||||
|
||||
> **Note:** The **Keep trying** option uses exponential backoff, in which Gemini
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> The **Keep trying** option uses exponential backoff, in which Gemini
|
||||
> CLI waits longer between each retry, when the system is busy. If the retry
|
||||
> doesn't happen immediately, please wait a few minutes for the request to
|
||||
> process.
|
||||
@@ -109,7 +115,7 @@ then:
|
||||
|
||||
Restart Gemini CLI and you should have access to Gemini 3.
|
||||
|
||||
## Need help?
|
||||
## Next steps
|
||||
|
||||
If you need help, we recommend searching for an existing
|
||||
[GitHub issue](https://github.com/google-gemini/gemini-cli/issues). If you
|
||||
|
||||
+3
-1
@@ -143,7 +143,9 @@ Hooks are executed with a sanitized environment.
|
||||
|
||||
## Security and risks
|
||||
|
||||
> **Warning: Hooks execute arbitrary code with your user privileges.** By
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> Hooks execute arbitrary code with your user privileges. By
|
||||
> configuring hooks, you are allowing scripts to run shell commands on your
|
||||
> machine.
|
||||
|
||||
|
||||
@@ -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](https://www.google.com/search?q=../extensions/index.md).
|
||||
This provides version control, easy distribution, and centralized management.
|
||||
[Gemini CLI extension](../extensions/index.md). This provides version control,
|
||||
easy distribution, and centralized management.
|
||||
|
||||
@@ -132,9 +132,11 @@ to the CLI whenever the user's context changes.
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The `openFiles` list should only include files that exist on disk.
|
||||
Virtual files (e.g., unsaved files without a path, editor settings pages)
|
||||
**MUST** be excluded.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> The `openFiles` list should only include files that exist on disk.
|
||||
> Virtual files (e.g., unsaved files without a path, editor settings pages)
|
||||
> **MUST** be excluded.
|
||||
|
||||
### How the CLI uses this context
|
||||
|
||||
|
||||
@@ -66,9 +66,11 @@ You can also install the extension directly from a marketplace.
|
||||
Follow your editor's instructions for installing extensions from this
|
||||
registry.
|
||||
|
||||
> NOTE: The "Gemini CLI Companion" extension may appear towards the bottom of
|
||||
> search results. If you don't see it immediately, try scrolling down or sorting
|
||||
> by "Newly Published".
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> The "Gemini CLI Companion" extension may appear towards the bottom of
|
||||
> search results. If you don't see it immediately, try scrolling down or
|
||||
> sorting by "Newly Published".
|
||||
>
|
||||
> After manually installing the extension, you must run `/ide enable` in the CLI
|
||||
> to activate the integration.
|
||||
@@ -103,7 +105,9 @@ IDE, run:
|
||||
If connected, this command will show the IDE it's connected to and a list of
|
||||
recently opened files it is aware of.
|
||||
|
||||
> [!NOTE] The file list is limited to 10 recently accessed files within your
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> The file list is limited to 10 recently accessed files within your
|
||||
> workspace and only includes local files on disk.)
|
||||
|
||||
### Working with diffs
|
||||
|
||||
@@ -14,7 +14,9 @@ feature), while the PR is the "how" (the implementation). This separation helps
|
||||
us track work, prioritize features, and maintain clear historical context. Our
|
||||
automation is built around this principle.
|
||||
|
||||
> **Note:** Issues tagged as "🔒Maintainers only" are reserved for project
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Issues tagged as "🔒Maintainers only" are reserved for project
|
||||
> maintainers. We will not accept pull requests related to these issues.
|
||||
|
||||
---
|
||||
|
||||
@@ -79,7 +79,9 @@ You can view traces in the Jaeger UI for local development.
|
||||
You can use an OpenTelemetry collector to forward telemetry data to Google Cloud
|
||||
Trace for custom processing or routing.
|
||||
|
||||
> **Warning:** Ensure you complete the
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> Ensure you complete the
|
||||
> [Google Cloud telemetry prerequisites](./cli/telemetry.md#prerequisites)
|
||||
> (Project ID, authentication, IAM roles, and APIs) before using this method.
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- `list` (selecting this opens the auto-saved session browser)
|
||||
- `-- checkpoints --`
|
||||
- `list`, `save`, `resume`, `delete`, `share` (manual tagged checkpoints)
|
||||
- **Note:** Unique prefixes (for example `/cha` or `/resum`) resolve to the
|
||||
same grouped menu.
|
||||
- Unique prefixes (for example `/cha` or `/resu`) resolve to the same grouped
|
||||
menu.
|
||||
- **Sub-commands:**
|
||||
- **`debug`**
|
||||
- **Description:** Export the most recent API request as a JSON payload.
|
||||
|
||||
@@ -25,7 +25,9 @@ overridden by higher numbers):
|
||||
Gemini CLI uses JSON settings files for persistent configuration. There are four
|
||||
locations for these files:
|
||||
|
||||
> **Tip:** JSON-aware editors can use autocomplete and validation by pointing to
|
||||
<!-- prettier-ignore -->
|
||||
> [!TIP]
|
||||
> JSON-aware editors can use autocomplete and validation by pointing to
|
||||
> the generated schema at `schemas/settings.schema.json` in this repository.
|
||||
> When working outside the repo, reference the hosted schema at
|
||||
> `https://raw.githubusercontent.com/google-gemini/gemini-cli/main/schemas/settings.schema.json`.
|
||||
@@ -66,9 +68,9 @@ an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like
|
||||
this: `"apiKey": "$MY_API_TOKEN"`. Additionally, each extension can have its own
|
||||
`.env` file in its directory, which will be loaded automatically.
|
||||
|
||||
> **Note for Enterprise Users:** For guidance on deploying and managing Gemini
|
||||
> CLI in a corporate environment, please see the
|
||||
> [Enterprise Configuration](../cli/enterprise.md) documentation.
|
||||
**Note for Enterprise Users:** For guidance on deploying and managing Gemini CLI
|
||||
in a corporate environment, please see the
|
||||
[Enterprise Configuration](../cli/enterprise.md) documentation.
|
||||
|
||||
### The `.gemini` directory in your project
|
||||
|
||||
@@ -684,6 +686,16 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
```json
|
||||
{
|
||||
"gemini-3.1-flash-lite-preview": {
|
||||
"tier": "flash-lite",
|
||||
"family": "gemini-3",
|
||||
"isPreview": true,
|
||||
"isVisible": true,
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": true
|
||||
}
|
||||
},
|
||||
"gemini-3.1-pro-preview": {
|
||||
"tier": "pro",
|
||||
"family": "gemini-3",
|
||||
@@ -795,7 +807,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"tier": "auto",
|
||||
"isPreview": true,
|
||||
"isVisible": true,
|
||||
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash",
|
||||
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash",
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": false
|
||||
@@ -824,6 +836,39 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
```json
|
||||
{
|
||||
"gemini-3.1-pro-preview": {
|
||||
"default": "gemini-3.1-pro-preview",
|
||||
"contexts": [
|
||||
{
|
||||
"condition": {
|
||||
"hasAccessToPreview": false
|
||||
},
|
||||
"target": "gemini-2.5-pro"
|
||||
}
|
||||
]
|
||||
},
|
||||
"gemini-3.1-pro-preview-customtools": {
|
||||
"default": "gemini-3.1-pro-preview-customtools",
|
||||
"contexts": [
|
||||
{
|
||||
"condition": {
|
||||
"hasAccessToPreview": false
|
||||
},
|
||||
"target": "gemini-2.5-pro"
|
||||
}
|
||||
]
|
||||
},
|
||||
"gemini-3-flash-preview": {
|
||||
"default": "gemini-3-flash-preview",
|
||||
"contexts": [
|
||||
{
|
||||
"condition": {
|
||||
"hasAccessToPreview": false
|
||||
},
|
||||
"target": "gemini-2.5-flash"
|
||||
}
|
||||
]
|
||||
},
|
||||
"gemini-3-pro-preview": {
|
||||
"default": "gemini-3-pro-preview",
|
||||
"contexts": [
|
||||
@@ -995,6 +1040,132 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`modelConfigs.modelChains`** (object):
|
||||
- **Description:** Availability policy chains defining fallback behavior for
|
||||
models.
|
||||
- **Default:**
|
||||
|
||||
```json
|
||||
{
|
||||
"preview": [
|
||||
{
|
||||
"model": "gemini-3-pro-preview",
|
||||
"actions": {
|
||||
"terminal": "prompt",
|
||||
"transient": "prompt",
|
||||
"not_found": "prompt",
|
||||
"unknown": "prompt"
|
||||
},
|
||||
"stateTransitions": {
|
||||
"terminal": "terminal",
|
||||
"transient": "terminal",
|
||||
"not_found": "terminal",
|
||||
"unknown": "terminal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "gemini-3-flash-preview",
|
||||
"isLastResort": true,
|
||||
"actions": {
|
||||
"terminal": "prompt",
|
||||
"transient": "prompt",
|
||||
"not_found": "prompt",
|
||||
"unknown": "prompt"
|
||||
},
|
||||
"stateTransitions": {
|
||||
"terminal": "terminal",
|
||||
"transient": "terminal",
|
||||
"not_found": "terminal",
|
||||
"unknown": "terminal"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default": [
|
||||
{
|
||||
"model": "gemini-2.5-pro",
|
||||
"actions": {
|
||||
"terminal": "prompt",
|
||||
"transient": "prompt",
|
||||
"not_found": "prompt",
|
||||
"unknown": "prompt"
|
||||
},
|
||||
"stateTransitions": {
|
||||
"terminal": "terminal",
|
||||
"transient": "terminal",
|
||||
"not_found": "terminal",
|
||||
"unknown": "terminal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "gemini-2.5-flash",
|
||||
"isLastResort": true,
|
||||
"actions": {
|
||||
"terminal": "prompt",
|
||||
"transient": "prompt",
|
||||
"not_found": "prompt",
|
||||
"unknown": "prompt"
|
||||
},
|
||||
"stateTransitions": {
|
||||
"terminal": "terminal",
|
||||
"transient": "terminal",
|
||||
"not_found": "terminal",
|
||||
"unknown": "terminal"
|
||||
}
|
||||
}
|
||||
],
|
||||
"lite": [
|
||||
{
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"actions": {
|
||||
"terminal": "silent",
|
||||
"transient": "silent",
|
||||
"not_found": "silent",
|
||||
"unknown": "silent"
|
||||
},
|
||||
"stateTransitions": {
|
||||
"terminal": "terminal",
|
||||
"transient": "terminal",
|
||||
"not_found": "terminal",
|
||||
"unknown": "terminal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "gemini-2.5-flash",
|
||||
"actions": {
|
||||
"terminal": "silent",
|
||||
"transient": "silent",
|
||||
"not_found": "silent",
|
||||
"unknown": "silent"
|
||||
},
|
||||
"stateTransitions": {
|
||||
"terminal": "terminal",
|
||||
"transient": "terminal",
|
||||
"not_found": "terminal",
|
||||
"unknown": "terminal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "gemini-2.5-pro",
|
||||
"isLastResort": true,
|
||||
"actions": {
|
||||
"terminal": "silent",
|
||||
"transient": "silent",
|
||||
"not_found": "silent",
|
||||
"unknown": "silent"
|
||||
},
|
||||
"stateTransitions": {
|
||||
"terminal": "terminal",
|
||||
"transient": "terminal",
|
||||
"not_found": "terminal",
|
||||
"unknown": "terminal"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `agents`
|
||||
|
||||
- **`agents.overrides`** (object):
|
||||
@@ -1039,6 +1210,17 @@ 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[]):
|
||||
@@ -1105,10 +1287,21 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Legacy full-process sandbox execution environment. Set to a
|
||||
boolean to enable or disable the sandbox, provide a string path to a sandbox
|
||||
profile, or specify an explicit sandbox command (e.g., "docker", "podman",
|
||||
"lxc").
|
||||
"lxc", "windows-native").
|
||||
- **Default:** `undefined`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`tools.sandboxAllowedPaths`** (array):
|
||||
- **Description:** List of additional paths that the sandbox is allowed to
|
||||
access.
|
||||
- **Default:** `[]`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`tools.sandboxNetworkAccess`** (boolean):
|
||||
- **Description:** Whether the sandbox is allowed to access the network.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`tools.shell.enableInteractiveShell`** (boolean):
|
||||
- **Description:** Use node-pty for an interactive shell experience. Fallback
|
||||
to child_process still applies.
|
||||
@@ -1345,6 +1538,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.worktrees`** (boolean):
|
||||
- **Description:** Enable automated Git worktree management for parallel work.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.extensionManagement`** (boolean):
|
||||
- **Description:** Enable extension management features.
|
||||
- **Default:** `true`
|
||||
@@ -1431,6 +1629,13 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `"gemma3-1b-gpu-custom"`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.memoryManager`** (boolean):
|
||||
- **Description:** Replace the built-in save_memory tool with a memory manager
|
||||
subagent that supports adding, removing, de-duplicating, and organizing
|
||||
memories.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.topicUpdateNarration`** (boolean):
|
||||
- **Description:** Enable the experimental Topic & Update communication model
|
||||
for reduced chattiness and structured progress reporting.
|
||||
@@ -1539,7 +1744,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`admin.mcp.config`** (object):
|
||||
- **Description:** Admin-configured MCP servers.
|
||||
- **Description:** Admin-configured MCP servers (allowlist).
|
||||
- **Default:** `{}`
|
||||
|
||||
- **`admin.mcp.requiredConfig`** (object):
|
||||
- **Description:** Admin-required MCP servers that are always injected.
|
||||
- **Default:** `{}`
|
||||
|
||||
- **`admin.skills.enabled`** (boolean):
|
||||
@@ -1559,7 +1768,9 @@ for compatibility. At least one of `command`, `url`, or `httpUrl` must be
|
||||
provided. If multiple are specified, the order of precedence is `httpUrl`, then
|
||||
`url`, then `command`.
|
||||
|
||||
> **Warning:** Avoid using underscores (`_`) in your server aliases (e.g., use
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> Avoid using underscores (`_`) in your server aliases (e.g., use
|
||||
> `my-server` instead of `my_server`). The underlying policy engine parses Fully
|
||||
> Qualified Names (`mcp_server_tool`) using the first underscore after the
|
||||
> `mcp_` prefix. An underscore in your server alias will cause the parser to
|
||||
|
||||
@@ -113,7 +113,9 @@ There are three possible decisions a rule can enforce:
|
||||
- `ask_user`: The user is prompted to approve or deny the tool call. (In
|
||||
non-interactive mode, this is treated as `deny`.)
|
||||
|
||||
> **Note:** The `deny` decision is the recommended way to exclude tools. The
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> The `deny` decision is the recommended way to exclude tools. The
|
||||
> legacy `tools.exclude` setting in `settings.json` is deprecated in favor of
|
||||
> policy rules with a `deny` decision.
|
||||
|
||||
@@ -239,15 +241,17 @@ directory are **ignored**.
|
||||
- **Linux / macOS:** Must be owned by `root` (UID 0) and NOT writable by group
|
||||
or others (e.g., `chmod 755`).
|
||||
- **Windows:** Must be in `C:\ProgramData`. Standard users (`Users`, `Everyone`)
|
||||
must NOT have `Write`, `Modify`, or `Full Control` permissions. _Tip: If you
|
||||
see a security warning, use the folder properties to remove write permissions
|
||||
for non-admin groups. You may need to "Disable inheritance" in Advanced
|
||||
Security Settings._
|
||||
must NOT have `Write`, `Modify`, or `Full Control` permissions. If you see a
|
||||
security warning, use the folder properties to remove write permissions for
|
||||
non-admin groups. You may need to "Disable inheritance" in Advanced Security
|
||||
Settings.
|
||||
|
||||
**Note:** Supplemental admin policies (provided via `--admin-policy` or
|
||||
`adminPolicyPaths` settings) are **NOT** subject to these strict ownership
|
||||
checks, as they are explicitly provided by the user or administrator in their
|
||||
current execution context.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Supplemental admin policies (provided via `--admin-policy` or
|
||||
> `adminPolicyPaths` settings) are **NOT** subject to these strict ownership
|
||||
> checks, as they are explicitly provided by the user or administrator in their
|
||||
> current execution context.
|
||||
|
||||
### TOML rule schema
|
||||
|
||||
@@ -258,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
|
||||
@@ -274,14 +278,17 @@ 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)"
|
||||
|
||||
@@ -291,14 +298,16 @@ 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
|
||||
```
|
||||
@@ -348,7 +357,9 @@ using the `mcpName` field. **This is the recommended approach** for defining MCP
|
||||
policies, as it is much more robust than manually writing Fully Qualified Names
|
||||
(FQNs) or string wildcards.
|
||||
|
||||
> **Warning:** Do not use underscores (`_`) in your MCP server names (e.g., use
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> Do not use underscores (`_`) in your MCP server names (e.g., use
|
||||
> `my-server` rather than `my_server`). The policy parser splits Fully Qualified
|
||||
> Names (`mcp_server_tool`) on the _first_ underscore following the `mcp_`
|
||||
> prefix. If your server name contains an underscore, the parser will
|
||||
|
||||
@@ -95,7 +95,9 @@ For developers, the tool system is designed to be extensible and robust. The
|
||||
You can extend Gemini CLI with custom tools by configuring
|
||||
`tools.discoveryCommand` in your settings or by connecting to MCP servers.
|
||||
|
||||
> **Note:** For a deep dive into the internal Tool API and how to implement your
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> For a deep dive into the internal Tool API and how to implement your
|
||||
> own tools in the codebase, see the `packages/core/src/tools/` directory in
|
||||
> GitHub.
|
||||
|
||||
|
||||
@@ -21,9 +21,13 @@ All workflows in `.github/workflows/ci.yml` must pass on the `main` branch (for
|
||||
nightly) or the release branch (for preview/stable).
|
||||
|
||||
- **Platforms:** Tests must pass on **Linux and macOS**.
|
||||
- _Note:_ Windows tests currently run with `continue-on-error: true`. While a
|
||||
failure here doesn't block the release technically, it should be
|
||||
investigated.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Windows tests currently run with `continue-on-error: true`. While a
|
||||
> failure here doesn't block the release technically, it should be
|
||||
> investigated.
|
||||
|
||||
- **Checks:**
|
||||
- **Linting:** No linting errors (ESLint, Prettier, etc.).
|
||||
- **Typechecking:** No TypeScript errors.
|
||||
|
||||
+11
-7
@@ -234,10 +234,12 @@ This workflow will automatically:
|
||||
Review the automatically created pull request(s) to ensure the cherry-pick was
|
||||
successful and the changes are correct. Once approved, merge the pull request.
|
||||
|
||||
**Security note:** The `release/*` branches are protected by branch protection
|
||||
rules. A pull request to one of these branches requires at least one review from
|
||||
a code owner before it can be merged. This ensures that no unauthorized code is
|
||||
released.
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> The `release/*` branches are protected by branch protection
|
||||
> rules. A pull request to one of these branches requires at least one review from
|
||||
> a code owner before it can be merged. This ensures that no unauthorized code is
|
||||
> released.
|
||||
|
||||
#### 2.5. Adding multiple commits to a hotfix (advanced)
|
||||
|
||||
@@ -524,9 +526,11 @@ Notifications use
|
||||
[GitHub for Google Chat](https://workspace.google.com/marketplace/app/github_for_google_chat/536184076190).
|
||||
To modify the notifications, use `/github-settings` within the chat space.
|
||||
|
||||
> [!WARNING] The following instructions describe a fragile workaround that
|
||||
> depends on the internal structure of the chat application's UI. It is likely
|
||||
> to break with future updates.
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> The following instructions describe a fragile workaround that depends on the
|
||||
> internal structure of the chat application's UI. It is likely to break with
|
||||
> future updates.
|
||||
|
||||
The list of available labels is not currently populated correctly. If you want
|
||||
to add a label that does not appear alphabetically in the first 30 labels in the
|
||||
|
||||
@@ -16,8 +16,10 @@ account.
|
||||
Your Gemini CLI Usage Statistics are handled in accordance with Google's Privacy
|
||||
Policy.
|
||||
|
||||
**Note:** See [quotas and pricing](quota-and-pricing.md) for the quota and
|
||||
pricing details that apply to your usage of the Gemini CLI.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> See [quotas and pricing](quota-and-pricing.md) for the quota and
|
||||
> pricing details that apply to your usage of the Gemini CLI.
|
||||
|
||||
## Supported authentication methods
|
||||
|
||||
|
||||
@@ -187,5 +187,7 @@ guide_, consider searching the Gemini CLI
|
||||
If you can't find an issue similar to yours, consider creating a new GitHub
|
||||
Issue with a detailed description. Pull requests are also welcome!
|
||||
|
||||
> **Note:** Issues tagged as "🔒Maintainers only" are reserved for project
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Issues tagged as "🔒Maintainers only" are reserved for project
|
||||
> maintainers. We will not accept pull requests related to these issues.
|
||||
|
||||
@@ -99,6 +99,11 @@
|
||||
{ "label": "Agent Skills", "slug": "docs/cli/skills" },
|
||||
{ "label": "Checkpointing", "slug": "docs/cli/checkpointing" },
|
||||
{ "label": "Headless mode", "slug": "docs/cli/headless" },
|
||||
{
|
||||
"label": "Git worktrees",
|
||||
"badge": "🔬",
|
||||
"slug": "docs/cli/git-worktrees"
|
||||
},
|
||||
{
|
||||
"label": "Hooks",
|
||||
"collapsed": true,
|
||||
|
||||
@@ -176,8 +176,8 @@ Each server configuration supports the following properties:
|
||||
enabled by default.
|
||||
- **`excludeTools`** (string[]): List of tool names to exclude from this MCP
|
||||
server. Tools listed here will not be available to the model, even if they are
|
||||
exposed by the server. **Note:** `excludeTools` takes precedence over
|
||||
`includeTools` - if a tool is in both lists, it will be excluded.
|
||||
exposed by the server. `excludeTools` takes precedence over `includeTools`. If
|
||||
a tool is in both lists, it will be excluded.
|
||||
- **`targetAudience`** (string): The OAuth Client ID allowlisted on the
|
||||
IAP-protected application you are trying to access. Used with
|
||||
`authProviderType: 'service_account_impersonation'`.
|
||||
@@ -238,7 +238,9 @@ This follows the security principle that if a variable is explicitly configured
|
||||
by the user for a specific server, it constitutes informed consent to share that
|
||||
specific data with that server.
|
||||
|
||||
> **Note:** Even when explicitly defined, you should avoid hardcoding secrets.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Even when explicitly defined, you should avoid hardcoding secrets.
|
||||
> Instead, use environment variable expansion (e.g., `"MY_KEY": "$MY_KEY"`) to
|
||||
> securely pull the value from your host environment at runtime.
|
||||
|
||||
@@ -283,10 +285,12 @@ When connecting to an OAuth-enabled server:
|
||||
|
||||
#### Browser redirect requirements
|
||||
|
||||
**Important:** OAuth authentication requires that your local machine can:
|
||||
|
||||
- Open a web browser for authentication
|
||||
- Receive redirects on `http://localhost:7777/oauth/callback`
|
||||
<!-- prettier-ignore -->
|
||||
> [!IMPORTANT]
|
||||
> OAuth authentication requires that your local machine can:
|
||||
>
|
||||
> - Open a web browser for authentication
|
||||
> - Receive redirects on `http://localhost:7777/oauth/callback`
|
||||
|
||||
This feature will not work in:
|
||||
|
||||
@@ -577,7 +581,9 @@ every discovered MCP tool is assigned a strict namespace.
|
||||
[Special syntax for MCP tools](../reference/policy-engine.md#special-syntax-for-mcp-tools)
|
||||
in the Policy Engine documentation.
|
||||
|
||||
> **Warning:** Do not use underscores (`_`) in your MCP server names (e.g., use
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> Do not use underscores (`_`) in your MCP server names (e.g., use
|
||||
> `my-server` rather than `my_server`). The policy parser splits Fully Qualified
|
||||
> Names (`mcp_server_tool`) on the _first_ underscore following the `mcp_`
|
||||
> prefix. If your server name contains an underscore, the parser will
|
||||
@@ -1116,7 +1122,9 @@ command has no flags.
|
||||
gemini mcp list
|
||||
```
|
||||
|
||||
> **Note on Trust:** For security, `stdio` MCP servers (those using the
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> For security, `stdio` MCP servers (those using the
|
||||
> `command` property) are only tested and displayed as "Connected" if the
|
||||
> current folder is trusted. If the folder is untrusted, they will show as
|
||||
> "Disconnected". Use `gemini trust` to trust the current folder.
|
||||
|
||||
@@ -11,7 +11,9 @@ by the agent when you ask it to "start a plan" using natural language. In this
|
||||
mode, the agent is restricted to read-only tools to allow for safe exploration
|
||||
and planning.
|
||||
|
||||
> **Note:** This tool is not available when the CLI is in YOLO mode.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This tool is not available when the CLI is in YOLO mode.
|
||||
|
||||
- **Tool name:** `enter_plan_mode`
|
||||
- **Display name:** Enter Plan Mode
|
||||
|
||||
+4
-4
@@ -57,8 +57,8 @@ implementation, which does not support interactive commands.
|
||||
### Showing color in output
|
||||
|
||||
To show color in the shell output, you need to set the `tools.shell.showColor`
|
||||
setting to `true`. **Note: This setting only applies when
|
||||
`tools.shell.enableInteractiveShell` is enabled.**
|
||||
setting to `true`. This setting only applies when
|
||||
`tools.shell.enableInteractiveShell` is enabled.
|
||||
|
||||
**Example `settings.json`:**
|
||||
|
||||
@@ -75,8 +75,8 @@ setting to `true`. **Note: This setting only applies when
|
||||
### Setting the pager
|
||||
|
||||
You can set a custom pager for the shell output by setting the
|
||||
`tools.shell.pager` setting. The default pager is `cat`. **Note: This setting
|
||||
only applies when `tools.shell.enableInteractiveShell` is enabled.**
|
||||
`tools.shell.pager` setting. The default pager is `cat`. This setting only
|
||||
applies when `tools.shell.enableInteractiveShell` is enabled.
|
||||
|
||||
**Example `settings.json`:**
|
||||
|
||||
|
||||
+16
-13
@@ -35,13 +35,19 @@ const commonRestrictedSyntaxRules = [
|
||||
message:
|
||||
'Do not throw string literals or non-Error objects. Throw new Error("...") instead.',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
'UnaryExpression[operator="typeof"] > MemberExpression[computed=true][property.type="Literal"]',
|
||||
message:
|
||||
'Do not use typeof to check object properties. Define a TypeScript interface and a type guard function instead.',
|
||||
},
|
||||
];
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
// Global ignores
|
||||
ignores: [
|
||||
'node_modules/*',
|
||||
'**/node_modules/**',
|
||||
'eslint.config.js',
|
||||
'packages/**/dist/**',
|
||||
'bundle/**',
|
||||
@@ -50,7 +56,7 @@ export default tseslint.config(
|
||||
'dist/**',
|
||||
'evals/**',
|
||||
'packages/test-utils/**',
|
||||
'.gemini/skills/**',
|
||||
'.gemini/**',
|
||||
'**/*.d.ts',
|
||||
],
|
||||
},
|
||||
@@ -133,16 +139,7 @@ export default tseslint.config(
|
||||
'no-cond-assign': 'error',
|
||||
'no-debugger': 'error',
|
||||
'no-duplicate-case': 'error',
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
...commonRestrictedSyntaxRules,
|
||||
{
|
||||
selector:
|
||||
'UnaryExpression[operator="typeof"] > MemberExpression[computed=true][property.type="Literal"]',
|
||||
message:
|
||||
'Do not use typeof to check object properties. Define a TypeScript interface and a type guard function instead.',
|
||||
},
|
||||
],
|
||||
'no-restricted-syntax': ['error', ...commonRestrictedSyntaxRules],
|
||||
'no-unsafe-finally': 'error',
|
||||
'no-unused-expressions': 'off', // Disable base rule
|
||||
'@typescript-eslint/no-unused-expressions': [
|
||||
@@ -161,6 +158,7 @@ export default tseslint.config(
|
||||
'@typescript-eslint/await-thenable': ['error'],
|
||||
'@typescript-eslint/no-floating-promises': ['error'],
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': ['error'],
|
||||
'@typescript-eslint/no-misused-spread': ['error'],
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
@@ -319,7 +317,12 @@ export default tseslint.config(
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['./scripts/**/*.js', 'esbuild.config.js', 'packages/core/scripts/**/*.{js,mjs}'],
|
||||
files: [
|
||||
'./scripts/**/*.js',
|
||||
'packages/*/scripts/**/*.js',
|
||||
'esbuild.config.js',
|
||||
'packages/core/scripts/**/*.{js,mjs}',
|
||||
],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
|
||||
@@ -15,9 +15,26 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { DEFAULT_GEMINI_MODEL } from '@google/gemini-cli-core';
|
||||
|
||||
/**
|
||||
* Config overrides for evals, with tool-restriction fields explicitly
|
||||
* forbidden. Evals must test against the full, default tool set to ensure
|
||||
* realistic behavior.
|
||||
*/
|
||||
interface EvalConfigOverrides {
|
||||
/** Restricting tools via excludeTools in evals is forbidden. */
|
||||
excludeTools?: never;
|
||||
/** Restricting tools via coreTools in evals is forbidden. */
|
||||
coreTools?: never;
|
||||
/** Restricting tools via allowedTools in evals is forbidden. */
|
||||
allowedTools?: never;
|
||||
/** Restricting tools via mainAgentTools in evals is forbidden. */
|
||||
mainAgentTools?: never;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AppEvalCase {
|
||||
name: string;
|
||||
configOverrides?: any;
|
||||
configOverrides?: EvalConfigOverrides;
|
||||
prompt: string;
|
||||
timeout?: number;
|
||||
files?: Record<string, string>;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
|
||||
describe('CliHelpAgent Delegation', () => {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should delegate to cli_help agent for subagent creation questions',
|
||||
params: {
|
||||
settings: {
|
||||
experimental: {
|
||||
enableAgents: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
prompt: 'Help me create a subagent in this project',
|
||||
timeout: 60000,
|
||||
assert: async (rig, _result) => {
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const toolCallIndex = toolLogs.findIndex(
|
||||
(log) => log.toolRequest.name === 'cli_help',
|
||||
);
|
||||
expect(toolCallIndex).toBeGreaterThan(-1);
|
||||
expect(toolCallIndex).toBeLessThan(5); // Called within first 5 turns
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,6 @@ describe('generalist_delegation', () => {
|
||||
experimental: {
|
||||
enableAgents: true,
|
||||
},
|
||||
excludeTools: ['run_shell_command'],
|
||||
},
|
||||
files: {
|
||||
'file1.ts': 'console.log("no semi")',
|
||||
@@ -65,7 +64,6 @@ describe('generalist_delegation', () => {
|
||||
experimental: {
|
||||
enableAgents: true,
|
||||
},
|
||||
excludeTools: ['run_shell_command'],
|
||||
},
|
||||
files: {
|
||||
'src/a.ts': 'export const a = 1;',
|
||||
@@ -106,7 +104,6 @@ describe('generalist_delegation', () => {
|
||||
experimental: {
|
||||
enableAgents: true,
|
||||
},
|
||||
excludeTools: ['run_shell_command'],
|
||||
},
|
||||
files: {
|
||||
'README.md': 'This is a proyect.',
|
||||
@@ -141,7 +138,6 @@ describe('generalist_delegation', () => {
|
||||
experimental: {
|
||||
enableAgents: true,
|
||||
},
|
||||
excludeTools: ['run_shell_command'],
|
||||
},
|
||||
files: {
|
||||
'src/VERSION': '1.2.3',
|
||||
|
||||
@@ -12,10 +12,9 @@ import { appEvalTest } from './app-test-helper.js';
|
||||
import { PolicyDecision } from '@google/gemini-cli-core';
|
||||
|
||||
describe('Model Steering Behavioral Evals', () => {
|
||||
appEvalTest('ALWAYS_PASSES', {
|
||||
appEvalTest('USUALLY_PASSES', {
|
||||
name: 'Corrective Hint: Model switches task based on hint during tool turn',
|
||||
configOverrides: {
|
||||
excludeTools: ['run_shell_command', 'ls', 'google_web_search'],
|
||||
modelSteering: true,
|
||||
},
|
||||
files: {
|
||||
@@ -52,10 +51,9 @@ describe('Model Steering Behavioral Evals', () => {
|
||||
},
|
||||
});
|
||||
|
||||
appEvalTest('ALWAYS_PASSES', {
|
||||
appEvalTest('USUALLY_PASSES', {
|
||||
name: 'Suggestive Hint: Model incorporates user guidance mid-stream',
|
||||
configOverrides: {
|
||||
excludeTools: ['run_shell_command', 'ls', 'google_web_search'],
|
||||
modelSteering: true,
|
||||
},
|
||||
files: {},
|
||||
|
||||
@@ -16,9 +16,7 @@ describe('save_memory', () => {
|
||||
const rememberingFavoriteColor = "Agent remembers user's favorite color";
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
name: rememberingFavoriteColor,
|
||||
params: {
|
||||
settings: { tools: { core: ['save_memory'] } },
|
||||
},
|
||||
|
||||
prompt: `remember that my favorite color is blue.
|
||||
|
||||
what is my favorite color? tell me that and surround it with $ symbol`,
|
||||
@@ -38,9 +36,7 @@ describe('save_memory', () => {
|
||||
const rememberingCommandRestrictions = 'Agent remembers command restrictions';
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: rememberingCommandRestrictions,
|
||||
params: {
|
||||
settings: { tools: { core: ['save_memory'] } },
|
||||
},
|
||||
|
||||
prompt: `I don't want you to ever run npm commands.`,
|
||||
assert: async (rig, result) => {
|
||||
const wasToolCalled = await rig.waitForToolCall('save_memory');
|
||||
@@ -59,9 +55,7 @@ describe('save_memory', () => {
|
||||
const rememberingWorkflow = 'Agent remembers workflow preferences';
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: rememberingWorkflow,
|
||||
params: {
|
||||
settings: { tools: { core: ['save_memory'] } },
|
||||
},
|
||||
|
||||
prompt: `I want you to always lint after building.`,
|
||||
assert: async (rig, result) => {
|
||||
const wasToolCalled = await rig.waitForToolCall('save_memory');
|
||||
@@ -81,9 +75,7 @@ describe('save_memory', () => {
|
||||
'Agent ignores temporary conversation details';
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
name: ignoringTemporaryInformation,
|
||||
params: {
|
||||
settings: { tools: { core: ['save_memory'] } },
|
||||
},
|
||||
|
||||
prompt: `I'm going to get a coffee.`,
|
||||
assert: async (rig, result) => {
|
||||
await rig.waitForTelemetryReady();
|
||||
@@ -106,9 +98,7 @@ describe('save_memory', () => {
|
||||
const rememberingPetName = "Agent remembers user's pet's name";
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
name: rememberingPetName,
|
||||
params: {
|
||||
settings: { tools: { core: ['save_memory'] } },
|
||||
},
|
||||
|
||||
prompt: `Please remember that my dog's name is Buddy.`,
|
||||
assert: async (rig, result) => {
|
||||
const wasToolCalled = await rig.waitForToolCall('save_memory');
|
||||
@@ -127,9 +117,7 @@ describe('save_memory', () => {
|
||||
const rememberingCommandAlias = 'Agent remembers custom command aliases';
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
name: rememberingCommandAlias,
|
||||
params: {
|
||||
settings: { tools: { core: ['save_memory'] } },
|
||||
},
|
||||
|
||||
prompt: `When I say 'start server', you should run 'npm run dev'.`,
|
||||
assert: async (rig, result) => {
|
||||
const wasToolCalled = await rig.waitForToolCall('save_memory');
|
||||
@@ -149,18 +137,6 @@ describe('save_memory', () => {
|
||||
"Agent ignores workspace's database schema location";
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: ignoringDbSchemaLocation,
|
||||
params: {
|
||||
settings: {
|
||||
tools: {
|
||||
core: [
|
||||
'save_memory',
|
||||
'list_directory',
|
||||
'read_file',
|
||||
'run_shell_command',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
prompt: `The database schema for this workspace is located in \`db/schema.sql\`.`,
|
||||
assert: async (rig, result) => {
|
||||
await rig.waitForTelemetryReady();
|
||||
@@ -180,9 +156,7 @@ describe('save_memory', () => {
|
||||
"Agent remembers user's coding style preference";
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
name: rememberingCodingStyle,
|
||||
params: {
|
||||
settings: { tools: { core: ['save_memory'] } },
|
||||
},
|
||||
|
||||
prompt: `I prefer to use tabs instead of spaces for indentation.`,
|
||||
assert: async (rig, result) => {
|
||||
const wasToolCalled = await rig.waitForToolCall('save_memory');
|
||||
@@ -202,18 +176,6 @@ describe('save_memory', () => {
|
||||
'Agent ignores workspace build artifact location';
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: ignoringBuildArtifactLocation,
|
||||
params: {
|
||||
settings: {
|
||||
tools: {
|
||||
core: [
|
||||
'save_memory',
|
||||
'list_directory',
|
||||
'read_file',
|
||||
'run_shell_command',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
prompt: `In this workspace, build artifacts are stored in the \`dist/artifacts\` directory.`,
|
||||
assert: async (rig, result) => {
|
||||
await rig.waitForTelemetryReady();
|
||||
@@ -232,18 +194,6 @@ describe('save_memory', () => {
|
||||
const ignoringMainEntryPoint = "Agent ignores workspace's main entry point";
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: ignoringMainEntryPoint,
|
||||
params: {
|
||||
settings: {
|
||||
tools: {
|
||||
core: [
|
||||
'save_memory',
|
||||
'list_directory',
|
||||
'read_file',
|
||||
'run_shell_command',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
prompt: `The main entry point for this workspace is \`src/index.js\`.`,
|
||||
assert: async (rig, result) => {
|
||||
await rig.waitForTelemetryReady();
|
||||
@@ -262,9 +212,7 @@ describe('save_memory', () => {
|
||||
const rememberingBirthday = "Agent remembers user's birthday";
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
name: rememberingBirthday,
|
||||
params: {
|
||||
settings: { tools: { core: ['save_memory'] } },
|
||||
},
|
||||
|
||||
prompt: `My birthday is on June 15th.`,
|
||||
assert: async (rig, result) => {
|
||||
const wasToolCalled = await rig.waitForToolCall('save_memory');
|
||||
|
||||
+17
-1
@@ -197,9 +197,25 @@ export function symlinkNodeModules(testDir: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings that are forbidden in evals. Evals should never restrict which
|
||||
* tools are available — they must test against the full, default tool set
|
||||
* to ensure realistic behavior.
|
||||
*/
|
||||
interface ForbiddenToolSettings {
|
||||
tools?: {
|
||||
/** Restricting core tools in evals is forbidden. */
|
||||
core?: never;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EvalCase {
|
||||
name: string;
|
||||
params?: Record<string, any>;
|
||||
params?: {
|
||||
settings?: ForbiddenToolSettings & Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
prompt: string;
|
||||
timeout?: number;
|
||||
files?: Record<string, string>;
|
||||
|
||||
@@ -16,6 +16,10 @@ export default defineConfig({
|
||||
},
|
||||
test: {
|
||||
testTimeout: 300000, // 5 minutes
|
||||
// Retry in CI but not nightly to avoid blocking on API error.
|
||||
retry: process.env['VITEST_RETRY']
|
||||
? parseInt(process.env['VITEST_RETRY'], 10)
|
||||
: 3,
|
||||
reporters: ['default', 'json'],
|
||||
outputFile: {
|
||||
json: 'evals/logs/report.json',
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll open https://example.com and check the page title for you."},{"functionCall":{"name":"browser_agent","args":{"task":"Open https://example.com and get the page title"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":35,"totalTokenCount":135}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The page title of https://example.com is \"Example Domain\". The browser session has been completed and cleaned up successfully."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":30,"totalTokenCount":230}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I have opened the page and the title is 'Example Domain'."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":30,"totalTokenCount":230}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The task is complete. The page title is 'Example Domain'."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":300,"candidatesTokenCount":20,"totalTokenCount":320}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Done."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":400,"candidatesTokenCount":5,"totalTokenCount":405}}]}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll help you with that."},{"functionCall":{"name":"browser_agent","args":{"task":"Open https://example.com and check if there is a heading"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"new_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"success":true,"summary":"SUCCESS_POLICY_TEST_COMPLETED"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Task completed successfully. The page has the heading \"Example Domain\"."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":50,"totalTokenCount":250}}]}
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, poll } from './test-helper.js';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const chromeAvailable = (() => {
|
||||
try {
|
||||
if (process.platform === 'darwin') {
|
||||
execSync(
|
||||
'test -d "/Applications/Google Chrome.app" || test -d "/Applications/Chromium.app"',
|
||||
{
|
||||
stdio: 'ignore',
|
||||
},
|
||||
);
|
||||
} else if (process.platform === 'linux') {
|
||||
execSync(
|
||||
'which google-chrome || which chromium-browser || which chromium',
|
||||
{ stdio: 'ignore' },
|
||||
);
|
||||
} else if (process.platform === 'win32') {
|
||||
const chromePaths = [
|
||||
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
||||
`${process.env['LOCALAPPDATA'] ?? ''}\\Google\\Chrome\\Application\\chrome.exe`,
|
||||
];
|
||||
const found = chromePaths.some((p) => existsSync(p));
|
||||
if (!found) {
|
||||
execSync('where chrome || where chromium', { stdio: 'ignore' });
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
describe.skipIf(!chromeAvailable)('browser-policy', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rig.cleanup();
|
||||
});
|
||||
|
||||
it('should skip confirmation when "Allow all server tools for this session" is chosen', async () => {
|
||||
rig.setup('browser-policy-skip-confirmation', {
|
||||
fakeResponsesPath: join(__dirname, 'browser-policy.responses'),
|
||||
settings: {
|
||||
agents: {
|
||||
overrides: {
|
||||
browser_agent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
browser: {
|
||||
headless: true,
|
||||
sessionMode: 'isolated',
|
||||
allowedDomains: ['example.com'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Manually trust the folder to avoid the dialog and enable option 3
|
||||
const geminiDir = join(rig.homeDir!, '.gemini');
|
||||
mkdirSync(geminiDir, { recursive: true });
|
||||
|
||||
// Write to trustedFolders.json
|
||||
const trustedFoldersPath = join(geminiDir, 'trustedFolders.json');
|
||||
const trustedFolders = {
|
||||
[rig.testDir!]: 'TRUST_FOLDER',
|
||||
};
|
||||
writeFileSync(trustedFoldersPath, JSON.stringify(trustedFolders, null, 2));
|
||||
|
||||
// Force confirmation for browser agent.
|
||||
// NOTE: We don't force confirm browser tools here because "Allow all server tools"
|
||||
// adds a rule with ALWAYS_ALLOW_PRIORITY (3.9x) which would be overshadowed by
|
||||
// a rule in the user tier (4.x) like the one from this TOML.
|
||||
// By removing the explicit mcp rule, the first MCP tool will still prompt
|
||||
// due to default approvalMode = 'default', and then "Allow all" will correctly
|
||||
// bypass subsequent tools.
|
||||
const policyFile = join(rig.testDir!, 'force-confirm.toml');
|
||||
writeFileSync(
|
||||
policyFile,
|
||||
`
|
||||
[[rule]]
|
||||
name = "Force confirm browser_agent"
|
||||
toolName = "browser_agent"
|
||||
decision = "ask_user"
|
||||
priority = 200
|
||||
`,
|
||||
);
|
||||
|
||||
// Update settings.json in both project and home directories to point to the policy file
|
||||
for (const baseDir of [rig.testDir!, rig.homeDir!]) {
|
||||
const settingsPath = join(baseDir, '.gemini', 'settings.json');
|
||||
if (existsSync(settingsPath)) {
|
||||
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||
settings.policyPaths = [policyFile];
|
||||
// Ensure folder trust is enabled
|
||||
settings.security = settings.security || {};
|
||||
settings.security.folderTrust = settings.security.folderTrust || {};
|
||||
settings.security.folderTrust.enabled = true;
|
||||
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
const run = await rig.runInteractive({
|
||||
approvalMode: 'default',
|
||||
env: {
|
||||
GEMINI_CLI_INTEGRATION_TEST: 'true',
|
||||
},
|
||||
});
|
||||
|
||||
await run.sendKeys(
|
||||
'Open https://example.com and check if there is a heading\r',
|
||||
);
|
||||
await run.sendKeys('\r');
|
||||
|
||||
// Handle confirmations.
|
||||
// 1. Initial browser_agent delegation (likely only 3 options, so use option 1: Allow once)
|
||||
await poll(
|
||||
() => stripAnsi(run.output).toLowerCase().includes('action required'),
|
||||
60000,
|
||||
1000,
|
||||
);
|
||||
await run.sendKeys('1\r');
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
// Handle privacy notice
|
||||
await poll(
|
||||
() => stripAnsi(run.output).toLowerCase().includes('privacy notice'),
|
||||
5000,
|
||||
100,
|
||||
);
|
||||
await run.sendKeys('1\r');
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
|
||||
// new_page (MCP tool, should have 4 options, use option 3: Allow all server tools)
|
||||
await poll(
|
||||
() => {
|
||||
const stripped = stripAnsi(run.output).toLowerCase();
|
||||
return (
|
||||
stripped.includes('new_page') &&
|
||||
stripped.includes('allow all server tools for this session')
|
||||
);
|
||||
},
|
||||
60000,
|
||||
1000,
|
||||
);
|
||||
|
||||
// Select "Allow all server tools for this session" (option 3)
|
||||
await run.sendKeys('3\r');
|
||||
await new Promise((r) => setTimeout(r, 30000));
|
||||
|
||||
const output = stripAnsi(run.output).toLowerCase();
|
||||
|
||||
expect(output).toContain('browser_agent');
|
||||
expect(output).toContain('completed successfully');
|
||||
});
|
||||
|
||||
it('should show the visible warning when browser agent starts in existing session mode', async () => {
|
||||
rig.setup('browser-session-warning', {
|
||||
fakeResponsesPath: join(__dirname, 'browser-agent.cleanup.responses'),
|
||||
settings: {
|
||||
general: {
|
||||
enableAutoUpdateNotification: false,
|
||||
},
|
||||
agents: {
|
||||
overrides: {
|
||||
browser_agent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
browser: {
|
||||
sessionMode: 'existing',
|
||||
headless: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const stdout = await rig.runCommand(['Open https://example.com'], {
|
||||
env: {
|
||||
GEMINI_API_KEY: 'fake-key',
|
||||
GEMINI_TELEMETRY_DISABLED: 'true',
|
||||
DEV: 'true',
|
||||
},
|
||||
});
|
||||
|
||||
expect(stdout).toContain('saved logins will be visible');
|
||||
});
|
||||
});
|
||||
@@ -34,16 +34,20 @@ describe('extension install', () => {
|
||||
writeFileSync(testServerPath, extension);
|
||||
try {
|
||||
const result = await rig.runCommand(
|
||||
['extensions', 'install', `${rig.testDir!}`],
|
||||
['--debug', 'extensions', 'install', `${rig.testDir!}`],
|
||||
{ stdin: 'y\n' },
|
||||
);
|
||||
expect(result).toContain('test-extension-install');
|
||||
|
||||
const listResult = await rig.runCommand(['extensions', 'list']);
|
||||
const listResult = await rig.runCommand([
|
||||
'--debug',
|
||||
'extensions',
|
||||
'list',
|
||||
]);
|
||||
expect(listResult).toContain('test-extension-install');
|
||||
writeFileSync(testServerPath, extensionUpdate);
|
||||
const updateResult = await rig.runCommand(
|
||||
['extensions', 'update', `test-extension-install`],
|
||||
['--debug', 'extensions', 'update', `test-extension-install`],
|
||||
{ stdin: 'y\n' },
|
||||
);
|
||||
expect(updateResult).toContain('0.0.2');
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('extension reloading', () => {
|
||||
}
|
||||
|
||||
const result = await rig.runCommand(
|
||||
['extensions', 'install', `${rig.testDir!}`],
|
||||
['--debug', 'extensions', 'install', `${rig.testDir!}`],
|
||||
{ stdin: 'y\n' },
|
||||
);
|
||||
expect(result).toContain('test-extension');
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, poll, normalizePath } from './test-helper.js';
|
||||
import { join } from 'node:path';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
|
||||
describe('Hooks System Integration', () => {
|
||||
describe('Hooks System Integration', { timeout: 120000 }, () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2016,6 +2017,10 @@ console.log(JSON.stringify({
|
||||
|
||||
// 3. Final setup with full settings
|
||||
rig.setup('Hook Disabling Multiple Ops', {
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'hooks-system.disabled-via-command.responses',
|
||||
),
|
||||
settings: {
|
||||
hooksConfig: {
|
||||
enabled: true,
|
||||
@@ -2230,7 +2235,7 @@ console.log(JSON.stringify({
|
||||
|
||||
// The hook should have stopped execution message (returned from tool)
|
||||
expect(result).toContain(
|
||||
'Agent execution stopped: Emergency Stop triggered by hook',
|
||||
'Agent execution stopped by hook: Emergency Stop triggered by hook',
|
||||
);
|
||||
|
||||
// Tool should NOT be called successfully (it was blocked/stopped)
|
||||
@@ -2242,4 +2247,210 @@ console.log(JSON.stringify({
|
||||
expect(writeFileCalls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hooks "ask" Decision Integration', () => {
|
||||
it(
|
||||
'should force confirmation prompt when hook returns "ask" decision even in YOLO mode',
|
||||
{ timeout: 60000 },
|
||||
async () => {
|
||||
const testName =
|
||||
'should force confirmation prompt when hook returns "ask" decision even in YOLO mode';
|
||||
|
||||
// 1. Setup hook script that returns 'ask' decision
|
||||
const hookOutput = {
|
||||
decision: 'ask',
|
||||
systemMessage: 'Confirmation forced by security hook',
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'BeforeTool',
|
||||
},
|
||||
};
|
||||
|
||||
const hookScript = `console.log(JSON.stringify(${JSON.stringify(
|
||||
hookOutput,
|
||||
)}));`;
|
||||
|
||||
// Create script path predictably
|
||||
const scriptPath = join(os.tmpdir(), 'gemini-cli-tests-ask-hook.js');
|
||||
writeFileSync(scriptPath, hookScript);
|
||||
|
||||
// 2. Setup rig with YOLO mode enabled but with the 'ask' hook
|
||||
rig.setup(testName, {
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'hooks-system.allow-tool.responses',
|
||||
),
|
||||
settings: {
|
||||
debugMode: true,
|
||||
tools: {
|
||||
approval: 'yolo',
|
||||
},
|
||||
general: {
|
||||
enableAutoUpdateNotification: false,
|
||||
},
|
||||
hooksConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
hooks: {
|
||||
BeforeTool: [
|
||||
{
|
||||
matcher: 'write_file',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: `node "${scriptPath}"`,
|
||||
timeout: 5000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Bypass terminal setup prompt and other startup banners
|
||||
const stateDir = join(rig.homeDir!, '.gemini');
|
||||
if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(stateDir, 'state.json'),
|
||||
JSON.stringify({
|
||||
terminalSetupPromptShown: true,
|
||||
hasSeenScreenReaderNudge: true,
|
||||
tipsShown: 100,
|
||||
}),
|
||||
);
|
||||
|
||||
// 3. Run interactive and verify prompt appears despite YOLO mode
|
||||
const run = await rig.runInteractive();
|
||||
|
||||
// Wait for prompt to appear
|
||||
await run.expectText('Type your message', 30000);
|
||||
|
||||
// Send prompt that will trigger write_file
|
||||
await run.type('Create a file called ask-test.txt with content "test"');
|
||||
await run.type('\r');
|
||||
|
||||
// Wait for the FORCED confirmation prompt to appear
|
||||
// It should contain the system message from the hook
|
||||
await run.expectText('Confirmation forced by security hook', 30000);
|
||||
await run.expectText('Allow', 5000);
|
||||
|
||||
// 4. Approve the permission
|
||||
await run.type('y');
|
||||
await run.type('\r');
|
||||
|
||||
// Wait for command to execute
|
||||
await run.expectText('approved.txt', 30000);
|
||||
|
||||
// Should find the tool call
|
||||
const foundWriteFile = await rig.waitForToolCall('write_file');
|
||||
expect(foundWriteFile).toBeTruthy();
|
||||
|
||||
// File should be created
|
||||
const fileContent = rig.readFile('approved.txt');
|
||||
expect(fileContent).toBe('Approved content');
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'should allow cancelling when hook forces "ask" decision',
|
||||
{ timeout: 60000 },
|
||||
async () => {
|
||||
const testName =
|
||||
'should allow cancelling when hook forces "ask" decision';
|
||||
const hookOutput = {
|
||||
decision: 'ask',
|
||||
systemMessage: 'Confirmation forced for cancellation test',
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'BeforeTool',
|
||||
},
|
||||
};
|
||||
|
||||
const hookScript = `console.log(JSON.stringify(${JSON.stringify(
|
||||
hookOutput,
|
||||
)}));`;
|
||||
|
||||
const scriptPath = join(
|
||||
os.tmpdir(),
|
||||
'gemini-cli-tests-ask-cancel-hook.js',
|
||||
);
|
||||
writeFileSync(scriptPath, hookScript);
|
||||
|
||||
rig.setup(testName, {
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'hooks-system.allow-tool.responses',
|
||||
),
|
||||
settings: {
|
||||
debugMode: true,
|
||||
tools: {
|
||||
approval: 'yolo',
|
||||
},
|
||||
general: {
|
||||
enableAutoUpdateNotification: false,
|
||||
},
|
||||
hooksConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
hooks: {
|
||||
BeforeTool: [
|
||||
{
|
||||
matcher: 'write_file',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: `node "${scriptPath}"`,
|
||||
timeout: 5000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Bypass terminal setup prompt and other startup banners
|
||||
const stateDir = join(rig.homeDir!, '.gemini');
|
||||
if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(stateDir, 'state.json'),
|
||||
JSON.stringify({
|
||||
terminalSetupPromptShown: true,
|
||||
hasSeenScreenReaderNudge: true,
|
||||
tipsShown: 100,
|
||||
}),
|
||||
);
|
||||
|
||||
const run = await rig.runInteractive();
|
||||
|
||||
// Wait for prompt to appear
|
||||
await run.expectText('Type your message', 30000);
|
||||
|
||||
await run.type(
|
||||
'Create a file called cancel-test.txt with content "test"',
|
||||
);
|
||||
await run.type('\r');
|
||||
|
||||
await run.expectText(
|
||||
'Confirmation forced for cancellation test',
|
||||
30000,
|
||||
);
|
||||
|
||||
// 4. Deny the permission using option 4
|
||||
await run.type('4');
|
||||
await run.type('\r');
|
||||
|
||||
// Wait for cancellation message
|
||||
await run.expectText('Cancelled', 15000);
|
||||
|
||||
// Tool should NOT be called successfully
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const writeFileCalls = toolLogs.filter(
|
||||
(t) =>
|
||||
t.toolRequest.name === 'write_file' &&
|
||||
t.toolRequest.success === true,
|
||||
);
|
||||
expect(writeFileCalls).toHaveLength(0);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+111
-191
@@ -22,7 +22,7 @@
|
||||
"gemini": "bundle/gemini.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@types/marked": "^5.0.2",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
@@ -80,13 +80,13 @@
|
||||
"@lydell/node-pty-win32-arm64": "1.1.0",
|
||||
"@lydell/node-pty-win32-x64": "1.1.0",
|
||||
"keytar": "^7.9.0",
|
||||
"node-pty": "^1.1.0"
|
||||
"node-pty": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@agentclientprotocol/sdk": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.12.0.tgz",
|
||||
"integrity": "sha512-V8uH/KK1t7utqyJmTA7y7DzKu6+jKFIXM+ZVouz8E55j8Ej2RV42rEvPKn3/PpBJlliI5crcGk1qQhZ7VwaepA==",
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.16.1.tgz",
|
||||
"integrity": "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
@@ -4906,6 +4906,53 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.6.0.tgz",
|
||||
"integrity": "sha512-u2ZoMfymRNJb14aHNawnXJtXHLXDVKc1oKZaH4VELKT/9iWKRVgtQOdwxCgtwSxJoqYvuK4hGlBWQJ05wxADhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/identity": "^4.1.0",
|
||||
"@secretlint/node": "^10.1.1",
|
||||
"@secretlint/secretlint-formatter-sarif": "^10.1.1",
|
||||
"@secretlint/secretlint-rule-no-dotenv": "^10.1.1",
|
||||
"@secretlint/secretlint-rule-preset-recommend": "^10.1.1",
|
||||
"@vscode/vsce-sign": "^2.0.0",
|
||||
"azure-devops-node-api": "^12.5.0",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "^1.0.0-rc.9",
|
||||
"cockatiel": "^3.1.2",
|
||||
"commander": "^12.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"glob": "^11.0.0",
|
||||
"hosted-git-info": "^4.0.2",
|
||||
"jsonc-parser": "^3.2.0",
|
||||
"leven": "^3.1.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"mime": "^1.3.4",
|
||||
"minimatch": "^3.0.3",
|
||||
"parse-semver": "^1.1.1",
|
||||
"read": "^1.0.7",
|
||||
"secretlint": "^10.1.1",
|
||||
"semver": "^7.5.2",
|
||||
"tmp": "^0.2.3",
|
||||
"typed-rest-client": "^1.8.4",
|
||||
"url-join": "^4.0.1",
|
||||
"xml2js": "^0.5.0",
|
||||
"yauzl": "^2.3.1",
|
||||
"yazl": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"vsce": "vsce"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"keytar": "^7.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce-sign": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.6.tgz",
|
||||
@@ -5051,6 +5098,52 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/hosted-git-info": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
|
||||
"integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz",
|
||||
@@ -6562,13 +6655,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/config-chain": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
||||
@@ -12107,6 +12193,13 @@
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
|
||||
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/nano-spawn": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.3.tgz",
|
||||
@@ -12250,23 +12343,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-pty": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz",
|
||||
"integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
|
||||
"integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^7.1.0"
|
||||
"nan": "^2.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-pty/node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-sarif-builder": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.2.0.tgz",
|
||||
@@ -17445,7 +17531,7 @@
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"@google/genai": "1.30.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
@@ -17910,23 +17996,6 @@
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"packages/snake-game": {
|
||||
"name": "@google/gemini-cli-snake-game",
|
||||
"version": "0.1.0",
|
||||
"extraneous": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.471.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
@@ -17961,7 +18030,7 @@
|
||||
"@types/vscode": "^1.99.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.1",
|
||||
"@typescript-eslint/parser": "^8.31.1",
|
||||
"@vscode/vsce": "^3.7.1",
|
||||
"@vscode/vsce": "^3.6.0",
|
||||
"esbuild": "^0.25.3",
|
||||
"eslint": "^9.25.1",
|
||||
"npm-run-all2": "^8.0.2",
|
||||
@@ -17978,155 +18047,6 @@
|
||||
"integrity": "sha512-30sjmas1hQ0gVbX68LAWlm/YYlEqUErunPJJKLpEl+xhK0mKn+jyzlCOpsdTwfkZfPy4U6CDkmygBLC3AB8W9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/vscode-ide-companion/node_modules/@vscode/vsce": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.7.1.tgz",
|
||||
"integrity": "sha512-OTm2XdMt2YkpSn2Nx7z2EJtSuhRHsTPYsSK59hr3v8jRArK+2UEoju4Jumn1CmpgoBLGI6ReHLJ/czYltNUW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/identity": "^4.1.0",
|
||||
"@secretlint/node": "^10.1.2",
|
||||
"@secretlint/secretlint-formatter-sarif": "^10.1.2",
|
||||
"@secretlint/secretlint-rule-no-dotenv": "^10.1.2",
|
||||
"@secretlint/secretlint-rule-preset-recommend": "^10.1.2",
|
||||
"@vscode/vsce-sign": "^2.0.0",
|
||||
"azure-devops-node-api": "^12.5.0",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "^1.0.0-rc.9",
|
||||
"cockatiel": "^3.1.2",
|
||||
"commander": "^12.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"glob": "^11.0.0",
|
||||
"hosted-git-info": "^4.0.2",
|
||||
"jsonc-parser": "^3.2.0",
|
||||
"leven": "^3.1.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"mime": "^1.3.4",
|
||||
"minimatch": "^3.0.3",
|
||||
"parse-semver": "^1.1.1",
|
||||
"read": "^1.0.7",
|
||||
"secretlint": "^10.1.2",
|
||||
"semver": "^7.5.2",
|
||||
"tmp": "^0.2.3",
|
||||
"typed-rest-client": "^1.8.4",
|
||||
"url-join": "^4.0.1",
|
||||
"xml2js": "^0.5.0",
|
||||
"yauzl": "^2.3.1",
|
||||
"yazl": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"vsce": "vsce"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"keytar": "^7.7.0"
|
||||
}
|
||||
},
|
||||
"packages/vscode-ide-companion/node_modules/@vscode/vsce/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"packages/vscode-ide-companion/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/vscode-ide-companion/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"packages/vscode-ide-companion/node_modules/glob": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
||||
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.1.1",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/vscode-ide-companion/node_modules/hosted-git-info": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
|
||||
"integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"packages/vscode-ide-companion/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"packages/vscode-ide-companion/node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"packages/vscode-ide-companion/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -51,7 +51,7 @@
|
||||
"test:integration:sandbox:none": "cross-env GEMINI_SANDBOX=false vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:docker": "cross-env GEMINI_SANDBOX=docker npm run build:sandbox && cross-env GEMINI_SANDBOX=docker vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:podman": "cross-env GEMINI_SANDBOX=podman vitest run --root ./integration-tests",
|
||||
"lint": "eslint . --cache",
|
||||
"lint": "eslint . --cache --max-warnings 0",
|
||||
"lint:fix": "eslint . --fix --ext .ts,.tsx && eslint integration-tests --fix && eslint scripts --fix && npm run format",
|
||||
"lint:ci": "npm run lint:all",
|
||||
"lint:all": "node scripts/lint.js",
|
||||
@@ -87,7 +87,7 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@types/marked": "^5.0.2",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
@@ -150,7 +150,7 @@
|
||||
"@lydell/node-pty-win32-arm64": "1.1.0",
|
||||
"@lydell/node-pty-win32-x64": "1.1.0",
|
||||
"keytar": "^7.9.0",
|
||||
"node-pty": "^1.1.0"
|
||||
"node-pty": "^1.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"@google/genai": "1.30.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
|
||||
@@ -177,6 +177,9 @@ describe('GeminiAgent', () => {
|
||||
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
|
||||
get config() {
|
||||
return this;
|
||||
},
|
||||
} as unknown as Mocked<Awaited<ReturnType<typeof loadCliConfig>>>;
|
||||
mockSettings = {
|
||||
merged: {
|
||||
@@ -548,7 +551,7 @@ describe('GeminiAgent', () => {
|
||||
});
|
||||
|
||||
expect(session.prompt).toHaveBeenCalled();
|
||||
expect(result).toEqual({ stopReason: 'end_turn' });
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
});
|
||||
|
||||
it('should delegate setMode to session', async () => {
|
||||
@@ -656,6 +659,12 @@ describe('Session', () => {
|
||||
getGitService: vi.fn().mockResolvedValue({} as GitService),
|
||||
waitForMcpInit: vi.fn(),
|
||||
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
|
||||
get config() {
|
||||
return this;
|
||||
},
|
||||
get toolRegistry() {
|
||||
return mockToolRegistry;
|
||||
},
|
||||
} as unknown as Mocked<Config>;
|
||||
mockConnection = {
|
||||
sessionUpdate: vi.fn(),
|
||||
@@ -741,7 +750,7 @@ describe('Session', () => {
|
||||
content: { type: 'text', text: 'Hello' },
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({ stopReason: 'end_turn' });
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
});
|
||||
|
||||
it('should handle /memory command', async () => {
|
||||
@@ -758,7 +767,7 @@ describe('Session', () => {
|
||||
prompt: [{ type: 'text', text: '/memory view' }],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ stopReason: 'end_turn' });
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
expect(handleCommandSpy).toHaveBeenCalledWith(
|
||||
'/memory view',
|
||||
expect.any(Object),
|
||||
@@ -780,7 +789,7 @@ describe('Session', () => {
|
||||
prompt: [{ type: 'text', text: '/extensions list' }],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ stopReason: 'end_turn' });
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
expect(handleCommandSpy).toHaveBeenCalledWith(
|
||||
'/extensions list',
|
||||
expect.any(Object),
|
||||
@@ -802,7 +811,7 @@ describe('Session', () => {
|
||||
prompt: [{ type: 'text', text: '/extensions explore' }],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ stopReason: 'end_turn' });
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
expect(handleCommandSpy).toHaveBeenCalledWith(
|
||||
'/extensions explore',
|
||||
expect.any(Object),
|
||||
@@ -824,7 +833,7 @@ describe('Session', () => {
|
||||
prompt: [{ type: 'text', text: '/restore' }],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ stopReason: 'end_turn' });
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
expect(handleCommandSpy).toHaveBeenCalledWith(
|
||||
'/restore',
|
||||
expect.any(Object),
|
||||
@@ -846,7 +855,7 @@ describe('Session', () => {
|
||||
prompt: [{ type: 'text', text: '/init' }],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ stopReason: 'end_turn' });
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
expect(handleCommandSpy).toHaveBeenCalledWith('/init', expect.any(Object));
|
||||
expect(mockChat.sendMessageStream).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -900,7 +909,7 @@ describe('Session', () => {
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({ stopReason: 'end_turn' });
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
});
|
||||
|
||||
it('should handle tool call permission request', async () => {
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
getDisplayString,
|
||||
type AgentLoopContext,
|
||||
} from '@google/gemini-cli-core';
|
||||
import * as acp from '@agentclientprotocol/sdk';
|
||||
import { AcpFileSystemService } from './fileSystemService.js';
|
||||
@@ -104,7 +105,7 @@ export class GeminiAgent {
|
||||
private customHeaders: Record<string, string> | undefined;
|
||||
|
||||
constructor(
|
||||
private config: Config,
|
||||
private context: AgentLoopContext,
|
||||
private settings: LoadedSettings,
|
||||
private argv: CliArgs,
|
||||
private connection: acp.AgentSideConnection,
|
||||
@@ -148,7 +149,7 @@ export class GeminiAgent {
|
||||
},
|
||||
];
|
||||
|
||||
await this.config.initialize();
|
||||
await this.context.config.initialize();
|
||||
const version = await getVersion();
|
||||
return {
|
||||
protocolVersion: acp.PROTOCOL_VERSION,
|
||||
@@ -220,7 +221,7 @@ export class GeminiAgent {
|
||||
this.baseUrl = baseUrl;
|
||||
this.customHeaders = headers;
|
||||
|
||||
await this.config.refreshAuth(
|
||||
await this.context.config.refreshAuth(
|
||||
method,
|
||||
apiKey ?? this.apiKey,
|
||||
baseUrl,
|
||||
@@ -537,7 +538,7 @@ export class Session {
|
||||
constructor(
|
||||
private readonly id: string,
|
||||
private readonly chat: GeminiChat,
|
||||
private readonly config: Config,
|
||||
private readonly context: AgentLoopContext,
|
||||
private readonly connection: acp.AgentSideConnection,
|
||||
private readonly settings: LoadedSettings,
|
||||
) {}
|
||||
@@ -552,13 +553,15 @@ export class Session {
|
||||
}
|
||||
|
||||
setMode(modeId: acp.SessionModeId): acp.SetSessionModeResponse {
|
||||
const availableModes = buildAvailableModes(this.config.isPlanEnabled());
|
||||
const availableModes = buildAvailableModes(
|
||||
this.context.config.isPlanEnabled(),
|
||||
);
|
||||
const mode = availableModes.find((m) => m.id === modeId);
|
||||
if (!mode) {
|
||||
throw new Error(`Invalid or unavailable mode: ${modeId}`);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
this.config.setApprovalMode(mode.id as ApprovalMode);
|
||||
this.context.config.setApprovalMode(mode.id as ApprovalMode);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -579,7 +582,7 @@ export class Session {
|
||||
}
|
||||
|
||||
setModel(modelId: acp.ModelId): acp.SetSessionModelResponse {
|
||||
this.config.setModel(modelId);
|
||||
this.context.config.setModel(modelId);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -634,7 +637,7 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
const tool = this.config.getToolRegistry().getTool(toolCall.name);
|
||||
const tool = this.context.toolRegistry.getTool(toolCall.name);
|
||||
|
||||
await this.sendUpdate({
|
||||
sessionUpdate: 'tool_call',
|
||||
@@ -658,7 +661,7 @@ export class Session {
|
||||
const pendingSend = new AbortController();
|
||||
this.pendingPrompt = pendingSend;
|
||||
|
||||
await this.config.waitForMcpInit();
|
||||
await this.context.config.waitForMcpInit();
|
||||
|
||||
const promptId = Math.random().toString(16).slice(2);
|
||||
const chat = this.chat;
|
||||
@@ -696,10 +699,22 @@ export class Session {
|
||||
// It uses `parts` argument but effectively ignores it in current implementation
|
||||
const handled = await this.handleCommand(commandText, parts);
|
||||
if (handled) {
|
||||
return { stopReason: 'end_turn' };
|
||||
return {
|
||||
stopReason: 'end_turn',
|
||||
_meta: {
|
||||
quota: {
|
||||
token_count: { input_tokens: 0, output_tokens: 0 },
|
||||
model_usage: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let totalInputTokens = 0;
|
||||
let totalOutputTokens = 0;
|
||||
const modelUsageMap = new Map<string, { input: number; output: number }>();
|
||||
|
||||
let nextMessage: Content | null = { role: 'user', parts };
|
||||
|
||||
while (nextMessage !== null) {
|
||||
@@ -712,8 +727,8 @@ export class Session {
|
||||
|
||||
try {
|
||||
const model = resolveModel(
|
||||
this.config.getModel(),
|
||||
(await this.config.getGemini31Launched?.()) ?? false,
|
||||
this.context.config.getModel(),
|
||||
(await this.context.config.getGemini31Launched?.()) ?? false,
|
||||
);
|
||||
const responseStream = await chat.sendMessageStream(
|
||||
{ model },
|
||||
@@ -724,11 +739,25 @@ export class Session {
|
||||
);
|
||||
nextMessage = null;
|
||||
|
||||
let turnInputTokens = 0;
|
||||
let turnOutputTokens = 0;
|
||||
let turnModelId = model;
|
||||
|
||||
for await (const resp of responseStream) {
|
||||
if (pendingSend.signal.aborted) {
|
||||
return { stopReason: CoreToolCallStatus.Cancelled };
|
||||
}
|
||||
|
||||
if (resp.type === StreamEventType.CHUNK && resp.value.usageMetadata) {
|
||||
turnInputTokens =
|
||||
resp.value.usageMetadata.promptTokenCount ?? turnInputTokens;
|
||||
turnOutputTokens =
|
||||
resp.value.usageMetadata.candidatesTokenCount ?? turnOutputTokens;
|
||||
if (resp.value.modelVersion) {
|
||||
turnModelId = resp.value.modelVersion;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
resp.type === StreamEventType.CHUNK &&
|
||||
resp.value.candidates &&
|
||||
@@ -760,6 +789,19 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
totalInputTokens += turnInputTokens;
|
||||
totalOutputTokens += turnOutputTokens;
|
||||
|
||||
if (turnInputTokens > 0 || turnOutputTokens > 0) {
|
||||
const existing = modelUsageMap.get(turnModelId) ?? {
|
||||
input: 0,
|
||||
output: 0,
|
||||
};
|
||||
existing.input += turnInputTokens;
|
||||
existing.output += turnOutputTokens;
|
||||
modelUsageMap.set(turnModelId, existing);
|
||||
}
|
||||
|
||||
if (pendingSend.signal.aborted) {
|
||||
return { stopReason: CoreToolCallStatus.Cancelled };
|
||||
}
|
||||
@@ -796,7 +838,28 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
return { stopReason: 'end_turn' };
|
||||
const modelUsageArray = Array.from(modelUsageMap.entries()).map(
|
||||
([modelName, counts]) => ({
|
||||
model: modelName,
|
||||
token_count: {
|
||||
input_tokens: counts.input,
|
||||
output_tokens: counts.output,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
stopReason: 'end_turn',
|
||||
_meta: {
|
||||
quota: {
|
||||
token_count: {
|
||||
input_tokens: totalInputTokens,
|
||||
output_tokens: totalOutputTokens,
|
||||
},
|
||||
model_usage: modelUsageArray,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async handleCommand(
|
||||
@@ -804,9 +867,9 @@ export class Session {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
parts: Part[],
|
||||
): Promise<boolean> {
|
||||
const gitService = await this.config.getGitService();
|
||||
const gitService = await this.context.config.getGitService();
|
||||
const commandContext = {
|
||||
config: this.config,
|
||||
agentContext: this.context,
|
||||
settings: this.settings,
|
||||
git: gitService,
|
||||
sendMessage: async (text: string) => {
|
||||
@@ -842,7 +905,7 @@ export class Session {
|
||||
const errorResponse = (error: Error) => {
|
||||
const durationMs = Date.now() - startTime;
|
||||
logToolCall(
|
||||
this.config,
|
||||
this.context.config,
|
||||
new ToolCallEvent(
|
||||
undefined,
|
||||
fc.name ?? '',
|
||||
@@ -872,7 +935,7 @@ export class Session {
|
||||
return errorResponse(new Error('Missing function name'));
|
||||
}
|
||||
|
||||
const toolRegistry = this.config.getToolRegistry();
|
||||
const toolRegistry = this.context.toolRegistry;
|
||||
const tool = toolRegistry.getTool(fc.name);
|
||||
|
||||
if (!tool) {
|
||||
@@ -908,7 +971,10 @@ export class Session {
|
||||
|
||||
const params: acp.RequestPermissionRequest = {
|
||||
sessionId: this.id,
|
||||
options: toPermissionOptions(confirmationDetails, this.config),
|
||||
options: toPermissionOptions(
|
||||
confirmationDetails,
|
||||
this.context.config,
|
||||
),
|
||||
toolCall: {
|
||||
toolCallId: callId,
|
||||
status: 'pending',
|
||||
@@ -974,7 +1040,7 @@ export class Session {
|
||||
|
||||
const durationMs = Date.now() - startTime;
|
||||
logToolCall(
|
||||
this.config,
|
||||
this.context.config,
|
||||
new ToolCallEvent(
|
||||
undefined,
|
||||
fc.name ?? '',
|
||||
@@ -988,7 +1054,7 @@ export class Session {
|
||||
),
|
||||
);
|
||||
|
||||
this.chat.recordCompletedToolCalls(this.config.getActiveModel(), [
|
||||
this.chat.recordCompletedToolCalls(this.context.config.getActiveModel(), [
|
||||
{
|
||||
status: CoreToolCallStatus.Success,
|
||||
request: {
|
||||
@@ -1006,8 +1072,8 @@ export class Session {
|
||||
fc.name,
|
||||
callId,
|
||||
toolResult.llmContent,
|
||||
this.config.getActiveModel(),
|
||||
this.config,
|
||||
this.context.config.getActiveModel(),
|
||||
this.context.config,
|
||||
),
|
||||
resultDisplay: toolResult.returnDisplay,
|
||||
error: undefined,
|
||||
@@ -1020,8 +1086,8 @@ export class Session {
|
||||
fc.name,
|
||||
callId,
|
||||
toolResult.llmContent,
|
||||
this.config.getActiveModel(),
|
||||
this.config,
|
||||
this.context.config.getActiveModel(),
|
||||
this.context.config,
|
||||
);
|
||||
} catch (e) {
|
||||
const error = e instanceof Error ? e : new Error(String(e));
|
||||
@@ -1036,7 +1102,7 @@ export class Session {
|
||||
kind: toAcpToolKind(tool.kind),
|
||||
});
|
||||
|
||||
this.chat.recordCompletedToolCalls(this.config.getActiveModel(), [
|
||||
this.chat.recordCompletedToolCalls(this.context.config.getActiveModel(), [
|
||||
{
|
||||
status: CoreToolCallStatus.Error,
|
||||
request: {
|
||||
@@ -1122,18 +1188,18 @@ export class Session {
|
||||
const atPathToResolvedSpecMap = new Map<string, string>();
|
||||
|
||||
// Get centralized file discovery service
|
||||
const fileDiscovery = this.config.getFileService();
|
||||
const fileDiscovery = this.context.config.getFileService();
|
||||
const fileFilteringOptions: FilterFilesOptions =
|
||||
this.config.getFileFilteringOptions();
|
||||
this.context.config.getFileFilteringOptions();
|
||||
|
||||
const pathSpecsToRead: string[] = [];
|
||||
const contentLabelsForDisplay: string[] = [];
|
||||
const ignoredPaths: string[] = [];
|
||||
|
||||
const toolRegistry = this.config.getToolRegistry();
|
||||
const toolRegistry = this.context.toolRegistry;
|
||||
const readManyFilesTool = new ReadManyFilesTool(
|
||||
this.config,
|
||||
this.config.getMessageBus(),
|
||||
this.context.config,
|
||||
this.context.messageBus,
|
||||
);
|
||||
const globTool = toolRegistry.getTool('glob');
|
||||
|
||||
@@ -1152,8 +1218,11 @@ export class Session {
|
||||
let currentPathSpec = pathName;
|
||||
let resolvedSuccessfully = false;
|
||||
try {
|
||||
const absolutePath = path.resolve(this.config.getTargetDir(), pathName);
|
||||
if (isWithinRoot(absolutePath, this.config.getTargetDir())) {
|
||||
const absolutePath = path.resolve(
|
||||
this.context.config.getTargetDir(),
|
||||
pathName,
|
||||
);
|
||||
if (isWithinRoot(absolutePath, this.context.config.getTargetDir())) {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
if (stats.isDirectory()) {
|
||||
currentPathSpec = pathName.endsWith('/')
|
||||
@@ -1173,7 +1242,7 @@ export class Session {
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||
if (this.config.getEnableRecursiveFileSearch() && globTool) {
|
||||
if (this.context.config.getEnableRecursiveFileSearch() && globTool) {
|
||||
this.debug(
|
||||
`Path ${pathName} not found directly, attempting glob search.`,
|
||||
);
|
||||
@@ -1181,7 +1250,7 @@ export class Session {
|
||||
const globResult = await globTool.buildAndExecute(
|
||||
{
|
||||
pattern: `**/*${pathName}*`,
|
||||
path: this.config.getTargetDir(),
|
||||
path: this.context.config.getTargetDir(),
|
||||
},
|
||||
abortSignal,
|
||||
);
|
||||
@@ -1195,7 +1264,7 @@ export class Session {
|
||||
if (lines.length > 1 && lines[1]) {
|
||||
const firstMatchAbsolute = lines[1].trim();
|
||||
currentPathSpec = path.relative(
|
||||
this.config.getTargetDir(),
|
||||
this.context.config.getTargetDir(),
|
||||
firstMatchAbsolute,
|
||||
);
|
||||
this.debug(
|
||||
@@ -1410,7 +1479,7 @@ export class Session {
|
||||
}
|
||||
|
||||
debug(msg: string) {
|
||||
if (this.config.getDebugMode()) {
|
||||
if (this.context.config.getDebugMode()) {
|
||||
debugLogger.warn(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,9 @@ describe('GeminiAgent Session Resume', () => {
|
||||
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
||||
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
|
||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||
get config() {
|
||||
return this;
|
||||
},
|
||||
} as unknown as Mocked<Config>;
|
||||
mockSettings = {
|
||||
merged: {
|
||||
@@ -158,9 +161,10 @@ describe('GeminiAgent Session Resume', () => {
|
||||
],
|
||||
};
|
||||
|
||||
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(mockConfig as any).toolRegistry = {
|
||||
getTool: vi.fn().mockReturnValue({ kind: 'read' }),
|
||||
});
|
||||
};
|
||||
|
||||
(SessionSelector as unknown as Mock).mockImplementation(() => ({
|
||||
resolveSession: vi.fn().mockResolvedValue({
|
||||
|
||||
@@ -53,7 +53,7 @@ export class ListExtensionsCommand implements Command {
|
||||
context: CommandContext,
|
||||
_: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const extensions = listExtensions(context.config);
|
||||
const extensions = listExtensions(context.agentContext.config);
|
||||
const data = extensions.length ? extensions : 'No extensions installed.';
|
||||
|
||||
return { name: this.name, data };
|
||||
@@ -134,7 +134,7 @@ export class EnableExtensionCommand implements Command {
|
||||
args: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const enableContext = getEnableDisableContext(
|
||||
context.config,
|
||||
context.agentContext.config,
|
||||
args,
|
||||
'enable',
|
||||
);
|
||||
@@ -156,7 +156,8 @@ export class EnableExtensionCommand implements Command {
|
||||
|
||||
if (extension?.mcpServers) {
|
||||
const mcpEnablementManager = McpServerEnablementManager.getInstance();
|
||||
const mcpClientManager = context.config.getMcpClientManager();
|
||||
const mcpClientManager =
|
||||
context.agentContext.config.getMcpClientManager();
|
||||
const enabledServers = await mcpEnablementManager.autoEnableServers(
|
||||
Object.keys(extension.mcpServers),
|
||||
);
|
||||
@@ -191,7 +192,7 @@ export class DisableExtensionCommand implements Command {
|
||||
args: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const enableContext = getEnableDisableContext(
|
||||
context.config,
|
||||
context.agentContext.config,
|
||||
args,
|
||||
'disable',
|
||||
);
|
||||
@@ -223,7 +224,7 @@ export class InstallExtensionCommand implements Command {
|
||||
context: CommandContext,
|
||||
args: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const extensionLoader = context.config.getExtensionLoader();
|
||||
const extensionLoader = context.agentContext.config.getExtensionLoader();
|
||||
if (!(extensionLoader instanceof ExtensionManager)) {
|
||||
return {
|
||||
name: this.name,
|
||||
@@ -268,7 +269,7 @@ export class LinkExtensionCommand implements Command {
|
||||
context: CommandContext,
|
||||
args: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const extensionLoader = context.config.getExtensionLoader();
|
||||
const extensionLoader = context.agentContext.config.getExtensionLoader();
|
||||
if (!(extensionLoader instanceof ExtensionManager)) {
|
||||
return {
|
||||
name: this.name,
|
||||
@@ -313,7 +314,7 @@ export class UninstallExtensionCommand implements Command {
|
||||
context: CommandContext,
|
||||
args: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const extensionLoader = context.config.getExtensionLoader();
|
||||
const extensionLoader = context.agentContext.config.getExtensionLoader();
|
||||
if (!(extensionLoader instanceof ExtensionManager)) {
|
||||
return {
|
||||
name: this.name,
|
||||
@@ -369,7 +370,7 @@ export class RestartExtensionCommand implements Command {
|
||||
context: CommandContext,
|
||||
args: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const extensionLoader = context.config.getExtensionLoader();
|
||||
const extensionLoader = context.agentContext.config.getExtensionLoader();
|
||||
if (!(extensionLoader instanceof ExtensionManager)) {
|
||||
return { name: this.name, data: 'Cannot restart extensions.' };
|
||||
}
|
||||
@@ -424,7 +425,7 @@ export class UpdateExtensionCommand implements Command {
|
||||
context: CommandContext,
|
||||
args: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const extensionLoader = context.config.getExtensionLoader();
|
||||
const extensionLoader = context.agentContext.config.getExtensionLoader();
|
||||
if (!(extensionLoader instanceof ExtensionManager)) {
|
||||
return { name: this.name, data: 'Cannot update extensions.' };
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export class InitCommand implements Command {
|
||||
context: CommandContext,
|
||||
_args: string[] = [],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const targetDir = context.config.getTargetDir();
|
||||
const targetDir = context.agentContext.config.getTargetDir();
|
||||
if (!targetDir) {
|
||||
throw new Error('Command requires a workspace.');
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export class ShowMemoryCommand implements Command {
|
||||
context: CommandContext,
|
||||
_: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const result = showMemory(context.config);
|
||||
const result = showMemory(context.agentContext.config);
|
||||
return { name: this.name, data: result.content };
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export class RefreshMemoryCommand implements Command {
|
||||
context: CommandContext,
|
||||
_: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const result = await refreshMemory(context.config);
|
||||
const result = await refreshMemory(context.agentContext.config);
|
||||
return { name: this.name, data: result.content };
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ export class ListMemoryCommand implements Command {
|
||||
context: CommandContext,
|
||||
_: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const result = listMemoryFiles(context.config);
|
||||
const result = listMemoryFiles(context.agentContext.config);
|
||||
return { name: this.name, data: result.content };
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export class AddMemoryCommand implements Command {
|
||||
return { name: this.name, data: result.content };
|
||||
}
|
||||
|
||||
const toolRegistry = context.config.getToolRegistry();
|
||||
const toolRegistry = context.agentContext.toolRegistry;
|
||||
const tool = toolRegistry.getTool(result.toolName);
|
||||
if (tool) {
|
||||
const abortController = new AbortController();
|
||||
@@ -106,10 +106,10 @@ export class AddMemoryCommand implements Command {
|
||||
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
|
||||
shellExecutionConfig: {
|
||||
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
|
||||
sandboxManager: context.config.sandboxManager,
|
||||
sandboxManager: context.agentContext.sandboxManager,
|
||||
},
|
||||
});
|
||||
await refreshMemory(context.config);
|
||||
await refreshMemory(context.agentContext.config);
|
||||
return {
|
||||
name: this.name,
|
||||
data: `Added memory: "${textToAdd}"`,
|
||||
|
||||
@@ -29,7 +29,8 @@ export class RestoreCommand implements Command {
|
||||
context: CommandContext,
|
||||
args: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const { config, git: gitService } = context;
|
||||
const { agentContext: agentContext, git: gitService } = context;
|
||||
const { config } = agentContext;
|
||||
const argsStr = args.join(' ');
|
||||
|
||||
try {
|
||||
@@ -116,7 +117,7 @@ export class ListCheckpointsCommand implements Command {
|
||||
readonly description = 'Lists all available checkpoints.';
|
||||
|
||||
async execute(context: CommandContext): Promise<CommandExecutionResponse> {
|
||||
const { config } = context;
|
||||
const { config } = context.agentContext;
|
||||
|
||||
try {
|
||||
if (!config.getCheckpointingEnabled()) {
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Config, GitService } from '@google/gemini-cli-core';
|
||||
import type { AgentLoopContext, GitService } from '@google/gemini-cli-core';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
|
||||
export interface CommandContext {
|
||||
config: Config;
|
||||
agentContext: AgentLoopContext;
|
||||
settings: LoadedSettings;
|
||||
git?: GitService;
|
||||
sendMessage: (text: string) => Promise<void>;
|
||||
|
||||
@@ -14,7 +14,7 @@ export class AcpFileSystemService implements FileSystemService {
|
||||
constructor(
|
||||
private readonly connection: acp.AgentSideConnection,
|
||||
private readonly sessionId: string,
|
||||
private readonly capabilities: acp.FileSystemCapability,
|
||||
private readonly capabilities: acp.FileSystemCapabilities,
|
||||
private readonly fallback: FileSystemService,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -12,48 +12,46 @@ import {
|
||||
beforeEach,
|
||||
afterEach,
|
||||
type MockInstance,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { handleInstall, installCommand } from './install.js';
|
||||
import yargs from 'yargs';
|
||||
import * as core from '@google/gemini-cli-core';
|
||||
import {
|
||||
ExtensionManager,
|
||||
type inferInstallMetadata,
|
||||
} from '../../config/extension-manager.js';
|
||||
import type {
|
||||
promptForConsentNonInteractive,
|
||||
requestConsentNonInteractive,
|
||||
} from '../../config/extensions/consent.js';
|
||||
import type {
|
||||
isWorkspaceTrusted,
|
||||
loadTrustedFolders,
|
||||
} from '../../config/trustedFolders.js';
|
||||
import type * as fs from 'node:fs/promises';
|
||||
import type { Stats } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
const mockInstallOrUpdateExtension: Mock<
|
||||
typeof ExtensionManager.prototype.installOrUpdateExtension
|
||||
> = vi.hoisted(() => vi.fn());
|
||||
const mockRequestConsentNonInteractive: Mock<
|
||||
typeof requestConsentNonInteractive
|
||||
> = vi.hoisted(() => vi.fn());
|
||||
const mockPromptForConsentNonInteractive: Mock<
|
||||
typeof promptForConsentNonInteractive
|
||||
> = vi.hoisted(() => vi.fn());
|
||||
const mockStat: Mock<typeof fs.stat> = vi.hoisted(() => vi.fn());
|
||||
const mockInferInstallMetadata: Mock<typeof inferInstallMetadata> = vi.hoisted(
|
||||
() => vi.fn(),
|
||||
);
|
||||
const mockIsWorkspaceTrusted: Mock<typeof isWorkspaceTrusted> = vi.hoisted(() =>
|
||||
vi.fn(),
|
||||
);
|
||||
const mockLoadTrustedFolders: Mock<typeof loadTrustedFolders> = vi.hoisted(() =>
|
||||
vi.fn(),
|
||||
);
|
||||
const mockDiscover: Mock<typeof core.FolderTrustDiscoveryService.discover> =
|
||||
vi.hoisted(() => vi.fn());
|
||||
const {
|
||||
mockInstallOrUpdateExtension,
|
||||
mockLoadExtensions,
|
||||
mockExtensionManager,
|
||||
mockRequestConsentNonInteractive,
|
||||
mockPromptForConsentNonInteractive,
|
||||
mockStat,
|
||||
mockInferInstallMetadata,
|
||||
mockIsWorkspaceTrusted,
|
||||
mockLoadTrustedFolders,
|
||||
mockDiscover,
|
||||
} = vi.hoisted(() => {
|
||||
const mockLoadExtensions = vi.fn();
|
||||
const mockInstallOrUpdateExtension = vi.fn();
|
||||
const mockExtensionManager = vi.fn().mockImplementation(() => ({
|
||||
loadExtensions: mockLoadExtensions,
|
||||
installOrUpdateExtension: mockInstallOrUpdateExtension,
|
||||
}));
|
||||
|
||||
return {
|
||||
mockLoadExtensions,
|
||||
mockInstallOrUpdateExtension,
|
||||
mockExtensionManager,
|
||||
mockRequestConsentNonInteractive: vi.fn(),
|
||||
mockPromptForConsentNonInteractive: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockInferInstallMetadata: vi.fn(),
|
||||
mockIsWorkspaceTrusted: vi.fn(),
|
||||
mockLoadTrustedFolders: vi.fn(),
|
||||
mockDiscover: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../config/extensions/consent.js', () => ({
|
||||
requestConsentNonInteractive: mockRequestConsentNonInteractive,
|
||||
@@ -84,6 +82,7 @@ vi.mock('../../config/extension-manager.js', async (importOriginal) => ({
|
||||
...(await importOriginal<
|
||||
typeof import('../../config/extension-manager.js')
|
||||
>()),
|
||||
ExtensionManager: mockExtensionManager,
|
||||
inferInstallMetadata: mockInferInstallMetadata,
|
||||
}));
|
||||
|
||||
@@ -117,19 +116,18 @@ describe('handleInstall', () => {
|
||||
let processSpy: MockInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
debugLogSpy = vi.spyOn(core.debugLogger, 'log');
|
||||
debugErrorSpy = vi.spyOn(core.debugLogger, 'error');
|
||||
debugLogSpy = vi
|
||||
.spyOn(core.debugLogger, 'log')
|
||||
.mockImplementation(() => {});
|
||||
debugErrorSpy = vi
|
||||
.spyOn(core.debugLogger, 'error')
|
||||
.mockImplementation(() => {});
|
||||
processSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation(() => undefined as never);
|
||||
|
||||
vi.spyOn(ExtensionManager.prototype, 'loadExtensions').mockResolvedValue(
|
||||
[],
|
||||
);
|
||||
vi.spyOn(
|
||||
ExtensionManager.prototype,
|
||||
'installOrUpdateExtension',
|
||||
).mockImplementation(mockInstallOrUpdateExtension);
|
||||
mockLoadExtensions.mockResolvedValue([]);
|
||||
mockInstallOrUpdateExtension.mockReset();
|
||||
|
||||
mockIsWorkspaceTrusted.mockReturnValue({ isTrusted: true, source: 'file' });
|
||||
mockDiscover.mockResolvedValue({
|
||||
@@ -163,12 +161,7 @@ describe('handleInstall', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockInstallOrUpdateExtension.mockClear();
|
||||
mockRequestConsentNonInteractive.mockClear();
|
||||
mockStat.mockClear();
|
||||
mockInferInstallMetadata.mockClear();
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function createMockExtension(
|
||||
@@ -288,6 +281,39 @@ describe('handleInstall', () => {
|
||||
expect(processSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should pass promptForSetting when skipSettings is not provided', async () => {
|
||||
mockInstallOrUpdateExtension.mockResolvedValue({
|
||||
name: 'test-extension',
|
||||
} as unknown as core.GeminiCLIExtension);
|
||||
|
||||
await handleInstall({
|
||||
source: 'http://google.com',
|
||||
});
|
||||
|
||||
expect(mockExtensionManager).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requestSetting: promptForSetting,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass null for requestSetting when skipSettings is true', async () => {
|
||||
mockInstallOrUpdateExtension.mockResolvedValue({
|
||||
name: 'test-extension',
|
||||
} as unknown as core.GeminiCLIExtension);
|
||||
|
||||
await handleInstall({
|
||||
source: 'http://google.com',
|
||||
skipSettings: true,
|
||||
});
|
||||
|
||||
expect(mockExtensionManager).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requestSetting: null,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should proceed if local path is already trusted', async () => {
|
||||
mockInstallOrUpdateExtension.mockResolvedValue(
|
||||
createMockExtension({
|
||||
|
||||
@@ -37,6 +37,7 @@ interface InstallArgs {
|
||||
autoUpdate?: boolean;
|
||||
allowPreRelease?: boolean;
|
||||
consent?: boolean;
|
||||
skipSettings?: boolean;
|
||||
}
|
||||
|
||||
export async function handleInstall(args: InstallArgs) {
|
||||
@@ -153,7 +154,7 @@ export async function handleInstall(args: InstallArgs) {
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent,
|
||||
requestSetting: promptForSetting,
|
||||
requestSetting: args.skipSettings ? null : promptForSetting,
|
||||
settings,
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
@@ -196,6 +197,11 @@ export const installCommand: CommandModule = {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
.option('skip-settings', {
|
||||
describe: 'Skip the configuration on install process.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
.check((argv) => {
|
||||
if (!argv.source) {
|
||||
throw new Error('The source argument must be provided.');
|
||||
@@ -214,6 +220,8 @@ export const installCommand: CommandModule = {
|
||||
allowPreRelease: argv['pre-release'] as boolean | undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
consent: argv['consent'] as boolean | undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
skipSettings: argv['skip-settings'] as boolean | undefined,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
|
||||
@@ -264,6 +264,7 @@ describe('mcp list command', () => {
|
||||
config: {
|
||||
'allowed-server': { url: 'http://allowed' },
|
||||
},
|
||||
requiredConfig: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ export async function getMcpServersFromConfig(
|
||||
return;
|
||||
}
|
||||
mcpServers[key] = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-spread
|
||||
...server,
|
||||
extension,
|
||||
};
|
||||
|
||||
@@ -226,6 +226,51 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('parseArguments', () => {
|
||||
describe('worktree', () => {
|
||||
it('should parse --worktree flag when provided with a name', async () => {
|
||||
process.argv = ['node', 'script.js', '--worktree', 'my-feature'];
|
||||
const settings = createTestMergedSettings();
|
||||
settings.experimental.worktrees = true;
|
||||
const argv = await parseArguments(settings);
|
||||
expect(argv.worktree).toBe('my-feature');
|
||||
});
|
||||
|
||||
it('should generate a random name when --worktree is provided without a name', async () => {
|
||||
process.argv = ['node', 'script.js', '--worktree'];
|
||||
const settings = createTestMergedSettings();
|
||||
settings.experimental.worktrees = true;
|
||||
const argv = await parseArguments(settings);
|
||||
expect(argv.worktree).toBeDefined();
|
||||
expect(argv.worktree).not.toBe('');
|
||||
expect(typeof argv.worktree).toBe('string');
|
||||
});
|
||||
|
||||
it('should throw an error when --worktree is used but experimental.worktrees is not enabled', async () => {
|
||||
process.argv = ['node', 'script.js', '--worktree', 'feature'];
|
||||
const settings = createTestMergedSettings();
|
||||
settings.experimental.worktrees = false;
|
||||
|
||||
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('process.exit called');
|
||||
});
|
||||
const mockConsoleError = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(parseArguments(settings)).rejects.toThrow(
|
||||
'process.exit called',
|
||||
);
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'The --worktree flag is only available when experimental.worktrees is enabled in your settings.',
|
||||
),
|
||||
);
|
||||
|
||||
mockExit.mockRestore();
|
||||
mockConsoleError.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
description: 'long flags',
|
||||
@@ -1671,6 +1716,7 @@ describe('loadCliConfig with admin.mcp.config', () => {
|
||||
|
||||
const serverA = config.getMcpServers()?.['serverA'];
|
||||
expect(serverA).toEqual({
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-spread
|
||||
...localMcpServers['serverA'],
|
||||
type: 'sse',
|
||||
url: 'https://admin-server-a.com/sse',
|
||||
@@ -1721,6 +1767,7 @@ describe('loadCliConfig with admin.mcp.config', () => {
|
||||
};
|
||||
const localMcpServersWithTools: Record<string, MCPServerConfig> = {
|
||||
serverA: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-spread
|
||||
...localMcpServers['serverA'],
|
||||
includeTools: ['local_tool'],
|
||||
timeout: 1234,
|
||||
@@ -1763,6 +1810,7 @@ describe('loadCliConfig with admin.mcp.config', () => {
|
||||
};
|
||||
const localMcpServersWithTools: Record<string, MCPServerConfig> = {
|
||||
serverA: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-spread
|
||||
...localMcpServers['serverA'],
|
||||
includeTools: ['local_tool'],
|
||||
},
|
||||
@@ -2225,6 +2273,30 @@ describe('loadCliConfig tool exclusions', () => {
|
||||
expect(config.getExcludeTools()).toContain('ask_user');
|
||||
});
|
||||
|
||||
it('should exclude ask_user in interactive mode when --acp is provided', async () => {
|
||||
process.stdin.isTTY = true;
|
||||
process.argv = ['node', 'script.js', '--acp'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const config = await loadCliConfig(
|
||||
createTestMergedSettings(),
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
expect(config.getExcludeTools()).toContain('ask_user');
|
||||
});
|
||||
|
||||
it('should exclude ask_user in interactive mode when --experimental-acp is provided', async () => {
|
||||
process.stdin.isTTY = true;
|
||||
process.argv = ['node', 'script.js', '--experimental-acp'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const config = await loadCliConfig(
|
||||
createTestMergedSettings(),
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
expect(config.getExcludeTools()).toContain('ask_user');
|
||||
});
|
||||
|
||||
it('should not exclude shell tool in non-interactive mode when --allowed-tools="ShellTool" is set', async () => {
|
||||
process.stdin.isTTY = false;
|
||||
process.argv = [
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import yargs from 'yargs/yargs';
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import process from 'node:process';
|
||||
import * as path from 'node:path';
|
||||
import { execa } from 'execa';
|
||||
import { mcpCommand } from '../commands/mcp.js';
|
||||
import { extensionsCommand } from '../commands/extensions.js';
|
||||
import { skillsCommand } from '../commands/skills.js';
|
||||
@@ -36,7 +37,11 @@ import {
|
||||
Config,
|
||||
resolveToRealPath,
|
||||
applyAdminAllowlist,
|
||||
applyRequiredServers,
|
||||
getAdminBlockedMcpServersMessage,
|
||||
getProjectRootForWorktree,
|
||||
isGeminiWorktree,
|
||||
type WorktreeSettings,
|
||||
type HookDefinition,
|
||||
type HookEventName,
|
||||
type OutputFormat,
|
||||
@@ -47,6 +52,8 @@ import {
|
||||
type MergedSettings,
|
||||
saveModelChange,
|
||||
loadSettings,
|
||||
isWorktreeEnabled,
|
||||
type LoadedSettings,
|
||||
} from './settings.js';
|
||||
|
||||
import { loadSandboxConfig } from './sandboxConfig.js';
|
||||
@@ -73,6 +80,7 @@ export interface CliArgs {
|
||||
debug: boolean | undefined;
|
||||
prompt: string | undefined;
|
||||
promptInteractive: string | undefined;
|
||||
worktree?: string;
|
||||
|
||||
yolo: boolean | undefined;
|
||||
approvalMode: string | undefined;
|
||||
@@ -97,7 +105,6 @@ export interface CliArgs {
|
||||
rawOutput: boolean | undefined;
|
||||
acceptRawOutputRisk: boolean | undefined;
|
||||
isCommand: boolean | undefined;
|
||||
simulateUser: boolean | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,6 +122,36 @@ const coerceCommaSeparated = (values: string[]): string[] => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Pre-parses the command line arguments to find the worktree flag.
|
||||
* Used for early setup before full argument parsing with settings.
|
||||
*/
|
||||
export function getWorktreeArg(argv: string[]): string | undefined {
|
||||
const result = yargs(hideBin(argv))
|
||||
.help(false)
|
||||
.version(false)
|
||||
.option('worktree', { alias: 'w', type: 'string' })
|
||||
.strict(false)
|
||||
.exitProcess(false)
|
||||
.parseSync();
|
||||
|
||||
if (result.worktree === undefined) return undefined;
|
||||
return typeof result.worktree === 'string' ? result.worktree.trim() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a worktree is requested via CLI and enabled in settings.
|
||||
* Returns the requested name (can be empty string for auto-generated) or undefined.
|
||||
*/
|
||||
export function getRequestedWorktreeName(
|
||||
settings: LoadedSettings,
|
||||
): string | undefined {
|
||||
if (!isWorktreeEnabled(settings)) {
|
||||
return undefined;
|
||||
}
|
||||
return getWorktreeArg(process.argv);
|
||||
}
|
||||
|
||||
export async function parseArguments(
|
||||
settings: MergedSettings,
|
||||
): Promise<CliArgs> {
|
||||
@@ -158,6 +195,20 @@ export async function parseArguments(
|
||||
description:
|
||||
'Execute the provided prompt and continue in interactive mode',
|
||||
})
|
||||
.option('worktree', {
|
||||
alias: 'w',
|
||||
type: 'string',
|
||||
skipValidation: true,
|
||||
description:
|
||||
'Start Gemini in a new git worktree. If no name is provided, one is generated automatically.',
|
||||
coerce: (value: unknown): string => {
|
||||
const trimmed = typeof value === 'string' ? value.trim() : '';
|
||||
if (trimmed === '') {
|
||||
return Math.random().toString(36).substring(2, 10);
|
||||
}
|
||||
return trimmed;
|
||||
},
|
||||
})
|
||||
.option('sandbox', {
|
||||
alias: 's',
|
||||
type: 'boolean',
|
||||
@@ -299,11 +350,6 @@ export async function parseArguments(
|
||||
.option('accept-raw-output-risk', {
|
||||
type: 'boolean',
|
||||
description: 'Suppress the security warning when using --raw-output.',
|
||||
})
|
||||
.option('simulate-user', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Run the user simulation agent in the background for evaluation purposes.',
|
||||
}),
|
||||
)
|
||||
// Register MCP subcommands
|
||||
@@ -340,6 +386,9 @@ export async function parseArguments(
|
||||
) {
|
||||
return `Invalid values:\n Argument: output-format, Given: "${argv['outputFormat']}", Choices: "text", "json", "stream-json"`;
|
||||
}
|
||||
if (argv['worktree'] && !settings.experimental?.worktrees) {
|
||||
return 'The --worktree flag is only available when experimental.worktrees is enabled in your settings.';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -425,6 +474,7 @@ export interface LoadCliConfigOptions {
|
||||
projectHooks?: { [K in HookEventName]?: HookDefinition[] } & {
|
||||
disabled?: string[];
|
||||
};
|
||||
worktreeSettings?: WorktreeSettings;
|
||||
}
|
||||
|
||||
export async function loadCliConfig(
|
||||
@@ -436,6 +486,9 @@ export async function loadCliConfig(
|
||||
const { cwd = process.cwd(), projectHooks } = options;
|
||||
const debugMode = isDebugMode(argv);
|
||||
|
||||
const worktreeSettings =
|
||||
options.worktreeSettings ?? (await resolveWorktreeSettings(cwd));
|
||||
|
||||
if (argv.sandbox) {
|
||||
process.env['GEMINI_SANDBOX'] = 'true';
|
||||
}
|
||||
@@ -654,12 +707,16 @@ export async function loadCliConfig(
|
||||
|
||||
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
|
||||
|
||||
const isAcpMode = !!argv.acp || !!argv.experimentalAcp;
|
||||
|
||||
// In non-interactive mode, exclude tools that require a prompt.
|
||||
const extraExcludes: string[] = [];
|
||||
if (!interactive) {
|
||||
if (!interactive || isAcpMode) {
|
||||
// The Policy Engine natively handles headless safety by translating ASK_USER
|
||||
// decisions to DENY. However, we explicitly block ask_user here to guarantee
|
||||
// it can never be allowed via a high-priority policy rule when no human is present.
|
||||
// We also exclude it in ACP mode as IDEs intercept tool calls and ask for permission,
|
||||
// breaking conversational flows.
|
||||
extraExcludes.push(ASK_USER_TOOL_NAME);
|
||||
}
|
||||
|
||||
@@ -708,6 +765,19 @@ export async function loadCliConfig(
|
||||
? defaultModel
|
||||
: specifiedModel || defaultModel;
|
||||
const sandboxConfig = await loadSandboxConfig(settings, argv);
|
||||
if (sandboxConfig) {
|
||||
const existingPaths = sandboxConfig.allowedPaths || [];
|
||||
if (settings.tools.sandboxAllowedPaths?.length) {
|
||||
sandboxConfig.allowedPaths = [
|
||||
...new Set([...existingPaths, ...settings.tools.sandboxAllowedPaths]),
|
||||
];
|
||||
}
|
||||
if (settings.tools.sandboxNetworkAccess !== undefined) {
|
||||
sandboxConfig.networkAccess =
|
||||
sandboxConfig.networkAccess || settings.tools.sandboxNetworkAccess;
|
||||
}
|
||||
}
|
||||
|
||||
const screenReader =
|
||||
argv.screenReader !== undefined
|
||||
? argv.screenReader
|
||||
@@ -743,7 +813,25 @@ export async function loadCliConfig(
|
||||
}
|
||||
}
|
||||
|
||||
const isAcpMode = !!argv.acp || !!argv.experimentalAcp;
|
||||
// Apply admin-required MCP servers (injected regardless of allowlist)
|
||||
if (mcpEnabled) {
|
||||
const requiredMcpConfig = settings.admin?.mcp?.requiredConfig;
|
||||
if (requiredMcpConfig && Object.keys(requiredMcpConfig).length > 0) {
|
||||
const requiredResult = applyRequiredServers(
|
||||
mcpServers ?? {},
|
||||
requiredMcpConfig,
|
||||
);
|
||||
mcpServers = requiredResult.mcpServers;
|
||||
|
||||
if (requiredResult.requiredServerNames.length > 0) {
|
||||
coreEvents.emitConsoleLog(
|
||||
'info',
|
||||
`Admin-required MCP servers injected: ${requiredResult.requiredServerNames.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let clientName: string | undefined = undefined;
|
||||
if (isAcpMode) {
|
||||
const ide = detectIdeFromEnv();
|
||||
@@ -772,6 +860,7 @@ export async function loadCliConfig(
|
||||
importFormat: settings.context?.importFormat,
|
||||
debugMode,
|
||||
question,
|
||||
worktreeSettings,
|
||||
|
||||
coreTools: settings.tools?.core || undefined,
|
||||
allowedTools: allowedTools.length > 0 ? allowedTools : undefined,
|
||||
@@ -805,7 +894,6 @@ export async function loadCliConfig(
|
||||
approvalMode,
|
||||
disableYoloMode:
|
||||
settings.security?.disableYoloMode || settings.admin?.secureModeEnabled,
|
||||
simulateUser: !!argv.simulateUser,
|
||||
disableAlwaysAllow:
|
||||
settings.security?.disableAlwaysAllow ||
|
||||
settings.admin?.secureModeEnabled,
|
||||
@@ -847,6 +935,7 @@ export async function loadCliConfig(
|
||||
skillsSupport: settings.skills?.enabled ?? true,
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
experimentalMemoryManager: settings.experimental?.memoryManager,
|
||||
modelSteering: settings.experimental?.modelSteering,
|
||||
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
|
||||
toolOutputMasking: settings.experimental?.toolOutputMasking,
|
||||
@@ -913,3 +1002,48 @@ function mergeExcludeTools(
|
||||
]);
|
||||
return Array.from(allExcludeTools);
|
||||
}
|
||||
|
||||
async function resolveWorktreeSettings(
|
||||
cwd: string,
|
||||
): Promise<WorktreeSettings | undefined> {
|
||||
let worktreePath: string | undefined;
|
||||
try {
|
||||
const { stdout } = await execa('git', ['rev-parse', '--show-toplevel'], {
|
||||
cwd,
|
||||
});
|
||||
const toplevel = stdout.trim();
|
||||
const projectRoot = await getProjectRootForWorktree(toplevel);
|
||||
|
||||
if (isGeminiWorktree(toplevel, projectRoot)) {
|
||||
worktreePath = toplevel;
|
||||
}
|
||||
} catch (_e) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!worktreePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let worktreeBaseSha: string | undefined;
|
||||
try {
|
||||
const { stdout } = await execa('git', ['rev-parse', 'HEAD'], {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
worktreeBaseSha = stdout.trim();
|
||||
} catch (e: unknown) {
|
||||
debugLogger.debug(
|
||||
`Failed to resolve worktree base SHA at ${worktreePath}: ${e instanceof Error ? e.message : String(e)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!worktreeBaseSha) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
name: path.basename(worktreePath),
|
||||
path: worktreePath,
|
||||
baseSha: worktreeBaseSha,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { copyExtension } from './extension-manager.js';
|
||||
|
||||
describe('copyExtension permissions', () => {
|
||||
let tempDir: string;
|
||||
let sourceDir: string;
|
||||
let destDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-permission-test-'));
|
||||
sourceDir = path.join(tempDir, 'source');
|
||||
destDir = path.join(tempDir, 'dest');
|
||||
fs.mkdirSync(sourceDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Ensure we can delete the temp directory by making everything writable again
|
||||
const makeWritableSync = (p: string) => {
|
||||
try {
|
||||
const stats = fs.lstatSync(p);
|
||||
fs.chmodSync(p, stats.mode | 0o700);
|
||||
if (stats.isDirectory()) {
|
||||
fs.readdirSync(p).forEach((child) =>
|
||||
makeWritableSync(path.join(p, child)),
|
||||
);
|
||||
}
|
||||
} catch (_e) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
};
|
||||
|
||||
if (fs.existsSync(tempDir)) {
|
||||
makeWritableSync(tempDir);
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should make destination writable even if source is read-only', async () => {
|
||||
const fileName = 'test.txt';
|
||||
const filePath = path.join(sourceDir, fileName);
|
||||
fs.writeFileSync(filePath, 'hello');
|
||||
|
||||
// Make source read-only: 0o555 for directory, 0o444 for file
|
||||
fs.chmodSync(filePath, 0o444);
|
||||
fs.chmodSync(sourceDir, 0o555);
|
||||
|
||||
// Verify source is read-only
|
||||
expect(() => fs.writeFileSync(filePath, 'fail')).toThrow();
|
||||
|
||||
// Perform copy
|
||||
await copyExtension(sourceDir, destDir);
|
||||
|
||||
// Verify destination is writable
|
||||
const destFilePath = path.join(destDir, fileName);
|
||||
const destFileStats = fs.statSync(destFilePath);
|
||||
const destDirStats = fs.statSync(destDir);
|
||||
|
||||
// Check that owner write bits are set (0o200)
|
||||
expect(destFileStats.mode & 0o200).toBe(0o200);
|
||||
expect(destDirStats.mode & 0o200).toBe(0o200);
|
||||
|
||||
// Verify we can actually write to the destination file
|
||||
fs.writeFileSync(destFilePath, 'writable');
|
||||
expect(fs.readFileSync(destFilePath, 'utf-8')).toBe('writable');
|
||||
|
||||
// Verify we can delete the destination (which requires write bit on destDir)
|
||||
fs.rmSync(destFilePath);
|
||||
expect(fs.existsSync(destFilePath)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle nested directories with restrictive permissions', async () => {
|
||||
const subDir = path.join(sourceDir, 'subdir');
|
||||
fs.mkdirSync(subDir);
|
||||
const fileName = 'nested.txt';
|
||||
const filePath = path.join(subDir, fileName);
|
||||
fs.writeFileSync(filePath, 'nested content');
|
||||
|
||||
// Make nested structure read-only
|
||||
fs.chmodSync(filePath, 0o444);
|
||||
fs.chmodSync(subDir, 0o555);
|
||||
fs.chmodSync(sourceDir, 0o555);
|
||||
|
||||
// Perform copy
|
||||
await copyExtension(sourceDir, destDir);
|
||||
|
||||
// Verify nested destination is writable
|
||||
const destSubDir = path.join(destDir, 'subdir');
|
||||
const destFilePath = path.join(destSubDir, fileName);
|
||||
|
||||
expect(fs.statSync(destSubDir).mode & 0o200).toBe(0o200);
|
||||
expect(fs.statSync(destFilePath).mode & 0o200).toBe(0o200);
|
||||
|
||||
// Verify we can delete the whole destination tree
|
||||
await fs.promises.rm(destDir, { recursive: true, force: true });
|
||||
expect(fs.existsSync(destDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('should not follow symlinks or modify symlink targets', async () => {
|
||||
const symlinkTarget = path.join(tempDir, 'external-target');
|
||||
fs.writeFileSync(symlinkTarget, 'external content');
|
||||
// Target is read-only
|
||||
fs.chmodSync(symlinkTarget, 0o444);
|
||||
|
||||
const symlinkPath = path.join(sourceDir, 'symlink-file');
|
||||
fs.symlinkSync(symlinkTarget, symlinkPath);
|
||||
|
||||
// Perform copy
|
||||
await copyExtension(sourceDir, destDir);
|
||||
|
||||
const destSymlinkPath = path.join(destDir, 'symlink-file');
|
||||
const destSymlinkStats = fs.lstatSync(destSymlinkPath);
|
||||
|
||||
// Verify it is still a symlink in the destination
|
||||
expect(destSymlinkStats.isSymbolicLink()).toBe(true);
|
||||
|
||||
// Verify the target (external to the extension) was NOT modified
|
||||
const targetStats = fs.statSync(symlinkTarget);
|
||||
// Owner write bit should still NOT be set (0o200)
|
||||
expect(targetStats.mode & 0o200).toBe(0o000);
|
||||
|
||||
// Clean up
|
||||
fs.chmodSync(symlinkTarget, 0o644);
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,10 @@ import { createExtension } from '../test-utils/createExtension.js';
|
||||
import { EXTENSIONS_DIRECTORY_NAME } from './extensions/variables.js';
|
||||
|
||||
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
|
||||
const mockIntegrityManager = vi.hoisted(() => ({
|
||||
verify: vi.fn().mockResolvedValue('verified'),
|
||||
store: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('node:os', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:os')>();
|
||||
@@ -31,6 +35,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
homedir: mockHomedir,
|
||||
ExtensionIntegrityManager: vi
|
||||
.fn()
|
||||
.mockImplementation(() => mockIntegrityManager),
|
||||
loadAgentsFromDirectory: vi
|
||||
.fn()
|
||||
.mockImplementation(async () => ({ agents: [], errors: [] })),
|
||||
@@ -64,6 +71,7 @@ describe('ExtensionManager skills validation', () => {
|
||||
requestConsent: vi.fn().mockResolvedValue(true),
|
||||
requestSetting: vi.fn(),
|
||||
workspaceDir: tempDir,
|
||||
integrityManager: mockIntegrityManager,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,6 +147,7 @@ describe('ExtensionManager skills validation', () => {
|
||||
requestConsent: vi.fn().mockResolvedValue(true),
|
||||
requestSetting: vi.fn(),
|
||||
workspaceDir: tempDir,
|
||||
integrityManager: mockIntegrityManager,
|
||||
});
|
||||
|
||||
// 4. Load extensions
|
||||
|
||||
@@ -637,64 +637,4 @@ 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,18 +982,11 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
plan: config.plan,
|
||||
};
|
||||
} catch (e) {
|
||||
const extName = path.basename(extensionDir);
|
||||
debugLogger.warn(
|
||||
`Warning: Removing broken extension ${extName}: ${getErrorMessage(e)}`,
|
||||
debugLogger.error(
|
||||
`Warning: Skipping extension in ${effectiveExtensionPath}: ${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;
|
||||
}
|
||||
}
|
||||
@@ -1248,11 +1241,32 @@ function filterMcpConfig(original: MCPServerConfig): MCPServerConfig {
|
||||
return Object.freeze(rest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively ensures that the owner has write permissions for all files
|
||||
* and directories within the target path.
|
||||
*/
|
||||
async function makeWritableRecursive(targetPath: string): Promise<void> {
|
||||
const stats = await fs.promises.lstat(targetPath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
// Ensure directory is rwx for the owner (0o700)
|
||||
await fs.promises.chmod(targetPath, stats.mode | 0o700);
|
||||
const children = await fs.promises.readdir(targetPath);
|
||||
for (const child of children) {
|
||||
await makeWritableRecursive(path.join(targetPath, child));
|
||||
}
|
||||
} else if (stats.isFile()) {
|
||||
// Ensure file is rw for the owner (0o600)
|
||||
await fs.promises.chmod(targetPath, stats.mode | 0o600);
|
||||
}
|
||||
}
|
||||
|
||||
export async function copyExtension(
|
||||
source: string,
|
||||
destination: string,
|
||||
): Promise<void> {
|
||||
await fs.promises.cp(source, destination, { recursive: true });
|
||||
await makeWritableRecursive(destination);
|
||||
}
|
||||
|
||||
function getContextFileNames(config: ExtensionConfig): string[] {
|
||||
|
||||
@@ -249,8 +249,10 @@ describe('extension tests', () => {
|
||||
expect(extensions[0].name).toBe('test-extension');
|
||||
});
|
||||
|
||||
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(() => {});
|
||||
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(() => {});
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'traversal-extension',
|
||||
@@ -660,8 +662,10 @@ name = "yolo-checker"
|
||||
expect(serverConfig.env!['MISSING_VAR_BRACES']).toBe('${ALSO_UNDEFINED}');
|
||||
});
|
||||
|
||||
it('should remove an extension with invalid JSON config and log a warning', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
it('should skip an extension with invalid JSON config and log an error', async () => {
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
// Good extension
|
||||
createExtension({
|
||||
@@ -682,15 +686,17 @@ name = "yolo-checker"
|
||||
expect(extensions[0].name).toBe('good-ext');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
`Warning: Removing broken extension bad-ext: Failed to load extension config from ${badConfigPath}`,
|
||||
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`,
|
||||
),
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should remove an extension with missing "name" in config and log a warning', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
it('should skip an extension with missing "name" in config and log an error', async () => {
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
// Good extension
|
||||
createExtension({
|
||||
@@ -711,7 +717,7 @@ name = "yolo-checker"
|
||||
expect(extensions[0].name).toBe('good-ext');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
`Warning: Removing broken extension bad-ext-no-name: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`,
|
||||
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -737,8 +743,10 @@ name = "yolo-checker"
|
||||
expect(extensions[0].mcpServers?.['test-server'].trust).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should log a warning for invalid extension names during loading', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
it('should log an error for invalid extension names during loading', async () => {
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'bad_name',
|
||||
|
||||
@@ -59,8 +59,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
});
|
||||
|
||||
async function expectConsentSnapshot(consentString: string) {
|
||||
const renderResult = render(React.createElement(Text, null, consentString));
|
||||
await renderResult.waitUntilReady();
|
||||
const renderResult = await render(
|
||||
React.createElement(Text, null, consentString),
|
||||
);
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ vi.mock('node:fs', async (importOriginal) => {
|
||||
rm: vi.fn(),
|
||||
cp: vi.fn(),
|
||||
readFile: vi.fn(),
|
||||
lstat: vi.fn(),
|
||||
chmod: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -143,6 +145,11 @@ describe('extensionUpdates', () => {
|
||||
vi.mocked(fs.promises.rm).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.promises.cp).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValue([]);
|
||||
vi.mocked(fs.promises.lstat).mockResolvedValue({
|
||||
isDirectory: () => true,
|
||||
mode: 0o755,
|
||||
} as unknown as fs.Stats);
|
||||
vi.mocked(fs.promises.chmod).mockResolvedValue(undefined);
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
|
||||
@@ -13,6 +13,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
Storage: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-spread
|
||||
...actual.Storage,
|
||||
getGlobalGeminiDir: () => '/virtual-home/.gemini',
|
||||
},
|
||||
|
||||
@@ -516,7 +516,9 @@ describe('Policy Engine Integration Tests', () => {
|
||||
);
|
||||
expect(mcpServerRule?.priority).toBe(4.1); // MCP allowed server
|
||||
|
||||
const readOnlyToolRule = rules.find((r) => r.toolName === 'glob');
|
||||
const readOnlyToolRule = rules.find(
|
||||
(r) => r.toolName === 'glob' && !r.subagent,
|
||||
);
|
||||
// Priority 70 in default tier → 1.07 (Overriding Plan Mode Deny)
|
||||
expect(readOnlyToolRule?.priority).toBeCloseTo(1.07, 5);
|
||||
|
||||
@@ -673,7 +675,7 @@ describe('Policy Engine Integration Tests', () => {
|
||||
const server1Rule = rules.find((r) => r.toolName === 'mcp_server1_*');
|
||||
expect(server1Rule?.priority).toBe(4.1); // Allowed servers (user tier)
|
||||
|
||||
const globRule = rules.find((r) => r.toolName === 'glob');
|
||||
const globRule = rules.find((r) => r.toolName === 'glob' && !r.subagent);
|
||||
// Priority 70 in default tier → 1.07
|
||||
expect(globRule?.priority).toBeCloseTo(1.07, 5); // Auto-accept read-only
|
||||
|
||||
|
||||
@@ -338,6 +338,8 @@ describe('loadSandboxConfig', () => {
|
||||
sandbox: {
|
||||
enabled: true,
|
||||
command: 'podman',
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -353,6 +355,8 @@ describe('loadSandboxConfig', () => {
|
||||
sandbox: {
|
||||
enabled: true,
|
||||
image: 'custom/image',
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -367,6 +371,8 @@ describe('loadSandboxConfig', () => {
|
||||
tools: {
|
||||
sandbox: {
|
||||
enabled: false,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -382,6 +388,7 @@ describe('loadSandboxConfig', () => {
|
||||
sandbox: {
|
||||
enabled: true,
|
||||
allowedPaths: ['/settings-path'],
|
||||
networkAccess: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ const VALID_SANDBOX_COMMANDS = [
|
||||
'sandbox-exec',
|
||||
'runsc',
|
||||
'lxc',
|
||||
'windows-native',
|
||||
];
|
||||
|
||||
function isSandboxCommand(
|
||||
@@ -75,8 +76,15 @@ function getSandboxCommand(
|
||||
'gVisor (runsc) sandboxing is only supported on Linux',
|
||||
);
|
||||
}
|
||||
// confirm that specified command exists
|
||||
if (!commandExists.sync(sandbox)) {
|
||||
// windows-native is only supported on Windows
|
||||
if (sandbox === 'windows-native' && os.platform() !== 'win32') {
|
||||
throw new FatalSandboxError(
|
||||
'Windows native sandboxing is only supported on Windows',
|
||||
);
|
||||
}
|
||||
|
||||
// confirm that specified command exists (unless it's built-in)
|
||||
if (sandbox !== 'windows-native' && !commandExists.sync(sandbox)) {
|
||||
throw new FatalSandboxError(
|
||||
`Missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
|
||||
);
|
||||
@@ -149,7 +157,12 @@ export async function loadSandboxConfig(
|
||||
customImage ??
|
||||
packageJson?.config?.sandboxImageUri;
|
||||
|
||||
return command && image
|
||||
const isNative =
|
||||
command === 'windows-native' ||
|
||||
command === 'sandbox-exec' ||
|
||||
command === 'lxc';
|
||||
|
||||
return command && (image || isNative)
|
||||
? { enabled: true, allowedPaths, networkAccess, command, image }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -2751,6 +2751,28 @@ describe('Settings Loading and Merging', () => {
|
||||
expect(loadedSettings.merged.admin?.mcp?.config).toEqual(mcpServers);
|
||||
});
|
||||
|
||||
it('should map requiredMcpConfig from remote settings', () => {
|
||||
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
const requiredMcpConfig = {
|
||||
'corp-tool': {
|
||||
url: 'https://mcp.corp/tool',
|
||||
type: 'http' as const,
|
||||
trust: true,
|
||||
},
|
||||
};
|
||||
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
mcpSetting: {
|
||||
mcpEnabled: true,
|
||||
requiredMcpConfig,
|
||||
},
|
||||
});
|
||||
|
||||
expect(loadedSettings.merged.admin?.mcp?.requiredConfig).toEqual(
|
||||
requiredMcpConfig,
|
||||
);
|
||||
});
|
||||
|
||||
it('should set skills based on unmanagedCapabilitiesEnabled', () => {
|
||||
const loadedSettings = loadSettings();
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
|
||||
@@ -480,6 +480,7 @@ export class LoadedSettings {
|
||||
admin.mcp = {
|
||||
enabled: mcpSetting?.mcpEnabled,
|
||||
config: mcpSetting?.mcpConfig?.mcpServers,
|
||||
requiredConfig: mcpSetting?.requiredMcpConfig,
|
||||
};
|
||||
admin.extensions = {
|
||||
enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled,
|
||||
@@ -631,6 +632,10 @@ export function resetSettingsCacheForTesting() {
|
||||
settingsCache.clear();
|
||||
}
|
||||
|
||||
export function isWorktreeEnabled(settings: LoadedSettings): boolean {
|
||||
return settings.merged.experimental.worktrees;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads settings from user and workspace directories.
|
||||
* Project settings override user settings.
|
||||
|
||||
@@ -538,8 +538,32 @@ describe('SettingsSchema', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const visitJsonSchema = (jsonSchema: Record<string, unknown>) => {
|
||||
const ref = jsonSchema['ref'];
|
||||
if (typeof ref === 'string') {
|
||||
referenced.add(ref);
|
||||
}
|
||||
const properties = jsonSchema['properties'];
|
||||
if (
|
||||
properties &&
|
||||
typeof properties === 'object' &&
|
||||
!Array.isArray(properties)
|
||||
) {
|
||||
Object.values(properties as Record<string, unknown>).forEach((prop) =>
|
||||
visitJsonSchema(prop as Record<string, unknown>),
|
||||
);
|
||||
}
|
||||
const items = jsonSchema['items'];
|
||||
if (items && typeof items === 'object' && !Array.isArray(items)) {
|
||||
visitJsonSchema(items as Record<string, unknown>);
|
||||
}
|
||||
};
|
||||
|
||||
Object.values(schema).forEach(visitDefinition);
|
||||
|
||||
// Also visit all definitions to find nested references
|
||||
Object.values(SETTINGS_SCHEMA_DEFINITIONS).forEach(visitJsonSchema);
|
||||
|
||||
// Ensure definitions map doesn't accumulate stale entries.
|
||||
Object.keys(SETTINGS_SCHEMA_DEFINITIONS).forEach((key) => {
|
||||
if (!referenced.has(key)) {
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
import {
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||
DEFAULT_MODEL_CONFIGS,
|
||||
AuthProviderType,
|
||||
type MCPServerConfig,
|
||||
type RequiredMcpServerConfig,
|
||||
type BugCommandSettings,
|
||||
type TelemetrySettings,
|
||||
type AuthType,
|
||||
@@ -1081,6 +1083,20 @@ const SETTINGS_SCHEMA = {
|
||||
ref: 'ModelResolution',
|
||||
},
|
||||
},
|
||||
modelChains: {
|
||||
type: 'object',
|
||||
label: 'Model Chains',
|
||||
category: 'Model',
|
||||
requiresRestart: true,
|
||||
default: DEFAULT_MODEL_CONFIGS.modelChains,
|
||||
description:
|
||||
'Availability policy chains defining fallback behavior for models.',
|
||||
showInDialog: false,
|
||||
additionalProperties: {
|
||||
type: 'array',
|
||||
ref: 'ModelPolicyChain',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1182,6 +1198,26 @@ 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1344,10 +1380,30 @@ const SETTINGS_SCHEMA = {
|
||||
description: oneLine`
|
||||
Legacy full-process sandbox execution environment.
|
||||
Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile,
|
||||
or specify an explicit sandbox command (e.g., "docker", "podman", "lxc").
|
||||
or specify an explicit sandbox command (e.g., "docker", "podman", "lxc", "windows-native").
|
||||
`,
|
||||
showInDialog: false,
|
||||
},
|
||||
sandboxAllowedPaths: {
|
||||
type: 'array',
|
||||
label: 'Sandbox Allowed Paths',
|
||||
category: 'Tools',
|
||||
requiresRestart: true,
|
||||
default: [] as string[],
|
||||
description:
|
||||
'List of additional paths that the sandbox is allowed to access.',
|
||||
showInDialog: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
sandboxNetworkAccess: {
|
||||
type: 'boolean',
|
||||
label: 'Sandbox Network Access',
|
||||
category: 'Tools',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Whether the sandbox is allowed to access the network.',
|
||||
showInDialog: true,
|
||||
},
|
||||
shell: {
|
||||
type: 'object',
|
||||
label: 'Shell',
|
||||
@@ -1870,6 +1926,16 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Enable local and remote subagents.',
|
||||
showInDialog: false,
|
||||
},
|
||||
worktrees: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Git Worktrees',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Enable automated Git worktree management for parallel work.',
|
||||
showInDialog: true,
|
||||
},
|
||||
extensionManagement: {
|
||||
type: 'boolean',
|
||||
label: 'Extension Management',
|
||||
@@ -2045,6 +2111,16 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
memoryManager: {
|
||||
type: 'boolean',
|
||||
label: 'Memory Manager Agent',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories.',
|
||||
showInDialog: true,
|
||||
},
|
||||
topicUpdateNarration: {
|
||||
type: 'boolean',
|
||||
label: 'Topic & Update Narration',
|
||||
@@ -2391,7 +2467,7 @@ const SETTINGS_SCHEMA = {
|
||||
category: 'Admin',
|
||||
requiresRestart: false,
|
||||
default: {} as Record<string, MCPServerConfig>,
|
||||
description: 'Admin-configured MCP servers.',
|
||||
description: 'Admin-configured MCP servers (allowlist).',
|
||||
showInDialog: false,
|
||||
mergeStrategy: MergeStrategy.REPLACE,
|
||||
additionalProperties: {
|
||||
@@ -2399,6 +2475,20 @@ const SETTINGS_SCHEMA = {
|
||||
ref: 'MCPServerConfig',
|
||||
},
|
||||
},
|
||||
requiredConfig: {
|
||||
type: 'object',
|
||||
label: 'Required MCP Config',
|
||||
category: 'Admin',
|
||||
requiresRestart: false,
|
||||
default: {} as Record<string, RequiredMcpServerConfig>,
|
||||
description: 'Admin-required MCP servers that are always injected.',
|
||||
showInDialog: false,
|
||||
mergeStrategy: MergeStrategy.REPLACE,
|
||||
additionalProperties: {
|
||||
type: 'object',
|
||||
ref: 'RequiredMcpServerConfig',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
skills: {
|
||||
@@ -2523,11 +2613,72 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
|
||||
type: 'string',
|
||||
description:
|
||||
'Authentication provider used for acquiring credentials (for example `dynamic_discovery`).',
|
||||
enum: [
|
||||
'dynamic_discovery',
|
||||
'google_credentials',
|
||||
'service_account_impersonation',
|
||||
],
|
||||
enum: Object.values(AuthProviderType),
|
||||
},
|
||||
targetAudience: {
|
||||
type: 'string',
|
||||
description:
|
||||
'OAuth target audience (CLIENT_ID.apps.googleusercontent.com).',
|
||||
},
|
||||
targetServiceAccount: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Service account email to impersonate (name@project.iam.gserviceaccount.com).',
|
||||
},
|
||||
},
|
||||
},
|
||||
RequiredMcpServerConfig: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Admin-required MCP server configuration (remote transports only).',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL for the required MCP server.',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Transport type for the required server.',
|
||||
enum: ['sse', 'http'],
|
||||
},
|
||||
headers: {
|
||||
type: 'object',
|
||||
description: 'Additional HTTP headers sent to the server.',
|
||||
additionalProperties: { type: 'string' },
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
description: 'Timeout in milliseconds for MCP requests.',
|
||||
},
|
||||
trust: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Marks the server as trusted. Defaults to true for admin-required servers.',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Human-readable description of the server.',
|
||||
},
|
||||
includeTools: {
|
||||
type: 'array',
|
||||
description: 'Subset of tools enabled for this server.',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
excludeTools: {
|
||||
type: 'array',
|
||||
description: 'Tools disabled for this server.',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
oauth: {
|
||||
type: 'object',
|
||||
description: 'OAuth configuration for authenticating with the server.',
|
||||
additionalProperties: true,
|
||||
},
|
||||
authProviderType: {
|
||||
type: 'string',
|
||||
description: 'Authentication provider used for acquiring credentials.',
|
||||
enum: Object.values(AuthProviderType),
|
||||
},
|
||||
targetAudience: {
|
||||
type: 'string',
|
||||
@@ -2867,6 +3018,42 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
|
||||
},
|
||||
},
|
||||
},
|
||||
ModelPolicyChain: {
|
||||
type: 'array',
|
||||
description: 'A chain of model policies for fallback behavior.',
|
||||
items: {
|
||||
type: 'object',
|
||||
ref: 'ModelPolicy',
|
||||
},
|
||||
},
|
||||
ModelPolicy: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Defines the policy for a single model in the availability chain.',
|
||||
properties: {
|
||||
model: { type: 'string' },
|
||||
isLastResort: { type: 'boolean' },
|
||||
actions: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
terminal: { type: 'string', enum: ['silent', 'prompt'] },
|
||||
transient: { type: 'string', enum: ['silent', 'prompt'] },
|
||||
not_found: { type: 'string', enum: ['silent', 'prompt'] },
|
||||
unknown: { type: 'string', enum: ['silent', 'prompt'] },
|
||||
},
|
||||
},
|
||||
stateTransitions: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
terminal: { type: 'string', enum: ['terminal', 'sticky_retry'] },
|
||||
transient: { type: 'string', enum: ['terminal', 'sticky_retry'] },
|
||||
not_found: { type: 'string', enum: ['terminal', 'sticky_retry'] },
|
||||
unknown: { type: 'string', enum: ['terminal', 'sticky_retry'] },
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['model'],
|
||||
},
|
||||
};
|
||||
|
||||
export function getSettingsSchema(): SettingsSchemaType {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user