feat(cli): deprecate --allowed-tools and excludeTools in favor of policy engine (#18508)

This commit is contained in:
Abhijit Balaji
2026-02-11 16:49:48 -08:00
committed by GitHub
parent c370d2397b
commit 0e85e021dc
9 changed files with 327 additions and 39 deletions
+23 -23
View File
@@ -27,29 +27,29 @@ and parameters.
## CLI Options ## CLI Options
| Option | Alias | Type | Default | Description | | Option | Alias | Type | Default | Description |
| -------------------------------- | ----- | ------- | --------- | ---------------------------------------------------------------------------------------------------------- | | -------------------------------- | ----- | ------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging | | `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging |
| `--version` | `-v` | - | - | Show CLI version number and exit | | `--version` | `-v` | - | - | Show CLI version number and exit |
| `--help` | `-h` | - | - | Show help information | | `--help` | `-h` | - | - | Show help information |
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. | | `--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` | `-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 | | `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution | | `--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` | | `--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. | | `--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-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration 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-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) | | `--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) | | `--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 | | `--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`) | | `--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 | | `--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) | | `--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) | | `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) |
| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility | | `--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` | | `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` |
## Model selection ## Model selection
+7 -4
View File
@@ -223,9 +223,9 @@ gemini
## Restricting tool access ## Restricting tool access
You can significantly enhance security by controlling which tools the Gemini You can significantly enhance security by controlling which tools the Gemini
model can use. This is achieved through the `tools.core` and `tools.exclude` model can use. This is achieved through the `tools.core` setting and the
settings. For a list of available tools, see the [Policy Engine](../core/policy-engine.md). For a list of available tools, see
[Tools documentation](../tools/index.md). the [Tools documentation](../tools/index.md).
### Allowlisting with `coreTools` ### 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 Alternatively, you can add specific tools that are considered dangerous in your
environment to a blocklist. environment to a blocklist.
+5 -3
View File
@@ -166,19 +166,21 @@ a few things you can try in order of recommendation:
- **Default:** All tools available for use by the Gemini model. - **Default:** All tools available for use by the Gemini model.
- **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "ShellTool(ls)"]`. - **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "ShellTool(ls)"]`.
- **`allowedTools`** (array of strings): - **`allowedTools`** (array of strings) [DEPRECATED]:
- **Default:** `undefined` - **Default:** `undefined`
- **Description:** A list of tool names that will bypass the confirmation - **Description:** A list of tool names that will bypass the confirmation
dialog. This is useful for tools that you trust and use frequently. The 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)"]`. - **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 - **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 be excluded from the model. A tool listed in both `excludeTools` and
`coreTools` is excluded. You can also specify command-specific restrictions `coreTools` is excluded. You can also specify command-specific restrictions
for tools that support it, like the `ShellTool`. For example, for tools that support it, like the `ShellTool`. For example,
`"excludeTools": ["ShellTool(rm -rf)"]` will block the `rm -rf` command. `"excludeTools": ["ShellTool(rm -rf)"]` will block the `rm -rf` command.
**Deprecated**: Use the [Policy Engine](../core/policy-engine.md) instead.
- **Default**: No tools excluded. - **Default**: No tools excluded.
- **Example:** `"excludeTools": ["run_shell_command", "findFiles"]`. - **Example:** `"excludeTools": ["run_shell_command", "findFiles"]`.
- **Security Note:** Command-specific restrictions in `excludeTools` for - **Security Note:** Command-specific restrictions in `excludeTools` for
+5 -4
View File
@@ -167,10 +167,11 @@ configuration file.
`"tools": {"core": ["run_shell_command(git)"]}` will only allow `git` `"tools": {"core": ["run_shell_command(git)"]}` will only allow `git`
commands. Including the generic `run_shell_command` acts as a wildcard, commands. Including the generic `run_shell_command` acts as a wildcard,
allowing any command not explicitly blocked. allowing any command not explicitly blocked.
- `tools.exclude`: To block specific commands, add entries to the `exclude` list - `tools.exclude` [DEPRECATED]: To block specific commands, use the
under the `tools` category in the format `run_shell_command(<command>)`. For [Policy Engine](../core/policy-engine.md). Historically, this setting allowed
example, `"tools": {"exclude": ["run_shell_command(rm)"]}` will block `rm` adding entries to the `exclude` list under the `tools` category in the format
commands. `run_shell_command(<command>)`. For example,
`"tools": {"exclude": ["run_shell_command(rm)"]}` will block `rm` commands.
The validation logic is designed to be secure and flexible: The validation logic is designed to be secure and flexible:
+2 -1
View File
@@ -177,7 +177,8 @@ export async function parseArguments(
type: 'array', type: 'array',
string: true, string: true,
nargs: 1, 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[]) => coerce: (tools: string[]) =>
// Handle comma-separated values // Handle comma-separated values
tools.flatMap((tool) => tool.split(',').map((t) => t.trim())), tools.flatMap((tool) => tool.split(',').map((t) => t.trim())),
+20
View File
@@ -361,6 +361,26 @@ export async function main() {
const argv = await parseArguments(settings.merged); const argv = await parseArguments(settings.merged);
parseArgsHandle?.end(); 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) { if (argv.startupMessages) {
argv.startupMessages.forEach((msg) => { argv.startupMessages.forEach((msg) => {
coreEvents.emitFeedback('info', msg); coreEvents.emitFeedback('info', msg);
+12 -1
View File
@@ -383,7 +383,9 @@ export interface ConfigParameters {
question?: string; question?: string;
coreTools?: string[]; coreTools?: string[];
/** @deprecated Use Policy Engine instead */
allowedTools?: string[]; allowedTools?: string[];
/** @deprecated Use Policy Engine instead */
excludeTools?: string[]; excludeTools?: string[];
toolDiscoveryCommand?: string; toolDiscoveryCommand?: string;
toolCallCommand?: string; toolCallCommand?: string;
@@ -516,7 +518,9 @@ export class Config {
private readonly question: string | undefined; private readonly question: string | undefined;
private readonly coreTools: string[] | undefined; private readonly coreTools: string[] | undefined;
/** @deprecated Use Policy Engine instead */
private readonly allowedTools: string[] | undefined; private readonly allowedTools: string[] | undefined;
/** @deprecated Use Policy Engine instead */
private readonly excludeTools: string[] | undefined; private readonly excludeTools: string[] | undefined;
private readonly toolDiscoveryCommand: string | undefined; private readonly toolDiscoveryCommand: string | undefined;
private readonly toolCallCommand: string | undefined; private readonly toolCallCommand: string | undefined;
@@ -1487,11 +1491,12 @@ export class Config {
/** /**
* All the excluded tools from static configuration, loaded extensions, or * All the excluded tools from static configuration, loaded extensions, or
* other sources. * other sources (like the Policy Engine).
* *
* May change over time. * May change over time.
*/ */
getExcludeTools(): Set<string> | undefined { getExcludeTools(): Set<string> | undefined {
// Right now this is present for backward compatibility with settings.json exclude
const excludeToolsSet = new Set([...(this.excludeTools ?? [])]); const excludeToolsSet = new Set([...(this.excludeTools ?? [])]);
for (const extension of this.getExtensionLoader().getExtensions()) { for (const extension of this.getExtensionLoader().getExtensions()) {
if (!extension.isActive) { if (!extension.isActive) {
@@ -1501,6 +1506,12 @@ export class Config {
excludeToolsSet.add(tool); excludeToolsSet.add(tool);
} }
} }
const policyExclusions = this.policyEngine.getExcludedTools();
for (const tool of policyExclusions) {
excludeToolsSet.add(tool);
}
return excludeToolsSet; return excludeToolsSet;
} }
@@ -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', () => { describe('YOLO mode with ask_user tool', () => {
it('should return ASK_USER for ask_user tool even in YOLO mode', async () => { it('should return ASK_USER for ask_user tool even in YOLO mode', async () => {
const rules: PolicyRule[] = [ const rules: PolicyRule[] = [
+103 -3
View File
@@ -26,6 +26,22 @@ import {
} from '../utils/shell-utils.js'; } from '../utils/shell-utils.js';
import { getToolAliases } from '../tools/tool-names.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( function ruleMatches(
rule: PolicyRule | SafetyCheckerRule, rule: PolicyRule | SafetyCheckerRule,
toolCall: FunctionCall, toolCall: FunctionCall,
@@ -43,8 +59,8 @@ function ruleMatches(
// Check tool name if specified // Check tool name if specified
if (rule.toolName) { if (rule.toolName) {
// Support wildcard patterns: "serverName__*" matches "serverName__anyTool" // Support wildcard patterns: "serverName__*" matches "serverName__anyTool"
if (rule.toolName.endsWith('__*')) { if (isWildcardPattern(rule.toolName)) {
const prefix = rule.toolName.slice(0, -3); // Remove "__*" const prefix = getWildcardPrefix(rule.toolName);
if (serverName !== undefined) { if (serverName !== undefined) {
// Robust check: if serverName is provided, it MUST match the prefix exactly. // 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". // 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 // 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; return false;
} }
} else if (toolCall.name !== rule.toolName) { } else if (toolCall.name !== rule.toolName) {
@@ -509,6 +525,90 @@ export class PolicyEngine {
return this.hookCheckers; 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<string> {
const excludedTools = new Set<string>();
const processedTools = new Set<string>();
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 { private applyNonInteractiveMode(decision: PolicyDecision): PolicyDecision {
// In non-interactive mode, ASK_USER becomes DENY // In non-interactive mode, ASK_USER becomes DENY
if (this.nonInteractive && decision === PolicyDecision.ASK_USER) { if (this.nonInteractive && decision === PolicyDecision.ASK_USER) {