mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 08:10:57 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68e33a1103 | |||
| 19601955fd |
@@ -2,8 +2,7 @@
|
||||
"experimental": {
|
||||
"extensionReloading": true,
|
||||
"modelSteering": true,
|
||||
"autoMemory": true,
|
||||
"gemma": true
|
||||
"autoMemory": true
|
||||
},
|
||||
"general": {
|
||||
"devtools": true
|
||||
|
||||
@@ -371,8 +371,6 @@ for planned features and priorities.
|
||||
|
||||
## 📖 Resources
|
||||
|
||||
- **[Free Course](https://learn.deeplearning.ai/courses/gemini-cli-code-and-create-with-an-open-source-agent/information)** -
|
||||
Learn the basics.
|
||||
- **[Official Roadmap](./ROADMAP.md)** - See what's coming next.
|
||||
- **[Changelog](https://www.geminicli.com/docs/changelogs)** - See recent
|
||||
notable updates.
|
||||
|
||||
+10
-8
@@ -99,13 +99,16 @@ they appear in the UI.
|
||||
|
||||
### Model
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ----------------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ----------- |
|
||||
| Model | `model.name` | The Gemini model to use for conversations. | `undefined` |
|
||||
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
|
||||
| Context Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
|
||||
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
|
||||
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| --------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
||||
| Model | `model.name` | The Gemini model to use for conversations. | `undefined` |
|
||||
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
|
||||
| Context Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
|
||||
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
|
||||
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
|
||||
| Best Effort Pro | `model.autoRouting.bestEffortPro` | Always prefer the Pro model unless it is unavailable (e.g., due to timeouts or quota), ignoring other routing hints. | `false` |
|
||||
| Pro Timeout (Minutes) | `model.autoRouting.proTimeoutMinutes` | If a Pro request takes longer than this many minutes, it will be marked as temporarily unavailable and fallback to Flash. | `5` |
|
||||
| Pro Timeout Fallback Duration (Minutes) | `model.autoRouting.proTimeoutFallbackDurationMinutes` | How long to route to Flash after Pro times out. | `60` |
|
||||
|
||||
### Agents
|
||||
|
||||
@@ -163,7 +166,6 @@ they appear in the UI.
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ---------------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Gemma Models | `experimental.gemma` | Enable access to Gemma 4 models (experimental). | `false` |
|
||||
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
|
||||
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
|
||||
@@ -117,10 +117,6 @@ the following methods:
|
||||
These methods will trust the current workspace for the duration of the session
|
||||
without prompting.
|
||||
|
||||
For detailed instructions on managing folder trust within CI/CD workflows,
|
||||
review the
|
||||
[Gemini CLI trust guidance for GitHub Actions](https://github.com/google-github-actions/run-gemini-cli/blob/main/docs/trust-guidance.md).
|
||||
|
||||
## Overriding the trust file location
|
||||
|
||||
By default, trust settings are saved to `~/.gemini/trustedFolders.json`. If you
|
||||
|
||||
@@ -483,6 +483,20 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Skip the next speaker check.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`model.autoRouting.bestEffortPro`** (boolean):
|
||||
- **Description:** Always prefer the Pro model unless it is unavailable (e.g.,
|
||||
due to timeouts or quota), ignoring other routing hints.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`model.autoRouting.proTimeoutMinutes`** (number):
|
||||
- **Description:** If a Pro request takes longer than this many minutes, it
|
||||
will be marked as temporarily unavailable and fallback to Flash.
|
||||
- **Default:** `5`
|
||||
|
||||
- **`model.autoRouting.proTimeoutFallbackDurationMinutes`** (number):
|
||||
- **Description:** How long to route to Flash after Pro times out.
|
||||
- **Default:** `60`
|
||||
|
||||
#### `modelConfigs`
|
||||
|
||||
- **`modelConfigs.aliases`** (object):
|
||||
@@ -563,18 +577,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"model": "gemini-2.5-flash-lite"
|
||||
}
|
||||
},
|
||||
"gemma-4-31b-it": {
|
||||
"extends": "chat-base-3",
|
||||
"modelConfig": {
|
||||
"model": "gemma-4-31b-it"
|
||||
}
|
||||
},
|
||||
"gemma-4-26b-a4b-it": {
|
||||
"extends": "chat-base-3",
|
||||
"modelConfig": {
|
||||
"model": "gemma-4-26b-a4b-it"
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash-base": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
@@ -846,28 +848,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"gemma-4-31b-it": {
|
||||
"displayName": "gemma-4-31b-it",
|
||||
"tier": "custom",
|
||||
"family": "gemma-4",
|
||||
"isPreview": false,
|
||||
"isVisible": true,
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"gemma-4-26b-a4b-it": {
|
||||
"displayName": "gemma-4-26b-a4b-it",
|
||||
"tier": "custom",
|
||||
"family": "gemma-4",
|
||||
"isPreview": false,
|
||||
"isVisible": true,
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"auto": {
|
||||
"tier": "auto",
|
||||
"isPreview": true,
|
||||
@@ -938,12 +918,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
```json
|
||||
{
|
||||
"gemma-4-31b-it": {
|
||||
"default": "gemma-4-31b-it"
|
||||
},
|
||||
"gemma-4-26b-a4b-it": {
|
||||
"default": "gemma-4-26b-a4b-it"
|
||||
},
|
||||
"gemini-3.1-pro-preview": {
|
||||
"default": "gemini-3.1-pro-preview",
|
||||
"contexts": [
|
||||
@@ -1507,12 +1481,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `undefined`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`tools.confirmationRequired`** (array):
|
||||
- **Description:** Tool names that always require user confirmation. Takes
|
||||
precedence over allowed tools and core tool allowlists.
|
||||
- **Default:** `undefined`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`tools.exclude`** (array):
|
||||
- **Description:** Tool names to exclude from discovery.
|
||||
- **Default:** `undefined`
|
||||
@@ -1686,11 +1654,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
#### `experimental`
|
||||
|
||||
- **`experimental.gemma`** (boolean):
|
||||
- **Description:** Enable access to Gemma 4 models (experimental).
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.adk.agentSessionNoninteractiveEnabled`** (boolean):
|
||||
- **Description:** Enable non-interactive agent sessions.
|
||||
- **Default:** `false`
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('Hierarchical Memory', () => {
|
||||
params: {
|
||||
settings: {
|
||||
security: {
|
||||
folderTrust: { enabled: false },
|
||||
folderTrust: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -55,7 +55,7 @@ What is my favorite fruit? Tell me just the name of the fruit.`,
|
||||
params: {
|
||||
settings: {
|
||||
security: {
|
||||
folderTrust: { enabled: false },
|
||||
folderTrust: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -96,7 +96,7 @@ Provide the answer as an XML block like this:
|
||||
params: {
|
||||
settings: {
|
||||
security: {
|
||||
folderTrust: { enabled: false },
|
||||
folderTrust: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -18,11 +18,6 @@ describe('save_memory', () => {
|
||||
suiteName: 'default',
|
||||
suiteType: 'behavioral',
|
||||
name: rememberingFavoriteColor,
|
||||
params: {
|
||||
settings: {
|
||||
experimental: { memoryV2: false },
|
||||
},
|
||||
},
|
||||
|
||||
prompt: `remember that my favorite color is blue.
|
||||
|
||||
@@ -45,11 +40,6 @@ describe('save_memory', () => {
|
||||
suiteName: 'default',
|
||||
suiteType: 'behavioral',
|
||||
name: rememberingCommandRestrictions,
|
||||
params: {
|
||||
settings: {
|
||||
experimental: { memoryV2: false },
|
||||
},
|
||||
},
|
||||
|
||||
prompt: `I don't want you to ever run npm commands.`,
|
||||
assert: async (rig, result) => {
|
||||
@@ -71,11 +61,6 @@ describe('save_memory', () => {
|
||||
suiteName: 'default',
|
||||
suiteType: 'behavioral',
|
||||
name: rememberingWorkflow,
|
||||
params: {
|
||||
settings: {
|
||||
experimental: { memoryV2: false },
|
||||
},
|
||||
},
|
||||
|
||||
prompt: `I want you to always lint after building.`,
|
||||
assert: async (rig, result) => {
|
||||
@@ -98,11 +83,6 @@ describe('save_memory', () => {
|
||||
suiteName: 'default',
|
||||
suiteType: 'behavioral',
|
||||
name: ignoringTemporaryInformation,
|
||||
params: {
|
||||
settings: {
|
||||
experimental: { memoryV2: false },
|
||||
},
|
||||
},
|
||||
|
||||
prompt: `I'm going to get a coffee.`,
|
||||
assert: async (rig, result) => {
|
||||
@@ -128,11 +108,6 @@ describe('save_memory', () => {
|
||||
suiteName: 'default',
|
||||
suiteType: 'behavioral',
|
||||
name: rememberingPetName,
|
||||
params: {
|
||||
settings: {
|
||||
experimental: { memoryV2: false },
|
||||
},
|
||||
},
|
||||
|
||||
prompt: `Please remember that my dog's name is Buddy.`,
|
||||
assert: async (rig, result) => {
|
||||
@@ -154,11 +129,6 @@ describe('save_memory', () => {
|
||||
suiteName: 'default',
|
||||
suiteType: 'behavioral',
|
||||
name: rememberingCommandAlias,
|
||||
params: {
|
||||
settings: {
|
||||
experimental: { memoryV2: false },
|
||||
},
|
||||
},
|
||||
|
||||
prompt: `When I say 'start server', you should run 'npm run dev'.`,
|
||||
assert: async (rig, result) => {
|
||||
@@ -181,11 +151,6 @@ describe('save_memory', () => {
|
||||
suiteName: 'default',
|
||||
suiteType: 'behavioral',
|
||||
name: savingDbSchemaLocationAsProjectMemory,
|
||||
params: {
|
||||
settings: {
|
||||
experimental: { memoryV2: false },
|
||||
},
|
||||
},
|
||||
prompt: `The database schema for this workspace is located in \`db/schema.sql\`.`,
|
||||
assert: async (rig, result) => {
|
||||
const wasToolCalled = await rig.waitForToolCall(
|
||||
@@ -215,11 +180,6 @@ describe('save_memory', () => {
|
||||
suiteName: 'default',
|
||||
suiteType: 'behavioral',
|
||||
name: rememberingCodingStyle,
|
||||
params: {
|
||||
settings: {
|
||||
experimental: { memoryV2: false },
|
||||
},
|
||||
},
|
||||
|
||||
prompt: `I prefer to use tabs instead of spaces for indentation.`,
|
||||
assert: async (rig, result) => {
|
||||
@@ -242,11 +202,6 @@ describe('save_memory', () => {
|
||||
suiteName: 'default',
|
||||
suiteType: 'behavioral',
|
||||
name: savingBuildArtifactLocationAsProjectMemory,
|
||||
params: {
|
||||
settings: {
|
||||
experimental: { memoryV2: false },
|
||||
},
|
||||
},
|
||||
prompt: `In this workspace, build artifacts are stored in the \`dist/artifacts\` directory.`,
|
||||
assert: async (rig, result) => {
|
||||
const wasToolCalled = await rig.waitForToolCall(
|
||||
@@ -276,11 +231,6 @@ describe('save_memory', () => {
|
||||
suiteName: 'default',
|
||||
suiteType: 'behavioral',
|
||||
name: savingMainEntryPointAsProjectMemory,
|
||||
params: {
|
||||
settings: {
|
||||
experimental: { memoryV2: false },
|
||||
},
|
||||
},
|
||||
prompt: `The main entry point for this workspace is \`src/index.js\`.`,
|
||||
assert: async (rig, result) => {
|
||||
const wasToolCalled = await rig.waitForToolCall(
|
||||
@@ -309,11 +259,6 @@ describe('save_memory', () => {
|
||||
suiteName: 'default',
|
||||
suiteType: 'behavioral',
|
||||
name: rememberingBirthday,
|
||||
params: {
|
||||
settings: {
|
||||
experimental: { memoryV2: false },
|
||||
},
|
||||
},
|
||||
|
||||
prompt: `My birthday is on June 15th.`,
|
||||
assert: async (rig, result) => {
|
||||
|
||||
@@ -172,7 +172,6 @@ export async function internalEvalTest(evalCase: EvalCase) {
|
||||
timeout: evalCase.timeout,
|
||||
env: {
|
||||
GEMINI_CLI_ACTIVITY_LOG_TARGET: activityLogFile,
|
||||
GEMINI_CLI_TRUST_WORKSPACE: 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Generated
+4
-389
@@ -991,37 +991,6 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz",
|
||||
@@ -1069,24 +1038,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||
@@ -1100,19 +1051,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz",
|
||||
@@ -3795,37 +3733,6 @@
|
||||
"path-browserify": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ts-morph/common/node_modules/brace-expansion": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
@@ -4986,13 +4893,6 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/hosted-git-info": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
|
||||
@@ -5032,30 +4932,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/minimatch/node_modules/brace-expansion": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
@@ -6553,13 +6429,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/config-chain": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
||||
@@ -7075,23 +6944,6 @@
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/depcheck/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/depcheck/node_modules/brace-expansion": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depcheck/node_modules/camelcase": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
@@ -7151,22 +7003,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/depcheck/node_modules/minimatch": {
|
||||
"version": "7.4.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.9.tgz",
|
||||
"integrity": "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/depcheck/node_modules/resolve-from": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
|
||||
@@ -8057,24 +7893,6 @@
|
||||
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
@@ -8085,19 +7903,6 @@
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
@@ -8154,37 +7959,6 @@
|
||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/resolve": {
|
||||
"version": "2.0.0-next.5",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
|
||||
@@ -8243,37 +8017,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint/node_modules/brace-expansion": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||
@@ -11914,12 +11657,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
|
||||
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
@@ -12100,37 +11843,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/multimatch/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/multimatch/node_modules/brace-expansion": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/multimatch/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mute-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
|
||||
@@ -12371,24 +12083,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-all/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/npm-run-all/node_modules/brace-expansion": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-all/node_modules/chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
@@ -12438,19 +12132,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/npm-run-all/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-all/node_modules/normalize-package-data": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||
@@ -15817,39 +15498,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/brace-expansion": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/text-decoder": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
|
||||
@@ -16608,23 +16256,6 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/brace-expansion": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/ignore": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
@@ -16635,22 +16266,6 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
|
||||
+2
-1
@@ -81,7 +81,8 @@
|
||||
"glob": "^12.0.0",
|
||||
"node-domexception": "npm:empty@^0.10.1",
|
||||
"prebuild-install": "npm:nop@1.0.0",
|
||||
"cross-spawn": "^7.0.6"
|
||||
"cross-spawn": "^7.0.6",
|
||||
"minimatch": "^10.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"gemini": "bundle/gemini.js"
|
||||
|
||||
@@ -112,7 +112,6 @@ export function createMockConfig(
|
||||
}),
|
||||
isContextManagementEnabled: vi.fn().mockReturnValue(false),
|
||||
getContextManagementConfig: vi.fn().mockReturnValue({ enabled: false }),
|
||||
getExperimentalGemma: vi.fn().mockReturnValue(false),
|
||||
...overrides,
|
||||
} as unknown as Config;
|
||||
|
||||
|
||||
@@ -3055,18 +3055,6 @@ describe('loadCliConfig gemmaModelRouter', () => {
|
||||
expect(gemmaSettings.classifier?.model).toBe('custom-gemma');
|
||||
});
|
||||
|
||||
it('should load experimental.gemma setting from merged settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: {
|
||||
gemma: true,
|
||||
},
|
||||
});
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getExperimentalGemma()).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle partial gemmaModelRouter settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
|
||||
@@ -1011,7 +1011,6 @@ export async function loadCliConfig(
|
||||
experimentalJitContext,
|
||||
experimentalMemoryV2: settings.experimental?.memoryV2,
|
||||
experimentalAutoMemory: settings.experimental?.autoMemory,
|
||||
experimentalGemma: settings.experimental?.gemma,
|
||||
contextManagement,
|
||||
modelSteering: settings.experimental?.modelSteering,
|
||||
topicUpdateNarration:
|
||||
@@ -1045,6 +1044,7 @@ export async function loadCliConfig(
|
||||
format: (argv.outputFormat ?? settings.output?.format) as OutputFormat,
|
||||
},
|
||||
gemmaModelRouter: settings.experimental?.gemmaModelRouter,
|
||||
autoRouting: settings.model?.autoRouting,
|
||||
adk: settings.experimental?.adk,
|
||||
fakeResponses: argv.fakeResponses,
|
||||
recordResponses: argv.recordResponses,
|
||||
|
||||
@@ -1112,6 +1112,46 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Skip the next speaker check.',
|
||||
showInDialog: true,
|
||||
},
|
||||
autoRouting: {
|
||||
type: 'object',
|
||||
label: 'Auto Routing',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: {},
|
||||
description: 'Settings for automatic model routing.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
bestEffortPro: {
|
||||
type: 'boolean',
|
||||
label: 'Best Effort Pro',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Always prefer the Pro model unless it is unavailable (e.g., due to timeouts or quota), ignoring other routing hints.',
|
||||
showInDialog: true,
|
||||
},
|
||||
proTimeoutMinutes: {
|
||||
type: 'number',
|
||||
label: 'Pro Timeout (Minutes)',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: 5,
|
||||
description:
|
||||
'If a Pro request takes longer than this many minutes, it will be marked as temporarily unavailable and fallback to Flash.',
|
||||
showInDialog: true,
|
||||
},
|
||||
proTimeoutFallbackDurationMinutes: {
|
||||
type: 'number',
|
||||
label: 'Pro Timeout Fallback Duration (Minutes)',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: 60,
|
||||
description: 'How long to route to Flash after Pro times out.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1667,19 +1707,6 @@ const SETTINGS_SCHEMA = {
|
||||
showInDialog: false,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
confirmationRequired: {
|
||||
type: 'array',
|
||||
label: 'Confirmation Required',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as string[] | undefined,
|
||||
description: oneLine`
|
||||
Tool names that always require user confirmation.
|
||||
Takes precedence over allowed tools and core tool allowlists.
|
||||
`,
|
||||
showInDialog: false,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
exclude: {
|
||||
type: 'array',
|
||||
label: 'Exclude Tools',
|
||||
@@ -2052,15 +2079,6 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Setting to enable experimental features',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
gemma: {
|
||||
type: 'boolean',
|
||||
label: 'Gemma Models',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable access to Gemma 4 models (experimental).',
|
||||
showInDialog: true,
|
||||
},
|
||||
adk: {
|
||||
type: 'object',
|
||||
label: 'ADK',
|
||||
|
||||
@@ -306,7 +306,6 @@ describe('gemini.tsx main function cleanup', () => {
|
||||
getMessageBus: () => ({ subscribe: vi.fn() }),
|
||||
getEnableHooks: vi.fn(() => true),
|
||||
getHookSystem: vi.fn(() => undefined),
|
||||
getExperimentalGemma: vi.fn(() => false),
|
||||
initialize: vi.fn(),
|
||||
storage: { initialize: vi.fn().mockResolvedValue(undefined) },
|
||||
getContentGeneratorConfig: vi.fn(),
|
||||
|
||||
@@ -89,7 +89,6 @@ describe('ShellProcessor', () => {
|
||||
getPolicyEngine: vi.fn().mockReturnValue({
|
||||
check: mockPolicyEngineCheck,
|
||||
}),
|
||||
getExperimentalGemma: vi.fn().mockReturnValue(false),
|
||||
get config() {
|
||||
return this as unknown as Config;
|
||||
},
|
||||
|
||||
@@ -124,6 +124,9 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
getCompressionThreshold: vi.fn().mockResolvedValue(undefined),
|
||||
getUserCaching: vi.fn().mockResolvedValue(false),
|
||||
getNumericalRoutingEnabled: vi.fn().mockResolvedValue(false),
|
||||
getBestEffortProEnabled: vi.fn().mockResolvedValue(false),
|
||||
getProTimeoutMinutes: vi.fn().mockResolvedValue(5),
|
||||
getProTimeoutFallbackDurationMinutes: vi.fn().mockResolvedValue(60),
|
||||
getClassifierThreshold: vi.fn().mockResolvedValue(undefined),
|
||||
getBannerTextNoCapacityIssues: vi.fn().mockResolvedValue(''),
|
||||
getBannerTextCapacityIssues: vi.fn().mockResolvedValue(''),
|
||||
@@ -168,7 +171,6 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
getAdminSkillsEnabled: vi.fn().mockReturnValue(false),
|
||||
getDisabledSkills: vi.fn().mockReturnValue([]),
|
||||
getExperimentalJitContext: vi.fn().mockReturnValue(false),
|
||||
getExperimentalGemma: vi.fn().mockReturnValue(false),
|
||||
getMemoryBoundaryMarkers: vi.fn().mockReturnValue(['.git']),
|
||||
getTerminalBackground: vi.fn().mockReturnValue(undefined),
|
||||
getEmbeddingModel: vi.fn().mockReturnValue('embedding-model'),
|
||||
|
||||
@@ -65,7 +65,6 @@ describe('<ModelDialog />', () => {
|
||||
getGemini31FlashLiteLaunchedSync: () => boolean;
|
||||
getProModelNoAccess: () => Promise<boolean>;
|
||||
getProModelNoAccessSync: () => boolean;
|
||||
getExperimentalGemma: () => boolean;
|
||||
getLastRetrievedQuota: () =>
|
||||
| {
|
||||
buckets: Array<{
|
||||
@@ -86,7 +85,6 @@ describe('<ModelDialog />', () => {
|
||||
getGemini31FlashLiteLaunchedSync: mockGetGemini31FlashLiteLaunchedSync,
|
||||
getProModelNoAccess: mockGetProModelNoAccess,
|
||||
getProModelNoAccessSync: mockGetProModelNoAccessSync,
|
||||
getExperimentalGemma: () => false,
|
||||
getLastRetrievedQuota: () => ({ buckets: [] }),
|
||||
getSessionId: () => 'test-session-id',
|
||||
};
|
||||
|
||||
@@ -19,8 +19,6 @@ import {
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
GEMMA_4_31B_IT_MODEL,
|
||||
GEMMA_4_26B_A4B_IT_MODEL,
|
||||
ModelSlashCommandEvent,
|
||||
logModelSlashCommand,
|
||||
getDisplayString,
|
||||
@@ -224,9 +222,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
}
|
||||
|
||||
// --- LEGACY PATH ---
|
||||
const showGemmaModels = config?.getExperimentalGemma() ?? false;
|
||||
|
||||
const options = [
|
||||
const list = [
|
||||
{
|
||||
value: DEFAULT_GEMINI_MODEL,
|
||||
title: getDisplayString(DEFAULT_GEMINI_MODEL),
|
||||
@@ -244,21 +240,6 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
},
|
||||
];
|
||||
|
||||
if (showGemmaModels) {
|
||||
options.push(
|
||||
{
|
||||
value: GEMMA_4_31B_IT_MODEL,
|
||||
title: getDisplayString(GEMMA_4_31B_IT_MODEL),
|
||||
key: GEMMA_4_31B_IT_MODEL,
|
||||
},
|
||||
{
|
||||
value: GEMMA_4_26B_A4B_IT_MODEL,
|
||||
title: getDisplayString(GEMMA_4_26B_A4B_IT_MODEL),
|
||||
key: GEMMA_4_26B_A4B_IT_MODEL,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldShowPreviewModels) {
|
||||
const previewProModel = useGemini31
|
||||
? PREVIEW_GEMINI_3_1_MODEL
|
||||
@@ -289,15 +270,15 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
});
|
||||
}
|
||||
|
||||
options.unshift(...previewOptions);
|
||||
list.unshift(...previewOptions);
|
||||
}
|
||||
|
||||
if (!hasAccessToProModel) {
|
||||
// Filter out all Pro models for free tier
|
||||
return options.filter((option) => !isProModel(option.value));
|
||||
return list.filter((option) => !isProModel(option.value));
|
||||
}
|
||||
|
||||
return options;
|
||||
return list;
|
||||
}, [
|
||||
shouldShowPreviewModels,
|
||||
useGemini31,
|
||||
|
||||
@@ -86,7 +86,6 @@ const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
getProjectTempDir: () => '/tmp/test',
|
||||
},
|
||||
getSessionId: () => 'default-session-id',
|
||||
getExperimentalGemma: () => false,
|
||||
...overrides,
|
||||
}) as Config;
|
||||
|
||||
|
||||
@@ -168,13 +168,6 @@ exports[`InputPrompt > mouse interaction > should toggle paste expansion on doub
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 4`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Pasted Text: 10 lines]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > multiline rendering > should correctly render multiline input including blank lines 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
> hello
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { HintMessage } from './HintMessage.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { makeFakeConfig } from '@google/gemini-cli-core';
|
||||
|
||||
describe('HintMessage', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders normal hint message with correct prefix', async () => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HintMessage text="Try this instead" />,
|
||||
{ width: 80 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('💡');
|
||||
expect(output).toContain('Steering Hint: Try this instead');
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('with NO_COLOR set', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('NO_COLOR', '1');
|
||||
});
|
||||
|
||||
it('uses margins instead of background blocks when NO_COLOR is set', async () => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HintMessage text="Try this instead" />,
|
||||
{ width: 80, config: makeFakeConfig({ useBackgroundColor: true }) },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
// In NO_COLOR mode, the block characters (▄/▀) should NOT be present.
|
||||
expect(output).not.toContain('▄');
|
||||
expect(output).not.toContain('▀');
|
||||
|
||||
const lines = output.split('\n').filter((l) => l.trim() !== '');
|
||||
expect(lines).toHaveLength(1);
|
||||
expect(lines[0]).toContain('💡');
|
||||
expect(lines[0]).toContain('Steering Hint: Try this instead');
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -19,9 +19,7 @@ export const HintMessage: React.FC<HintMessageProps> = ({ text }) => {
|
||||
const prefix = '💡 ';
|
||||
const prefixWidth = prefix.length;
|
||||
const config = useConfig();
|
||||
const useBackgroundColorSetting = config.getUseBackgroundColor();
|
||||
const useBackgroundColor =
|
||||
useBackgroundColorSetting && !!theme.background.message;
|
||||
const useBackgroundColor = config.getUseBackgroundColor();
|
||||
|
||||
return (
|
||||
<HalfLinePaddedBox
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { UserMessage } from './UserMessage.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { makeFakeConfig } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock the commandUtils to control isSlashCommand behavior
|
||||
vi.mock('../../utils/commandUtils.js', () => ({
|
||||
@@ -15,11 +14,6 @@ vi.mock('../../utils/commandUtils.js', () => ({
|
||||
}));
|
||||
|
||||
describe('UserMessage', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders normal user message with correct prefix', async () => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<UserMessage text="Hello Gemini" width={80} />,
|
||||
@@ -66,32 +60,4 @@ describe('UserMessage', () => {
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('with NO_COLOR set', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('NO_COLOR', '1');
|
||||
});
|
||||
|
||||
it('uses margins instead of background blocks when NO_COLOR is set', async () => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<UserMessage text="Hello Gemini" width={80} />,
|
||||
{ width: 80, config: makeFakeConfig({ useBackgroundColor: true }) },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
// In NO_COLOR mode, the block characters (▄/▀) should NOT be present.
|
||||
expect(output).not.toContain('▄');
|
||||
expect(output).not.toContain('▀');
|
||||
|
||||
// There should be empty lines above and below the message due to marginY={1}.
|
||||
// lastFrame() returns the full buffer, so we can check for leading/trailing newlines or empty lines.
|
||||
const lines = output.split('\n').filter((l) => l.trim() !== '');
|
||||
expect(lines).toHaveLength(1);
|
||||
expect(lines[0]).toContain('> Hello Gemini');
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,9 +27,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({ text, width }) => {
|
||||
const prefixWidth = prefix.length;
|
||||
const isSlashCommand = checkIsSlashCommand(text);
|
||||
const config = useConfig();
|
||||
const useBackgroundColorSetting = config.getUseBackgroundColor();
|
||||
const useBackgroundColor =
|
||||
useBackgroundColorSetting && !!theme.background.message;
|
||||
const useBackgroundColor = config.getUseBackgroundColor();
|
||||
|
||||
const textColor = isSlashCommand ? theme.text.accent : theme.text.primary;
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { UserShellMessage } from './UserShellMessage.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { makeFakeConfig } from '@google/gemini-cli-core';
|
||||
|
||||
describe('UserShellMessage', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders normal shell message with correct prefix', async () => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<UserShellMessage text="ls -la" width={80} />,
|
||||
{ width: 80 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('$ ls -la');
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('with NO_COLOR set', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('NO_COLOR', '1');
|
||||
});
|
||||
|
||||
it('uses margins instead of background blocks when NO_COLOR is set', async () => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<UserShellMessage text="ls -la" width={80} />,
|
||||
{ width: 80, config: makeFakeConfig({ useBackgroundColor: true }) },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
// In NO_COLOR mode, the block characters (▄/▀) should NOT be present.
|
||||
expect(output).not.toContain('▄');
|
||||
expect(output).not.toContain('▀');
|
||||
|
||||
const lines = output.split('\n').filter((l) => l.trim() !== '');
|
||||
expect(lines).toHaveLength(1);
|
||||
expect(lines[0]).toContain('$ ls -la');
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,9 +20,7 @@ export const UserShellMessage: React.FC<UserShellMessageProps> = ({
|
||||
width,
|
||||
}) => {
|
||||
const config = useConfig();
|
||||
const useBackgroundColorSetting = config.getUseBackgroundColor();
|
||||
const useBackgroundColor =
|
||||
useBackgroundColorSetting && !!theme.background.message;
|
||||
const useBackgroundColor = config.getUseBackgroundColor();
|
||||
|
||||
// Remove leading '!' if present, as App.tsx adds it for the processor.
|
||||
const commandToDisplay = text.startsWith('!') ? text.substring(1) : text;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`HintMessage > with NO_COLOR set > uses margins instead of background blocks when NO_COLOR is set 1`] = `
|
||||
"
|
||||
💡 Steering Hint: Try this instead
|
||||
"
|
||||
`;
|
||||
@@ -28,9 +28,3 @@ exports[`UserMessage > transforms image paths in user message 1`] = `
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`UserMessage > with NO_COLOR set > uses margins instead of background blocks when NO_COLOR is set 1`] = `
|
||||
"
|
||||
> Hello Gemini
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`UserShellMessage > with NO_COLOR set > uses margins instead of background blocks when NO_COLOR is set 1`] = `
|
||||
"
|
||||
$ ls -la
|
||||
"
|
||||
`;
|
||||
@@ -69,7 +69,6 @@ describe('Session Cleanup (Refactored)', () => {
|
||||
},
|
||||
getSessionId: () => 'current123',
|
||||
getDebugMode: () => false,
|
||||
getExperimentalGemma: () => false,
|
||||
initialize: async () => {},
|
||||
...overrides,
|
||||
} as unknown as Config;
|
||||
|
||||
@@ -96,9 +96,7 @@ const folderTrustCheck: WarningCheck = {
|
||||
|
||||
if (isHeadlessMode()) {
|
||||
throw new FatalUntrustedWorkspaceError(
|
||||
'Gemini CLI is not running in a trusted directory. To proceed, either use `--skip-trust`, ' +
|
||||
'set the `GEMINI_CLI_TRUST_WORKSPACE=true` environment variable, or trust this directory in interactive mode. ' +
|
||||
'For more details, see https://geminicli.com/docs/cli/trusted-folders/#headless-and-automated-environments',
|
||||
'Gemini CLI is not running in a trusted directory. To proceed, either use `--skip-trust`, set the `GEMINI_CLI_TRUST_WORKSPACE=true` environment variable, or trust this directory in interactive mode.',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ export {
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
GEMMA_4_31B_IT_MODEL,
|
||||
GEMMA_4_26B_A4B_IT_MODEL,
|
||||
} from './src/config/models.js';
|
||||
export {
|
||||
serializeTerminalToObject,
|
||||
|
||||
@@ -8,13 +8,15 @@ export type ModelId = string;
|
||||
|
||||
type TerminalUnavailabilityReason = 'quota' | 'capacity';
|
||||
export type TurnUnavailabilityReason = 'retry_once_per_turn';
|
||||
export type TemporaryUnavailabilityReason = 'timeout';
|
||||
|
||||
export type UnavailabilityReason =
|
||||
| TerminalUnavailabilityReason
|
||||
| TurnUnavailabilityReason
|
||||
| TemporaryUnavailabilityReason
|
||||
| 'unknown';
|
||||
|
||||
export type ModelHealthStatus = 'terminal' | 'sticky_retry';
|
||||
export type ModelHealthStatus = 'terminal' | 'sticky_retry' | 'temporary';
|
||||
|
||||
type HealthState =
|
||||
| { status: 'terminal'; reason: TerminalUnavailabilityReason }
|
||||
@@ -22,6 +24,11 @@ type HealthState =
|
||||
status: 'sticky_retry';
|
||||
reason: TurnUnavailabilityReason;
|
||||
consumed: boolean;
|
||||
}
|
||||
| {
|
||||
status: 'temporary';
|
||||
reason: TemporaryUnavailabilityReason;
|
||||
untilMs: number;
|
||||
};
|
||||
|
||||
export interface ModelAvailabilitySnapshot {
|
||||
@@ -48,6 +55,18 @@ export class ModelAvailabilityService {
|
||||
});
|
||||
}
|
||||
|
||||
markTemporarilyUnavailable(
|
||||
model: ModelId,
|
||||
reason: TemporaryUnavailabilityReason,
|
||||
durationMs: number,
|
||||
) {
|
||||
this.setState(model, {
|
||||
status: 'temporary',
|
||||
reason,
|
||||
untilMs: Date.now() + durationMs,
|
||||
});
|
||||
}
|
||||
|
||||
markHealthy(model: ModelId) {
|
||||
this.clearState(model);
|
||||
}
|
||||
@@ -95,6 +114,15 @@ export class ModelAvailabilityService {
|
||||
return { available: false, reason: state.reason };
|
||||
}
|
||||
|
||||
if (state.status === 'temporary') {
|
||||
if (Date.now() < state.untilMs) {
|
||||
return { available: false, reason: state.reason };
|
||||
} else {
|
||||
this.clearState(model);
|
||||
return { available: true };
|
||||
}
|
||||
}
|
||||
|
||||
return { available: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -960,11 +960,8 @@ describe('Server Config (config.ts)', () => {
|
||||
});
|
||||
|
||||
await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
|
||||
await config.getExperimentsAsync();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(config.getModel()).toBe(PREVIEW_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
expect(config.getModel()).toBe(PREVIEW_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
|
||||
it('should NOT switch to flash model if user has Pro access and model is auto', async () => {
|
||||
@@ -3645,47 +3642,6 @@ describe('Config JIT Initialization', () => {
|
||||
expect(config.isAutoMemoryEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when experimentalGemma is true', () => {
|
||||
const params: ConfigParameters = {
|
||||
sessionId: 'test-session',
|
||||
targetDir: '/tmp/test',
|
||||
debugMode: false,
|
||||
model: 'test-model',
|
||||
cwd: '/tmp/test',
|
||||
experimentalGemma: true,
|
||||
};
|
||||
|
||||
config = new Config(params);
|
||||
expect(config.getExperimentalGemma()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when experimentalGemma is false', () => {
|
||||
const params: ConfigParameters = {
|
||||
sessionId: 'test-session',
|
||||
targetDir: '/tmp/test',
|
||||
debugMode: false,
|
||||
model: 'test-model',
|
||||
cwd: '/tmp/test',
|
||||
experimentalGemma: false,
|
||||
};
|
||||
|
||||
config = new Config(params);
|
||||
expect(config.getExperimentalGemma()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when experimentalGemma is not provided', () => {
|
||||
const params: ConfigParameters = {
|
||||
sessionId: 'test-session',
|
||||
targetDir: '/tmp/test',
|
||||
debugMode: false,
|
||||
model: 'test-model',
|
||||
cwd: '/tmp/test',
|
||||
};
|
||||
|
||||
config = new Config(params);
|
||||
expect(config.getExperimentalGemma()).toBe(false);
|
||||
});
|
||||
|
||||
it('should be independent of experimentalMemoryV2', () => {
|
||||
const params: ConfigParameters = {
|
||||
sessionId: 'test-session',
|
||||
|
||||
@@ -679,6 +679,11 @@ export interface ConfigParameters {
|
||||
policyUpdateConfirmationRequest?: PolicyUpdateConfirmationRequest;
|
||||
output?: OutputSettings;
|
||||
gemmaModelRouter?: GemmaModelRouterSettings;
|
||||
autoRouting?: {
|
||||
bestEffortPro?: boolean;
|
||||
proTimeoutMinutes?: number;
|
||||
proTimeoutFallbackDurationMinutes?: number;
|
||||
};
|
||||
adk?: ADKSettings;
|
||||
disableModelRouterForAuth?: AuthType[];
|
||||
continueOnFailedApiCall?: boolean;
|
||||
@@ -711,7 +716,6 @@ export interface ConfigParameters {
|
||||
autoDistillation?: boolean;
|
||||
experimentalMemoryV2?: boolean;
|
||||
experimentalAutoMemory?: boolean;
|
||||
experimentalGemma?: boolean;
|
||||
experimentalContextManagementConfig?: string;
|
||||
experimentalAgentHistoryTruncation?: boolean;
|
||||
experimentalAgentHistoryTruncationThreshold?: number;
|
||||
@@ -957,7 +961,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly experimentalJitContext: boolean;
|
||||
private readonly experimentalMemoryV2: boolean;
|
||||
private readonly experimentalAutoMemory: boolean;
|
||||
private readonly experimentalGemma: boolean;
|
||||
private readonly experimentalContextManagementConfig?: string;
|
||||
private readonly memoryBoundaryMarkers: readonly string[];
|
||||
private readonly topicUpdateNarration: boolean;
|
||||
@@ -965,6 +968,9 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly planEnabled: boolean;
|
||||
private readonly trackerEnabled: boolean;
|
||||
private readonly planModeRoutingEnabled: boolean;
|
||||
private readonly autoRoutingBestEffortPro: boolean;
|
||||
private readonly autoRoutingProTimeoutMinutes: number;
|
||||
private readonly autoRoutingProTimeoutFallbackDurationMinutes: number;
|
||||
private readonly modelSteering: boolean;
|
||||
private memoryContextManager?: MemoryContextManager;
|
||||
private readonly contextManagement: ContextManagementConfig;
|
||||
@@ -1119,6 +1125,11 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.planEnabled = params.plan ?? true;
|
||||
this.trackerEnabled = params.tracker ?? false;
|
||||
this.planModeRoutingEnabled = params.planSettings?.modelRouting ?? true;
|
||||
this.autoRoutingBestEffortPro = params.autoRouting?.bestEffortPro ?? false;
|
||||
this.autoRoutingProTimeoutMinutes =
|
||||
params.autoRouting?.proTimeoutMinutes ?? 5;
|
||||
this.autoRoutingProTimeoutFallbackDurationMinutes =
|
||||
params.autoRouting?.proTimeoutFallbackDurationMinutes ?? 60;
|
||||
this.enableEventDrivenScheduler = params.enableEventDrivenScheduler ?? true;
|
||||
this.skillsSupport = params.skillsSupport ?? true;
|
||||
this.disabledSkills = params.disabledSkills ?? [];
|
||||
@@ -1176,7 +1187,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.experimentalJitContext = params.experimentalJitContext ?? true;
|
||||
this.experimentalMemoryV2 = params.experimentalMemoryV2 ?? true;
|
||||
this.experimentalAutoMemory = params.experimentalAutoMemory ?? false;
|
||||
this.experimentalGemma = params.experimentalGemma ?? false;
|
||||
this.experimentalContextManagementConfig =
|
||||
params.experimentalContextManagementConfig;
|
||||
this.memoryBoundaryMarkers = params.memoryBoundaryMarkers ?? ['.git'];
|
||||
@@ -1593,12 +1603,8 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const [experiments] = await Promise.all([
|
||||
this.experimentsPromise,
|
||||
quotaPromise.catch((e) => {
|
||||
debugLogger.error('Failed to fetch user quota', e);
|
||||
}),
|
||||
]);
|
||||
// Fetch experiments and update timeouts before continuing initialization
|
||||
const experiments = await this.experimentsPromise;
|
||||
|
||||
const requestTimeoutMs = this.getRequestTimeoutMs();
|
||||
if (requestTimeoutMs !== undefined) {
|
||||
@@ -1608,6 +1614,8 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
// Initialize BaseLlmClient now that the ContentGenerator and experiments are available
|
||||
this.baseLlmClient = new BaseLlmClient(this.contentGenerator, this);
|
||||
|
||||
await quotaPromise;
|
||||
|
||||
const authType = this.contentGeneratorConfig.authType;
|
||||
if (
|
||||
authType === AuthType.USE_GEMINI ||
|
||||
@@ -1628,21 +1636,16 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
const adminControlsEnabled =
|
||||
experiments?.flags[ExperimentFlags.ENABLE_ADMIN_CONTROLS]?.boolValue ??
|
||||
false;
|
||||
|
||||
try {
|
||||
const adminControls = await fetchAdminControls(
|
||||
codeAssistServer,
|
||||
this.getRemoteAdminSettings(),
|
||||
adminControlsEnabled,
|
||||
(newSettings: AdminControlsSettings) => {
|
||||
this.setRemoteAdminSettings(newSettings);
|
||||
coreEvents.emitAdminSettingsChanged();
|
||||
},
|
||||
);
|
||||
this.setRemoteAdminSettings(adminControls);
|
||||
} catch (e) {
|
||||
debugLogger.error('Failed to fetch admin controls', e);
|
||||
}
|
||||
const adminControls = await fetchAdminControls(
|
||||
codeAssistServer,
|
||||
this.getRemoteAdminSettings(),
|
||||
adminControlsEnabled,
|
||||
(newSettings: AdminControlsSettings) => {
|
||||
this.setRemoteAdminSettings(newSettings);
|
||||
coreEvents.emitAdminSettingsChanged();
|
||||
},
|
||||
);
|
||||
this.setRemoteAdminSettings(adminControls);
|
||||
|
||||
if ((await this.getProModelNoAccess()) && isAutoModel(this.model)) {
|
||||
this.setModel(PREVIEW_GEMINI_FLASH_MODEL);
|
||||
@@ -2524,10 +2527,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.experimentalAutoMemory;
|
||||
}
|
||||
|
||||
getExperimentalGemma(): boolean {
|
||||
return this.experimentalGemma;
|
||||
}
|
||||
|
||||
getExperimentalContextManagementConfig(): string | undefined {
|
||||
return this.experimentalContextManagementConfig;
|
||||
}
|
||||
@@ -3158,6 +3157,18 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return flag?.boolValue ?? true;
|
||||
}
|
||||
|
||||
async getBestEffortProEnabled(): Promise<boolean> {
|
||||
return this.autoRoutingBestEffortPro;
|
||||
}
|
||||
|
||||
async getProTimeoutMinutes(): Promise<number> {
|
||||
return this.autoRoutingProTimeoutMinutes;
|
||||
}
|
||||
|
||||
async getProTimeoutFallbackDurationMinutes(): Promise<number> {
|
||||
return this.autoRoutingProTimeoutFallbackDurationMinutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resolved complexity threshold for routing.
|
||||
* If a remote threshold is provided and within range (0-100), it is returned.
|
||||
|
||||
@@ -89,19 +89,6 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
model: 'gemini-2.5-flash-lite',
|
||||
},
|
||||
},
|
||||
'gemma-4-31b-it': {
|
||||
extends: 'chat-base-3',
|
||||
modelConfig: {
|
||||
model: 'gemma-4-31b-it',
|
||||
},
|
||||
},
|
||||
'gemma-4-26b-a4b-it': {
|
||||
extends: 'chat-base-3',
|
||||
modelConfig: {
|
||||
model: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
},
|
||||
|
||||
// Bases for the internal model configs.
|
||||
'gemini-2.5-flash-base': {
|
||||
extends: 'base',
|
||||
@@ -330,23 +317,6 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
isVisible: true,
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
'gemma-4-31b-it': {
|
||||
displayName: 'gemma-4-31b-it',
|
||||
tier: 'custom',
|
||||
family: 'gemma-4',
|
||||
isPreview: false,
|
||||
isVisible: true,
|
||||
features: { thinking: true, multimodalToolUse: false },
|
||||
},
|
||||
'gemma-4-26b-a4b-it': {
|
||||
displayName: 'gemma-4-26b-a4b-it',
|
||||
tier: 'custom',
|
||||
family: 'gemma-4',
|
||||
isPreview: false,
|
||||
isVisible: true,
|
||||
features: { thinking: true, multimodalToolUse: false },
|
||||
},
|
||||
|
||||
// Aliases
|
||||
auto: {
|
||||
tier: 'auto',
|
||||
@@ -392,13 +362,6 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
},
|
||||
},
|
||||
modelIdResolutions: {
|
||||
'gemma-4-31b-it': {
|
||||
default: 'gemma-4-31b-it',
|
||||
},
|
||||
'gemma-4-26b-a4b-it': {
|
||||
default: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
|
||||
'gemini-3.1-pro-preview': {
|
||||
default: 'gemini-3.1-pro-preview',
|
||||
contexts: [
|
||||
|
||||
@@ -32,8 +32,6 @@ import {
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
isPreviewModel,
|
||||
isProModel,
|
||||
GEMMA_4_31B_IT_MODEL,
|
||||
GEMMA_4_26B_A4B_IT_MODEL,
|
||||
} from './models.js';
|
||||
import type { Config } from './config.js';
|
||||
import { ModelConfigService } from '../services/modelConfigService.js';
|
||||
@@ -358,10 +356,6 @@ describe('getDisplayString', () => {
|
||||
|
||||
it('should return the model name as is for other models', () => {
|
||||
expect(getDisplayString('custom-model')).toBe('custom-model');
|
||||
expect(getDisplayString(GEMMA_4_31B_IT_MODEL)).toBe(GEMMA_4_31B_IT_MODEL);
|
||||
expect(getDisplayString(GEMMA_4_26B_A4B_IT_MODEL)).toBe(
|
||||
GEMMA_4_26B_A4B_IT_MODEL,
|
||||
);
|
||||
expect(getDisplayString(DEFAULT_GEMINI_FLASH_LITE_MODEL)).toBe(
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
);
|
||||
@@ -579,17 +573,6 @@ describe('isActiveModel', () => {
|
||||
expect(isActiveModel(DEFAULT_GEMINI_FLASH_MODEL)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for Gemma 4 models only when experimentalGemma is true', () => {
|
||||
expect(isActiveModel(GEMMA_4_31B_IT_MODEL)).toBe(false);
|
||||
expect(isActiveModel(GEMMA_4_26B_A4B_IT_MODEL)).toBe(false);
|
||||
expect(isActiveModel(GEMMA_4_31B_IT_MODEL, false, false, false, true)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
isActiveModel(GEMMA_4_26B_A4B_IT_MODEL, false, false, false, true),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for Gemini 3.1 models when Gemini 3.1 is not launched', () => {
|
||||
expect(isActiveModel(PREVIEW_GEMINI_3_1_MODEL)).toBe(false);
|
||||
expect(isActiveModel(PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL)).toBe(false);
|
||||
|
||||
@@ -61,9 +61,6 @@ export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro';
|
||||
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash';
|
||||
export const DEFAULT_GEMINI_FLASH_LITE_MODEL = 'gemini-2.5-flash-lite';
|
||||
|
||||
export const GEMMA_4_31B_IT_MODEL = 'gemma-4-31b-it';
|
||||
export const GEMMA_4_26B_A4B_IT_MODEL = 'gemma-4-26b-a4b-it';
|
||||
|
||||
export const VALID_GEMINI_MODELS = new Set([
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
@@ -73,9 +70,6 @@ export const VALID_GEMINI_MODELS = new Set([
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
|
||||
GEMMA_4_31B_IT_MODEL,
|
||||
GEMMA_4_26B_A4B_IT_MODEL,
|
||||
]);
|
||||
|
||||
export const PREVIEW_GEMINI_MODEL_AUTO = 'auto-gemini-3';
|
||||
@@ -263,10 +257,6 @@ export function getDisplayString(
|
||||
return 'Auto (Gemini 3)';
|
||||
case DEFAULT_GEMINI_MODEL_AUTO:
|
||||
return 'Auto (Gemini 2.5)';
|
||||
case GEMMA_4_31B_IT_MODEL:
|
||||
return GEMMA_4_31B_IT_MODEL;
|
||||
case GEMMA_4_26B_A4B_IT_MODEL:
|
||||
return GEMMA_4_26B_A4B_IT_MODEL;
|
||||
case GEMINI_MODEL_ALIAS_PRO:
|
||||
return PREVIEW_GEMINI_MODEL;
|
||||
case GEMINI_MODEL_ALIAS_FLASH:
|
||||
@@ -448,14 +438,10 @@ export function isActiveModel(
|
||||
useGemini3_1: boolean = false,
|
||||
useGemini3_1FlashLite: boolean = false,
|
||||
useCustomToolModel: boolean = false,
|
||||
experimentalGemma: boolean = false,
|
||||
): boolean {
|
||||
if (!VALID_GEMINI_MODELS.has(model)) {
|
||||
return false;
|
||||
}
|
||||
if (model === GEMMA_4_31B_IT_MODEL || model === GEMMA_4_26B_A4B_IT_MODEL) {
|
||||
return experimentalGemma;
|
||||
}
|
||||
if (model === PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL) {
|
||||
return useGemini3_1FlashLite;
|
||||
}
|
||||
|
||||
@@ -109,6 +109,8 @@ describe('BaseLlmClient', () => {
|
||||
.mockReturnValue({ authType: AuthType.USE_GEMINI }),
|
||||
getEmbeddingModel: vi.fn().mockReturnValue('test-embedding-model'),
|
||||
isInteractive: vi.fn().mockReturnValue(false),
|
||||
getProTimeoutMinutes: vi.fn().mockResolvedValue(5),
|
||||
getProTimeoutFallbackDurationMinutes: vi.fn().mockResolvedValue(60),
|
||||
modelConfigService: {
|
||||
getResolvedConfig: vi
|
||||
.fn()
|
||||
|
||||
@@ -325,11 +325,19 @@ export class BaseLlmClient {
|
||||
);
|
||||
};
|
||||
|
||||
const proTimeoutMinutes = await this.config.getProTimeoutMinutes();
|
||||
const proTimeoutFallbackDurationMinutes =
|
||||
await this.config.getProTimeoutFallbackDurationMinutes();
|
||||
|
||||
return await retryWithBackoff(apiCall, {
|
||||
shouldRetryOnContent,
|
||||
maxAttempts:
|
||||
availabilityMaxAttempts ?? maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
||||
getAvailabilityContext,
|
||||
timeoutFallback: {
|
||||
timeoutMs: proTimeoutMinutes * 60 * 1000,
|
||||
fallbackDurationMs: proTimeoutFallbackDurationMinutes * 60 * 1000,
|
||||
},
|
||||
onPersistent429: this.config.isInteractive()
|
||||
? (authType, error) =>
|
||||
handleFallback(this.config, currentModel, authType, error)
|
||||
|
||||
@@ -160,6 +160,8 @@ describe('GeminiChat', () => {
|
||||
authType: 'oauth-personal',
|
||||
model: currentModel,
|
||||
})),
|
||||
getProTimeoutMinutes: vi.fn().mockResolvedValue(5),
|
||||
getProTimeoutFallbackDurationMinutes: vi.fn().mockResolvedValue(60),
|
||||
getModel: vi.fn().mockImplementation(() => currentModel),
|
||||
setModel: vi.fn().mockImplementation((m: string) => {
|
||||
currentModel = m;
|
||||
|
||||
@@ -687,6 +687,10 @@ export class GeminiChat {
|
||||
);
|
||||
};
|
||||
|
||||
const proTimeoutMinutes = await this.context.config.getProTimeoutMinutes();
|
||||
const proTimeoutFallbackDurationMinutes =
|
||||
await this.context.config.getProTimeoutFallbackDurationMinutes();
|
||||
|
||||
const streamResponse = await retryWithBackoff(apiCall, {
|
||||
onPersistent429: onPersistent429Callback,
|
||||
onValidationRequired: onValidationRequiredCallback,
|
||||
@@ -696,6 +700,10 @@ export class GeminiChat {
|
||||
maxAttempts:
|
||||
availabilityMaxAttempts ?? this.context.config.getMaxAttempts(),
|
||||
getAvailabilityContext,
|
||||
timeoutFallback: {
|
||||
timeoutMs: proTimeoutMinutes * 60 * 1000,
|
||||
fallbackDurationMs: proTimeoutFallbackDurationMinutes * 60 * 1000,
|
||||
},
|
||||
onRetry: (attempt, error, delayMs) => {
|
||||
coreEvents.emitRetryAttempt({
|
||||
attempt,
|
||||
|
||||
@@ -103,6 +103,8 @@ describe('GeminiChat Network Retries', () => {
|
||||
authType: 'oauth-personal',
|
||||
model: 'test-model',
|
||||
}),
|
||||
getProTimeoutMinutes: vi.fn().mockResolvedValue(5),
|
||||
getProTimeoutFallbackDurationMinutes: vi.fn().mockResolvedValue(60),
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getActiveModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
setActiveModel: vi.fn(),
|
||||
|
||||
@@ -10,23 +10,17 @@ import {
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
GEMMA_4_31B_IT_MODEL,
|
||||
GEMMA_4_26B_A4B_IT_MODEL,
|
||||
} from '../config/models.js';
|
||||
|
||||
type Model = string;
|
||||
type TokenCount = number;
|
||||
|
||||
export const DEFAULT_TOKEN_LIMIT = 1_048_576;
|
||||
export const GEMMA_4_TOKEN_LIMIT = 256_000;
|
||||
|
||||
export function tokenLimit(model: Model): TokenCount {
|
||||
// Add other models as they become relevant or if specified by config
|
||||
// Pulled from https://ai.google.dev/gemini-api/docs/models
|
||||
switch (model) {
|
||||
case GEMMA_4_31B_IT_MODEL:
|
||||
case GEMMA_4_26B_A4B_IT_MODEL:
|
||||
return GEMMA_4_TOKEN_LIMIT;
|
||||
case PREVIEW_GEMINI_MODEL:
|
||||
case PREVIEW_GEMINI_FLASH_MODEL:
|
||||
case DEFAULT_GEMINI_MODEL:
|
||||
|
||||
@@ -74,9 +74,7 @@ export const ADMIN_POLICY_TIER = 5;
|
||||
|
||||
export const MCP_EXCLUDED_PRIORITY = USER_POLICY_TIER + 0.9;
|
||||
export const EXCLUDE_TOOLS_FLAG_PRIORITY = USER_POLICY_TIER + 0.4;
|
||||
export const CONFIRMATION_REQUIRED_PRIORITY = USER_POLICY_TIER + 0.35;
|
||||
export const ALLOWED_TOOLS_FLAG_PRIORITY = USER_POLICY_TIER + 0.3;
|
||||
export const CORE_TOOLS_FLAG_PRIORITY = USER_POLICY_TIER + 0.25;
|
||||
export const TRUSTED_MCP_SERVER_PRIORITY = USER_POLICY_TIER + 0.2;
|
||||
export const ALLOWED_MCP_SERVER_PRIORITY = USER_POLICY_TIER + 0.1;
|
||||
|
||||
@@ -436,21 +434,10 @@ export async function createPolicyEngineConfig(
|
||||
}
|
||||
}
|
||||
|
||||
const nonPlanModes = [
|
||||
ApprovalMode.DEFAULT,
|
||||
ApprovalMode.AUTO_EDIT,
|
||||
ApprovalMode.YOLO,
|
||||
];
|
||||
|
||||
const mapToolsToRules = (
|
||||
tools: string[],
|
||||
priority: number,
|
||||
source: string,
|
||||
modes?: ApprovalMode[],
|
||||
addDefaultDenyForTools = false,
|
||||
) => {
|
||||
const toolsWithNarrowing = new Set<string>();
|
||||
for (const tool of tools) {
|
||||
// Tools that are explicitly allowed in the settings.
|
||||
// Priority: ALLOWED_TOOLS_FLAG_PRIORITY (user tier - explicit temporary allows)
|
||||
if (settings.tools?.allowed) {
|
||||
for (const tool of settings.tools.allowed) {
|
||||
// Check for legacy format: toolName(args)
|
||||
const match = tool.match(/^([a-zA-Z0-9_-]+)\((.*)\)$/);
|
||||
if (match) {
|
||||
@@ -462,17 +449,15 @@ export async function createPolicyEngineConfig(
|
||||
|
||||
// Treat args as a command prefix for shell tool
|
||||
if (toolName === SHELL_TOOL_NAME) {
|
||||
toolsWithNarrowing.add(toolName);
|
||||
const patterns = buildArgsPatterns(undefined, args);
|
||||
for (const pattern of patterns) {
|
||||
if (pattern) {
|
||||
rules.push({
|
||||
toolName,
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority,
|
||||
priority: ALLOWED_TOOLS_FLAG_PRIORITY,
|
||||
argsPattern: new RegExp(pattern),
|
||||
source,
|
||||
modes,
|
||||
source: 'Settings (Tools Allowed)',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -482,9 +467,8 @@ export async function createPolicyEngineConfig(
|
||||
rules.push({
|
||||
toolName,
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority,
|
||||
source,
|
||||
modes,
|
||||
priority: ALLOWED_TOOLS_FLAG_PRIORITY,
|
||||
source: 'Settings (Tools Allowed)',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -495,70 +479,11 @@ export async function createPolicyEngineConfig(
|
||||
rules.push({
|
||||
toolName,
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority,
|
||||
source,
|
||||
modes,
|
||||
priority: ALLOWED_TOOLS_FLAG_PRIORITY,
|
||||
source: 'Settings (Tools Allowed)',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (addDefaultDenyForTools) {
|
||||
for (const toolName of toolsWithNarrowing) {
|
||||
rules.push({
|
||||
toolName,
|
||||
decision: PolicyDecision.DENY,
|
||||
priority: priority - 0.01,
|
||||
source: `${source} (Narrowing Enforcement)`,
|
||||
modes,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Tools that are explicitly allowed in the settings.
|
||||
// Priority: ALLOWED_TOOLS_FLAG_PRIORITY (user tier - explicit temporary allows)
|
||||
if (settings.tools?.allowed) {
|
||||
mapToolsToRules(
|
||||
settings.tools.allowed,
|
||||
ALLOWED_TOOLS_FLAG_PRIORITY,
|
||||
'Settings (Tools Allowed)',
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
// Tools that explicitly require confirmation in the settings.
|
||||
// Priority: CONFIRMATION_REQUIRED_PRIORITY (overrides allowed and core)
|
||||
if (settings.tools?.confirmationRequired) {
|
||||
for (const tool of settings.tools.confirmationRequired) {
|
||||
rules.push({
|
||||
toolName: SHELL_TOOL_NAMES.includes(tool) ? SHELL_TOOL_NAME : tool,
|
||||
decision: PolicyDecision.ASK_USER,
|
||||
priority: CONFIRMATION_REQUIRED_PRIORITY,
|
||||
source: 'Settings (Confirmation Required)',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Core tools that are restricted in the settings.
|
||||
// Priority: CORE_TOOLS_FLAG_PRIORITY (user tier - core tool allowlist)
|
||||
if (settings.tools?.core) {
|
||||
mapToolsToRules(
|
||||
settings.tools.core,
|
||||
CORE_TOOLS_FLAG_PRIORITY,
|
||||
'Settings (Core Tools)',
|
||||
nonPlanModes,
|
||||
);
|
||||
|
||||
// If core tools are restricted, we should add a default DENY rule for everything else
|
||||
// at a slightly lower priority than the explicit allows.
|
||||
rules.push({
|
||||
toolName: '*',
|
||||
decision: PolicyDecision.DENY,
|
||||
priority: CORE_TOOLS_FLAG_PRIORITY - 0.01,
|
||||
source: 'Settings (Core Tools Allowlist Enforcement)',
|
||||
modes: nonPlanModes,
|
||||
});
|
||||
}
|
||||
|
||||
// MCP servers that are trusted in the settings.
|
||||
@@ -576,7 +501,6 @@ export async function createPolicyEngineConfig(
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: TRUSTED_MCP_SERVER_PRIORITY,
|
||||
source: 'Settings (MCP Trusted)',
|
||||
modes: nonPlanModes,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -595,7 +519,6 @@ export async function createPolicyEngineConfig(
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: ALLOWED_MCP_SERVER_PRIORITY,
|
||||
source: 'Settings (MCP Allowed)',
|
||||
modes: nonPlanModes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createPolicyEngineConfig } from './config.js';
|
||||
import { PolicyEngine } from './policy-engine.js';
|
||||
import { PolicyDecision, ApprovalMode } from './types.js';
|
||||
|
||||
describe('PolicyEngine - Core Tools Mapping', () => {
|
||||
it('should allow tools explicitly listed in settings.tools.core', async () => {
|
||||
const settings = {
|
||||
tools: {
|
||||
core: ['run_shell_command(ls)', 'run_shell_command(git status)'],
|
||||
},
|
||||
};
|
||||
|
||||
const config = await createPolicyEngineConfig(
|
||||
settings,
|
||||
ApprovalMode.DEFAULT,
|
||||
undefined,
|
||||
true, // interactive
|
||||
);
|
||||
|
||||
const engine = new PolicyEngine(config);
|
||||
|
||||
// Test simple tool name
|
||||
const result1 = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'ls' } },
|
||||
undefined,
|
||||
);
|
||||
expect(result1.decision).toBe(PolicyDecision.ALLOW);
|
||||
|
||||
// Test tool name with args
|
||||
const result2 = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'git status' } },
|
||||
undefined,
|
||||
);
|
||||
expect(result2.decision).toBe(PolicyDecision.ALLOW);
|
||||
|
||||
// Test tool not in core list
|
||||
const result3 = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'npm test' } },
|
||||
undefined,
|
||||
);
|
||||
// Should be DENIED because of strict allowlist
|
||||
expect(result3.decision).toBe(PolicyDecision.DENY);
|
||||
});
|
||||
|
||||
it('should allow tools in tools.core even if they are restricted by default policies', async () => {
|
||||
// By default run_shell_command is ASK_USER.
|
||||
// Putting it in tools.core should make it ALLOW.
|
||||
const settings = {
|
||||
tools: {
|
||||
core: ['run_shell_command'],
|
||||
},
|
||||
};
|
||||
|
||||
const config = await createPolicyEngineConfig(
|
||||
settings,
|
||||
ApprovalMode.DEFAULT,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
const engine = new PolicyEngine(config);
|
||||
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'any command' } },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.ALLOW);
|
||||
});
|
||||
});
|
||||
@@ -43,35 +43,6 @@ vi.mock('../utils/shell-utils.js', async (importOriginal) => {
|
||||
}
|
||||
return [command];
|
||||
}),
|
||||
parseCommandDetails: vi.fn().mockImplementation((command: string) => {
|
||||
// Basic mock implementation for PolicyEngine test needs
|
||||
const commands = command.includes('&&')
|
||||
? command.split('&&').map((c) => c.trim())
|
||||
: [command.trim()];
|
||||
|
||||
// Detect $(...) or `...` and add as sub-commands for recursion tests
|
||||
const subCommands = [...commands];
|
||||
for (const cmd of commands) {
|
||||
const subMatch = cmd.match(/\$\((.*)\)/) || cmd.match(/`(.*)`/);
|
||||
if (subMatch?.[1]) {
|
||||
subCommands.push(subMatch[1].trim());
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
details: subCommands.map((c, i) => ({
|
||||
name: c.split(' ')[0],
|
||||
text: c,
|
||||
startIndex: i === 0 ? 0 : -1, // Simple root indication
|
||||
})),
|
||||
hasError: false,
|
||||
};
|
||||
}),
|
||||
stripShellWrapper: vi.fn().mockImplementation((command: string) => {
|
||||
// Simple mock for stripping wrappers
|
||||
const match = command.match(/^(?:bash|sh|zsh)\s+-c\s+["'](.*)["']$/i);
|
||||
return match ? match[1] : command;
|
||||
}),
|
||||
hasRedirection: vi.fn().mockImplementation(
|
||||
(command: string) =>
|
||||
// Simple mock: true if '>' is present, unless it looks like "-> arrow"
|
||||
@@ -1891,6 +1862,7 @@ describe('PolicyEngine', () => {
|
||||
});
|
||||
|
||||
it('should return ASK_USER in non-YOLO mode if shell command parsing fails', async () => {
|
||||
const { splitCommands } = await import('../utils/shell-utils.js');
|
||||
const rules: PolicyRule[] = [
|
||||
{
|
||||
toolName: 'run_shell_command',
|
||||
@@ -1905,11 +1877,7 @@ describe('PolicyEngine', () => {
|
||||
});
|
||||
|
||||
// Simulate parsing failure
|
||||
const { parseCommandDetails } = await import('../utils/shell-utils.js');
|
||||
vi.mocked(parseCommandDetails).mockReturnValueOnce({
|
||||
details: [],
|
||||
hasError: true,
|
||||
});
|
||||
vi.mocked(splitCommands).mockReturnValueOnce([]);
|
||||
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'complex command' } },
|
||||
|
||||
@@ -7,10 +7,8 @@
|
||||
import { type FunctionCall } from '@google/genai';
|
||||
import {
|
||||
SHELL_TOOL_NAMES,
|
||||
REDIRECTION_NAMES,
|
||||
initializeShellParsers,
|
||||
parseCommandDetails,
|
||||
stripShellWrapper,
|
||||
splitCommands,
|
||||
hasRedirection,
|
||||
extractStringFromParseEntry,
|
||||
} from '../utils/shell-utils.js';
|
||||
@@ -361,8 +359,7 @@ export class PolicyEngine {
|
||||
}
|
||||
|
||||
await initializeShellParsers();
|
||||
const parsed = parseCommandDetails(command);
|
||||
const subCommands = parsed?.details ?? [];
|
||||
const subCommands = splitCommands(command);
|
||||
|
||||
if (subCommands.length === 0) {
|
||||
// If the matched rule says DENY, we should respect it immediately even if parsing fails.
|
||||
@@ -383,109 +380,115 @@ export class PolicyEngine {
|
||||
);
|
||||
|
||||
// Parsing logic failed, we can't trust it. Use default decision ASK_USER (or DENY in non-interactive).
|
||||
// We return the rule that matched so the evaluation loop terminates.
|
||||
return {
|
||||
decision: this.defaultDecision,
|
||||
rule,
|
||||
};
|
||||
}
|
||||
|
||||
debugLogger.debug(
|
||||
`[PolicyEngine.check] Validating shell command: ${subCommands.length} parts`,
|
||||
);
|
||||
// If there are multiple parts, or if we just want to validate the single part against DENY rules
|
||||
if (subCommands.length > 0) {
|
||||
debugLogger.debug(
|
||||
`[PolicyEngine.check] Validating shell command: ${subCommands.length} parts`,
|
||||
);
|
||||
|
||||
if (ruleDecision === PolicyDecision.DENY) {
|
||||
return { decision: PolicyDecision.DENY, rule };
|
||||
}
|
||||
if (ruleDecision === PolicyDecision.DENY) {
|
||||
return { decision: PolicyDecision.DENY, rule };
|
||||
}
|
||||
|
||||
// Start with the decision from the rule or heuristics.
|
||||
// If the tool call was already downgraded (e.g. by heuristics), we start there.
|
||||
let aggregateDecision = ruleDecision;
|
||||
// Start optimistically. If all parts are ALLOW, the whole is ALLOW.
|
||||
// We will downgrade if any part is ASK_USER or DENY.
|
||||
let aggregateDecision = PolicyDecision.ALLOW;
|
||||
let responsibleRule: PolicyRule | undefined;
|
||||
|
||||
// If heuristics downgraded the decision, we don't blame the rule.
|
||||
let responsibleRule: PolicyRule | undefined =
|
||||
rule && ruleDecision === rule.decision ? rule : undefined;
|
||||
|
||||
// Check for redirection on the full command string.
|
||||
// Redirection always downgrades ALLOW to ASK_USER (it never upgrades).
|
||||
if (this.shouldDowngradeForRedirection(command, allowRedirection)) {
|
||||
if (aggregateDecision === PolicyDecision.ALLOW) {
|
||||
// Check for redirection on the full command string
|
||||
if (this.shouldDowngradeForRedirection(command, allowRedirection)) {
|
||||
debugLogger.debug(
|
||||
`[PolicyEngine.check] Downgrading ALLOW to ASK_USER for redirected command: ${command}`,
|
||||
);
|
||||
aggregateDecision = PolicyDecision.ASK_USER;
|
||||
responsibleRule = undefined; // Inherent policy
|
||||
}
|
||||
}
|
||||
|
||||
for (const detail of subCommands) {
|
||||
if (REDIRECTION_NAMES.has(detail.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const subCmd = detail.text.trim();
|
||||
const isAtomic =
|
||||
subCmd === command ||
|
||||
(detail.startIndex === 0 && detail.text.length === command.length);
|
||||
|
||||
// Recursive check for shell wrappers (bash -c, etc.)
|
||||
const stripped = stripShellWrapper(subCmd);
|
||||
if (stripped !== subCmd) {
|
||||
const wrapperResult = await this.check(
|
||||
{ name: toolName, args: { command: stripped, dir_path } },
|
||||
serverName,
|
||||
toolAnnotations,
|
||||
subagent,
|
||||
true,
|
||||
);
|
||||
|
||||
if (wrapperResult.decision === PolicyDecision.DENY)
|
||||
return wrapperResult;
|
||||
if (wrapperResult.decision === PolicyDecision.ASK_USER) {
|
||||
if (aggregateDecision === PolicyDecision.ALLOW) {
|
||||
responsibleRule = wrapperResult.rule;
|
||||
for (const rawSubCmd of subCommands) {
|
||||
const subCmd = rawSubCmd.trim();
|
||||
// Prevent infinite recursion for the root command
|
||||
if (subCmd === command) {
|
||||
if (this.shouldDowngradeForRedirection(subCmd, allowRedirection)) {
|
||||
debugLogger.debug(
|
||||
`[PolicyEngine.check] Downgrading ALLOW to ASK_USER for redirected command: ${subCmd}`,
|
||||
);
|
||||
// Redirection always downgrades ALLOW to ASK_USER
|
||||
if (aggregateDecision === PolicyDecision.ALLOW) {
|
||||
aggregateDecision = PolicyDecision.ASK_USER;
|
||||
responsibleRule = undefined; // Inherent policy
|
||||
}
|
||||
} else {
|
||||
responsibleRule ??= wrapperResult.rule;
|
||||
// Atomic command matching the rule.
|
||||
if (
|
||||
ruleDecision === PolicyDecision.ASK_USER &&
|
||||
aggregateDecision === PolicyDecision.ALLOW
|
||||
) {
|
||||
aggregateDecision = PolicyDecision.ASK_USER;
|
||||
responsibleRule = rule;
|
||||
}
|
||||
}
|
||||
aggregateDecision = PolicyDecision.ASK_USER;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAtomic) {
|
||||
const subResult = await this.check(
|
||||
{ name: toolName, args: { command: subCmd, dir_path } },
|
||||
serverName,
|
||||
toolAnnotations,
|
||||
subagent,
|
||||
true,
|
||||
);
|
||||
|
||||
if (subResult.decision === PolicyDecision.DENY) return subResult;
|
||||
// subResult.decision is already filtered through applyNonInteractiveMode by this.check()
|
||||
const subDecision = subResult.decision;
|
||||
|
||||
if (subResult.decision === PolicyDecision.ASK_USER) {
|
||||
if (aggregateDecision === PolicyDecision.ALLOW) {
|
||||
responsibleRule = subResult.rule;
|
||||
} else {
|
||||
responsibleRule ??= subResult.rule;
|
||||
}
|
||||
aggregateDecision = PolicyDecision.ASK_USER;
|
||||
// If any part is DENIED, the whole command is DENY
|
||||
if (subDecision === PolicyDecision.DENY) {
|
||||
return {
|
||||
decision: PolicyDecision.DENY,
|
||||
rule: subResult.rule,
|
||||
};
|
||||
}
|
||||
|
||||
// Downgrade if sub-command has redirection
|
||||
// If any part requires ASK_USER, the whole command requires ASK_USER
|
||||
if (subDecision === PolicyDecision.ASK_USER) {
|
||||
aggregateDecision = PolicyDecision.ASK_USER;
|
||||
if (!responsibleRule) {
|
||||
responsibleRule = subResult.rule;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for redirection in allowed sub-commands
|
||||
if (
|
||||
subResult.decision === PolicyDecision.ALLOW &&
|
||||
subDecision === PolicyDecision.ALLOW &&
|
||||
this.shouldDowngradeForRedirection(subCmd, allowRedirection)
|
||||
) {
|
||||
debugLogger.debug(
|
||||
`[PolicyEngine.check] Downgrading ALLOW to ASK_USER for redirected command: ${subCmd}`,
|
||||
);
|
||||
if (aggregateDecision === PolicyDecision.ALLOW) {
|
||||
aggregateDecision = PolicyDecision.ASK_USER;
|
||||
responsibleRule = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
decision: aggregateDecision,
|
||||
// If we stayed at ALLOW, we return the original rule (if any).
|
||||
// If we downgraded, we return the responsible rule (or undefined if implicit).
|
||||
rule: aggregateDecision === ruleDecision ? rule : responsibleRule,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
decision: aggregateDecision,
|
||||
rule: aggregateDecision === ruleDecision ? rule : responsibleRule,
|
||||
decision: ruleDecision,
|
||||
rule,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -498,7 +501,6 @@ export class PolicyEngine {
|
||||
serverName: string | undefined,
|
||||
toolAnnotations?: Record<string, unknown>,
|
||||
subagent?: string,
|
||||
skipHeuristics = false,
|
||||
): Promise<CheckResult> {
|
||||
// Case 1: Metadata injection is the primary and safest way to identify an MCP server.
|
||||
// If we have explicit `_serverName` metadata (usually injected by tool-registry for active tools), use it.
|
||||
@@ -592,7 +594,6 @@ export class PolicyEngine {
|
||||
|
||||
let ruleDecision = rule.decision;
|
||||
if (
|
||||
!skipHeuristics &&
|
||||
isShellCommand &&
|
||||
command &&
|
||||
!('commandPrefix' in rule) &&
|
||||
@@ -614,10 +615,12 @@ export class PolicyEngine {
|
||||
subagent,
|
||||
);
|
||||
decision = shellResult.decision;
|
||||
matchedRule = shellResult.rule;
|
||||
break;
|
||||
if (shellResult.rule) {
|
||||
matchedRule = shellResult.rule;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
decision = ruleDecision;
|
||||
decision = rule.decision;
|
||||
matchedRule = rule;
|
||||
break;
|
||||
}
|
||||
@@ -640,7 +643,7 @@ export class PolicyEngine {
|
||||
);
|
||||
if (toolName && SHELL_TOOL_NAMES.includes(toolName)) {
|
||||
let heuristicDecision = this.defaultDecision;
|
||||
if (!skipHeuristics && command) {
|
||||
if (command) {
|
||||
heuristicDecision = await this.applyShellHeuristics(
|
||||
command,
|
||||
heuristicDecision,
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { PolicyEngine } from './policy-engine.js';
|
||||
import { PolicyDecision, ApprovalMode } from './types.js';
|
||||
import { initializeShellParsers } from '../utils/shell-utils.js';
|
||||
import { buildArgsPatterns } from './utils.js';
|
||||
|
||||
describe('PolicyEngine - Shell Safety Regression Suite', () => {
|
||||
let engine: PolicyEngine;
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeShellParsers();
|
||||
});
|
||||
|
||||
const setupEngine = (allowedCommands: string[]) => {
|
||||
const rules = allowedCommands.map((cmd) => ({
|
||||
toolName: 'run_shell_command',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
argsPattern: new RegExp(buildArgsPatterns(undefined, cmd)[0]!),
|
||||
priority: 10,
|
||||
}));
|
||||
|
||||
return new PolicyEngine({
|
||||
rules,
|
||||
approvalMode: ApprovalMode.DEFAULT,
|
||||
defaultDecision: PolicyDecision.ASK_USER,
|
||||
});
|
||||
};
|
||||
|
||||
it('should block unauthorized chained command with &&', async () => {
|
||||
engine = setupEngine(['echo']);
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'echo hi && ls' } },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.ASK_USER);
|
||||
});
|
||||
|
||||
it('should allow authorized chained command with &&', async () => {
|
||||
engine = setupEngine(['echo', 'ls']);
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'echo hi && ls' } },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.ALLOW);
|
||||
});
|
||||
|
||||
it('should block unauthorized chained command with ||', async () => {
|
||||
engine = setupEngine(['false']);
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'false || ls' } },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.ASK_USER);
|
||||
});
|
||||
|
||||
it('should block unauthorized chained command with ;', async () => {
|
||||
engine = setupEngine(['echo']);
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'echo hi; ls' } },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.ASK_USER);
|
||||
});
|
||||
|
||||
it('should block unauthorized command in pipe |', async () => {
|
||||
engine = setupEngine(['echo']);
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'echo hi | grep "hi"' } },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.ASK_USER);
|
||||
});
|
||||
|
||||
it('should allow authorized command in pipe |', async () => {
|
||||
engine = setupEngine(['echo', 'grep']);
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'echo hi | grep "hi"' } },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.ALLOW);
|
||||
});
|
||||
|
||||
it('should block unauthorized chained command with &', async () => {
|
||||
engine = setupEngine(['echo']);
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'echo hi & ls' } },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.ASK_USER);
|
||||
});
|
||||
|
||||
it('should allow authorized chained command with &', async () => {
|
||||
engine = setupEngine(['echo', 'ls']);
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'echo hi & ls' } },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.ALLOW);
|
||||
});
|
||||
|
||||
it('should block unauthorized command in nested substitution', async () => {
|
||||
engine = setupEngine(['echo', 'cat']);
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'echo $(cat $(ls))' } },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.ASK_USER);
|
||||
});
|
||||
|
||||
it('should allow authorized command in nested substitution', async () => {
|
||||
engine = setupEngine(['echo', 'cat', 'ls']);
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'echo $(cat $(ls))' } },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.ALLOW);
|
||||
});
|
||||
|
||||
it('should block command redirection if not explicitly allowed', async () => {
|
||||
engine = setupEngine(['echo']);
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'echo hi > /tmp/test' } },
|
||||
undefined,
|
||||
);
|
||||
// Inherent policy: redirection downgrades to ASK_USER
|
||||
expect(result.decision).toBe(PolicyDecision.ASK_USER);
|
||||
});
|
||||
});
|
||||
@@ -59,30 +59,6 @@ vi.mock('../utils/shell-utils.js', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
initializeShellParsers: vi.fn(),
|
||||
parseCommandDetails: (command: string) => {
|
||||
if (Object.prototype.hasOwnProperty.call(commandMap, command)) {
|
||||
const subcommands = commandMap[command];
|
||||
return {
|
||||
details: subcommands.map((text) => ({
|
||||
name: text.split(' ')[0],
|
||||
text,
|
||||
startIndex: command.indexOf(text),
|
||||
})),
|
||||
hasError: subcommands.length === 0 && command.includes('&&&'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
details: [
|
||||
{
|
||||
name: command.split(' ')[0],
|
||||
text: command,
|
||||
startIndex: 0,
|
||||
},
|
||||
],
|
||||
hasError: false,
|
||||
};
|
||||
},
|
||||
stripShellWrapper: (command: string) => command,
|
||||
splitCommands: (command: string) => {
|
||||
if (Object.prototype.hasOwnProperty.call(commandMap, command)) {
|
||||
return commandMap[command];
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { expect, describe, it, beforeAll, vi } from 'vitest';
|
||||
import { PolicyEngine } from './policy-engine.js';
|
||||
import { PolicyDecision } from './types.js';
|
||||
import { initializeShellParsers } from '../utils/shell-utils.js';
|
||||
|
||||
// Mock node:os to ensure shell-utils logic always thinks it's on a POSIX-like system.
|
||||
// This ensures that internal calls to getShellConfiguration() and isWindows()
|
||||
// within the shell-utils module return 'bash' configuration, even on Windows CI.
|
||||
vi.mock('node:os', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:os')>();
|
||||
return {
|
||||
...actual,
|
||||
default: {
|
||||
...actual,
|
||||
platform: () => 'linux',
|
||||
},
|
||||
platform: () => 'linux',
|
||||
};
|
||||
});
|
||||
|
||||
// Mock shell-utils to ensure consistent behavior across platforms (especially Windows CI)
|
||||
// We want to test PolicyEngine logic with Bash syntax rules.
|
||||
vi.mock('../utils/shell-utils.js', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../utils/shell-utils.js')>();
|
||||
return {
|
||||
...actual,
|
||||
getShellConfiguration: () => ({
|
||||
executable: 'bash',
|
||||
argsPrefix: ['-c'],
|
||||
shell: 'bash',
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('PolicyEngine Command Substitution Validation', () => {
|
||||
beforeAll(async () => {
|
||||
await initializeShellParsers();
|
||||
});
|
||||
|
||||
const setupEngine = (blockedCmd: string) =>
|
||||
new PolicyEngine({
|
||||
defaultDecision: PolicyDecision.ALLOW,
|
||||
rules: [
|
||||
{
|
||||
toolName: 'run_shell_command',
|
||||
argsPattern: new RegExp(`"command":"${blockedCmd}"`),
|
||||
decision: PolicyDecision.DENY,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
it('should block echo $(dangerous_cmd) when dangerous_cmd is explicitly blocked', async () => {
|
||||
const engine = setupEngine('dangerous_cmd');
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'echo $(dangerous_cmd)' } },
|
||||
'test-server',
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.DENY);
|
||||
});
|
||||
|
||||
it('should block backtick substitution `dangerous_cmd`', async () => {
|
||||
const engine = setupEngine('dangerous_cmd');
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: 'echo `dangerous_cmd`' } },
|
||||
'test-server',
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.DENY);
|
||||
});
|
||||
|
||||
it('should block commands inside subshells (dangerous_cmd)', async () => {
|
||||
const engine = setupEngine('dangerous_cmd');
|
||||
const result = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command: '(dangerous_cmd)' } },
|
||||
'test-server',
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.DENY);
|
||||
});
|
||||
|
||||
it('should handle nested substitutions deeply', async () => {
|
||||
const engine = setupEngine('deep_danger');
|
||||
const result = await engine.check(
|
||||
{
|
||||
name: 'run_shell_command',
|
||||
args: { command: 'echo $(ls $(deep_danger))' },
|
||||
},
|
||||
'test-server',
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.DENY);
|
||||
});
|
||||
});
|
||||
@@ -335,10 +335,8 @@ export interface PolicySettings {
|
||||
allowed?: string[];
|
||||
};
|
||||
tools?: {
|
||||
core?: string[];
|
||||
exclude?: string[];
|
||||
allowed?: string[];
|
||||
confirmationRequired?: string[];
|
||||
};
|
||||
mcpServers?: Record<string, { trust?: boolean }>;
|
||||
// User provided policies that will replace the USER level policies in ~/.gemini/policies
|
||||
|
||||
@@ -32,6 +32,9 @@ vi.mock('./strategies/overrideStrategy.js');
|
||||
vi.mock('./strategies/approvalModeStrategy.js');
|
||||
vi.mock('./strategies/classifierStrategy.js');
|
||||
vi.mock('./strategies/numericalClassifierStrategy.js');
|
||||
import { BestEffortProStrategy } from './strategies/bestEffortProStrategy.js';
|
||||
|
||||
vi.mock('./strategies/bestEffortProStrategy.js');
|
||||
vi.mock('./strategies/gemmaClassifierStrategy.js');
|
||||
vi.mock('../telemetry/loggers.js');
|
||||
vi.mock('../telemetry/types.js');
|
||||
@@ -74,6 +77,7 @@ describe('ModelRouterService', () => {
|
||||
[
|
||||
new FallbackStrategy(),
|
||||
new OverrideStrategy(),
|
||||
new BestEffortProStrategy(),
|
||||
new ApprovalModeStrategy(),
|
||||
new ClassifierStrategy(),
|
||||
new NumericalClassifierStrategy(),
|
||||
@@ -104,13 +108,14 @@ describe('ModelRouterService', () => {
|
||||
const compositeStrategyArgs = vi.mocked(CompositeStrategy).mock.calls[0];
|
||||
const childStrategies = compositeStrategyArgs[0];
|
||||
|
||||
expect(childStrategies.length).toBe(6);
|
||||
expect(childStrategies.length).toBe(7);
|
||||
expect(childStrategies[0]).toBeInstanceOf(FallbackStrategy);
|
||||
expect(childStrategies[1]).toBeInstanceOf(OverrideStrategy);
|
||||
expect(childStrategies[2]).toBeInstanceOf(ApprovalModeStrategy);
|
||||
expect(childStrategies[3]).toBeInstanceOf(ClassifierStrategy);
|
||||
expect(childStrategies[4]).toBeInstanceOf(NumericalClassifierStrategy);
|
||||
expect(childStrategies[5]).toBeInstanceOf(DefaultStrategy);
|
||||
expect(childStrategies[2]).toBeInstanceOf(BestEffortProStrategy);
|
||||
expect(childStrategies[3]).toBeInstanceOf(ApprovalModeStrategy);
|
||||
expect(childStrategies[4]).toBeInstanceOf(ClassifierStrategy);
|
||||
expect(childStrategies[5]).toBeInstanceOf(NumericalClassifierStrategy);
|
||||
expect(childStrategies[6]).toBeInstanceOf(DefaultStrategy);
|
||||
expect(compositeStrategyArgs[1]).toBe('agent-router');
|
||||
});
|
||||
|
||||
@@ -133,14 +138,15 @@ describe('ModelRouterService', () => {
|
||||
const compositeStrategyArgs = vi.mocked(CompositeStrategy).mock.calls[0];
|
||||
const childStrategies = compositeStrategyArgs[0];
|
||||
|
||||
expect(childStrategies.length).toBe(7);
|
||||
expect(childStrategies.length).toBe(8);
|
||||
expect(childStrategies[0]).toBeInstanceOf(FallbackStrategy);
|
||||
expect(childStrategies[1]).toBeInstanceOf(OverrideStrategy);
|
||||
expect(childStrategies[2]).toBeInstanceOf(ApprovalModeStrategy);
|
||||
expect(childStrategies[3]).toBeInstanceOf(GemmaClassifierStrategy);
|
||||
expect(childStrategies[4]).toBeInstanceOf(ClassifierStrategy);
|
||||
expect(childStrategies[5]).toBeInstanceOf(NumericalClassifierStrategy);
|
||||
expect(childStrategies[6]).toBeInstanceOf(DefaultStrategy);
|
||||
expect(childStrategies[2]).toBeInstanceOf(BestEffortProStrategy);
|
||||
expect(childStrategies[3]).toBeInstanceOf(ApprovalModeStrategy);
|
||||
expect(childStrategies[4]).toBeInstanceOf(GemmaClassifierStrategy);
|
||||
expect(childStrategies[5]).toBeInstanceOf(ClassifierStrategy);
|
||||
expect(childStrategies[6]).toBeInstanceOf(NumericalClassifierStrategy);
|
||||
expect(childStrategies[7]).toBeInstanceOf(DefaultStrategy);
|
||||
expect(compositeStrategyArgs[1]).toBe('agent-router');
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { NumericalClassifierStrategy } from './strategies/numericalClassifierStr
|
||||
import { CompositeStrategy } from './strategies/compositeStrategy.js';
|
||||
import { FallbackStrategy } from './strategies/fallbackStrategy.js';
|
||||
import { OverrideStrategy } from './strategies/overrideStrategy.js';
|
||||
import { BestEffortProStrategy } from './strategies/bestEffortProStrategy.js';
|
||||
import { ApprovalModeStrategy } from './strategies/approvalModeStrategy.js';
|
||||
|
||||
import { logModelRouting } from '../telemetry/loggers.js';
|
||||
@@ -43,6 +44,9 @@ export class ModelRouterService {
|
||||
strategies.push(new FallbackStrategy());
|
||||
strategies.push(new OverrideStrategy());
|
||||
|
||||
// Best Effort Pro is next.
|
||||
strategies.push(new BestEffortProStrategy());
|
||||
|
||||
// Approval mode is next.
|
||||
strategies.push(new ApprovalModeStrategy());
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Config } from '../../config/config.js';
|
||||
import { isAutoModel, resolveModel } from '../../config/models.js';
|
||||
import type {
|
||||
RoutingStrategy,
|
||||
RoutingDecision,
|
||||
RoutingContext,
|
||||
} from '../routingStrategy.js';
|
||||
|
||||
/**
|
||||
* A routing strategy that respects the "Best Effort Pro" setting.
|
||||
* If the setting is enabled and the Pro model is available, it routes to Pro
|
||||
* regardless of complexity. If Pro is unavailable, it routes to Flash.
|
||||
*/
|
||||
export class BestEffortProStrategy implements RoutingStrategy {
|
||||
name = 'best-effort-pro';
|
||||
|
||||
async route(
|
||||
context: RoutingContext,
|
||||
config: Config,
|
||||
): Promise<RoutingDecision | null> {
|
||||
const requestedModel = config.getModel();
|
||||
if (!isAutoModel(requestedModel)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isBestEffortProEnabled = await config.getBestEffortProEnabled();
|
||||
if (!isBestEffortProEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const useGemini3_1 = (await config.getGemini31Launched?.()) ?? false;
|
||||
const useGemini3_1FlashLite =
|
||||
(await config.getGemini31FlashLiteLaunched?.()) ?? false;
|
||||
const hasAccessToPreview = config.getHasAccessToPreviewModel?.() ?? true;
|
||||
|
||||
const availabilityService = config.getModelAvailabilityService();
|
||||
const proModel = resolveModel(
|
||||
'gemini-3.1-pro',
|
||||
useGemini3_1,
|
||||
useGemini3_1FlashLite,
|
||||
false,
|
||||
hasAccessToPreview,
|
||||
config,
|
||||
);
|
||||
const flashModel = resolveModel(
|
||||
'gemini-3.1-flash',
|
||||
useGemini3_1,
|
||||
useGemini3_1FlashLite,
|
||||
false,
|
||||
hasAccessToPreview,
|
||||
config,
|
||||
);
|
||||
|
||||
const proSnapshot = availabilityService.snapshot(proModel);
|
||||
|
||||
if (proSnapshot.available) {
|
||||
return {
|
||||
model: proModel,
|
||||
metadata: {
|
||||
source: this.name,
|
||||
latencyMs: 0,
|
||||
reasoning:
|
||||
'Best Effort Pro is enabled and the Pro model is available.',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
model: flashModel,
|
||||
metadata: {
|
||||
source: this.name,
|
||||
latencyMs: 0,
|
||||
reasoning: `Best Effort Pro is enabled, but Pro is unavailable (${proSnapshot.reason}). Falling back to Flash.`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,30 +97,6 @@
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemma-4-31b-it": {
|
||||
"model": "gemma-4-31b-it",
|
||||
"generateContentConfig": {
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingLevel": "HIGH"
|
||||
},
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemma-4-26b-a4b-it": {
|
||||
"model": "gemma-4-26b-a4b-it",
|
||||
"generateContentConfig": {
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingLevel": "HIGH"
|
||||
},
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash-base": {
|
||||
"model": "gemini-2.5-flash",
|
||||
"generateContentConfig": {
|
||||
|
||||
@@ -97,30 +97,6 @@
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemma-4-31b-it": {
|
||||
"model": "gemma-4-31b-it",
|
||||
"generateContentConfig": {
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingLevel": "HIGH"
|
||||
},
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemma-4-26b-a4b-it": {
|
||||
"model": "gemma-4-26b-a4b-it",
|
||||
"generateContentConfig": {
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingLevel": "HIGH"
|
||||
},
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash-base": {
|
||||
"model": "gemini-2.5-flash",
|
||||
"generateContentConfig": {
|
||||
|
||||
@@ -1461,8 +1461,8 @@ describe('mcp-client', () => {
|
||||
// Trigger notification - should fail internally but catch the error
|
||||
await notificationCallback();
|
||||
|
||||
// Should NOT try to remove tools when discovery fails
|
||||
expect(mockedToolRegistry.removeMcpToolsByServer).not.toHaveBeenCalled();
|
||||
// Should try to remove tools
|
||||
expect(mockedToolRegistry.removeMcpToolsByServer).toHaveBeenCalled();
|
||||
|
||||
// Should NOT emit success feedback
|
||||
expect(coreEvents.emitFeedback).not.toHaveBeenCalledWith(
|
||||
|
||||
@@ -1344,7 +1344,6 @@ export async function discoverTools(
|
||||
error,
|
||||
mcpServerName,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1993,15 +1993,13 @@ describe('getRipgrepPath', () => {
|
||||
vi.mocked(fileExists).mockImplementation(
|
||||
async (checkPath) =>
|
||||
checkPath.includes(path.normalize('core/vendor/ripgrep')) &&
|
||||
!checkPath.includes(path.join(path.sep, 'tools', path.sep)),
|
||||
!checkPath.includes('tools'),
|
||||
);
|
||||
|
||||
const resolvedPath = await getRipgrepPath();
|
||||
expect(resolvedPath).not.toBeNull();
|
||||
expect(resolvedPath).toContain(path.normalize('core/vendor/ripgrep'));
|
||||
expect(resolvedPath).not.toContain(
|
||||
path.join(path.sep, 'tools', path.sep),
|
||||
);
|
||||
expect(resolvedPath).not.toContain('tools');
|
||||
});
|
||||
|
||||
it('should return null if binary is missing from both paths', async () => {
|
||||
|
||||
@@ -37,7 +37,6 @@ describe('Tracker Tools Integration', () => {
|
||||
model: 'gemini-3-flash',
|
||||
debugMode: false,
|
||||
});
|
||||
await config.initialize();
|
||||
messageBus = new MessageBus(null as unknown as PolicyEngine, false);
|
||||
});
|
||||
|
||||
@@ -121,14 +120,8 @@ describe('Tracker Tools Integration', () => {
|
||||
);
|
||||
|
||||
const tasks = await config.getTrackerService().listTasks();
|
||||
const parentTask = tasks.find((t) => t.title === 'Parent Task');
|
||||
const childTask = tasks.find((t) => t.title === 'Child Task');
|
||||
|
||||
expect(parentTask).toBeDefined();
|
||||
expect(childTask).toBeDefined();
|
||||
|
||||
const parentId = parentTask!.id;
|
||||
const childId = childTask!.id;
|
||||
const parentId = tasks.find((t) => t.title === 'Parent Task')!.id;
|
||||
const childId = tasks.find((t) => t.title === 'Child Task')!.id;
|
||||
|
||||
// Add Dependency
|
||||
const addDepTool = new TrackerAddDependencyTool(config, messageBus);
|
||||
|
||||
@@ -37,6 +37,10 @@ export interface RetryOptions {
|
||||
signal?: AbortSignal;
|
||||
getAvailabilityContext?: () => RetryAvailabilityContext | undefined;
|
||||
onRetry?: (attempt: number, error: unknown, delayMs: number) => void;
|
||||
timeoutFallback?: {
|
||||
timeoutMs: number;
|
||||
fallbackDurationMs: number;
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_RETRY_OPTIONS: RetryOptions = {
|
||||
@@ -240,6 +244,7 @@ export async function retryWithBackoff<T>(
|
||||
signal,
|
||||
getAvailabilityContext,
|
||||
onRetry,
|
||||
timeoutFallback,
|
||||
} = {
|
||||
...DEFAULT_RETRY_OPTIONS,
|
||||
shouldRetryOnError: isRetryableError,
|
||||
@@ -248,6 +253,7 @@ export async function retryWithBackoff<T>(
|
||||
|
||||
let attempt = 0;
|
||||
let currentDelay = initialDelayMs;
|
||||
let startTime = Date.now();
|
||||
const throwIfAborted = () => {
|
||||
if (signal?.aborted) {
|
||||
throw createAbortError();
|
||||
@@ -294,6 +300,42 @@ export async function retryWithBackoff<T>(
|
||||
|
||||
const errorCode = getErrorStatus(error);
|
||||
|
||||
const isTimeout =
|
||||
(error instanceof Error &&
|
||||
error.message.toLowerCase().includes('timeout')) ||
|
||||
getRetryErrorType(error) === 'ETIMEDOUT' ||
|
||||
getRetryErrorType(error) === 'FETCH_FAILED';
|
||||
|
||||
if (isTimeout && timeoutFallback) {
|
||||
if (Date.now() - startTime >= timeoutFallback.timeoutMs) {
|
||||
const successContext = getAvailabilityContext?.();
|
||||
if (successContext) {
|
||||
successContext.service.markTemporarilyUnavailable(
|
||||
successContext.policy.model,
|
||||
'timeout',
|
||||
timeoutFallback.fallbackDurationMs,
|
||||
);
|
||||
}
|
||||
if (onPersistent429) {
|
||||
try {
|
||||
const fallbackModel = await onPersistent429(
|
||||
authType,
|
||||
new Error('Request timed out'),
|
||||
);
|
||||
if (fallbackModel) {
|
||||
attempt = 0;
|
||||
currentDelay = initialDelayMs;
|
||||
startTime = Date.now();
|
||||
continue;
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
debugLogger.warn('Model fallback failed:', fallbackError);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
classifiedError instanceof TerminalQuotaError ||
|
||||
classifiedError instanceof ModelNotFoundError
|
||||
|
||||
@@ -240,15 +240,11 @@ foreach ($commandAst in $commandAsts) {
|
||||
'utf16le',
|
||||
).toString('base64');
|
||||
|
||||
export const REDIRECTION_NAMES = new Set([
|
||||
const REDIRECTION_NAMES = new Set([
|
||||
'redirection (<)',
|
||||
'redirection (>)',
|
||||
'heredoc (<<)',
|
||||
'herestring (<<<)',
|
||||
'command substitution',
|
||||
'backtick substitution',
|
||||
'process substitution',
|
||||
'subshell',
|
||||
]);
|
||||
|
||||
function createParser(): Parser | null {
|
||||
@@ -364,14 +360,6 @@ function extractNameFromNode(node: Node): string | null {
|
||||
return 'heredoc (<<)';
|
||||
case 'herestring_redirect':
|
||||
return 'herestring (<<<)';
|
||||
case 'command_substitution':
|
||||
return 'command substitution';
|
||||
case 'backtick_substitution':
|
||||
return 'backtick substitution';
|
||||
case 'process_substitution':
|
||||
return 'process substitution';
|
||||
case 'subshell':
|
||||
return 'subshell';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -680,7 +680,6 @@ export class TestRig {
|
||||
key !== 'GEMINI_DEBUG' &&
|
||||
key !== 'GEMINI_CLI_TEST_VAR' &&
|
||||
key !== 'GEMINI_CLI_INTEGRATION_TEST' &&
|
||||
key !== 'GOOGLE_GEMINI_BASE_URL' &&
|
||||
!key.startsWith('GEMINI_CLI_ACTIVITY_LOG')
|
||||
) {
|
||||
delete cleanEnv[key];
|
||||
|
||||
+35
-100
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user