Compare commits

...

10 Commits

Author SHA1 Message Date
Christian Gunderman c6efa6f61f Add script for running nightly evals locally. 2026-02-11 16:20:59 -08:00
Christian Gunderman 6c1773170e More grep prompt tweaks (#18846) 2026-02-11 21:55:27 +00:00
Dev Randalpura 00966062b8 Removed getPlainTextLength (#18848) 2026-02-11 21:47:02 +00:00
Adam Weidman 4138667bae feat(a2a): add value-resolver for auth credential resolution (#18653) 2026-02-11 21:23:28 +00:00
Sandy Tao bfa791e13d feat(core): update internal utility models to Gemini 3 (#18773) 2026-02-11 20:20:14 +00:00
Adib234 e9a9474810 Revert unintended credentials exposure (#18840) 2026-02-11 20:06:28 +00:00
Jerop Kipruto 02adfe2bca docs(plan): add ask_user tool documentation (#18830) 2026-02-11 20:04:01 +00:00
Christian Gunderman 2a08456ed0 Update prompt and grep tool definition to limit context size (#18780) 2026-02-11 19:20:51 +00:00
christine betts 2dac98dc8d Remove experimental note in extension settings docs (#18822) 2026-02-11 19:17:51 +00:00
Jerop Kipruto 77849cadac docs(plan): add documentation for plan mode tools (#18827) 2026-02-11 18:53:43 +00:00
39 changed files with 1501 additions and 263 deletions
+9 -5
View File
@@ -63,11 +63,12 @@ You can enter Plan Mode in three ways:
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
(`Default` -> `Plan` -> `Auto-Edit`).
2. **Command:** Type `/plan` in the input box.
3. **Natural Language:** Ask the agent to "start a plan for...".
3. **Natural Language:** Ask the agent to "start a plan for...". The agent will
then call the [`enter_plan_mode`] tool to switch modes.
### The Planning Workflow
1. **Requirements:** The agent clarifies goals using `ask_user`.
1. **Requirements:** The agent clarifies goals using [`ask_user`].
2. **Exploration:** The agent uses read-only tools (like [`read_file`]) to map
the codebase and validate assumptions.
3. **Design:** The agent proposes alternative approaches with a recommended
@@ -83,8 +84,8 @@ You can enter Plan Mode in three ways:
To exit Plan Mode:
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle to the desired mode.
1. **Tool:** The agent calls the `exit_plan_mode` tool to present the finalized
plan for your approval.
2. **Tool:** The agent calls the [`exit_plan_mode`] tool to present the
finalized plan for your approval.
## Tool Restrictions
@@ -94,7 +95,7 @@ These are the only allowed tools:
- **FileSystem (Read):** [`read_file`], [`list_directory`], [`glob`]
- **Search:** [`grep_search`], [`google_web_search`]
- **Interaction:** `ask_user`
- **Interaction:** [`ask_user`]
- **MCP Tools (Read):** Read-only [MCP tools] (e.g., `github_read_issue`,
`postgres_read_schema`) are allowed.
- **Planning (Write):** [`write_file`] and [`replace`] ONLY allowed for `.md`
@@ -180,3 +181,6 @@ Guide].
[`activate_skill`]: /docs/cli/skills.md
[experimental research sub-agents]: /docs/core/subagents.md
[Policy Engine Guide]: /docs/core/policy-engine.md
[`enter_plan_mode`]: /docs/tools/planning.md#1-enter_plan_mode-enterplanmode
[`exit_plan_mode`]: /docs/tools/planning.md#2-exit_plan_mode-exitplanmode
[`ask_user`]: /docs/tools/ask-user.md
-3
View File
@@ -179,9 +179,6 @@ precedence.
### Settings
_Note: This is an experimental feature. We do not yet recommend extension
authors introduce settings as part of their core flows._
Extensions can define settings that the user will be prompted to provide upon
installation. This is useful for things like API keys, URLs, or other
configuration that the extension needs to function.
+14 -8
View File
@@ -447,6 +447,12 @@ their corresponding top-level category object in your `settings.json` file.
"model": "gemini-2.5-flash"
}
},
"gemini-3-flash-base": {
"extends": "base",
"modelConfig": {
"model": "gemini-3-flash-preview"
}
},
"classifier": {
"extends": "base",
"modelConfig": {
@@ -502,7 +508,7 @@ their corresponding top-level category object in your `settings.json` file.
}
},
"web-search": {
"extends": "gemini-2.5-flash-base",
"extends": "gemini-3-flash-base",
"modelConfig": {
"generateContentConfig": {
"tools": [
@@ -514,7 +520,7 @@ their corresponding top-level category object in your `settings.json` file.
}
},
"web-fetch": {
"extends": "gemini-2.5-flash-base",
"extends": "gemini-3-flash-base",
"modelConfig": {
"generateContentConfig": {
"tools": [
@@ -526,25 +532,25 @@ their corresponding top-level category object in your `settings.json` file.
}
},
"web-fetch-fallback": {
"extends": "gemini-2.5-flash-base",
"extends": "gemini-3-flash-base",
"modelConfig": {}
},
"loop-detection": {
"extends": "gemini-2.5-flash-base",
"extends": "gemini-3-flash-base",
"modelConfig": {}
},
"loop-detection-double-check": {
"extends": "base",
"modelConfig": {
"model": "gemini-2.5-pro"
"model": "gemini-3-pro-preview"
}
},
"llm-edit-fixer": {
"extends": "gemini-2.5-flash-base",
"extends": "gemini-3-flash-base",
"modelConfig": {}
},
"next-speaker-checker": {
"extends": "gemini-2.5-flash-base",
"extends": "gemini-3-flash-base",
"modelConfig": {}
},
"chat-compression-3-pro": {
@@ -574,7 +580,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"chat-compression-default": {
"modelConfig": {
"model": "gemini-2.5-pro"
"model": "gemini-3-pro-preview"
}
}
}
+95
View File
@@ -0,0 +1,95 @@
# Ask User Tool
The `ask_user` tool allows the agent to ask you one or more questions to gather
preferences, clarify requirements, or make decisions. It supports multiple
question types including multiple-choice, free-form text, and Yes/No
confirmation.
## `ask_user` (Ask User)
- **Tool name:** `ask_user`
- **Display name:** Ask User
- **File:** `ask-user.ts`
- **Parameters:**
- `questions` (array of objects, required): A list of 1 to 4 questions to ask.
Each question object has the following properties:
- `question` (string, required): The complete question text.
- `header` (string, required): A short label (max 16 chars) displayed as a
chip/tag (e.g., "Auth", "Database").
- `type` (string, optional): The type of question. Defaults to `'choice'`.
- `'choice'`: Multiple-choice with options (supports multi-select).
- `'text'`: Free-form text input.
- `'yesno'`: Yes/No confirmation.
- `options` (array of objects, optional): Required for `'choice'` type. 2-4
selectable options.
- `label` (string, required): Display text (1-5 words).
- `description` (string, required): Brief explanation.
- `multiSelect` (boolean, optional): For `'choice'` type, allows selecting
multiple options.
- `placeholder` (string, optional): Hint text for input fields.
- **Behavior:**
- Presents an interactive dialog to the user with the specified questions.
- Pauses execution until the user provides answers or dismisses the dialog.
- Returns the user's answers to the model.
- **Output (`llmContent`):** A JSON string containing the user's answers,
indexed by question position (e.g.,
`{"answers":{"0": "Option A", "1": "Some text"}}`).
- **Confirmation:** Yes. The tool inherently involves user interaction.
## Usage Examples
### Multiple Choice Question
```json
{
"questions": [
{
"header": "Database",
"question": "Which database would you like to use?",
"type": "choice",
"options": [
{
"label": "PostgreSQL",
"description": "Powerful, open source object-relational database system."
},
{
"label": "SQLite",
"description": "C-library that implements a SQL database engine."
}
]
}
]
}
```
### Text Input Question
```json
{
"questions": [
{
"header": "Project Name",
"question": "What is the name of your new project?",
"type": "text",
"placeholder": "e.g., my-awesome-app"
}
]
}
```
### Yes/No Question
```json
{
"questions": [
{
"header": "Deploy",
"question": "Do you want to deploy the application now?",
"type": "yesno"
}
]
}
```
+3
View File
@@ -86,6 +86,9 @@ Gemini CLI's built-in tools can be broadly categorized as follows:
information across sessions.
- **[Todo Tool](./todos.md) (`write_todos`):** For managing subtasks of complex
requests.
- **[Planning Tools](./planning.md):** For entering and exiting Plan Mode.
- **[Ask User Tool](./ask-user.md) (`ask_user`):** For gathering user input and
making decisions.
Additionally, these tools incorporate:
+1 -12
View File
@@ -739,21 +739,10 @@ The MCP integration tracks several states:
cautiously and only for servers you completely control
- **Access tokens:** Be security-aware when configuring environment variables
containing API keys or tokens
- **Environment variable redaction:** By default, the Gemini CLI redacts
sensitive environment variables (such as `GEMINI_API_KEY`, `GOOGLE_API_KEY`,
and variables matching patterns like `*TOKEN*`, `*SECRET*`, `*PASSWORD*`) when
spawning MCP servers using the `stdio` transport. This prevents unintended
exposure of your credentials to third-party servers.
- **Explicit environment variables:** If you need to pass a specific environment
variable to an MCP server, you should define it explicitly in the `env`
property of the server configuration in `settings.json`.
- **Sandbox compatibility:** When using sandboxing, ensure MCP servers are
available within the sandbox environment.
available within the sandbox environment
- **Private data:** Using broadly scoped personal access tokens can lead to
information leakage between repositories.
- **Untrusted servers:** Be extremely cautious when adding MCP servers from
untrusted or third-party sources. Malicious servers could attempt to
exfiltrate data or perform unauthorized actions through the tools they expose.
### Performance and resource management
+55
View File
@@ -0,0 +1,55 @@
# Gemini CLI planning tools
Planning tools allow the Gemini model to switch into a safe, read-only "Plan
Mode" for researching and planning complex changes, and to signal the
finalization of a plan to the user.
## 1. `enter_plan_mode` (EnterPlanMode)
`enter_plan_mode` switches the CLI to Plan Mode. This tool is typically called
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.
- **Tool name:** `enter_plan_mode`
- **Display name:** Enter Plan Mode
- **File:** `enter-plan-mode.ts`
- **Parameters:**
- `reason` (string, optional): A short reason explaining why the agent is
entering plan mode (e.g., "Starting a complex feature implementation").
- **Behavior:**
- Switches the CLI's approval mode to `PLAN`.
- Notifies the user that the agent has entered Plan Mode.
- **Output (`llmContent`):** A message indicating the switch, e.g.,
`Switching to Plan mode.`
- **Confirmation:** Yes. The user is prompted to confirm entering Plan Mode.
## 2. `exit_plan_mode` (ExitPlanMode)
`exit_plan_mode` signals that the planning phase is complete. It presents the
finalized plan to the user and requests approval to start the implementation.
- **Tool name:** `exit_plan_mode`
- **Display name:** Exit Plan Mode
- **File:** `exit-plan-mode.ts`
- **Parameters:**
- `plan_path` (string, required): The path to the finalized Markdown plan
file. This file MUST be located within the project's temporary plans
directory (e.g., `~/.gemini/tmp/<project>/plans/`).
- **Behavior:**
- Validates that the `plan_path` is within the allowed directory and that the
file exists and has content.
- Presents the plan to the user for review.
- If the user approves the plan:
- Switches the CLI's approval mode to the user's chosen approval mode (
`DEFAULT` or `AUTO_EDIT`).
- Marks the plan as approved for implementation.
- If the user rejects the plan:
- Stays in Plan Mode.
- Returns user feedback to the model to refine the plan.
- **Output (`llmContent`):**
- On approval: A message indicating the plan was approved and the new approval
mode.
- On rejection: A message containing the user's feedback.
- **Confirmation:** Yes. Shows the finalized plan and asks for user approval to
proceed with implementation.
+21
View File
@@ -114,6 +114,27 @@ npm run test:all_evals
This command sets the `RUN_EVALS` environment variable to `1`, which enables the
`USUALLY_PASSES` tests.
### All Evals (All Models)
To run the full evaluation suite across all supported models and generate a
local markdown report (mirroring the nightly CI workflow):
```bash
npm run test:all_evals_all_models
```
This script will:
1. Build the project.
2. Run `test:all_evals` for each model in the nightly rotation.
3. Collect logs and aggregate them using `scripts/aggregate_evals.js`.
4. Generate a `local_evals_summary.md` file with the results.
You can also filter by test name and specify the number of attempts:
```bash
npm run test:all_evals_all_models -- "my-test-pattern" --attempts 3
```
## Reporting
Results for evaluations are available on GitHub Actions:
+144
View File
@@ -0,0 +1,144 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
/**
* Evals to verify that the agent uses search tools efficiently (frugally)
* by utilizing limiting parameters like `total_max_matches` and `max_matches_per_file`.
* This ensures the agent doesn't flood the context window with unnecessary search results.
*/
describe('Frugal Search', () => {
const getGrepParams = (call: any): any => {
let args = call.toolRequest.args;
if (typeof args === 'string') {
try {
args = JSON.parse(args);
} catch (e) {
// Ignore parse errors
}
}
return args;
};
evalTest('USUALLY_PASSES', {
name: 'should use targeted search with limit',
prompt: 'find me a sample usage of path.resolve() in the codebase',
files: {
'package.json': JSON.stringify({
name: 'test-project',
version: '1.0.0',
main: 'dist/index.js',
scripts: {
build: 'tsc',
test: 'vitest',
},
dependencies: {
typescript: '^5.0.0',
'@types/node': '^20.0.0',
vitest: '^1.0.0',
},
}),
'src/index.ts': `
import { App } from './app.ts';
const app = new App();
app.start();
`,
'src/app.ts': `
import * as path from 'path';
import { UserController } from './controllers/user.ts';
export class App {
constructor() {
console.log('App initialized');
}
public start(): void {
const userController = new UserController();
console.log('Static path:', path.resolve(__dirname, '../public'));
}
}
`,
'src/utils.ts': `
import * as path from 'path';
import * as fs from 'fs';
export function resolvePath(p: string): string {
return path.resolve(process.cwd(), p);
}
export function ensureDir(dirPath: string): void {
const absolutePath = path.resolve(dirPath);
if (!fs.existsSync(absolutePath)) {
fs.mkdirSync(absolutePath, { recursive: true });
}
}
`,
'src/config.ts': `
import * as path from 'path';
export const config = {
dbPath: path.resolve(process.cwd(), 'data/db.sqlite'),
logLevel: 'info',
};
`,
'src/controllers/user.ts': `
import * as path from 'path';
export class UserController {
public getUsers(): any[] {
console.log('Loading users from:', path.resolve('data/users.json'));
return [{ id: 1, name: 'Alice' }];
}
}
`,
'tests/app.test.ts': `
import { describe, it, expect } from 'vitest';
import * as path from 'path';
describe('App', () => {
it('should resolve paths', () => {
const p = path.resolve('test');
expect(p).toBeDefined();
});
});
`,
},
assert: async (rig) => {
const toolCalls = rig.readToolLogs();
const grepCalls = toolCalls.filter(
(call) => call.toolRequest.name === 'grep_search',
);
expect(grepCalls.length).toBeGreaterThan(0);
const grepParams = grepCalls.map(getGrepParams);
const hasTotalMaxLimit = grepParams.some(
(p) => p.total_max_matches !== undefined && p.total_max_matches <= 100,
);
expect(
hasTotalMaxLimit,
`Expected agent to use a small total_max_matches (<= 100) for a sample usage request. Actual values: ${JSON.stringify(
grepParams.map((p) => p.total_max_matches),
)}`,
).toBe(true);
const hasMaxMatchesPerFileLimit = grepParams.some(
(p) =>
p.max_matches_per_file !== undefined && p.max_matches_per_file <= 5,
);
expect(
hasMaxMatchesPerFileLimit,
`Expected agent to use a small max_matches_per_file (<= 5) for a sample usage request. Actual values: ${JSON.stringify(
grepParams.map((p) => p.max_matches_per_file),
)}`,
).toBe(true);
},
});
});
+1
View File
@@ -43,6 +43,7 @@
"test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",
"test:always_passing_evals": "vitest run --config evals/vitest.config.ts",
"test:all_evals": "cross-env RUN_EVALS=1 vitest run --config evals/vitest.config.ts",
"test:all_evals_all_models": "node scripts/run_local_evals.js",
"test:e2e": "cross-env VERBOSE=true KEEP_OUTPUT=true npm run test:integration:sandbox:none",
"test:integration:all": "npm run test:integration:sandbox:none && npm run test:integration:sandbox:docker && npm run test:integration:sandbox:podman",
"test:integration:sandbox:none": "cross-env GEMINI_SANDBOX=false vitest run --root ./integration-tests",
-7
View File
@@ -128,13 +128,6 @@ async function addMcpServer(
settings.setValue(settingsScope, 'mcpServers', mcpServers);
if (transport === 'stdio') {
debugLogger.warn(
'Security Warning: Running MCP servers with stdio transport can expose inherited environment variables. ' +
'While the Gemini CLI redacts common API keys and secrets by default, you should only run servers from trusted sources.',
);
}
if (isExistingServer) {
debugLogger.log(`MCP server "${name}" updated in ${scope} settings.`);
} else {
@@ -1,25 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { getPlainTextLength } from './InlineMarkdownRenderer.js';
import { describe, it, expect } from 'vitest';
describe('getPlainTextLength', () => {
it.each([
['**Primary Go', 12],
['*Primary Go', 11],
['**Primary Go**', 10],
['*Primary Go*', 10],
['**', 2],
['*', 1],
['compile-time**', 14],
])(
'should measure markdown text length correctly for "%s"',
(input, expected) => {
expect(getPlainTextLength(input)).toBe(expected);
},
);
});
@@ -7,7 +7,6 @@
import React from 'react';
import { Text } from 'ink';
import { theme } from '../semantic-colors.js';
import stringWidth from 'string-width';
import { debugLogger } from '@google/gemini-cli-core';
// Constants for Markdown parsing
@@ -171,19 +170,3 @@ const RenderInlineInternal: React.FC<RenderInlineProps> = ({
};
export const RenderInline = React.memo(RenderInlineInternal);
/**
* Utility function to get the plain text length of a string with markdown formatting
* This is useful for calculating column widths in tables
*/
export const getPlainTextLength = (text: string): number => {
const cleanText = text
.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/\*(.+?)\*/g, '$1')
.replace(/_(.*?)_/g, '$1')
.replace(/~~(.*?)~~/g, '$1')
.replace(/`(.*?)`/g, '$1')
.replace(/<u>(.*?)<\/u>/g, '$1')
.replace(/.*\[(.*?)\]\(.*\)/g, '$1');
return stringWidth(cleanText);
};
@@ -363,4 +363,171 @@ Hidden`,
expect(result.errors).toHaveLength(1);
});
});
describe('remote agent auth configuration', () => {
it('should parse remote agent with apiKey auth', async () => {
const filePath = await writeAgentMarkdown(`---
kind: remote
name: api-key-agent
agent_card_url: https://example.com/card
auth:
type: apiKey
key: $MY_API_KEY
in: header
name: X-Custom-Key
---
`);
const result = await parseAgentMarkdown(filePath);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
kind: 'remote',
name: 'api-key-agent',
auth: {
type: 'apiKey',
key: '$MY_API_KEY',
in: 'header',
name: 'X-Custom-Key',
},
});
});
it('should parse remote agent with http Bearer auth', async () => {
const filePath = await writeAgentMarkdown(`---
kind: remote
name: bearer-agent
agent_card_url: https://example.com/card
auth:
type: http
scheme: Bearer
token: $BEARER_TOKEN
---
`);
const result = await parseAgentMarkdown(filePath);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
kind: 'remote',
name: 'bearer-agent',
auth: {
type: 'http',
scheme: 'Bearer',
token: '$BEARER_TOKEN',
},
});
});
it('should parse remote agent with http Basic auth', async () => {
const filePath = await writeAgentMarkdown(`---
kind: remote
name: basic-agent
agent_card_url: https://example.com/card
auth:
type: http
scheme: Basic
username: $AUTH_USER
password: $AUTH_PASS
---
`);
const result = await parseAgentMarkdown(filePath);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
kind: 'remote',
name: 'basic-agent',
auth: {
type: 'http',
scheme: 'Basic',
username: '$AUTH_USER',
password: '$AUTH_PASS',
},
});
});
it('should throw error for Bearer auth without token', async () => {
const filePath = await writeAgentMarkdown(`---
kind: remote
name: invalid-bearer
agent_card_url: https://example.com/card
auth:
type: http
scheme: Bearer
---
`);
await expect(parseAgentMarkdown(filePath)).rejects.toThrow(
/Bearer scheme requires "token"/,
);
});
it('should throw error for Basic auth without credentials', async () => {
const filePath = await writeAgentMarkdown(`---
kind: remote
name: invalid-basic
agent_card_url: https://example.com/card
auth:
type: http
scheme: Basic
username: user
---
`);
await expect(parseAgentMarkdown(filePath)).rejects.toThrow(
/Basic scheme requires "username" and "password"/,
);
});
it('should throw error for apiKey auth without key', async () => {
const filePath = await writeAgentMarkdown(`---
kind: remote
name: invalid-apikey
agent_card_url: https://example.com/card
auth:
type: apiKey
---
`);
await expect(parseAgentMarkdown(filePath)).rejects.toThrow(
/auth\.key.*Required/,
);
});
it('should convert auth config in markdownToAgentDefinition', () => {
const markdown = {
kind: 'remote' as const,
name: 'auth-agent',
agent_card_url: 'https://example.com/card',
auth: {
type: 'apiKey' as const,
key: '$API_KEY',
in: 'header' as const,
},
};
const result = markdownToAgentDefinition(markdown);
expect(result).toMatchObject({
kind: 'remote',
name: 'auth-agent',
auth: {
type: 'apiKey',
key: '$API_KEY',
location: 'header',
},
});
});
it('should parse auth with agent_card_requires_auth flag', async () => {
const filePath = await writeAgentMarkdown(`---
kind: remote
name: protected-card-agent
agent_card_url: https://example.com/card
auth:
type: apiKey
key: $MY_API_KEY
agent_card_requires_auth: true
---
`);
const result = await parseAgentMarkdown(filePath);
expect(result[0]).toMatchObject({
auth: {
type: 'apiKey',
agent_card_requires_auth: true,
},
});
});
});
});
+153
View File
@@ -15,6 +15,7 @@ import {
DEFAULT_MAX_TURNS,
DEFAULT_MAX_TIME_MINUTES,
} from './types.js';
import type { A2AAuthConfig } from './auth-provider/types.js';
import { isValidToolName } from '../tools/tool-names.js';
import { FRONTMATTER_REGEX } from '../skills/skillLoader.js';
import { getErrorMessage } from '../utils/errors.js';
@@ -39,11 +40,29 @@ interface FrontmatterLocalAgentDefinition
timeout_mins?: number;
}
/**
* Authentication configuration for remote agents in frontmatter format.
*/
interface FrontmatterAuthConfig {
type: 'apiKey' | 'http';
agent_card_requires_auth?: boolean;
// API Key
key?: string;
in?: 'header' | 'query' | 'cookie';
name?: string;
// HTTP
scheme?: 'Bearer' | 'Basic';
token?: string;
username?: string;
password?: string;
}
interface FrontmatterRemoteAgentDefinition
extends FrontmatterBaseAgentDefinition {
kind: 'remote';
description?: string;
agent_card_url: string;
auth?: FrontmatterAuthConfig;
}
type FrontmatterAgentDefinition =
@@ -95,6 +114,66 @@ const localAgentSchema = z
})
.strict();
/**
* Base fields shared by all auth configs.
*/
const baseAuthFields = {
agent_card_requires_auth: z.boolean().optional(),
};
/**
* API Key auth schema.
* Supports sending key in header, query parameter, or cookie.
*/
const apiKeyAuthSchema = z.object({
...baseAuthFields,
type: z.literal('apiKey'),
key: z.string().min(1, 'API key is required'),
in: z.enum(['header', 'query', 'cookie']).optional(),
name: z.string().optional(),
});
/**
* HTTP auth schema (Bearer or Basic).
* Note: Validation for scheme-specific fields is applied in authConfigSchema
* since discriminatedUnion doesn't support refined schemas directly.
*/
const httpAuthSchemaBase = z.object({
...baseAuthFields,
type: z.literal('http'),
scheme: z.enum(['Bearer', 'Basic']),
token: z.string().optional(),
username: z.string().optional(),
password: z.string().optional(),
});
/**
* Combined auth schema - discriminated union of all auth types.
* Note: We use the base schema for discriminatedUnion, then apply refinements
* via superRefine since discriminatedUnion doesn't support refined schemas directly.
*/
const authConfigSchema = z
.discriminatedUnion('type', [apiKeyAuthSchema, httpAuthSchemaBase])
.superRefine((data, ctx) => {
// Apply HTTP auth validation after union parsing
if (data.type === 'http') {
if (data.scheme === 'Bearer' && !data.token) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Bearer scheme requires "token"',
path: ['token'],
});
}
if (data.scheme === 'Basic' && (!data.username || !data.password)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Basic scheme requires "username" and "password"',
path: data.username ? ['password'] : ['username'],
});
}
}
});
const remoteAgentSchema = z
.object({
kind: z.literal('remote').optional().default('remote'),
@@ -102,6 +181,7 @@ const remoteAgentSchema = z
description: z.string().optional(),
display_name: z.string().optional(),
agent_card_url: z.string().url(),
auth: authConfigSchema.optional(),
})
.strict();
@@ -238,6 +318,76 @@ export async function parseAgentMarkdown(
return [agentDef];
}
/**
* Converts frontmatter auth config to the internal A2AAuthConfig type.
* This handles the mapping from snake_case YAML to the internal type structure.
*/
function convertFrontmatterAuthToConfig(
frontmatter: FrontmatterAuthConfig,
): A2AAuthConfig {
const base = {
agent_card_requires_auth: frontmatter.agent_card_requires_auth,
};
switch (frontmatter.type) {
case 'apiKey':
if (!frontmatter.key) {
throw new Error('Internal error: API key missing after validation.');
}
return {
...base,
type: 'apiKey',
key: frontmatter.key,
location: frontmatter.in,
name: frontmatter.name,
};
case 'http': {
if (!frontmatter.scheme) {
throw new Error(
'Internal error: HTTP scheme missing after validation.',
);
}
switch (frontmatter.scheme) {
case 'Bearer':
if (!frontmatter.token) {
throw new Error(
'Internal error: Bearer token missing after validation.',
);
}
return {
...base,
type: 'http',
scheme: 'Bearer',
token: frontmatter.token,
};
case 'Basic':
if (!frontmatter.username || !frontmatter.password) {
throw new Error(
'Internal error: Basic auth credentials missing after validation.',
);
}
return {
...base,
type: 'http',
scheme: 'Basic',
username: frontmatter.username,
password: frontmatter.password,
};
default: {
const exhaustive: never = frontmatter.scheme;
throw new Error(`Unknown HTTP scheme: ${exhaustive}`);
}
}
}
default: {
const exhaustive: never = frontmatter.type;
throw new Error(`Unknown auth type: ${exhaustive}`);
}
}
}
/**
* Converts a FrontmatterAgentDefinition DTO to the internal AgentDefinition structure.
*
@@ -270,6 +420,9 @@ export function markdownToAgentDefinition(
description: markdown.description || '(Loading description...)',
displayName: markdown.display_name,
agentCardUrl: markdown.agent_card_url,
auth: markdown.auth
? convertFrontmatterAuthToConfig(markdown.auth)
: undefined,
inputConfig,
metadata,
};
@@ -9,17 +9,33 @@ import type { A2AAuthProvider, A2AAuthProviderType } from './types.js';
/**
* Abstract base class for A2A authentication providers.
* Provides default implementations for optional methods.
*/
export abstract class BaseA2AAuthProvider implements A2AAuthProvider {
/**
* The type of authentication provider.
*/
abstract readonly type: A2AAuthProviderType;
/**
* Get the HTTP headers to include in requests.
* Subclasses must implement this method.
*/
abstract headers(): Promise<HttpHeaders>;
private static readonly MAX_AUTH_RETRIES = 2;
private authRetryCount = 0;
/**
* Default: retry on 401/403 with fresh headers.
* Subclasses with cached tokens must override to force-refresh to avoid infinite retries.
* Check if a request should be retried with new headers.
*
* The default implementation checks for 401/403 status codes and
* returns fresh headers for retry. Subclasses can override for
* custom retry logic.
*
* @param _req The original request init
* @param res The response from the server
* @returns New headers for retry, or undefined if no retry should be made
*/
async shouldRetryWithHeaders(
_req: RequestInit,
@@ -32,10 +48,15 @@ export abstract class BaseA2AAuthProvider implements A2AAuthProvider {
this.authRetryCount++;
return this.headers();
}
// Reset on success
// Reset count if not an auth error
this.authRetryCount = 0;
return undefined;
}
async initialize(): Promise<void> {}
/**
* Initialize the provider. Override in subclasses that need async setup.
*/
async initialize(): Promise<void> {
// Default: no-op
}
}
@@ -0,0 +1,136 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, afterEach, vi } from 'vitest';
import {
resolveAuthValue,
needsResolution,
maskSensitiveValue,
} from './value-resolver.js';
describe('value-resolver', () => {
describe('resolveAuthValue', () => {
describe('environment variables', () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it('should resolve environment variable with $ prefix', async () => {
vi.stubEnv('TEST_API_KEY', 'secret-key-123');
const result = await resolveAuthValue('$TEST_API_KEY');
expect(result).toBe('secret-key-123');
});
it('should throw error for unset environment variable', async () => {
await expect(resolveAuthValue('$UNSET_VAR_12345')).rejects.toThrow(
"Environment variable 'UNSET_VAR_12345' is not set or is empty",
);
});
it('should throw error for empty environment variable', async () => {
vi.stubEnv('EMPTY_VAR', '');
await expect(resolveAuthValue('$EMPTY_VAR')).rejects.toThrow(
"Environment variable 'EMPTY_VAR' is not set or is empty",
);
});
});
describe('shell commands', () => {
it('should execute shell command with ! prefix', async () => {
const result = await resolveAuthValue('!echo hello');
expect(result).toBe('hello');
});
it('should trim whitespace from command output', async () => {
const result = await resolveAuthValue('!echo " hello "');
expect(result).toBe('hello');
});
it('should throw error for empty command', async () => {
await expect(resolveAuthValue('!')).rejects.toThrow(
'Empty command in auth value',
);
});
it('should throw error for command that returns empty output', async () => {
await expect(resolveAuthValue('!echo -n ""')).rejects.toThrow(
'returned empty output',
);
});
it('should throw error for failed command', async () => {
await expect(
resolveAuthValue('!nonexistent-command-12345'),
).rejects.toThrow(/Command.*failed/);
});
});
describe('literal values', () => {
it('should return literal value as-is', async () => {
const result = await resolveAuthValue('literal-api-key');
expect(result).toBe('literal-api-key');
});
it('should return empty string as-is', async () => {
const result = await resolveAuthValue('');
expect(result).toBe('');
});
it('should not treat values starting with other characters as special', async () => {
const result = await resolveAuthValue('api-key-123');
expect(result).toBe('api-key-123');
});
});
describe('escaped literals', () => {
it('should return $ literal when value starts with $$', async () => {
const result = await resolveAuthValue('$$LITERAL');
expect(result).toBe('$LITERAL');
});
it('should return ! literal when value starts with !!', async () => {
const result = await resolveAuthValue('!!not-a-command');
expect(result).toBe('!not-a-command');
});
});
});
describe('needsResolution', () => {
it('should return true for environment variable reference', () => {
expect(needsResolution('$ENV_VAR')).toBe(true);
});
it('should return true for command reference', () => {
expect(needsResolution('!command')).toBe(true);
});
it('should return false for literal value', () => {
expect(needsResolution('literal')).toBe(false);
});
it('should return false for empty string', () => {
expect(needsResolution('')).toBe(false);
});
});
describe('maskSensitiveValue', () => {
it('should mask value longer than 12 characters', () => {
expect(maskSensitiveValue('1234567890abcd')).toBe('12****cd');
});
it('should return **** for short values', () => {
expect(maskSensitiveValue('short')).toBe('****');
});
it('should return **** for exactly 12 characters', () => {
expect(maskSensitiveValue('123456789012')).toBe('****');
});
it('should return **** for empty string', () => {
expect(maskSensitiveValue('')).toBe('****');
});
});
});
@@ -0,0 +1,102 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { debugLogger } from '../../utils/debugLogger.js';
import { getShellConfiguration, spawnAsync } from '../../utils/shell-utils.js';
const COMMAND_TIMEOUT_MS = 60_000;
/**
* Resolves a value that may be an environment variable reference,
* a shell command, or a literal value.
*
* Supported formats:
* - `$ENV_VAR`: Read from environment variable
* - `!command`: Execute shell command and use output (trimmed)
* - `$$` or `!!`: Escape prefix, returns rest as literal
* - Any other string: Use as literal value
*
* @param value The value to resolve
* @returns The resolved value
* @throws Error if environment variable is not set or command fails
*/
export async function resolveAuthValue(value: string): Promise<string> {
// Support escaping with double prefix (e.g. $$ or !!).
// Strips one prefix char: $$FOO → $FOO, !!cmd → !cmd (literal, not resolved).
if (value.startsWith('$$') || value.startsWith('!!')) {
return value.slice(1);
}
// Environment variable: $MY_VAR
if (value.startsWith('$')) {
const envVar = value.slice(1);
const resolved = process.env[envVar];
if (resolved === undefined || resolved === '') {
throw new Error(
`Environment variable '${envVar}' is not set or is empty. ` +
`Please set it before using this agent.`,
);
}
debugLogger.debug(`[AuthValueResolver] Resolved env var: ${envVar}`);
return resolved;
}
// Shell command: !command arg1 arg2
if (value.startsWith('!')) {
const command = value.slice(1).trim();
if (!command) {
throw new Error('Empty command in auth value. Expected format: !command');
}
debugLogger.debug(`[AuthValueResolver] Executing command for auth value`);
const shellConfig = getShellConfiguration();
try {
const { stdout } = await spawnAsync(
shellConfig.executable,
[...shellConfig.argsPrefix, command],
{
signal: AbortSignal.timeout(COMMAND_TIMEOUT_MS),
windowsHide: true,
},
);
const trimmed = stdout.trim();
if (!trimmed) {
throw new Error(`Command '${command}' returned empty output`);
}
return trimmed;
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(
`Command '${command}' timed out after ${COMMAND_TIMEOUT_MS / 1000} seconds`,
);
}
throw error;
}
}
// Literal value - return as-is
return value;
}
/**
* Check if a value needs resolution (is an env var or command reference).
*/
export function needsResolution(value: string): boolean {
return value.startsWith('$') || value.startsWith('!');
}
/**
* Mask a sensitive value for logging purposes.
* Shows the first and last 2 characters with asterisks in between.
*/
export function maskSensitiveValue(value: string): string {
if (value.length <= 12) {
return '****';
}
return `${value.slice(0, 2)}****${value.slice(-2)}`;
}
@@ -58,7 +58,7 @@ describe('Fallback Integration', () => {
);
});
it('should NOT fallback if config is NOT in AUTO mode', () => {
it('should fallback for Gemini 3 models even if config is NOT in AUTO mode', () => {
// 1. Config is explicitly set to Pro, not Auto
vi.spyOn(config, 'getModel').mockReturnValue(PREVIEW_GEMINI_MODEL);
@@ -71,7 +71,7 @@ describe('Fallback Integration', () => {
// 4. Apply model selection
const result = applyModelSelection(config, { model: requestedModel });
// 5. Expect it to stay on Pro (because single model chain)
expect(result.model).toBe(PREVIEW_GEMINI_MODEL);
// 5. Expect it to fallback to Flash (because Gemini 3 uses PREVIEW_CHAIN)
expect(result.model).toBe(PREVIEW_GEMINI_FLASH_MODEL);
});
});
@@ -115,6 +115,19 @@ describe('policyHelpers', () => {
expect(chain[0]?.model).toBe('gemini-2.5-flash');
expect(chain[1]?.model).toBe('gemini-2.5-pro');
});
it('proactively returns Gemini 2.5 chain if Gemini 3 requested but user lacks access', () => {
const config = createMockConfig({
getModel: () => 'auto-gemini-3',
getHasAccessToPreviewModel: () => false,
});
const chain = resolvePolicyChain(config);
// Should downgrade to [Pro 2.5, Flash 2.5]
expect(chain).toHaveLength(2);
expect(chain[0]?.model).toBe('gemini-2.5-pro');
expect(chain[1]?.model).toBe('gemini-2.5-flash');
});
});
describe('buildFallbackPolicyContext', () => {
@@ -24,6 +24,7 @@ import {
DEFAULT_GEMINI_MODEL,
PREVIEW_GEMINI_MODEL_AUTO,
isAutoModel,
isGemini3Model,
resolveModel,
} from '../config/models.js';
import type { ModelSelectionResult } from './modelAvailabilityService.js';
@@ -46,17 +47,32 @@ export function resolvePolicyChain(
const resolvedModel = resolveModel(modelFromConfig);
const isAutoPreferred = preferredModel ? isAutoModel(preferredModel) : false;
const isAutoConfigured = isAutoModel(configuredModel);
const hasAccessToPreview = config.getHasAccessToPreviewModel?.() ?? true;
if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) {
chain = getFlashLitePolicyChain();
} else if (isAutoPreferred || isAutoConfigured) {
const previewEnabled =
preferredModel === PREVIEW_GEMINI_MODEL_AUTO ||
configuredModel === PREVIEW_GEMINI_MODEL_AUTO;
chain = getModelPolicyChain({
previewEnabled,
userTier: config.getUserTier(),
});
} else if (
isGemini3Model(resolvedModel) ||
isAutoPreferred ||
isAutoConfigured
) {
if (hasAccessToPreview) {
const previewEnabled =
isGemini3Model(resolvedModel) ||
preferredModel === PREVIEW_GEMINI_MODEL_AUTO ||
configuredModel === PREVIEW_GEMINI_MODEL_AUTO;
chain = getModelPolicyChain({
previewEnabled,
userTier: config.getUserTier(),
});
} else {
// User requested Gemini 3 but has no access. Proactively downgrade
// to the stable Gemini 2.5 chain.
return getModelPolicyChain({
previewEnabled: false,
userTier: config.getUserTier(),
});
}
} else {
chain = createSingleModelChain(modelFromConfig);
}
@@ -96,6 +96,12 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
model: 'gemini-2.5-flash',
},
},
'gemini-3-flash-base': {
extends: 'base',
modelConfig: {
model: 'gemini-3-flash-preview',
},
},
classifier: {
extends: 'base',
modelConfig: {
@@ -151,7 +157,7 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
},
},
'web-search': {
extends: 'gemini-2.5-flash-base',
extends: 'gemini-3-flash-base',
modelConfig: {
generateContentConfig: {
tools: [{ googleSearch: {} }],
@@ -159,7 +165,7 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
},
},
'web-fetch': {
extends: 'gemini-2.5-flash-base',
extends: 'gemini-3-flash-base',
modelConfig: {
generateContentConfig: {
tools: [{ urlContext: {} }],
@@ -168,25 +174,25 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
},
// TODO(joshualitt): During cleanup, make modelConfig optional.
'web-fetch-fallback': {
extends: 'gemini-2.5-flash-base',
extends: 'gemini-3-flash-base',
modelConfig: {},
},
'loop-detection': {
extends: 'gemini-2.5-flash-base',
extends: 'gemini-3-flash-base',
modelConfig: {},
},
'loop-detection-double-check': {
extends: 'base',
modelConfig: {
model: 'gemini-2.5-pro',
model: 'gemini-3-pro-preview',
},
},
'llm-edit-fixer': {
extends: 'gemini-2.5-flash-base',
extends: 'gemini-3-flash-base',
modelConfig: {},
},
'next-speaker-checker': {
extends: 'gemini-2.5-flash-base',
extends: 'gemini-3-flash-base',
modelConfig: {},
},
'chat-compression-3-pro': {
@@ -216,7 +222,7 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
},
'chat-compression-default': {
modelConfig: {
model: 'gemini-2.5-pro',
model: 'gemini-3-pro-preview',
},
},
},
@@ -519,6 +519,10 @@ exports[`Core System Prompt (prompts.ts) > should append userMemory with separat
- **Credential Protection:** Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \`.env\` files, \`.git\`, and system configuration folders.
- **Source Control:** Do not stage or commit changes unless specifically requested by the user.
## Context Efficiency:
- Always scope and limit your searches to avoid context window exhaustion and ensure high-signal results. Use include to target relevant files and strictly limit results using total_max_matches and max_matches_per_file, especially during the research phase.
- For broad discovery, use names_only=true or max_matches_per_file=1 to identify files without retrieving their context.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
@@ -646,6 +650,10 @@ exports[`Core System Prompt (prompts.ts) > should handle CodebaseInvestigator wi
- **Credential Protection:** Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \`.env\` files, \`.git\`, and system configuration folders.
- **Source Control:** Do not stage or commit changes unless specifically requested by the user.
## Context Efficiency:
- Always scope and limit your searches to avoid context window exhaustion and ensure high-signal results. Use include to target relevant files and strictly limit results using total_max_matches and max_matches_per_file, especially during the research phase.
- For broad discovery, use names_only=true or max_matches_per_file=1 to identify files without retrieving their context.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
@@ -738,6 +746,10 @@ exports[`Core System Prompt (prompts.ts) > should handle CodebaseInvestigator wi
- **Credential Protection:** Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \`.env\` files, \`.git\`, and system configuration folders.
- **Source Control:** Do not stage or commit changes unless specifically requested by the user.
## Context Efficiency:
- Always scope and limit your searches to avoid context window exhaustion and ensure high-signal results. Use include to target relevant files and strictly limit results using total_max_matches and max_matches_per_file, especially during the research phase.
- For broad discovery, use names_only=true or max_matches_per_file=1 to identify files without retrieving their context.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
@@ -1299,6 +1311,10 @@ exports[`Core System Prompt (prompts.ts) > should include available_skills with
- **Credential Protection:** Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \`.env\` files, \`.git\`, and system configuration folders.
- **Source Control:** Do not stage or commit changes unless specifically requested by the user.
## Context Efficiency:
- Always scope and limit your searches to avoid context window exhaustion and ensure high-signal results. Use include to target relevant files and strictly limit results using total_max_matches and max_matches_per_file, especially during the research phase.
- For broad discovery, use names_only=true or max_matches_per_file=1 to identify files without retrieving their context.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
@@ -1422,6 +1438,10 @@ exports[`Core System Prompt (prompts.ts) > should include correct sandbox instru
- **Credential Protection:** Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \`.env\` files, \`.git\`, and system configuration folders.
- **Source Control:** Do not stage or commit changes unless specifically requested by the user.
## Context Efficiency:
- Always scope and limit your searches to avoid context window exhaustion and ensure high-signal results. Use include to target relevant files and strictly limit results using total_max_matches and max_matches_per_file, especially during the research phase.
- For broad discovery, use names_only=true or max_matches_per_file=1 to identify files without retrieving their context.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
@@ -1536,6 +1556,10 @@ exports[`Core System Prompt (prompts.ts) > should include correct sandbox instru
- **Credential Protection:** Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \`.env\` files, \`.git\`, and system configuration folders.
- **Source Control:** Do not stage or commit changes unless specifically requested by the user.
## Context Efficiency:
- Always scope and limit your searches to avoid context window exhaustion and ensure high-signal results. Use include to target relevant files and strictly limit results using total_max_matches and max_matches_per_file, especially during the research phase.
- For broad discovery, use names_only=true or max_matches_per_file=1 to identify files without retrieving their context.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
@@ -1650,6 +1674,10 @@ exports[`Core System Prompt (prompts.ts) > should include correct sandbox instru
- **Credential Protection:** Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \`.env\` files, \`.git\`, and system configuration folders.
- **Source Control:** Do not stage or commit changes unless specifically requested by the user.
## Context Efficiency:
- Always scope and limit your searches to avoid context window exhaustion and ensure high-signal results. Use include to target relevant files and strictly limit results using total_max_matches and max_matches_per_file, especially during the research phase.
- For broad discovery, use names_only=true or max_matches_per_file=1 to identify files without retrieving their context.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
@@ -1760,6 +1788,10 @@ exports[`Core System Prompt (prompts.ts) > should include planning phase suggest
- **Credential Protection:** Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \`.env\` files, \`.git\`, and system configuration folders.
- **Source Control:** Do not stage or commit changes unless specifically requested by the user.
## Context Efficiency:
- Always scope and limit your searches to avoid context window exhaustion and ensure high-signal results. Use include to target relevant files and strictly limit results using total_max_matches and max_matches_per_file, especially during the research phase.
- For broad discovery, use names_only=true or max_matches_per_file=1 to identify files without retrieving their context.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
@@ -1870,6 +1902,10 @@ exports[`Core System Prompt (prompts.ts) > should include sub-agents in XML for
- **Credential Protection:** Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \`.env\` files, \`.git\`, and system configuration folders.
- **Source Control:** Do not stage or commit changes unless specifically requested by the user.
## Context Efficiency:
- Always scope and limit your searches to avoid context window exhaustion and ensure high-signal results. Use include to target relevant files and strictly limit results using total_max_matches and max_matches_per_file, especially during the research phase.
- For broad discovery, use names_only=true or max_matches_per_file=1 to identify files without retrieving their context.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
@@ -2219,6 +2255,10 @@ exports[`Core System Prompt (prompts.ts) > should return the base prompt when us
- **Credential Protection:** Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \`.env\` files, \`.git\`, and system configuration folders.
- **Source Control:** Do not stage or commit changes unless specifically requested by the user.
## Context Efficiency:
- Always scope and limit your searches to avoid context window exhaustion and ensure high-signal results. Use include to target relevant files and strictly limit results using total_max_matches and max_matches_per_file, especially during the research phase.
- For broad discovery, use names_only=true or max_matches_per_file=1 to identify files without retrieving their context.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
@@ -2329,6 +2369,10 @@ exports[`Core System Prompt (prompts.ts) > should return the base prompt when us
- **Credential Protection:** Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \`.env\` files, \`.git\`, and system configuration folders.
- **Source Control:** Do not stage or commit changes unless specifically requested by the user.
## Context Efficiency:
- Always scope and limit your searches to avoid context window exhaustion and ensure high-signal results. Use include to target relevant files and strictly limit results using total_max_matches and max_matches_per_file, especially during the research phase.
- For broad discovery, use names_only=true or max_matches_per_file=1 to identify files without retrieving their context.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
@@ -2550,6 +2594,10 @@ exports[`Core System Prompt (prompts.ts) > should use chatty system prompt for p
- **Credential Protection:** Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \`.env\` files, \`.git\`, and system configuration folders.
- **Source Control:** Do not stage or commit changes unless specifically requested by the user.
## Context Efficiency:
- Always scope and limit your searches to avoid context window exhaustion and ensure high-signal results. Use include to target relevant files and strictly limit results using total_max_matches and max_matches_per_file, especially during the research phase.
- For broad discovery, use names_only=true or max_matches_per_file=1 to identify files without retrieving their context.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
@@ -2660,6 +2708,10 @@ exports[`Core System Prompt (prompts.ts) > should use chatty system prompt for p
- **Credential Protection:** Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \`.env\` files, \`.git\`, and system configuration folders.
- **Source Control:** Do not stage or commit changes unless specifically requested by the user.
## Context Efficiency:
- Always scope and limit your searches to avoid context window exhaustion and ensure high-signal results. Use include to target relevant files and strictly limit results using total_max_matches and max_matches_per_file, especially during the research phase.
- For broad discovery, use names_only=true or max_matches_per_file=1 to identify files without retrieving their context.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
+4
View File
@@ -164,6 +164,10 @@ export function renderCoreMandates(options?: CoreMandatesOptions): string {
- **Credential Protection:** Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \`.env\` files, \`.git\`, and system configuration folders.
- **Source Control:** Do not stage or commit changes unless specifically requested by the user.
## Context Efficiency:
- Always scope and limit your searches to avoid context window exhaustion and ensure high-signal results. Use include to target relevant files and strictly limit results using total_max_matches and max_matches_per_file, especially during the research phase.
- For broad discovery, use names_only=true or max_matches_per_file=1 to identify files without retrieving their context.
## Engineering Standards
- **Contextual Precedence:** Instructions found in ${formattedFilenames} files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
@@ -46,9 +46,6 @@ describe('sanitizeEnvironment', () => {
CLIENT_ID: 'sensitive-id',
DB_URI: 'sensitive-uri',
DATABASE_URL: 'sensitive-url',
GEMINI_API_KEY: 'sensitive-gemini-key',
GOOGLE_API_KEY: 'sensitive-google-key',
GOOGLE_APPLICATION_CREDENTIALS: '/path/to/creds.json',
SAFE_VAR: 'is-safe',
};
const sanitized = sanitizeEnvironment(env, EMPTY_OPTIONS);
@@ -103,9 +103,6 @@ export const NEVER_ALLOWED_ENVIRONMENT_VARIABLES: ReadonlySet<string> = new Set(
'GOOGLE_CLOUD_PROJECT',
'GOOGLE_CLOUD_ACCOUNT',
'FIREBASE_PROJECT_ID',
'GEMINI_API_KEY',
'GOOGLE_API_KEY',
'GOOGLE_APPLICATION_CREDENTIALS',
],
);
@@ -104,6 +104,13 @@
"topP": 1
}
},
"gemini-3-flash-base": {
"model": "gemini-3-flash-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1
}
},
"classifier": {
"model": "gemini-2.5-flash-lite",
"generateContentConfig": {
@@ -153,7 +160,7 @@
}
},
"web-search": {
"model": "gemini-2.5-flash",
"model": "gemini-3-flash-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1,
@@ -165,7 +172,7 @@
}
},
"web-fetch": {
"model": "gemini-2.5-flash",
"model": "gemini-3-flash-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1,
@@ -177,35 +184,35 @@
}
},
"web-fetch-fallback": {
"model": "gemini-2.5-flash",
"model": "gemini-3-flash-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1
}
},
"loop-detection": {
"model": "gemini-2.5-flash",
"model": "gemini-3-flash-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1
}
},
"loop-detection-double-check": {
"model": "gemini-2.5-pro",
"model": "gemini-3-pro-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1
}
},
"llm-edit-fixer": {
"model": "gemini-2.5-flash",
"model": "gemini-3-flash-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1
}
},
"next-speaker-checker": {
"model": "gemini-2.5-flash",
"model": "gemini-3-flash-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1
@@ -232,7 +239,7 @@
"generateContentConfig": {}
},
"chat-compression-default": {
"model": "gemini-2.5-pro",
"model": "gemini-3-pro-preview",
"generateContentConfig": {}
}
}
@@ -104,6 +104,13 @@
"topP": 1
}
},
"gemini-3-flash-base": {
"model": "gemini-3-flash-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1
}
},
"classifier": {
"model": "gemini-2.5-flash-lite",
"generateContentConfig": {
@@ -153,7 +160,7 @@
}
},
"web-search": {
"model": "gemini-2.5-flash",
"model": "gemini-3-flash-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1,
@@ -165,7 +172,7 @@
}
},
"web-fetch": {
"model": "gemini-2.5-flash",
"model": "gemini-3-flash-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1,
@@ -177,35 +184,35 @@
}
},
"web-fetch-fallback": {
"model": "gemini-2.5-flash",
"model": "gemini-3-flash-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1
}
},
"loop-detection": {
"model": "gemini-2.5-flash",
"model": "gemini-3-flash-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1
}
},
"loop-detection-double-check": {
"model": "gemini-2.5-pro",
"model": "gemini-3-pro-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1
}
},
"llm-edit-fixer": {
"model": "gemini-2.5-flash",
"model": "gemini-3-flash-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1
}
},
"next-speaker-checker": {
"model": "gemini-2.5-flash",
"model": "gemini-3-flash-preview",
"generateContentConfig": {
"temperature": 0,
"topP": 1
@@ -232,7 +239,7 @@
"generateContentConfig": {}
},
"chat-compression-default": {
"model": "gemini-2.5-pro",
"model": "gemini-3-pro-preview",
"generateContentConfig": {}
}
}
@@ -45,6 +45,10 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps
"description": "Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.",
"type": "string",
},
"exclude_pattern": {
"description": "Optional: A regular expression pattern to exclude from the search results. If a line matches both the pattern and the exclude_pattern, it will be omitted.",
"type": "string",
},
"include": {
"description": "Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores).",
"type": "string",
@@ -54,6 +58,10 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps
"minimum": 1,
"type": "integer",
},
"names_only": {
"description": "Optional: If true, only the file paths of the matches will be returned, without the line content or line numbers. This is useful for gathering a list of files.",
"type": "boolean",
},
"pattern": {
"description": "The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*').",
"type": "string",
@@ -254,6 +262,10 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview >
"description": "Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.",
"type": "string",
},
"exclude_pattern": {
"description": "Optional: A regular expression pattern to exclude from the search results. If a line matches both the pattern and the exclude_pattern, it will be omitted.",
"type": "string",
},
"include": {
"description": "Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores).",
"type": "string",
@@ -263,6 +275,10 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview >
"minimum": 1,
"type": "integer",
},
"names_only": {
"description": "Optional: If true, only the file paths of the matches will be returned, without the line content or line numbers. This is useful for gathering a list of files.",
"type": "boolean",
},
"pattern": {
"description": "The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*').",
"type": "string",
@@ -98,6 +98,16 @@ export const GREP_DEFINITION: ToolDefinition = {
description: `Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores).`,
type: 'string',
},
exclude_pattern: {
description:
'Optional: A regular expression pattern to exclude from the search results. If a line matches both the pattern and the exclude_pattern, it will be omitted.',
type: 'string',
},
names_only: {
description:
'Optional: If true, only the file paths of the matches will be returned, without the line content or line numbers. This is useful for gathering a list of files.',
type: 'boolean',
},
max_matches_per_file: {
description:
'Optional: Maximum number of matches to return per file. Use this to prevent being overwhelmed by repetitive matches in large files.',
+35
View File
@@ -498,6 +498,41 @@ describe('GrepTool', () => {
expect(result.llmContent).toContain('File: sub/fileC.txt');
expect(result.llmContent).toContain('L1: another world in sub dir');
});
it('should return only file paths when names_only is true', async () => {
const params: GrepToolParams = {
pattern: 'world',
names_only: true,
};
const invocation = grepTool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('Found 2 files with matches');
expect(result.llmContent).toContain('fileA.txt');
expect(result.llmContent).toContain('sub/fileC.txt');
expect(result.llmContent).not.toContain('L1:');
expect(result.llmContent).not.toContain('hello world');
});
it('should filter out matches based on exclude_pattern', async () => {
await fs.writeFile(
path.join(tempRootDir, 'copyright.txt'),
'Copyright 2025 Google LLC\nCopyright 2026 Google LLC',
);
const params: GrepToolParams = {
pattern: 'Copyright .* Google LLC',
exclude_pattern: '2026',
dir_path: '.',
};
const invocation = grepTool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('Found 1 match');
expect(result.llmContent).toContain('copyright.txt');
expect(result.llmContent).toContain('Copyright 2025 Google LLC');
expect(result.llmContent).not.toContain('Copyright 2026 Google LLC');
});
});
describe('getDescription', () => {
+45
View File
@@ -49,6 +49,16 @@ export interface GrepToolParams {
*/
include?: string;
/**
* Optional: A regular expression pattern to exclude from the search results.
*/
exclude_pattern?: string;
/**
* Optional: If true, only the file paths of the matches will be returned.
*/
names_only?: boolean;
/**
* Optional: Maximum number of matches to return per file. Use this to prevent being overwhelmed by repetitive matches in large files.
*/
@@ -225,6 +235,7 @@ class GrepToolInvocation extends BaseToolInvocation<
pattern: this.params.pattern,
path: searchDir,
include: this.params.include,
exclude_pattern: this.params.exclude_pattern,
maxMatches: remainingLimit,
max_matches_per_file: this.params.max_matches_per_file,
signal: timeoutController.signal,
@@ -280,6 +291,16 @@ class GrepToolInvocation extends BaseToolInvocation<
const matchCount = allMatches.length;
const matchTerm = matchCount === 1 ? 'match' : 'matches';
if (this.params.names_only) {
const filePaths = Object.keys(matchesByFile).sort();
let llmContent = `Found ${filePaths.length} files with matches for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}${wasTruncated ? ` (results limited to ${totalMaxMatches} matches for performance)` : ''}:\n`;
llmContent += filePaths.join('\n');
return {
llmContent: llmContent.trim(),
returnDisplay: `Found ${filePaths.length} files${wasTruncated ? ' (limited)' : ''}`,
};
}
let llmContent = `Found ${matchCount} ${matchTerm} for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}`;
if (wasTruncated) {
@@ -354,6 +375,7 @@ class GrepToolInvocation extends BaseToolInvocation<
pattern: string;
path: string; // Expects absolute path
include?: string;
exclude_pattern?: string;
maxMatches: number;
max_matches_per_file?: number;
signal: AbortSignal;
@@ -362,12 +384,18 @@ class GrepToolInvocation extends BaseToolInvocation<
pattern,
path: absolutePath,
include,
exclude_pattern,
maxMatches,
max_matches_per_file,
} = options;
let strategyUsed = 'none';
try {
let excludeRegex: RegExp | null = null;
if (exclude_pattern) {
excludeRegex = new RegExp(exclude_pattern, 'i');
}
// --- Strategy 1: git grep ---
const isGit = isGitRepository(absolutePath);
const gitAvailable = isGit && (await this.isCommandAvailable('git'));
@@ -400,6 +428,9 @@ class GrepToolInvocation extends BaseToolInvocation<
for await (const line of generator) {
const match = this.parseGrepLine(line, absolutePath);
if (match) {
if (excludeRegex && excludeRegex.test(match.line)) {
continue;
}
results.push(match);
if (results.length >= maxMatches) {
break;
@@ -467,6 +498,9 @@ class GrepToolInvocation extends BaseToolInvocation<
for await (const line of generator) {
const match = this.parseGrepLine(line, absolutePath);
if (match) {
if (excludeRegex && excludeRegex.test(match.line)) {
continue;
}
results.push(match);
if (results.length >= maxMatches) {
break;
@@ -528,6 +562,9 @@ class GrepToolInvocation extends BaseToolInvocation<
for (let index = 0; index < lines.length; index++) {
const line = lines[index];
if (regex.test(line)) {
if (excludeRegex && excludeRegex.test(line)) {
continue;
}
allMatches.push({
filePath:
path.relative(absolutePath, fileAbsolutePath) ||
@@ -637,6 +674,14 @@ export class GrepTool extends BaseDeclarativeTool<GrepToolParams, ToolResult> {
return `Invalid regular expression pattern provided: ${params.pattern}. Error: ${getErrorMessage(error)}`;
}
if (params.exclude_pattern) {
try {
new RegExp(params.exclude_pattern);
} catch (error) {
return `Invalid exclude regular expression pattern provided: ${params.exclude_pattern}. Error: ${getErrorMessage(error)}`;
}
}
if (
params.max_matches_per_file !== undefined &&
params.max_matches_per_file < 1
+2 -71
View File
@@ -1623,7 +1623,7 @@ describe('mcp-client', () => {
{
command: 'test-command',
args: ['--foo', 'bar'],
env: { GEMINI_CLI_FOO: 'bar' },
env: { FOO: 'bar' },
cwd: 'test/cwd',
},
false,
@@ -1634,80 +1634,11 @@ describe('mcp-client', () => {
command: 'test-command',
args: ['--foo', 'bar'],
cwd: 'test/cwd',
env: expect.objectContaining({ GEMINI_CLI_FOO: 'bar' }),
env: expect.objectContaining({ FOO: 'bar' }),
stderr: 'pipe',
});
});
it('should redact sensitive environment variables for command transport', async () => {
const mockedTransport = vi
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
.mockReturnValue({} as SdkClientStdioLib.StdioClientTransport);
const originalEnv = process.env;
process.env = {
...originalEnv,
GEMINI_API_KEY: 'sensitive-key',
GEMINI_CLI_SAFE_VAR: 'safe-value',
};
// Ensure strict sanitization is not triggered for this test
delete process.env['GITHUB_SHA'];
delete process.env['SURFACE'];
try {
await createTransport(
'test-server',
{
command: 'test-command',
},
false,
EMPTY_CONFIG,
);
const callArgs = mockedTransport.mock.calls[0][0];
expect(callArgs.env).toBeDefined();
expect(callArgs.env!['GEMINI_CLI_SAFE_VAR']).toBe('safe-value');
expect(callArgs.env!['GEMINI_API_KEY']).toBeUndefined();
} finally {
process.env = originalEnv;
}
});
it('should include extension settings in environment', async () => {
const mockedTransport = vi
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
.mockReturnValue({} as SdkClientStdioLib.StdioClientTransport);
await createTransport(
'test-server',
{
command: 'test-command',
extension: {
name: 'test-ext',
resolvedSettings: [
{
envVar: 'GEMINI_CLI_EXT_VAR',
value: 'ext-value',
sensitive: false,
name: 'ext-setting',
},
],
version: '',
isActive: false,
path: '',
contextFiles: [],
id: '',
},
},
false,
EMPTY_CONFIG,
);
const callArgs = mockedTransport.mock.calls[0][0];
expect(callArgs.env).toBeDefined();
expect(callArgs.env!['GEMINI_CLI_EXT_VAR']).toBe('ext-value');
});
it('should exclude extension settings with undefined values from environment', async () => {
const mockedTransport = vi
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
+5 -36
View File
@@ -34,11 +34,7 @@ import {
} from '@modelcontextprotocol/sdk/types.js';
import { ApprovalMode, PolicyDecision } from '../policy/types.js';
import { parse } from 'shell-quote';
import type {
Config,
GeminiCLIExtension,
MCPServerConfig,
} from '../config/config.js';
import type { Config, MCPServerConfig } from '../config/config.js';
import { AuthProviderType } from '../config/config.js';
import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js';
import { ServiceAccountImpersonationProvider } from '../mcp/sa-impersonation-provider.js';
@@ -1902,23 +1898,10 @@ export async function createTransport(
command: mcpServerConfig.command,
args: mcpServerConfig.args || [],
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
env: sanitizeEnvironment(
{
...process.env,
...getExtensionEnvironment(mcpServerConfig.extension),
...(mcpServerConfig.env || {}),
},
{
...sanitizationConfig,
allowedEnvironmentVariables: [
...(sanitizationConfig.allowedEnvironmentVariables ?? []),
...(mcpServerConfig.extension?.resolvedSettings?.map(
(s) => s.envVar,
) ?? []),
],
enableEnvironmentVariableRedaction: true,
},
) as Record<string, string>,
env: {
...sanitizeEnvironment(process.env, sanitizationConfig),
...(mcpServerConfig.env || {}),
} as Record<string, string>,
cwd: mcpServerConfig.cwd,
stderr: 'pipe',
});
@@ -1993,17 +1976,3 @@ export function isEnabled(
)
);
}
function getExtensionEnvironment(
extension?: GeminiCLIExtension,
): Record<string, string> {
const env: Record<string, string> = {};
if (extension?.resolvedSettings) {
for (const setting of extension.resolvedSettings) {
if (setting.value) {
env[setting.envVar] = setting.value;
}
}
}
return env;
}
+79
View File
@@ -1930,6 +1930,85 @@ describe('RipGrepTool', () => {
expect(result.llmContent).not.toContain('L3: match 3');
expect(result.returnDisplay).toBe('Found 2 matches (limited)');
});
it('should return only file paths when names_only is true', async () => {
mockSpawn.mockImplementationOnce(
createMockSpawn({
outputData:
JSON.stringify({
type: 'match',
data: {
path: { text: 'fileA.txt' },
line_number: 1,
lines: { text: 'hello world\n' },
},
}) +
'\n' +
JSON.stringify({
type: 'match',
data: {
path: { text: 'fileB.txt' },
line_number: 5,
lines: { text: 'hello again\n' },
},
}) +
'\n',
exitCode: 0,
}),
);
const params: RipGrepToolParams = {
pattern: 'hello',
names_only: true,
};
const invocation = grepTool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('Found 2 files with matches');
expect(result.llmContent).toContain('fileA.txt');
expect(result.llmContent).toContain('fileB.txt');
expect(result.llmContent).not.toContain('L1:');
expect(result.llmContent).not.toContain('hello world');
});
it('should filter out matches based on exclude_pattern', async () => {
mockSpawn.mockImplementationOnce(
createMockSpawn({
outputData:
JSON.stringify({
type: 'match',
data: {
path: { text: 'fileA.txt' },
line_number: 1,
lines: { text: 'Copyright 2025 Google LLC\n' },
},
}) +
'\n' +
JSON.stringify({
type: 'match',
data: {
path: { text: 'fileB.txt' },
line_number: 1,
lines: { text: 'Copyright 2026 Google LLC\n' },
},
}) +
'\n',
exitCode: 0,
}),
);
const params: RipGrepToolParams = {
pattern: 'Copyright .* Google LLC',
exclude_pattern: '2026',
};
const invocation = grepTool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('Found 1 match');
expect(result.llmContent).toContain('fileA.txt');
expect(result.llmContent).not.toContain('fileB.txt');
expect(result.llmContent).toContain('Copyright 2025 Google LLC');
});
});
});
+53 -1
View File
@@ -102,6 +102,16 @@ export interface RipGrepToolParams {
*/
include?: string;
/**
* Optional: A regular expression pattern to exclude from the search results.
*/
exclude_pattern?: string;
/**
* Optional: If true, only the file paths of the matches will be returned.
*/
names_only?: boolean;
/**
* If true, searches case-sensitively. Defaults to false.
*/
@@ -244,6 +254,7 @@ class GrepToolInvocation extends BaseToolInvocation<
pattern: this.params.pattern,
path: searchDirAbs,
include: this.params.include,
exclude_pattern: this.params.exclude_pattern,
case_sensitive: this.params.case_sensitive,
fixed_strings: this.params.fixed_strings,
context: this.params.context,
@@ -299,6 +310,16 @@ class GrepToolInvocation extends BaseToolInvocation<
const wasTruncated = matchCount >= totalMaxMatches;
if (this.params.names_only) {
const filePaths = Object.keys(matchesByFile).sort();
let llmContent = `Found ${filePaths.length} files with matches for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}${wasTruncated ? ` (results limited to ${totalMaxMatches} matches for performance)` : ''}:\n`;
llmContent += filePaths.join('\n');
return {
llmContent: llmContent.trim(),
returnDisplay: `Found ${filePaths.length} files${wasTruncated ? ' (limited)' : ''}`,
};
}
let llmContent = `Found ${matchCount} ${matchTerm} for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}${wasTruncated ? ` (results limited to ${totalMaxMatches} matches for performance)` : ''}:\n---\n`;
for (const filePath in matchesByFile) {
@@ -330,6 +351,7 @@ class GrepToolInvocation extends BaseToolInvocation<
pattern: string;
path: string;
include?: string;
exclude_pattern?: string;
case_sensitive?: boolean;
fixed_strings?: boolean;
context?: number;
@@ -344,6 +366,7 @@ class GrepToolInvocation extends BaseToolInvocation<
pattern,
path: absolutePath,
include,
exclude_pattern,
case_sensitive,
fixed_strings,
context,
@@ -423,9 +446,18 @@ class GrepToolInvocation extends BaseToolInvocation<
});
let matchesFound = 0;
let excludeRegex: RegExp | null = null;
if (exclude_pattern) {
excludeRegex = new RegExp(exclude_pattern, case_sensitive ? '' : 'i');
}
for await (const line of generator) {
const match = this.parseRipgrepJsonLine(line, absolutePath);
if (match) {
if (excludeRegex && excludeRegex.test(match.line)) {
continue;
}
results.push(match);
if (!match.isContext) {
matchesFound++;
@@ -527,7 +559,7 @@ export class RipGrepTool extends BaseDeclarativeTool<
super(
RipGrepTool.Name,
'SearchText',
'Searches for a regular expression pattern within file contents. Max 100 matches.',
'Searches for a regular expression pattern within file contents.',
Kind.Search,
{
properties: {
@@ -546,6 +578,16 @@ export class RipGrepTool extends BaseDeclarativeTool<
"Glob pattern to filter files (e.g., '*.ts', 'src/**'). Recommended for large repositories to reduce noise. Defaults to all files if omitted.",
type: 'string',
},
exclude_pattern: {
description:
'Optional: A regular expression pattern to exclude from the search results. If a line matches both the pattern and the exclude_pattern, it will be omitted.',
type: 'string',
},
names_only: {
description:
'Optional: If true, only the file paths of the matches will be returned, without the line content or line numbers. This is useful for gathering a list of files.',
type: 'boolean',
},
case_sensitive: {
description:
'If true, search is case-sensitive. Defaults to false (ignore case) if omitted.',
@@ -565,11 +607,13 @@ export class RipGrepTool extends BaseDeclarativeTool<
description:
'Show this many lines after each match (equivalent to grep -A). Defaults to 0 if omitted.',
type: 'integer',
minimum: 0,
},
before: {
description:
'Show this many lines before each match (equivalent to grep -B). Defaults to 0 if omitted.',
type: 'integer',
minimum: 0,
},
no_ignore: {
description:
@@ -618,6 +662,14 @@ export class RipGrepTool extends BaseDeclarativeTool<
}
}
if (params.exclude_pattern) {
try {
new RegExp(params.exclude_pattern);
} catch (error) {
return `Invalid exclude regular expression pattern provided: ${params.exclude_pattern}. Error: ${getErrorMessage(error)}`;
}
}
if (
params.max_matches_per_file !== undefined &&
params.max_matches_per_file < 1
File diff suppressed because one or more lines are too long
+19 -15
View File
@@ -12,6 +12,7 @@ import { execSync } from 'node:child_process';
import os from 'node:os';
const artifactsDir = process.argv[2] || '.';
const outputFile = process.argv[3];
const MAX_HISTORY = 10;
// Find all report.json files recursively
@@ -145,10 +146,8 @@ function fetchHistoricalData() {
}
function generateMarkdown(currentStatsByModel, history) {
console.log('### Evals Nightly Summary\n');
console.log(
'See [evals/README.md](https://github.com/google-gemini/gemini-cli/tree/main/evals) for more details.\n',
);
let md = '### Evals Nightly Summary\n\n';
md += 'See [evals/README.md](https://github.com/google-gemini/gemini-cli/tree/main/evals) for more details.\n\n';
// Reverse history to show oldest first
const reversedHistory = [...history].reverse();
@@ -171,8 +170,8 @@ function generateMarkdown(currentStatsByModel, history) {
? ((totalStats.passed / totalStats.total) * 100).toFixed(1) + '%'
: 'N/A';
console.log(`#### Model: ${model}`);
console.log(`**Total Pass Rate: ${totalPassRate}**\n`);
md += `#### Model: ${model}\n`;
md += `**Total Pass Rate: ${totalPassRate}**\n\n`;
// Header
let header = '| Test Name |';
@@ -187,8 +186,8 @@ function generateMarkdown(currentStatsByModel, history) {
header += ' Current |';
separator += ' :---: |';
console.log(header);
console.log(separator);
md += header + '\n';
md += separator + '\n';
// Collect all test names for this model
const allTestNames = new Set(Object.keys(currentStats));
@@ -224,23 +223,28 @@ function generateMarkdown(currentStatsByModel, history) {
row += ' - |';
}
console.log(row);
md += row + '\n';
}
console.log('\n');
md += '\n';
}
return md;
}
// --- Main ---
const currentReports = findReports(artifactsDir);
if (currentReports.length === 0) {
console.log('No reports found.');
// We don't exit here because we might still want to see history if available,
// but practically if current has no reports, something is wrong.
// Sticking to original behavior roughly, but maybe we can continue.
console.error('No reports found.');
process.exit(0);
}
const currentStats = getStats(currentReports);
const history = fetchHistoricalData();
generateMarkdown(currentStats, history);
const markdown = generateMarkdown(currentStats, history);
if (outputFile) {
fs.writeFileSync(outputFile, markdown);
console.log(`Summary written to ${outputFile}`);
} else {
console.log(markdown);
}
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env node
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import path from 'node:path';
import { execSync } from 'node:child_process';
const models = [
'gemini-3-pro-preview',
'gemini-3-flash-preview',
'gemini-2.5-pro',
'gemini-2.5-flash',
'gemini-2.5-flash-lite',
];
const artifactsDir = path.resolve('artifacts');
const logsDir = path.resolve('evals/logs');
// Parse arguments
const args = process.argv.slice(2);
let testPattern = '';
let attempts = 1;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--attempts') {
attempts = parseInt(args[i + 1], 10);
i++;
} else if (!args[i].startsWith('-')) {
testPattern = args[i];
}
}
// Ensure GEMINI_API_KEY is set
if (!process.env.GEMINI_API_KEY) {
console.error('Error: GEMINI_API_KEY environment variable is not set.');
process.exit(1);
}
// Prepare artifacts directory
if (fs.existsSync(artifactsDir)) {
console.log(`Cleaning artifacts directory: ${artifactsDir}`);
fs.rmSync(artifactsDir, { recursive: true, force: true });
}
fs.mkdirSync(artifactsDir);
// Build project
console.log('Building project...');
try {
execSync('npm run build', { stdio: 'inherit' });
} catch (e) {
console.error('Build failed.');
process.exit(1);
}
console.log(`
Starting evals with ${attempts} attempt(s) per model.`);
for (const model of models) {
for (let attempt = 1; attempt <= attempts; attempt++) {
console.log(`
--------------------------------------------------`);
console.log(`Running evals for ${model} (Attempt ${attempt}/${attempts})`);
console.log(`--------------------------------------------------
`);
// Clean logs directory for this run
if (fs.existsSync(logsDir)) {
fs.rmSync(logsDir, { recursive: true, force: true });
}
fs.mkdirSync(logsDir, { recursive: true });
try {
// Construct command
let cmd = 'npm run test:all_evals';
if (testPattern) {
if (
testPattern.endsWith('.ts') ||
testPattern.endsWith('.js') ||
testPattern.includes('/')
) {
cmd += ` -- "${testPattern}"`;
} else {
cmd += ` -- -t "${testPattern}"`;
}
}
// Run evals
execSync(cmd, {
stdio: 'inherit',
env: {
...process.env,
GEMINI_MODEL: model,
RUN_EVALS: 'true',
},
});
} catch (e) {
console.log(
`
Evals for ${model} (Attempt ${attempt}) finished with failures.`,
);
}
// Copy logs to artifacts
// Format: eval-logs-<model>-<attempt>
const artifactName = `eval-logs-${model}-${attempt}`;
const artifactPath = path.join(artifactsDir, artifactName);
// Ensure parent dir exists (though artifactsDir should exist)
if (fs.existsSync(logsDir)) {
console.log(`Copying logs to ${artifactPath}`);
fs.cpSync(logsDir, artifactPath, { recursive: true });
} else {
console.error(`Warning: No logs found in ${logsDir}`);
}
}
}
console.log('\n--------------------------------------------------');
console.log('Aggregating results...');
console.log('--------------------------------------------------\n');
try {
const summaryFile = 'local_evals_summary.md';
execSync(`node scripts/aggregate_evals.js "${artifactsDir}" "${summaryFile}"`, {
stdio: 'inherit',
env: {
...process.env,
},
});
console.log(`\nSummary written to ${summaryFile}`);
console.log('\nPreview:\n');
console.log(fs.readFileSync(summaryFile, 'utf-8'));
} catch (e) {
console.error('Aggregation failed:', e);
}