diff --git a/docs/cli/cli-reference.md b/docs/cli/cli-reference.md index d1094a15e20..8199445625d 100644 --- a/docs/cli/cli-reference.md +++ b/docs/cli/cli-reference.md @@ -27,29 +27,29 @@ and parameters. ## CLI Options -| Option | Alias | Type | Default | Description | -| -------------------------------- | ----- | ------- | --------- | ---------------------------------------------------------------------------------------------------------- | -| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging | -| `--version` | `-v` | - | - | Show CLI version number and exit | -| `--help` | `-h` | - | - | Show help information | -| `--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. **Deprecated:** Use positional arguments instead. | -| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode | -| `--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. | -| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** | -| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** | -| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) | -| `--allowed-tools` | - | array | - | Tools that are allowed to run without confirmation (comma-separated or multiple flags) | -| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) | -| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit | -| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) | -| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit | -| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) | -| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) | -| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility | -| `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` | +| Option | Alias | Type | Default | Description | +| -------------------------------- | ----- | ------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging | +| `--version` | `-v` | - | - | Show CLI version number and exit | +| `--help` | `-h` | - | - | Show help information | +| `--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. **Deprecated:** Use positional arguments instead. | +| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode | +| `--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. | +| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** | +| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** | +| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) | +| `--allowed-tools` | - | array | - | **Deprecated.** Use the [Policy Engine](../core/policy-engine.md) instead. Tools that are allowed to run without confirmation (comma-separated or multiple flags) | +| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) | +| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit | +| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) | +| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit | +| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) | +| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) | +| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility | +| `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` | ## Model selection diff --git a/docs/cli/enterprise.md b/docs/cli/enterprise.md index f22ec81c37c..861fc68c715 100644 --- a/docs/cli/enterprise.md +++ b/docs/cli/enterprise.md @@ -223,9 +223,9 @@ gemini ## Restricting tool access You can significantly enhance security by controlling which tools the Gemini -model can use. This is achieved through the `tools.core` and `tools.exclude` -settings. For a list of available tools, see the -[Tools documentation](../tools/index.md). +model can use. This is achieved through the `tools.core` setting and the +[Policy Engine](../core/policy-engine.md). For a list of available tools, see +the [Tools documentation](../tools/index.md). ### Allowlisting with `coreTools` @@ -243,7 +243,10 @@ on the approved list. } ``` -### Blocklisting with `excludeTools` +### Blocklisting with `excludeTools` (Deprecated) + +> **Deprecated:** Use the [Policy Engine](../core/policy-engine.md) for more +> robust control. Alternatively, you can add specific tools that are considered dangerous in your environment to a blocklist. diff --git a/docs/get-started/configuration-v1.md b/docs/get-started/configuration-v1.md index 050dce32b6c..cd1325b977f 100644 --- a/docs/get-started/configuration-v1.md +++ b/docs/get-started/configuration-v1.md @@ -166,19 +166,21 @@ a few things you can try in order of recommendation: - **Default:** All tools available for use by the Gemini model. - **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "ShellTool(ls)"]`. -- **`allowedTools`** (array of strings): +- **`allowedTools`** (array of strings) [DEPRECATED]: - **Default:** `undefined` - **Description:** A list of tool names that will bypass the confirmation dialog. This is useful for tools that you trust and use frequently. The - match semantics are the same as `coreTools`. + match semantics are the same as `coreTools`. **Deprecated**: Use the + [Policy Engine](../core/policy-engine.md) instead. - **Example:** `"allowedTools": ["ShellTool(git status)"]`. -- **`excludeTools`** (array of strings): +- **`excludeTools`** (array of strings) [DEPRECATED]: - **Description:** Allows you to specify a list of core tool names that should be excluded from the model. A tool listed in both `excludeTools` and `coreTools` is excluded. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"excludeTools": ["ShellTool(rm -rf)"]` will block the `rm -rf` command. + **Deprecated**: Use the [Policy Engine](../core/policy-engine.md) instead. - **Default**: No tools excluded. - **Example:** `"excludeTools": ["run_shell_command", "findFiles"]`. - **Security Note:** Command-specific restrictions in `excludeTools` for diff --git a/docs/tools/shell.md b/docs/tools/shell.md index 0bb4b682442..48854e82f1e 100644 --- a/docs/tools/shell.md +++ b/docs/tools/shell.md @@ -167,10 +167,11 @@ configuration file. `"tools": {"core": ["run_shell_command(git)"]}` will only allow `git` commands. Including the generic `run_shell_command` acts as a wildcard, allowing any command not explicitly blocked. -- `tools.exclude`: To block specific commands, add entries to the `exclude` list - under the `tools` category in the format `run_shell_command()`. For - example, `"tools": {"exclude": ["run_shell_command(rm)"]}` will block `rm` - commands. +- `tools.exclude` [DEPRECATED]: To block specific commands, use the + [Policy Engine](../core/policy-engine.md). Historically, this setting allowed + adding entries to the `exclude` list under the `tools` category in the format + `run_shell_command()`. For example, + `"tools": {"exclude": ["run_shell_command(rm)"]}` will block `rm` commands. The validation logic is designed to be secure and flexible: diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 87eb1e8fa7d..4dea16df0e8 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -177,7 +177,8 @@ export async function parseArguments( type: 'array', string: true, nargs: 1, - description: 'Tools that are allowed to run without confirmation', + description: + '[DEPRECATED: Use Policy Engine instead See https://geminicli.com/docs/core/policy-engine] Tools that are allowed to run without confirmation', coerce: (tools: string[]) => // Handle comma-separated values tools.flatMap((tool) => tool.split(',').map((t) => t.trim())), diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index a18f3ace378..e138cfe03a0 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -361,6 +361,26 @@ export async function main() { const argv = await parseArguments(settings.merged); parseArgsHandle?.end(); + if ( + (argv.allowedTools && argv.allowedTools.length > 0) || + (settings.merged.tools?.allowed && settings.merged.tools.allowed.length > 0) + ) { + coreEvents.emitFeedback( + 'warning', + 'Warning: --allowed-tools cli argument and tools.allowed in settings.json are deprecated and will be removed in 1.0: Migrate to Policy Engine: https://geminicli.com/docs/core/policy-engine/', + ); + } + + if ( + settings.merged.tools?.exclude && + settings.merged.tools.exclude.length > 0 + ) { + coreEvents.emitFeedback( + 'warning', + 'Warning: tools.exclude in settings.json is deprecated and will be removed in 1.0. Migrate to Policy Engine: https://geminicli.com/docs/core/policy-engine/', + ); + } + if (argv.startupMessages) { argv.startupMessages.forEach((msg) => { coreEvents.emitFeedback('info', msg); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 6d811799bc6..db4085c1fa4 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -383,7 +383,9 @@ export interface ConfigParameters { question?: string; coreTools?: string[]; + /** @deprecated Use Policy Engine instead */ allowedTools?: string[]; + /** @deprecated Use Policy Engine instead */ excludeTools?: string[]; toolDiscoveryCommand?: string; toolCallCommand?: string; @@ -516,7 +518,9 @@ export class Config { private readonly question: string | undefined; private readonly coreTools: string[] | undefined; + /** @deprecated Use Policy Engine instead */ private readonly allowedTools: string[] | undefined; + /** @deprecated Use Policy Engine instead */ private readonly excludeTools: string[] | undefined; private readonly toolDiscoveryCommand: string | undefined; private readonly toolCallCommand: string | undefined; @@ -1487,11 +1491,12 @@ export class Config { /** * All the excluded tools from static configuration, loaded extensions, or - * other sources. + * other sources (like the Policy Engine). * * May change over time. */ getExcludeTools(): Set | undefined { + // Right now this is present for backward compatibility with settings.json exclude const excludeToolsSet = new Set([...(this.excludeTools ?? [])]); for (const extension of this.getExtensionLoader().getExtensions()) { if (!extension.isActive) { @@ -1501,6 +1506,12 @@ export class Config { excludeToolsSet.add(tool); } } + + const policyExclusions = this.policyEngine.getExcludedTools(); + for (const tool of policyExclusions) { + excludeToolsSet.add(tool); + } + return excludeToolsSet; } diff --git a/packages/core/src/policy/policy-engine.test.ts b/packages/core/src/policy/policy-engine.test.ts index 59b0fd8106b..26aecaa1ebf 100644 --- a/packages/core/src/policy/policy-engine.test.ts +++ b/packages/core/src/policy/policy-engine.test.ts @@ -2031,6 +2031,156 @@ describe('PolicyEngine', () => { }); }); + describe('getExcludedTools', () => { + interface TestCase { + name: string; + rules: PolicyRule[]; + approvalMode?: ApprovalMode; + nonInteractive?: boolean; + expected: string[]; + } + + const testCases: TestCase[] = [ + { + name: 'should return empty set when no rules provided', + rules: [], + expected: [], + }, + { + name: 'should include tools with DENY decision', + rules: [ + { toolName: 'tool1', decision: PolicyDecision.DENY }, + { toolName: 'tool2', decision: PolicyDecision.ALLOW }, + ], + expected: ['tool1'], + }, + { + name: 'should respect priority and ignore lower priority rules (DENY wins)', + rules: [ + { toolName: 'tool1', decision: PolicyDecision.DENY, priority: 100 }, + { toolName: 'tool1', decision: PolicyDecision.ALLOW, priority: 10 }, + ], + expected: ['tool1'], + }, + { + name: 'should respect priority and ignore lower priority rules (ALLOW wins)', + rules: [ + { toolName: 'tool1', decision: PolicyDecision.ALLOW, priority: 100 }, + { toolName: 'tool1', decision: PolicyDecision.DENY, priority: 10 }, + ], + expected: [], + }, + { + name: 'should NOT include ASK_USER tools even in non-interactive mode', + rules: [{ toolName: 'tool1', decision: PolicyDecision.ASK_USER }], + nonInteractive: true, + expected: [], + }, + { + name: 'should ignore rules with argsPattern', + rules: [ + { + toolName: 'tool1', + decision: PolicyDecision.DENY, + argsPattern: /something/, + }, + ], + expected: [], + }, + { + name: 'should respect approval mode (PLAN mode)', + rules: [ + { + toolName: 'tool1', + decision: PolicyDecision.DENY, + modes: [ApprovalMode.PLAN], + }, + ], + approvalMode: ApprovalMode.PLAN, + expected: ['tool1'], + }, + { + name: 'should respect approval mode (DEFAULT mode)', + rules: [ + { + toolName: 'tool1', + decision: PolicyDecision.DENY, + modes: [ApprovalMode.PLAN], + }, + ], + approvalMode: ApprovalMode.DEFAULT, + expected: [], + }, + { + name: 'should respect wildcard ALLOW rules (e.g. YOLO mode)', + rules: [ + { + decision: PolicyDecision.ALLOW, + priority: 999, + modes: [ApprovalMode.YOLO], + }, + { + toolName: 'dangerous-tool', + decision: PolicyDecision.DENY, + priority: 10, + }, + ], + approvalMode: ApprovalMode.YOLO, + expected: [], + }, + { + name: 'should respect server wildcard DENY', + rules: [{ toolName: 'server__*', decision: PolicyDecision.DENY }], + expected: ['server__*'], + }, + { + name: 'should expand server wildcard for specific tools if already processed', + rules: [ + { + toolName: 'server__*', + decision: PolicyDecision.DENY, + priority: 100, + }, + { + toolName: 'server__tool1', + decision: PolicyDecision.DENY, + priority: 10, + }, + ], + expected: ['server__*', 'server__tool1'], + }, + { + name: 'should NOT exclude tool if covered by a higher priority wildcard ALLOW', + rules: [ + { + toolName: 'server__*', + decision: PolicyDecision.ALLOW, + priority: 100, + }, + { + toolName: 'server__tool1', + decision: PolicyDecision.DENY, + priority: 10, + }, + ], + expected: [], + }, + ]; + + it.each(testCases)( + '$name', + ({ rules, approvalMode, nonInteractive, expected }) => { + engine = new PolicyEngine({ + rules, + approvalMode: approvalMode ?? ApprovalMode.DEFAULT, + nonInteractive: nonInteractive ?? false, + }); + const excluded = engine.getExcludedTools(); + expect(Array.from(excluded).sort()).toEqual(expected.sort()); + }, + ); + }); + describe('YOLO mode with ask_user tool', () => { it('should return ASK_USER for ask_user tool even in YOLO mode', async () => { const rules: PolicyRule[] = [ diff --git a/packages/core/src/policy/policy-engine.ts b/packages/core/src/policy/policy-engine.ts index 8a643c89304..1fc5e7cde52 100644 --- a/packages/core/src/policy/policy-engine.ts +++ b/packages/core/src/policy/policy-engine.ts @@ -26,6 +26,22 @@ import { } from '../utils/shell-utils.js'; import { getToolAliases } from '../tools/tool-names.js'; +function isWildcardPattern(name: string): boolean { + return name.endsWith('__*'); +} + +function getWildcardPrefix(pattern: string): string { + return pattern.slice(0, -3); +} + +function matchesWildcard(pattern: string, toolName: string): boolean { + if (!isWildcardPattern(pattern)) { + return false; + } + const prefix = getWildcardPrefix(pattern); + return toolName.startsWith(prefix + '__'); +} + function ruleMatches( rule: PolicyRule | SafetyCheckerRule, toolCall: FunctionCall, @@ -43,8 +59,8 @@ function ruleMatches( // Check tool name if specified if (rule.toolName) { // Support wildcard patterns: "serverName__*" matches "serverName__anyTool" - if (rule.toolName.endsWith('__*')) { - const prefix = rule.toolName.slice(0, -3); // Remove "__*" + if (isWildcardPattern(rule.toolName)) { + const prefix = getWildcardPrefix(rule.toolName); if (serverName !== undefined) { // Robust check: if serverName is provided, it MUST match the prefix exactly. // This prevents "malicious-server" from spoofing "trusted-server" by naming itself "trusted-server__malicious". @@ -53,7 +69,7 @@ function ruleMatches( } } // Always verify the prefix, even if serverName matched - if (!toolCall.name || !toolCall.name.startsWith(prefix + '__')) { + if (!toolCall.name || !matchesWildcard(rule.toolName, toolCall.name)) { return false; } } else if (toolCall.name !== rule.toolName) { @@ -509,6 +525,90 @@ export class PolicyEngine { return this.hookCheckers; } + /** + * Get tools that are effectively denied by the current rules. + * This takes into account: + * 1. Global rules (no argsPattern) + * 2. Priority order (higher priority wins) + * 3. Non-interactive mode (ASK_USER becomes DENY) + */ + getExcludedTools(): Set { + const excludedTools = new Set(); + const processedTools = new Set(); + let globalVerdict: PolicyDecision | undefined; + + for (const rule of this.rules) { + // We only care about rules without args pattern for exclusion from the model + if (rule.argsPattern) { + continue; + } + + // Check if rule applies to current approval mode + if (rule.modes && rule.modes.length > 0) { + if (!rule.modes.includes(this.approvalMode)) { + continue; + } + } + + // Handle Global Rules + if (!rule.toolName) { + if (globalVerdict === undefined) { + globalVerdict = rule.decision; + if (globalVerdict !== PolicyDecision.DENY) { + // Global ALLOW/ASK found. + // Since rules are sorted by priority, this overrides any lower-priority rules. + // We can stop processing because nothing else will be excluded. + break; + } + // If Global DENY, we continue to find specific tools to add to excluded set + } + continue; + } + + const toolName = rule.toolName; + + // Check if already processed (exact match) + if (processedTools.has(toolName)) { + continue; + } + + // Check if covered by a processed wildcard + let coveredByWildcard = false; + for (const processed of processedTools) { + if ( + isWildcardPattern(processed) && + matchesWildcard(processed, toolName) + ) { + // It's covered by a higher-priority wildcard rule. + // If that wildcard rule resulted in exclusion, this tool should also be excluded. + if (excludedTools.has(processed)) { + excludedTools.add(toolName); + } + coveredByWildcard = true; + break; + } + } + if (coveredByWildcard) { + continue; + } + + processedTools.add(toolName); + + // Determine decision + let decision: PolicyDecision; + if (globalVerdict !== undefined) { + decision = globalVerdict; + } else { + decision = rule.decision; + } + + if (decision === PolicyDecision.DENY) { + excludedTools.add(toolName); + } + } + return excludedTools; + } + private applyNonInteractiveMode(decision: PolicyDecision): PolicyDecision { // In non-interactive mode, ASK_USER becomes DENY if (this.nonInteractive && decision === PolicyDecision.ASK_USER) {