fix(policy): enforce ask_user as DENY in headless/non-interactive mode#20438
fix(policy): enforce ask_user as DENY in headless/non-interactive mode#20438Devnil434 wants to merge 4 commits intogoogle-gemini:mainfrom
Conversation
In non-interactive (headless) mode, policy rules with decision=ask_user should be treated as DENY since there is no user to ask. The getExcludedTools() method in PolicyEngine was computing which tools to hide from the LLM, but was not applying the nonInteractive mode conversion. This caused tools like run_shell_command to remain available even when a policy rule marked them as ask_user. Fix: apply applyNonInteractiveMode() when determining the effective policy decision in getExcludedTools(), so ask_user becomes DENY in headless mode. Also adds an integration test (policy_repro.test.ts) that verifies tools marked ask_user are blocked (not successfully executed) when the CLI runs headlessly with -p/--policy flags. Fixes google-gemini#19773
Summary of ChangesHello @Devnil434, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request resolves a critical issue in the policy engine where rules configured to Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request addresses a bug where ask_user policies were not enforced as DENY in non-interactive mode for getExcludedTools. While the change correctly handles tool-specific rules, it misses a critical scenario with global rules. A global ask_user policy will still be incorrectly treated as permissive in non-interactive mode, leading to a security vulnerability where tools that should be blocked are made available to the model. I have added a critical review comment on packages/core/src/policy/policy-engine.ts detailing the vulnerability and the required fix. I also recommend enhancing the new integration test to cover this global rule case.
I am having trouble creating individual review comments. Click here to see my feedback.
packages/core/src/policy/policy-engine.ts (620-622)
This change correctly applies non-interactive mode to the decision for tool-specific rules, but the fix is incomplete and a critical security vulnerability remains with global rules.
If the highest-priority global rule has decision = "ask_user", the logic around lines 577-583 will cause the loop to break prematurely when in non-interactive mode, bypassing this fix entirely.
Vulnerability Details:
- A global rule with
decision = "ask_user"is processed. globalVerdictis set toASK_USER.- The check
if (globalVerdict !== PolicyDecision.DENY)at line 578 passes. - The loop
breaks. getExcludedTools()returns an empty set.- In non-interactive mode, this effectively bypasses the policy, as no tools are excluded and they will be presented to the LLM. The expected behavior is for
ask_userto be treated asDENY, which should prevent tools from being available.
To properly fix this, the check for the global verdict must also be updated to account for non-interactive mode:
// Around line 577
globalVerdict = rule.decision;
const effectiveGlobalVerdict = this.applyNonInteractiveMode(globalVerdict);
if (effectiveGlobalVerdict !== PolicyDecision.DENY) {
// Global ALLOW/ASK found.
// ...
break;
}This ensures that a global ask_user rule in non-interactive mode is correctly treated as a DENY and does not cause an early exit.
Additionally, the new integration test in integration-tests/policy_repro.test.ts should be updated to include a case for a global ask_user policy to verify this fix and prevent regressions.
|
Thanks for the detailed review — you’re absolutely right. The previous change only applied non-interactive handling to tool-specific rules and did not account for global ask_user verdicts, which could cause premature loop exit and bypass policy enforcement. Updated the global verdict evaluation to apply All tests passing. |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request addresses an important policy enforcement bug in headless mode, specifically correcting an issue where ASK_USER policy decisions were not being treated as DENY in headless/non-interactive mode within the getExcludedTools function. While the core logic changes are on the right track, the fix is incomplete as it misses several code paths within the getExcludedTools function, potentially exposing tools to the LLM despite policy requirements, which violates the 'fail-closed' security principle. Additionally, there are generated files that should be removed from the commit. Please address these points to finalize the PR and ensure full compliance with the intended security policy.
| /** | ||
| * @license | ||
| * Copyright 2025 Google LLC | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| import { z } from 'zod'; | ||
| export declare const settingsZodSchema: z.ZodObject< | ||
| Record<string, z.ZodTypeAny>, | ||
| z.UnknownKeysParam, | ||
| z.ZodTypeAny, | ||
| { | ||
| [x: string]: any; | ||
| }, | ||
| { | ||
| [x: string]: any; | ||
| } | ||
| >; | ||
| /** | ||
| * Validates settings data against the Zod schema | ||
| */ | ||
| export declare function validateSettings(data: unknown): { | ||
| success: boolean; | ||
| data?: unknown; | ||
| error?: z.ZodError; | ||
| }; | ||
| /** | ||
| * Format a Zod error into a helpful error message | ||
| */ | ||
| export declare function formatValidationError( | ||
| error: z.ZodError, | ||
| filePath: string, | ||
| ): string; |
Summary
Fixes a bug where
--policyrules are ignored in headless mode (-p/--prompt).When a policy rule has
decision = "ask_user"and the CLI runs non-interactively,the policy engine should treat that as
DENY(since there's no user to ask).Previously,
getExcludedTools()did not apply the non-interactive mode conversion,so tools like
run_shell_commandremained available to the LLM even when a policyrequired user confirmation.
Details
The fix is in
PolicyEngine.getExcludedTools(): it now callsapplyNonInteractiveMode()when computing the effective per-rule decision. Thisensures
ask_user→DENYin headless mode, excluding those tools from the toolregistry and preventing the LLM from invoking them.
An integration test (
integration-tests/policy_repro.test.ts) is added toreproduce the issue and verify the fix end-to-end.
Related Issues
Fixes #19773
How to Validate
Build the bundle:
npm run bundleRun the integration test:
Expected:
1 passed (1)Manual reproduction:
Create a policy file
block-shell.toml:Run:
gemini -p "run echo hello" --policy block-shell.tomlExpected: the shell command is NOT executed (tool blocked by policy).
Pre-Merge Checklist