mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
feat(plan): document and validate Plan Mode policy overrides (#18825)
This commit is contained in:
@@ -30,6 +30,7 @@ implementation strategy.
|
||||
- [The Planning Workflow](#the-planning-workflow)
|
||||
- [Exiting Plan Mode](#exiting-plan-mode)
|
||||
- [Tool Restrictions](#tool-restrictions)
|
||||
- [Customizing Policies](#customizing-policies)
|
||||
|
||||
## Starting in Plan Mode
|
||||
|
||||
@@ -98,6 +99,53 @@ These are the only allowed tools:
|
||||
- **Planning (Write):** [`write_file`] and [`replace`] ONLY allowed for `.md`
|
||||
files in the `~/.gemini/tmp/<project>/plans/` directory.
|
||||
|
||||
### Customizing Policies
|
||||
|
||||
Plan Mode is designed to be read-only by default to ensure safety during the
|
||||
research phase. However, you may occasionally need to allow specific tools to
|
||||
assist in your planning.
|
||||
|
||||
Because user policies (Tier 2) have a higher base priority than built-in
|
||||
policies (Tier 1), you can override Plan Mode's default restrictions by creating
|
||||
a rule in your `~/.gemini/policies/` directory.
|
||||
|
||||
#### Example: Allow `git status` and `git diff` in Plan Mode
|
||||
|
||||
This rule allows you to check the repository status and see changes while in
|
||||
Plan Mode.
|
||||
|
||||
`~/.gemini/policies/git-research.toml`
|
||||
|
||||
```toml
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = ["git status", "git diff"]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan"]
|
||||
```
|
||||
|
||||
#### Example: Enable research sub-agents in Plan Mode
|
||||
|
||||
You can enable [experimental research sub-agents] like `codebase_investigator`
|
||||
to help gather architecture details during the planning phase.
|
||||
|
||||
`~/.gemini/policies/research-subagents.toml`
|
||||
|
||||
```toml
|
||||
[[rule]]
|
||||
toolName = "codebase_investigator"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan"]
|
||||
```
|
||||
|
||||
Tell the agent it can use these tools in your prompt, for example: _"You can
|
||||
check ongoing changes in git."_
|
||||
|
||||
For more information on how the policy engine works, see the [Policy Engine
|
||||
Guide].
|
||||
|
||||
[`list_directory`]: /docs/tools/file-system.md#1-list_directory-readfolder
|
||||
[`read_file`]: /docs/tools/file-system.md#2-read_file-readfile
|
||||
[`grep_search`]: /docs/tools/file-system.md#5-grep_search-searchtext
|
||||
@@ -106,3 +154,5 @@ These are the only allowed tools:
|
||||
[`google_web_search`]: /docs/tools/web-search.md
|
||||
[`replace`]: /docs/tools/file-system.md#6-replace-edit
|
||||
[MCP tools]: /docs/tools/mcp-server.md
|
||||
[experimental research sub-agents]: /docs/core/subagents.md
|
||||
[Policy Engine Guide]: /docs/core/policy-engine.md
|
||||
|
||||
@@ -119,9 +119,17 @@ For example:
|
||||
|
||||
Approval modes allow the policy engine to apply different sets of rules based on
|
||||
the CLI's operational mode. A rule can be associated with one or more modes
|
||||
(e.g., `yolo`, `autoEdit`). The rule will only be active if the CLI is running
|
||||
in one of its specified modes. If a rule has no modes specified, it is always
|
||||
active.
|
||||
(e.g., `yolo`, `autoEdit`, `plan`). The rule will only be active if the CLI is
|
||||
running in one of its specified modes. If a rule has no modes specified, it is
|
||||
always active.
|
||||
|
||||
- `default`: The standard interactive mode where most write tools require
|
||||
confirmation.
|
||||
- `autoEdit`: Optimized for automated code editing; some write tools may be
|
||||
auto-approved.
|
||||
- `plan`: A strict, read-only mode for research and design. See [Customizing
|
||||
Plan Mode Policies].
|
||||
- `yolo`: A mode where all tools are auto-approved (use with extreme caution).
|
||||
|
||||
## Rule matching
|
||||
|
||||
@@ -303,3 +311,5 @@ out-of-the-box experience.
|
||||
- In **`yolo`** mode, a high-priority rule allows all tools.
|
||||
- In **`autoEdit`** mode, rules allow certain write operations to happen without
|
||||
prompting.
|
||||
|
||||
[Customizing Plan Mode Policies]: /docs/cli/plan-mode.md#customizing-policies
|
||||
|
||||
@@ -951,4 +951,107 @@ name = "invalid-name"
|
||||
|
||||
vi.doUnmock('node:fs/promises');
|
||||
});
|
||||
|
||||
it('should allow overriding Plan Mode deny with user policy', async () => {
|
||||
const actualFs =
|
||||
await vi.importActual<typeof import('node:fs/promises')>(
|
||||
'node:fs/promises',
|
||||
);
|
||||
|
||||
const mockReaddir = vi.fn(
|
||||
async (
|
||||
path: string | Buffer | URL,
|
||||
options?: Parameters<typeof actualFs.readdir>[1],
|
||||
) => {
|
||||
const normalizedPath = nodePath.normalize(path.toString());
|
||||
if (normalizedPath.includes(nodePath.normalize('.gemini/policies'))) {
|
||||
return [
|
||||
{
|
||||
name: 'user-plan.toml',
|
||||
isFile: () => true,
|
||||
isDirectory: () => false,
|
||||
},
|
||||
] as unknown as Awaited<ReturnType<typeof actualFs.readdir>>;
|
||||
}
|
||||
return actualFs.readdir(
|
||||
path,
|
||||
options as Parameters<typeof actualFs.readdir>[1],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const mockReadFile = vi.fn(
|
||||
async (
|
||||
path: Parameters<typeof actualFs.readFile>[0],
|
||||
options: Parameters<typeof actualFs.readFile>[1],
|
||||
) => {
|
||||
const normalizedPath = nodePath.normalize(path.toString());
|
||||
if (normalizedPath.includes('user-plan.toml')) {
|
||||
return `
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = ["git status", "git diff"]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan"]
|
||||
|
||||
[[rule]]
|
||||
toolName = "codebase_investigator"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan"]
|
||||
`;
|
||||
}
|
||||
return actualFs.readFile(path, options);
|
||||
},
|
||||
);
|
||||
|
||||
vi.doMock('node:fs/promises', () => ({
|
||||
...actualFs,
|
||||
default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
|
||||
readFile: mockReadFile,
|
||||
readdir: mockReaddir,
|
||||
}));
|
||||
|
||||
vi.resetModules();
|
||||
const { createPolicyEngineConfig } = await import('./config.js');
|
||||
|
||||
const settings: PolicySettings = {};
|
||||
const config = await createPolicyEngineConfig(
|
||||
settings,
|
||||
ApprovalMode.PLAN,
|
||||
nodePath.join(__dirname, 'policies'),
|
||||
);
|
||||
|
||||
const shellRules = config.rules?.filter(
|
||||
(r) =>
|
||||
r.toolName === 'run_shell_command' &&
|
||||
r.decision === PolicyDecision.ALLOW &&
|
||||
r.modes?.includes(ApprovalMode.PLAN) &&
|
||||
r.argsPattern,
|
||||
);
|
||||
expect(shellRules).toHaveLength(2);
|
||||
expect(
|
||||
shellRules?.some((r) => r.argsPattern?.test('{"command":"git status"}')),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shellRules?.some((r) => r.argsPattern?.test('{"command":"git diff"}')),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shellRules?.every(
|
||||
(r) => !r.argsPattern?.test('{"command":"git commit"}'),
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const subagentRule = config.rules?.find(
|
||||
(r) =>
|
||||
r.toolName === 'codebase_investigator' &&
|
||||
r.decision === PolicyDecision.ALLOW &&
|
||||
r.modes?.includes(ApprovalMode.PLAN),
|
||||
);
|
||||
expect(subagentRule).toBeDefined();
|
||||
expect(subagentRule?.priority).toBeCloseTo(2.1, 5);
|
||||
|
||||
vi.doUnmock('node:fs/promises');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user