mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-25 09:10:59 -07:00
Compare commits
10 Commits
ept-debugging
...
v0.37.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b2d4f1000 | |||
| b569a98c02 | |||
| ea7dfe8914 | |||
| 527a08d76a | |||
| 352bbe1493 | |||
| ae9d4e16a3 | |||
| ac34520776 | |||
| 7cd6dde12a | |||
| 24810f97ef | |||
| c0f8f3cb7b |
+10
-3
@@ -180,9 +180,16 @@ As described in the
|
||||
rule that does not explicitly specify `modes` is considered "always active" and
|
||||
will apply to Plan Mode as well.
|
||||
|
||||
If you want a rule to apply to other modes but _not_ to Plan Mode, you must
|
||||
explicitly specify the target modes. For example, to allow `npm test` in default
|
||||
and Auto-Edit modes but not in Plan Mode:
|
||||
To maintain the integrity of Plan Mode as a safe research environment,
|
||||
persistent tool approvals are context-aware. Approvals granted in modes like
|
||||
Default or Auto-Edit do not apply to Plan Mode, ensuring that tools trusted for
|
||||
implementation don't automatically execute while you're researching. However,
|
||||
approvals granted while in Plan Mode are treated as intentional choices for
|
||||
global trust and apply to all modes.
|
||||
|
||||
If you want to manually restrict a rule to other modes but _not_ to Plan Mode,
|
||||
you must explicitly specify the target modes. For example, to allow `npm test`
|
||||
in default and Auto-Edit modes but not in Plan Mode:
|
||||
|
||||
```toml
|
||||
[[rule]]
|
||||
|
||||
+33
-33
@@ -47,39 +47,39 @@ they appear in the UI.
|
||||
|
||||
### UI
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
||||
| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` |
|
||||
| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` |
|
||||
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
|
||||
| Inline Thinking | `ui.inlineThinkingMode` | Display model thinking inline: off or full. | `"off"` |
|
||||
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
|
||||
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
|
||||
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
|
||||
| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` |
|
||||
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
|
||||
| Escape Pasted @ Symbols | `ui.escapePastedAtSymbols` | When enabled, @ symbols in pasted text are escaped to prevent unintended @path expansion. | `false` |
|
||||
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
|
||||
| Compact Tool Output | `ui.compactToolOutput` | Display tool outputs (like directory listings and file reads) in a compact, structured format. | `false` |
|
||||
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
|
||||
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
|
||||
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory in the footer. | `false` |
|
||||
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
|
||||
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
|
||||
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window usage percentage. | `true` |
|
||||
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
|
||||
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
|
||||
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
|
||||
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
|
||||
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
|
||||
| Show User Identity | `ui.showUserIdentity` | Show the signed-in user's identity (e.g. email) in the UI. | `true` |
|
||||
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
|
||||
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
|
||||
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
|
||||
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
|
||||
| Loading Phrases | `ui.loadingPhrases` | What to show while the model is working: tips, witty comments, both, or nothing. | `"tips"` |
|
||||
| Error Verbosity | `ui.errorVerbosity` | Controls whether recoverable errors are hidden (low) or fully shown (full). | `"low"` |
|
||||
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` |
|
||||
| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` |
|
||||
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
|
||||
| Inline Thinking | `ui.inlineThinkingMode` | Display model thinking inline: off or full. | `"off"` |
|
||||
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
|
||||
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
|
||||
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
|
||||
| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` |
|
||||
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
|
||||
| Escape Pasted @ Symbols | `ui.escapePastedAtSymbols` | When enabled, @ symbols in pasted text are escaped to prevent unintended @path expansion. | `false` |
|
||||
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
|
||||
| Compact Tool Output | `ui.compactToolOutput` | Display tool outputs (like directory listings and file reads) in a compact, structured format. | `false` |
|
||||
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
|
||||
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
|
||||
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory in the footer. | `false` |
|
||||
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
|
||||
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
|
||||
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window usage percentage. | `true` |
|
||||
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
|
||||
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
|
||||
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
|
||||
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
|
||||
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
|
||||
| Show User Identity | `ui.showUserIdentity` | Show the signed-in user's identity (e.g. email) in the UI. | `true` |
|
||||
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
|
||||
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
|
||||
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
|
||||
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
|
||||
| Loading Phrases | `ui.loadingPhrases` | What to show while the model is working: tips, witty comments, all, or off. | `"off"` |
|
||||
| Error Verbosity | `ui.errorVerbosity` | Controls whether recoverable errors are hidden (low) or fully shown (full). | `"low"` |
|
||||
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
|
||||
|
||||
### IDE
|
||||
|
||||
|
||||
@@ -354,8 +354,8 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
- **`ui.loadingPhrases`** (enum):
|
||||
- **Description:** What to show while the model is working: tips, witty
|
||||
comments, both, or nothing.
|
||||
- **Default:** `"tips"`
|
||||
comments, all, or off.
|
||||
- **Default:** `"off"`
|
||||
- **Values:** `"tips"`, `"witty"`, `"all"`, `"off"`
|
||||
|
||||
- **`ui.errorVerbosity`** (enum):
|
||||
|
||||
@@ -171,6 +171,24 @@ modes specified, it is always active.
|
||||
[Customizing Plan Mode Policies](../cli/plan-mode.md#customizing-policies).
|
||||
- `yolo`: A mode where all tools are auto-approved (use with extreme caution).
|
||||
|
||||
To maintain the integrity of Plan Mode as a safe research environment,
|
||||
persistent tool approvals are context-aware. When you select **"Allow for all
|
||||
future sessions"**, the policy engine explicitly includes the current mode and
|
||||
all more permissive modes in the hierarchy (`plan` < `default` < `autoEdit` <
|
||||
`yolo`).
|
||||
|
||||
- **Approvals in `plan` mode**: These represent an intentional choice to trust a
|
||||
tool globally. The resulting rule explicitly includes all modes (`plan`,
|
||||
`default`, `autoEdit`, and `yolo`).
|
||||
- **Approvals in other modes**: These only apply to the current mode and those
|
||||
more permissive. For example:
|
||||
- An approval granted in **`default`** mode applies to `default`, `autoEdit`,
|
||||
and `yolo`.
|
||||
- An approval granted in **`autoEdit`** mode applies to `autoEdit` and `yolo`.
|
||||
- An approval granted in **`yolo`** mode applies only to `yolo`. This ensures
|
||||
that trust flows correctly to more permissive environments while maintaining
|
||||
the safety of more restricted modes like `plan`.
|
||||
|
||||
## Rule matching
|
||||
|
||||
When a tool call is made, the engine checks it against all active rules,
|
||||
@@ -304,7 +322,8 @@ priority = 10
|
||||
denyMessage = "Deletion is permanent"
|
||||
|
||||
# (Optional) An array of approval modes where this rule is active.
|
||||
modes = ["autoEdit"]
|
||||
# If omitted or empty, the rule applies to all modes.
|
||||
modes = ["default", "autoEdit", "yolo"]
|
||||
|
||||
# (Optional) A boolean to restrict the rule to interactive (true) or
|
||||
# non-interactive (false) environments.
|
||||
|
||||
Generated
+9
-9
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -17389,7 +17389,7 @@
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
@@ -17504,7 +17504,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
@@ -17676,7 +17676,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
@@ -17942,7 +17942,7 @@
|
||||
},
|
||||
"packages/devtools": {
|
||||
"name": "@google/gemini-cli-devtools",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"ws": "^8.16.0"
|
||||
@@ -17957,7 +17957,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@google/gemini-cli-sdk",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -17974,7 +17974,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -17991,7 +17991,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.37.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -27,7 +27,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.37.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
|
||||
@@ -875,6 +875,32 @@ describe('Session', () => {
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
});
|
||||
|
||||
it('should handle prompt with no finish reason (InvalidStreamError)', async () => {
|
||||
mockChat.sendMessageStream.mockRejectedValue(
|
||||
new InvalidStreamError('No finish reason', 'NO_FINISH_REASON'),
|
||||
);
|
||||
|
||||
const result = await session.prompt({
|
||||
sessionId: 'session-1',
|
||||
prompt: [{ type: 'text', text: 'Hi' }],
|
||||
});
|
||||
|
||||
expect(mockChat.sendMessageStream).toHaveBeenCalled();
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
});
|
||||
|
||||
it('should handle prompt with no finish reason (NO_FINISH_REASON anomaly)', async () => {
|
||||
mockChat.sendMessageStream.mockRejectedValue({ type: 'NO_FINISH_REASON' });
|
||||
|
||||
const result = await session.prompt({
|
||||
sessionId: 'session-1',
|
||||
prompt: [{ type: 'text', text: 'Hi' }],
|
||||
});
|
||||
|
||||
expect(mockChat.sendMessageStream).toHaveBeenCalled();
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
});
|
||||
|
||||
it('should handle /memory command', async () => {
|
||||
const handleCommandSpy = vi
|
||||
.spyOn(
|
||||
|
||||
@@ -863,7 +863,10 @@ export class Session {
|
||||
(error &&
|
||||
typeof error === 'object' &&
|
||||
'type' in error &&
|
||||
error.type === 'NO_RESPONSE_TEXT')
|
||||
(error.type === 'NO_RESPONSE_TEXT' ||
|
||||
error.type === 'NO_FINISH_REASON' ||
|
||||
error.type === 'MALFORMED_FUNCTION_CALL' ||
|
||||
error.type === 'UNEXPECTED_TOOL_CALL'))
|
||||
) {
|
||||
// The stream ended with an empty response or malformed tool call.
|
||||
// Treat this as a graceful end to the model's turn rather than a crash.
|
||||
|
||||
@@ -26,5 +26,11 @@ describe('CommandHandler', () => {
|
||||
|
||||
const init = parse('/init');
|
||||
expect(init.commandToExecute?.name).toBe('init');
|
||||
|
||||
const about = parse('/about');
|
||||
expect(about.commandToExecute?.name).toBe('about');
|
||||
|
||||
const help = parse('/help');
|
||||
expect(help.commandToExecute?.name).toBe('help');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,8 @@ import { MemoryCommand } from './commands/memory.js';
|
||||
import { ExtensionsCommand } from './commands/extensions.js';
|
||||
import { InitCommand } from './commands/init.js';
|
||||
import { RestoreCommand } from './commands/restore.js';
|
||||
import { AboutCommand } from './commands/about.js';
|
||||
import { HelpCommand } from './commands/help.js';
|
||||
|
||||
export class CommandHandler {
|
||||
private registry: CommandRegistry;
|
||||
@@ -24,6 +26,8 @@ export class CommandHandler {
|
||||
registry.register(new ExtensionsCommand());
|
||||
registry.register(new InitCommand());
|
||||
registry.register(new RestoreCommand());
|
||||
registry.register(new AboutCommand());
|
||||
registry.register(new HelpCommand(registry));
|
||||
return registry;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
IdeClient,
|
||||
UserAccountManager,
|
||||
getVersion,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type {
|
||||
Command,
|
||||
CommandContext,
|
||||
CommandExecutionResponse,
|
||||
} from './types.js';
|
||||
import process from 'node:process';
|
||||
|
||||
export class AboutCommand implements Command {
|
||||
readonly name = 'about';
|
||||
readonly description = 'Show version and environment info';
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
_args: string[] = [],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const osVersion = process.platform;
|
||||
let sandboxEnv = 'no sandbox';
|
||||
if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') {
|
||||
sandboxEnv = process.env['SANDBOX'];
|
||||
} else if (process.env['SANDBOX'] === 'sandbox-exec') {
|
||||
sandboxEnv = `sandbox-exec (${
|
||||
process.env['SEATBELT_PROFILE'] || 'unknown'
|
||||
})`;
|
||||
}
|
||||
const modelVersion = context.agentContext.config.getModel() || 'Unknown';
|
||||
const cliVersion = await getVersion();
|
||||
const selectedAuthType =
|
||||
context.settings.merged?.security?.auth?.selectedType ?? '';
|
||||
const gcpProject = process.env['GOOGLE_CLOUD_PROJECT'] || '';
|
||||
const ideClient = await getIdeClientName(context);
|
||||
|
||||
const userAccountManager = new UserAccountManager();
|
||||
const cachedAccount = userAccountManager.getCachedGoogleAccount();
|
||||
const userEmail = cachedAccount ?? 'Unknown';
|
||||
|
||||
const tier = context.agentContext.config.getUserTierName() || 'Unknown';
|
||||
|
||||
const info = [
|
||||
`- Version: ${cliVersion}`,
|
||||
`- OS: ${osVersion}`,
|
||||
`- Sandbox: ${sandboxEnv}`,
|
||||
`- Model: ${modelVersion}`,
|
||||
`- Auth Type: ${selectedAuthType}`,
|
||||
`- GCP Project: ${gcpProject}`,
|
||||
`- IDE Client: ${ideClient}`,
|
||||
`- User Email: ${userEmail}`,
|
||||
`- Tier: ${tier}`,
|
||||
].join('\n');
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
data: `Gemini CLI Info:\n${info}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getIdeClientName(context: CommandContext) {
|
||||
if (!context.agentContext.config.getIdeMode()) {
|
||||
return '';
|
||||
}
|
||||
const ideClient = await IdeClient.getInstance();
|
||||
return ideClient?.getDetectedIdeDisplayName() ?? '';
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { HelpCommand } from './help.js';
|
||||
import { CommandRegistry } from './commandRegistry.js';
|
||||
import type { Command, CommandContext } from './types.js';
|
||||
|
||||
describe('HelpCommand', () => {
|
||||
it('returns formatted help text with sorted commands', async () => {
|
||||
const registry = new CommandRegistry();
|
||||
|
||||
const cmdB: Command = {
|
||||
name: 'bravo',
|
||||
description: 'Bravo command',
|
||||
execute: async () => ({ name: 'bravo', data: '' }),
|
||||
};
|
||||
|
||||
const cmdA: Command = {
|
||||
name: 'alpha',
|
||||
description: 'Alpha command',
|
||||
execute: async () => ({ name: 'alpha', data: '' }),
|
||||
};
|
||||
|
||||
registry.register(cmdB);
|
||||
registry.register(cmdA);
|
||||
|
||||
const helpCommand = new HelpCommand(registry);
|
||||
|
||||
const context = {} as CommandContext;
|
||||
|
||||
const response = await helpCommand.execute(context, []);
|
||||
|
||||
expect(response.name).toBe('help');
|
||||
|
||||
const data = response.data as string;
|
||||
|
||||
expect(data).toContain('Gemini CLI Help:');
|
||||
expect(data).toContain('### Basics');
|
||||
expect(data).toContain('### Commands');
|
||||
|
||||
const lines = data.split('\n');
|
||||
const alphaIndex = lines.findIndex((l) => l.includes('/alpha'));
|
||||
const bravoIndex = lines.findIndex((l) => l.includes('/bravo'));
|
||||
|
||||
expect(alphaIndex).toBeLessThan(bravoIndex);
|
||||
expect(alphaIndex).not.toBe(-1);
|
||||
expect(bravoIndex).not.toBe(-1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {
|
||||
Command,
|
||||
CommandContext,
|
||||
CommandExecutionResponse,
|
||||
} from './types.js';
|
||||
import type { CommandRegistry } from './commandRegistry.js';
|
||||
|
||||
export class HelpCommand implements Command {
|
||||
readonly name = 'help';
|
||||
readonly description = 'Show available commands';
|
||||
|
||||
constructor(private registry: CommandRegistry) {}
|
||||
|
||||
async execute(
|
||||
_context: CommandContext,
|
||||
_args: string[] = [],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const commands = this.registry
|
||||
.getAllCommands()
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push('Gemini CLI Help:');
|
||||
lines.push('');
|
||||
lines.push('### Basics');
|
||||
lines.push(
|
||||
'- **Add context**: Use `@` to specify files for context (e.g., `@src/myFile.ts`) to target specific files or folders.',
|
||||
);
|
||||
lines.push('');
|
||||
|
||||
lines.push('### Commands');
|
||||
for (const cmd of commands) {
|
||||
if (cmd.description) {
|
||||
lines.push(`- **/${cmd.name}** - ${cmd.description}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
data: lines.join('\n'),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ describe('SettingsSchema', () => {
|
||||
const definition = getSettingsSchema().ui?.properties?.loadingPhrases;
|
||||
expect(definition).toBeDefined();
|
||||
expect(definition?.type).toBe('enum');
|
||||
expect(definition?.default).toBe('tips');
|
||||
expect(definition?.default).toBe('off');
|
||||
expect(definition?.options?.map((o) => o.value)).toEqual([
|
||||
'tips',
|
||||
'witty',
|
||||
|
||||
@@ -776,9 +776,9 @@ const SETTINGS_SCHEMA = {
|
||||
label: 'Loading Phrases',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: 'tips',
|
||||
default: 'off',
|
||||
description:
|
||||
'What to show while the model is working: tips, witty comments, both, or nothing.',
|
||||
'What to show while the model is working: tips, witty comments, all, or off.',
|
||||
showInDialog: true,
|
||||
options: [
|
||||
{ value: 'tips', label: 'Tips' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"description": "Gemini CLI Core",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { type FunctionCall } from '@google/genai';
|
||||
import { type ApprovalMode } from '../policy/types.js';
|
||||
import type {
|
||||
ToolConfirmationOutcome,
|
||||
ToolConfirmationPayload,
|
||||
@@ -150,6 +151,7 @@ export interface UpdatePolicy {
|
||||
commandPrefix?: string | string[];
|
||||
mcpName?: string;
|
||||
allowRedirection?: boolean;
|
||||
modes?: ApprovalMode[];
|
||||
}
|
||||
|
||||
export interface ToolPolicyRejection {
|
||||
|
||||
@@ -533,7 +533,6 @@ export async function createPolicyEngineConfig(
|
||||
disableAlwaysAllow: settings.disableAlwaysAllow,
|
||||
};
|
||||
}
|
||||
|
||||
interface TomlRule {
|
||||
toolName?: string;
|
||||
mcpName?: string;
|
||||
@@ -542,10 +541,64 @@ interface TomlRule {
|
||||
commandPrefix?: string | string[];
|
||||
argsPattern?: string;
|
||||
allowRedirection?: boolean;
|
||||
modes?: ApprovalMode[];
|
||||
// Index signature to satisfy Record type if needed for toml.stringify
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a rule in the rule array that matches the given criteria.
|
||||
*/
|
||||
function findMatchingRule(
|
||||
rules: TomlRule[],
|
||||
criteria: {
|
||||
toolName: string;
|
||||
mcpName?: string;
|
||||
commandPrefix?: string | string[];
|
||||
argsPattern?: string;
|
||||
},
|
||||
): TomlRule | undefined {
|
||||
return rules.find(
|
||||
(r) =>
|
||||
r.toolName === criteria.toolName &&
|
||||
r.mcpName === criteria.mcpName &&
|
||||
JSON.stringify(r.commandPrefix) ===
|
||||
JSON.stringify(criteria.commandPrefix) &&
|
||||
r.argsPattern === criteria.argsPattern,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new TOML rule object from the given tool name and message.
|
||||
*/
|
||||
function createTomlRule(toolName: string, message: UpdatePolicy): TomlRule {
|
||||
const rule: TomlRule = {
|
||||
decision: 'allow',
|
||||
priority: getAlwaysAllowPriorityFraction(),
|
||||
toolName,
|
||||
};
|
||||
|
||||
if (message.mcpName) {
|
||||
rule.mcpName = message.mcpName;
|
||||
}
|
||||
|
||||
if (message.commandPrefix) {
|
||||
rule.commandPrefix = message.commandPrefix;
|
||||
} else if (message.argsPattern) {
|
||||
rule.argsPattern = message.argsPattern;
|
||||
}
|
||||
|
||||
if (message.allowRedirection !== undefined) {
|
||||
rule.allowRedirection = message.allowRedirection;
|
||||
}
|
||||
|
||||
if (message.modes) {
|
||||
rule.modes = message.modes;
|
||||
}
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
export function createPolicyUpdater(
|
||||
policyEngine: PolicyEngine,
|
||||
messageBus: MessageBus,
|
||||
@@ -585,6 +638,7 @@ export function createPolicyUpdater(
|
||||
priority,
|
||||
argsPattern: new RegExp(pattern),
|
||||
mcpName: message.mcpName,
|
||||
modes: message.modes,
|
||||
source: 'Dynamic (Confirmed)',
|
||||
allowRedirection: message.allowRedirection,
|
||||
});
|
||||
@@ -622,6 +676,7 @@ export function createPolicyUpdater(
|
||||
priority,
|
||||
argsPattern,
|
||||
mcpName: message.mcpName,
|
||||
modes: message.modes,
|
||||
source: 'Dynamic (Confirmed)',
|
||||
allowRedirection: message.allowRedirection,
|
||||
});
|
||||
@@ -662,39 +717,36 @@ export function createPolicyUpdater(
|
||||
existingData.rule = [];
|
||||
}
|
||||
|
||||
// Create new rule object
|
||||
const newRule: TomlRule = {
|
||||
decision: 'allow',
|
||||
priority: getAlwaysAllowPriorityFraction(),
|
||||
};
|
||||
|
||||
// Normalize tool name for MCP
|
||||
let normalizedToolName = toolName;
|
||||
if (message.mcpName) {
|
||||
newRule.mcpName = message.mcpName;
|
||||
|
||||
const expectedPrefix = `${MCP_TOOL_PREFIX}${message.mcpName}_`;
|
||||
if (toolName.startsWith(expectedPrefix)) {
|
||||
newRule.toolName = toolName.slice(expectedPrefix.length);
|
||||
} else {
|
||||
newRule.toolName = toolName;
|
||||
normalizedToolName = toolName.slice(expectedPrefix.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Look for an existing rule to update
|
||||
const existingRule = findMatchingRule(existingData.rule, {
|
||||
toolName: normalizedToolName,
|
||||
mcpName: message.mcpName,
|
||||
commandPrefix: message.commandPrefix,
|
||||
argsPattern: message.argsPattern,
|
||||
});
|
||||
|
||||
if (existingRule) {
|
||||
if (message.allowRedirection !== undefined) {
|
||||
existingRule.allowRedirection = message.allowRedirection;
|
||||
}
|
||||
if (message.modes) {
|
||||
existingRule.modes = message.modes;
|
||||
}
|
||||
} else {
|
||||
newRule.toolName = toolName;
|
||||
existingData.rule.push(
|
||||
createTomlRule(normalizedToolName, message),
|
||||
);
|
||||
}
|
||||
|
||||
if (message.commandPrefix) {
|
||||
newRule.commandPrefix = message.commandPrefix;
|
||||
} else if (message.argsPattern) {
|
||||
// message.argsPattern was already validated above
|
||||
newRule.argsPattern = message.argsPattern;
|
||||
}
|
||||
|
||||
if (message.allowRedirection !== undefined) {
|
||||
newRule.allowRedirection = message.allowRedirection;
|
||||
}
|
||||
|
||||
// Add to rules
|
||||
existingData.rule.push(newRule);
|
||||
|
||||
// Serialize back to TOML
|
||||
// @iarna/toml stringify might not produce beautiful output but it handles escaping correctly
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
|
||||
@@ -242,4 +242,57 @@ decision = "deny"
|
||||
const content = memfs.readFileSync(policyFile, 'utf-8') as string;
|
||||
expect(content).toContain('toolName = "test_tool"');
|
||||
});
|
||||
|
||||
it('should include modes if provided', async () => {
|
||||
createPolicyUpdater(policyEngine, messageBus, mockStorage);
|
||||
|
||||
const policyFile = '/mock/user/.gemini/policies/auto-saved.toml';
|
||||
vi.spyOn(mockStorage, 'getAutoSavedPolicyPath').mockReturnValue(policyFile);
|
||||
|
||||
await messageBus.publish({
|
||||
type: MessageBusType.UPDATE_POLICY,
|
||||
toolName: 'test_tool',
|
||||
persist: true,
|
||||
modes: [ApprovalMode.DEFAULT, ApprovalMode.YOLO],
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
const content = memfs.readFileSync(policyFile, 'utf-8') as string;
|
||||
expect(content).toContain('modes = [ "default", "yolo" ]');
|
||||
});
|
||||
|
||||
it('should update existing rule modes instead of appending redundant rule', async () => {
|
||||
createPolicyUpdater(policyEngine, messageBus, mockStorage);
|
||||
|
||||
const policyFile = '/mock/user/.gemini/policies/auto-saved.toml';
|
||||
vi.spyOn(mockStorage, 'getAutoSavedPolicyPath').mockReturnValue(policyFile);
|
||||
|
||||
const existingContent = `
|
||||
[[rule]]
|
||||
decision = "allow"
|
||||
priority = 950
|
||||
toolName = "test_tool"
|
||||
modes = [ "autoEdit", "yolo" ]
|
||||
`;
|
||||
const dir = path.dirname(policyFile);
|
||||
memfs.mkdirSync(dir, { recursive: true });
|
||||
memfs.writeFileSync(policyFile, existingContent);
|
||||
|
||||
// Now grant in DEFAULT mode, which should include [default, autoEdit, yolo]
|
||||
await messageBus.publish({
|
||||
type: MessageBusType.UPDATE_POLICY,
|
||||
toolName: 'test_tool',
|
||||
persist: true,
|
||||
modes: [ApprovalMode.DEFAULT, ApprovalMode.AUTO_EDIT, ApprovalMode.YOLO],
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
const content = memfs.readFileSync(policyFile, 'utf-8') as string;
|
||||
// Should NOT have two [[rule]] entries for test_tool
|
||||
const ruleCount = (content.match(/\[\[rule\]\]/g) || []).length;
|
||||
expect(ruleCount).toBe(1);
|
||||
expect(content).toContain('modes = [ "default", "autoEdit", "yolo" ]');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,6 +52,18 @@ export enum ApprovalMode {
|
||||
PLAN = 'plan',
|
||||
}
|
||||
|
||||
/**
|
||||
* The order of permissiveness for approval modes.
|
||||
* Tools allowed in a less permissive mode should also be allowed
|
||||
* in more permissive modes.
|
||||
*/
|
||||
export const MODES_BY_PERMISSIVENESS = [
|
||||
ApprovalMode.PLAN,
|
||||
ApprovalMode.DEFAULT,
|
||||
ApprovalMode.AUTO_EDIT,
|
||||
ApprovalMode.YOLO,
|
||||
];
|
||||
|
||||
/**
|
||||
* Configuration for the built-in allowed-path checker.
|
||||
*/
|
||||
|
||||
@@ -49,6 +49,7 @@ describe('policy.ts', () => {
|
||||
} as unknown as Mocked<PolicyEngine>;
|
||||
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -76,6 +77,7 @@ describe('policy.ts', () => {
|
||||
} as unknown as Mocked<PolicyEngine>;
|
||||
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -106,6 +108,7 @@ describe('policy.ts', () => {
|
||||
} as unknown as Mocked<PolicyEngine>;
|
||||
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
|
||||
getDisableAlwaysAllow: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Mocked<Config>;
|
||||
@@ -132,6 +135,7 @@ describe('policy.ts', () => {
|
||||
} as unknown as Mocked<PolicyEngine>;
|
||||
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
|
||||
isInteractive: vi.fn().mockReturnValue(false),
|
||||
} as unknown as Mocked<Config>;
|
||||
@@ -155,6 +159,7 @@ describe('policy.ts', () => {
|
||||
} as unknown as Mocked<PolicyEngine>;
|
||||
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -176,6 +181,7 @@ describe('policy.ts', () => {
|
||||
} as unknown as Mocked<PolicyEngine>;
|
||||
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
|
||||
isInteractive: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Mocked<Config>;
|
||||
@@ -198,6 +204,7 @@ describe('policy.ts', () => {
|
||||
} as unknown as Mocked<PolicyEngine>;
|
||||
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
|
||||
isInteractive: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Mocked<Config>;
|
||||
@@ -217,6 +224,7 @@ describe('policy.ts', () => {
|
||||
} as unknown as Mocked<PolicyEngine>;
|
||||
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -233,6 +241,7 @@ describe('policy.ts', () => {
|
||||
describe('updatePolicy', () => {
|
||||
it('should set AUTO_EDIT mode for auto-edit transition tools', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -262,6 +271,7 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should handle standard policy updates (persist=false)', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -293,6 +303,7 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should handle standard policy updates with persistence', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
isTrustedFolder: vi.fn().mockReturnValue(false),
|
||||
getWorkspacePoliciesDir: vi.fn().mockReturnValue(undefined),
|
||||
setApprovalMode: vi.fn(),
|
||||
@@ -326,6 +337,7 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should handle shell command prefixes', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -365,6 +377,7 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should handle MCP policy updates (server scope)', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -405,6 +418,7 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should NOT publish update for ProceedOnce', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -431,6 +445,7 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should NOT publish update for Cancel', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -456,6 +471,7 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should NOT publish update for ModifyWithEditor', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -481,6 +497,7 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should handle MCP ProceedAlwaysTool (specific tool name)', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -521,6 +538,7 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should handle MCP ProceedAlways (persist: false)', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -561,6 +579,7 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should handle MCP ProceedAlwaysAndSave (persist: true)', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
isTrustedFolder: vi.fn().mockReturnValue(false),
|
||||
getWorkspacePoliciesDir: vi.fn().mockReturnValue(undefined),
|
||||
setApprovalMode: vi.fn(),
|
||||
@@ -603,6 +622,7 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should determine persistScope: workspace in trusted folders', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
isTrustedFolder: vi.fn().mockReturnValue(true),
|
||||
getWorkspacePoliciesDir: vi
|
||||
.fn()
|
||||
@@ -633,6 +653,7 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should determine persistScope: user in untrusted folders', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
isTrustedFolder: vi.fn().mockReturnValue(false),
|
||||
getWorkspacePoliciesDir: vi
|
||||
.fn()
|
||||
@@ -663,6 +684,7 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should narrow edit tools with argsPattern', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
isTrustedFolder: vi.fn().mockReturnValue(false),
|
||||
getWorkspacePoliciesDir: vi.fn().mockReturnValue(undefined),
|
||||
getTargetDir: vi.fn().mockReturnValue('/mock/dir'),
|
||||
@@ -703,6 +725,7 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should work when context is created via Object.create (prototype chain)', async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
const mockMessageBus = {
|
||||
@@ -868,4 +891,78 @@ describe('Plan Mode Denial Consistency', () => {
|
||||
expect(resultMessage).toBe('Tool execution denied by policy.');
|
||||
expect(resultErrorType).toBe(ToolErrorType.POLICY_VIOLATION);
|
||||
});
|
||||
|
||||
describe('updatePolicy - context-aware modes', () => {
|
||||
const testCases = [
|
||||
{
|
||||
currentMode: ApprovalMode.DEFAULT,
|
||||
expectedModes: [
|
||||
ApprovalMode.DEFAULT,
|
||||
ApprovalMode.AUTO_EDIT,
|
||||
ApprovalMode.YOLO,
|
||||
],
|
||||
description:
|
||||
'include current and more permissive modes in DEFAULT mode',
|
||||
},
|
||||
{
|
||||
currentMode: ApprovalMode.AUTO_EDIT,
|
||||
expectedModes: [ApprovalMode.AUTO_EDIT, ApprovalMode.YOLO],
|
||||
description:
|
||||
'include current and more permissive modes in AUTO_EDIT mode',
|
||||
},
|
||||
{
|
||||
currentMode: ApprovalMode.YOLO,
|
||||
expectedModes: [ApprovalMode.YOLO],
|
||||
description: 'include current and more permissive modes in YOLO mode',
|
||||
},
|
||||
{
|
||||
currentMode: ApprovalMode.PLAN,
|
||||
expectedModes: [
|
||||
ApprovalMode.PLAN,
|
||||
ApprovalMode.DEFAULT,
|
||||
ApprovalMode.AUTO_EDIT,
|
||||
ApprovalMode.YOLO,
|
||||
],
|
||||
description: 'include all modes explicitly when granted in PLAN mode',
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach(({ currentMode, expectedModes, description }) => {
|
||||
it(`should ${description}`, async () => {
|
||||
const mockConfig = {
|
||||
getApprovalMode: vi.fn().mockReturnValue(currentMode),
|
||||
isTrustedFolder: vi.fn().mockReturnValue(false),
|
||||
getWorkspacePoliciesDir: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
const mockMessageBus = {
|
||||
publish: vi.fn(),
|
||||
} as unknown as Mocked<MessageBus>;
|
||||
|
||||
const context = {
|
||||
config: mockConfig,
|
||||
messageBus: mockMessageBus,
|
||||
} as unknown as AgentLoopContext;
|
||||
|
||||
const tool = { name: 'test-tool' } as AnyDeclarativeTool;
|
||||
|
||||
await updatePolicy(
|
||||
tool,
|
||||
ToolConfirmationOutcome.ProceedAlwaysAndSave,
|
||||
undefined,
|
||||
context,
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
expect(mockMessageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.UPDATE_POLICY,
|
||||
toolName: 'test-tool',
|
||||
persist: true,
|
||||
modes: expectedModes,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { ToolErrorType } from '../tools/tool-error.js';
|
||||
import {
|
||||
ApprovalMode,
|
||||
MODES_BY_PERMISSIVENESS,
|
||||
PolicyDecision,
|
||||
type CheckResult,
|
||||
type PolicyRule,
|
||||
@@ -126,6 +127,23 @@ export async function updatePolicy(
|
||||
|
||||
// Determine persist scope if we are persisting.
|
||||
let persistScope: 'workspace' | 'user' | undefined;
|
||||
let modes: ApprovalMode[] | undefined;
|
||||
const currentMode = context.config.getApprovalMode();
|
||||
|
||||
// If this is an 'Always Allow' selection, we restrict it to the current mode
|
||||
// and more permissive modes.
|
||||
if (
|
||||
outcome === ToolConfirmationOutcome.ProceedAlways ||
|
||||
outcome === ToolConfirmationOutcome.ProceedAlwaysTool ||
|
||||
outcome === ToolConfirmationOutcome.ProceedAlwaysServer ||
|
||||
outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave
|
||||
) {
|
||||
const modeIndex = MODES_BY_PERMISSIVENESS.indexOf(currentMode);
|
||||
if (modeIndex !== -1) {
|
||||
modes = MODES_BY_PERMISSIVENESS.slice(modeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave) {
|
||||
// If folder is trusted and workspace policies are enabled, we prefer workspace scope.
|
||||
if (
|
||||
@@ -147,6 +165,7 @@ export async function updatePolicy(
|
||||
confirmationDetails,
|
||||
messageBus,
|
||||
persistScope,
|
||||
modes,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -160,6 +179,7 @@ export async function updatePolicy(
|
||||
persistScope,
|
||||
toolInvocation,
|
||||
context.config,
|
||||
modes,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -192,6 +212,7 @@ async function handleStandardPolicyUpdate(
|
||||
persistScope?: 'workspace' | 'user',
|
||||
toolInvocation?: AnyToolInvocation,
|
||||
config?: Config,
|
||||
modes?: ApprovalMode[],
|
||||
): Promise<void> {
|
||||
if (
|
||||
outcome === ToolConfirmationOutcome.ProceedAlways ||
|
||||
@@ -214,6 +235,7 @@ async function handleStandardPolicyUpdate(
|
||||
toolName: tool.name,
|
||||
persist: outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave,
|
||||
persistScope,
|
||||
modes,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -232,6 +254,7 @@ async function handleMcpPolicyUpdate(
|
||||
>,
|
||||
messageBus: MessageBus,
|
||||
persistScope?: 'workspace' | 'user',
|
||||
modes?: ApprovalMode[],
|
||||
): Promise<void> {
|
||||
const isMcpAlways =
|
||||
outcome === ToolConfirmationOutcome.ProceedAlways ||
|
||||
@@ -257,5 +280,6 @@ async function handleMcpPolicyUpdate(
|
||||
mcpName: confirmationDetails.serverName,
|
||||
persist,
|
||||
persistScope,
|
||||
modes,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-devtools",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-sdk",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"description": "Gemini CLI SDK",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"displayName": "Gemini CLI Companion",
|
||||
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.37.1",
|
||||
"publisher": "google",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
|
||||
@@ -478,9 +478,9 @@
|
||||
},
|
||||
"loadingPhrases": {
|
||||
"title": "Loading Phrases",
|
||||
"description": "What to show while the model is working: tips, witty comments, both, or nothing.",
|
||||
"markdownDescription": "What to show while the model is working: tips, witty comments, both, or nothing.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `tips`",
|
||||
"default": "tips",
|
||||
"description": "What to show while the model is working: tips, witty comments, all, or off.",
|
||||
"markdownDescription": "What to show while the model is working: tips, witty comments, all, or off.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `off`",
|
||||
"default": "off",
|
||||
"type": "string",
|
||||
"enum": ["tips", "witty", "all", "off"]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user