Merge branch 'main' into fix/default-theme-256

This commit is contained in:
Mark McLaughlin
2026-03-24 15:17:10 -07:00
committed by GitHub
25 changed files with 291 additions and 125 deletions
+5 -5
View File
@@ -104,7 +104,7 @@ Gemini CLI supports the following authentication types:
| `apiKey` | Send a static API key as an HTTP header. |
| `http` | HTTP authentication (Bearer token, Basic credentials, or any IANA-registered scheme). |
| `google-credentials` | Google Application Default Credentials (ADC). Automatically selects access or identity tokens. |
| `oauth2` | OAuth 2.0 Authorization Code flow with PKCE. Opens a browser for interactive sign-in. |
| `oauth` | OAuth 2.0 Authorization Code flow with PKCE. Opens a browser for interactive sign-in. |
### Dynamic values
@@ -263,7 +263,7 @@ hosts:
Requests to any other host will be rejected with an error. If your agent is
hosted on a different domain, use one of the other auth types (`apiKey`, `http`,
or `oauth2`).
or `oauth`).
#### Examples
@@ -297,7 +297,7 @@ auth:
---
```
### OAuth 2.0 (`oauth2`)
### OAuth 2.0 (`oauth`)
Performs an interactive OAuth 2.0 Authorization Code flow with PKCE. On first
use, Gemini CLI opens your browser for sign-in and persists the resulting tokens
@@ -305,7 +305,7 @@ for subsequent requests.
| Field | Type | Required | Description |
| :------------------ | :------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | string | Yes | Must be `oauth2`. |
| `type` | string | Yes | Must be `oauth`. |
| `client_id` | string | Yes\* | OAuth client ID. Required for interactive auth. |
| `client_secret` | string | No\* | OAuth client secret. Required by most authorization servers (confidential clients). Can be omitted for public clients that don't require a secret. |
| `scopes` | string[] | No | Requested scopes. Can also be discovered from the agent card. |
@@ -318,7 +318,7 @@ kind: remote
name: oauth-agent
agent_card_url: https://example.com/.well-known/agent.json
auth:
type: oauth2
type: oauth
client_id: my-client-id.apps.example.com
---
```
+6 -1
View File
@@ -1215,6 +1215,11 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Disable user input on browser window during automation.
- **Default:** `true`
- **`agents.browser.maxActionsPerTask`** (number):
- **Description:** The maximum number of tool calls allowed per browser task.
Enforcement is hard: the agent will be terminated when the limit is reached.
- **Default:** `100`
- **`agents.browser.confirmSensitiveActions`** (boolean):
- **Description:** Require manual confirmation for sensitive browser actions
(e.g., fill_form, evaluate_script).
@@ -1540,7 +1545,7 @@ their corresponding top-level category object in your `settings.json` file.
- **`experimental.enableAgents`** (boolean):
- **Description:** Enable local and remote subagents.
- **Default:** `false`
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.worktrees`** (boolean):
+1 -1
View File
@@ -79,7 +79,7 @@ export function appEvalTest(policy: EvalPolicy, evalCase: AppEvalCase) {
}
// Render the app!
rig.render();
await rig.render();
// Wait for initial ready state
await rig.waitForIdle();
+17 -32
View File
@@ -9,27 +9,7 @@ import path from 'node:path';
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
const DOCS_AGENT_DEFINITION = `---
name: docs-agent
description: An agent with expertise in updating documentation.
tools:
- read_file
- write_file
---
You are the docs agent. Update documentation clearly and accurately.
`;
const TEST_AGENT_DEFINITION = `---
name: test-agent
description: An agent with expertise in writing and updating tests.
tools:
- read_file
- write_file
---
You are the test agent. Add or update tests.
`;
import { evalTest, TEST_AGENTS } from './test-helper.js';
const INDEX_TS = 'export const add = (a: number, b: number) => a + b;\n';
@@ -62,12 +42,12 @@ describe('subagent eval test cases', () => {
},
prompt: 'Please update README.md with a description of this library.',
files: {
'.gemini/agents/docs-agent.md': DOCS_AGENT_DEFINITION,
...TEST_AGENTS.DOCS_AGENT.asFile(),
'index.ts': INDEX_TS,
'README.md': 'TODO: update the README.\n',
},
assert: async (rig, _result) => {
await rig.expectToolCallSuccess(['docs-agent']);
await rig.expectToolCallSuccess([TEST_AGENTS.DOCS_AGENT.name]);
},
});
@@ -92,7 +72,7 @@ describe('subagent eval test cases', () => {
prompt:
'Rename the exported function in index.ts from add to sum and update the file directly.',
files: {
'.gemini/agents/docs-agent.md': DOCS_AGENT_DEFINITION,
...TEST_AGENTS.DOCS_AGENT.asFile(),
'index.ts': INDEX_TS,
},
assert: async (rig, _result) => {
@@ -102,9 +82,11 @@ describe('subagent eval test cases', () => {
}>;
expect(updatedIndex).toContain('export const sum =');
expect(toolLogs.some((l) => l.toolRequest.name === 'docs-agent')).toBe(
false,
);
expect(
toolLogs.some(
(l) => l.toolRequest.name === TEST_AGENTS.DOCS_AGENT.name,
),
).toBe(false);
expect(toolLogs.some((l) => l.toolRequest.name === 'generalist')).toBe(
false,
);
@@ -133,7 +115,7 @@ describe('subagent eval test cases', () => {
},
prompt: 'Please add a small test file that verifies add(1, 2) returns 3.',
files: {
'.gemini/agents/test-agent.md': TEST_AGENT_DEFINITION,
...TEST_AGENTS.TESTING_AGENT.asFile(),
'index.ts': INDEX_TS,
'package.json': JSON.stringify(
{
@@ -150,7 +132,7 @@ describe('subagent eval test cases', () => {
toolRequest: { name: string };
}>;
await rig.expectToolCallSuccess(['test-agent']);
await rig.expectToolCallSuccess([TEST_AGENTS.TESTING_AGENT.name]);
expect(toolLogs.some((l) => l.toolRequest.name === 'generalist')).toBe(
false,
);
@@ -178,8 +160,8 @@ describe('subagent eval test cases', () => {
prompt:
'Add a short README description for this library and also add a test file that verifies add(1, 2) returns 3.',
files: {
'.gemini/agents/docs-agent.md': DOCS_AGENT_DEFINITION,
'.gemini/agents/test-agent.md': TEST_AGENT_DEFINITION,
...TEST_AGENTS.DOCS_AGENT.asFile(),
...TEST_AGENTS.TESTING_AGENT.asFile(),
'index.ts': INDEX_TS,
'README.md': 'TODO: update the README.\n',
'package.json': JSON.stringify(
@@ -198,7 +180,10 @@ describe('subagent eval test cases', () => {
}>;
const readme = readProjectFile(rig, 'README.md');
await rig.expectToolCallSuccess(['docs-agent', 'test-agent']);
await rig.expectToolCallSuccess([
TEST_AGENTS.DOCS_AGENT.name,
TEST_AGENTS.TESTING_AGENT.name,
]);
expect(readme).not.toContain('TODO: update the README.');
expect(toolLogs.some((l) => l.toolRequest.name === 'generalist')).toBe(
false,
-6
View File
@@ -63,9 +63,6 @@ describe.skipIf(!chromeAvailable)('browser-policy', () => {
rig.setup('browser-policy-skip-confirmation', {
fakeResponsesPath: join(__dirname, 'browser-policy.responses'),
settings: {
experimental: {
enableAgents: true,
},
agents: {
overrides: {
browser_agent: {
@@ -183,9 +180,6 @@ priority = 200
rig.setup('browser-session-warning', {
fakeResponsesPath: join(__dirname, 'browser-agent.cleanup.responses'),
settings: {
experimental: {
enableAgents: true,
},
general: {
enableAutoUpdateNotification: false,
},
+24 -52
View File
@@ -486,8 +486,7 @@
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
"integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
"license": "(Apache-2.0 AND BSD-3-Clause)",
"peer": true
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@bundled-es-modules/cookie": {
"version": "2.0.1",
@@ -1490,7 +1489,6 @@
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
@@ -2197,7 +2195,6 @@
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.2",
@@ -2378,7 +2375,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -2428,7 +2424,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
"integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
@@ -2803,7 +2798,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz",
"integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
@@ -2837,7 +2831,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz",
"integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/resources": "2.5.0"
@@ -2892,7 +2885,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz",
"integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/resources": "2.5.0",
@@ -4129,7 +4121,6 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4404,7 +4395,6 @@
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
@@ -5278,7 +5268,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -7413,8 +7402,7 @@
"version": "0.0.1581282",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
"integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
"license": "BSD-3-Clause",
"peer": true
"license": "BSD-3-Clause"
},
"node_modules/dezalgo": {
"version": "1.0.4",
@@ -7998,7 +7986,6 @@
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -8516,7 +8503,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -8710,9 +8696,9 @@
"license": "BSD-3-Clause"
},
"node_modules/fast-xml-builder": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.2.tgz",
"integrity": "sha512-NJAmiuVaJEjVa7TjLZKlYd7RqmzOC91EtPFXHvlTcqBVo50Qh7XV5IwvXi1c7NRz2Q/majGX9YLcwJtWgHjtkA==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
"integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
"funding": [
{
"type": "github",
@@ -8725,9 +8711,9 @@
}
},
"node_modules/fast-xml-parser": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.3.tgz",
"integrity": "sha512-Ymnuefk6VzAhT3SxLzVUw+nMio/wB1NGypHkgetwtXcK1JfryaHk4DWQFGVwQ9XgzyS5iRZ7C2ZGI4AMsdMZ6A==",
"version": "5.5.9",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.9.tgz",
"integrity": "sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==",
"funding": [
{
"type": "github",
@@ -8736,9 +8722,9 @@
],
"license": "MIT",
"dependencies": {
"fast-xml-builder": "^1.1.2",
"path-expression-matcher": "^1.1.3",
"strnum": "^2.1.2"
"fast-xml-builder": "^1.1.4",
"path-expression-matcher": "^1.2.0",
"strnum": "^2.2.2"
},
"bin": {
"fxparser": "src/cli/cli.js"
@@ -8914,9 +8900,9 @@
}
},
"node_modules/flatted": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
@@ -9829,7 +9815,6 @@
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz",
"integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -10108,7 +10093,6 @@
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.11.tgz",
"integrity": "sha512-93LQlzT7vvZ1XJcmOMwN4s+6W334QegendeHOMnEJBlhnpIzr8bws6/aOEHG8ZCuVD/vNeeea5m1msHIdAY6ig==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.1",
"ansi-escapes": "^7.0.0",
@@ -13216,9 +13200,9 @@
}
},
"node_modules/path-expression-matcher": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz",
"integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz",
"integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==",
"funding": [
{
"type": "github",
@@ -13866,7 +13850,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -13877,7 +13860,6 @@
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"shell-quote": "^1.6.1",
"ws": "^7"
@@ -15483,9 +15465,9 @@
}
},
"node_modules/strnum": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz",
"integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz",
"integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==",
"funding": [
{
"type": "github",
@@ -16027,7 +16009,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -16250,8 +16231,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"peer": true
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.20.3",
@@ -16259,7 +16239,6 @@
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
@@ -16425,7 +16404,6 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16491,9 +16469,9 @@
"license": "MIT"
},
"node_modules/undici": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.19.0.tgz",
"integrity": "sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ==",
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz",
"integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
@@ -16648,7 +16626,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -16762,7 +16739,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -16775,7 +16751,6 @@
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -17423,7 +17398,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -17867,7 +17841,6 @@
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
"integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@grpc/proto-loader": "^0.8.0",
"@js-sdsl/ordered-map": "^4.4.2"
@@ -17971,7 +17944,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -29,6 +29,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
PRIORITY_YOLO_ALLOW_ALL: 998,
Config: vi.fn().mockImplementation((params) => {
const mockConfig = {
...params,
@@ -341,11 +342,11 @@ describe('loadConfig', () => {
);
});
it('should default enableAgents to false when not provided', async () => {
it('should default enableAgents to true when not provided', async () => {
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenCalledWith(
expect.objectContaining({
enableAgents: false,
enableAgents: true,
}),
);
});
+1 -1
View File
@@ -128,7 +128,7 @@ export async function loadConfig(
interactive: !isHeadlessMode(),
enableInteractiveShell: !isHeadlessMode(),
ptyInfo: 'auto',
enableAgents: settings.experimental?.enableAgents ?? false,
enableAgents: settings.experimental?.enableAgents ?? true,
};
const fileService = new FileDiscoveryService(workspaceDir, {
@@ -400,7 +400,7 @@ describe('SettingsSchema', () => {
expect(setting).toBeDefined();
expect(setting.type).toBe('boolean');
expect(setting.category).toBe('Experimental');
expect(setting.default).toBe(false);
expect(setting.default).toBe(true);
expect(setting.requiresRestart).toBe(true);
expect(setting.showInDialog).toBe(false);
expect(setting.description).toBe('Enable local and remote subagents.');
+11 -1
View File
@@ -1208,6 +1208,16 @@ const SETTINGS_SCHEMA = {
'Disable user input on browser window during automation.',
showInDialog: false,
},
maxActionsPerTask: {
type: 'number',
label: 'Max Actions Per Task',
category: 'Advanced',
requiresRestart: false,
default: 100,
description:
'The maximum number of tool calls allowed per browser task. Enforcement is hard: the agent will be terminated when the limit is reached.',
showInDialog: false,
},
confirmSensitiveActions: {
type: 'boolean',
label: 'Confirm Sensitive Actions',
@@ -1932,7 +1942,7 @@ const SETTINGS_SCHEMA = {
label: 'Enable Agents',
category: 'Experimental',
requiresRestart: true,
default: false,
default: true,
description: 'Enable local and remote subagents.',
showInDialog: false,
},
+4 -1
View File
@@ -66,7 +66,10 @@ beforeEach(() => {
? stackLines.slice(lastReactFrameIndex + 1).join('\n')
: stackLines.slice(1).join('\n');
if (relevantStack.includes('OverflowContext.tsx')) {
if (
relevantStack.includes('OverflowContext.tsx') ||
relevantStack.includes('useTimedMessage.ts')
) {
return;
}
+9 -9
View File
@@ -617,7 +617,7 @@ kind: remote
name: oauth2-agent
agent_card_url: https://example.com/card
auth:
type: oauth2
type: oauth
client_id: $MY_OAUTH_CLIENT_ID
scopes:
- read
@@ -630,7 +630,7 @@ auth:
kind: 'remote',
name: 'oauth2-agent',
auth: {
type: 'oauth2',
type: 'oauth',
client_id: '$MY_OAUTH_CLIENT_ID',
scopes: ['read', 'write'],
},
@@ -643,7 +643,7 @@ kind: remote
name: oauth2-full-agent
agent_card_url: https://example.com/card
auth:
type: oauth2
type: oauth
client_id: my-client-id
client_secret: my-client-secret
scopes:
@@ -659,7 +659,7 @@ auth:
kind: 'remote',
name: 'oauth2-full-agent',
auth: {
type: 'oauth2',
type: 'oauth',
client_id: 'my-client-id',
client_secret: 'my-client-secret',
scopes: ['openid', 'profile'],
@@ -675,7 +675,7 @@ kind: remote
name: oauth2-minimal-agent
agent_card_url: https://example.com/card
auth:
type: oauth2
type: oauth
---
`);
const result = await parseAgentMarkdown(filePath);
@@ -684,7 +684,7 @@ auth:
kind: 'remote',
name: 'oauth2-minimal-agent',
auth: {
type: 'oauth2',
type: 'oauth',
},
});
});
@@ -695,7 +695,7 @@ kind: remote
name: invalid-oauth2-agent
agent_card_url: https://example.com/card
auth:
type: oauth2
type: oauth
client_id: my-client
authorization_url: not-a-valid-url
---
@@ -709,7 +709,7 @@ kind: remote
name: invalid-oauth2-agent
agent_card_url: https://example.com/card
auth:
type: oauth2
type: oauth
client_id: my-client
token_url: not-a-valid-url
---
@@ -723,7 +723,7 @@ auth:
name: 'oauth2-convert-agent',
agent_card_url: 'https://example.com/card',
auth: {
type: 'oauth2' as const,
type: 'oauth' as const,
client_id: '$MY_CLIENT_ID',
scopes: ['read'],
authorization_url: 'https://auth.example.com/authorize',
+3 -3
View File
@@ -63,7 +63,7 @@ interface FrontmatterLocalAgentDefinition
* Authentication configuration for remote agents in frontmatter format.
*/
interface FrontmatterAuthConfig {
type: 'apiKey' | 'http' | 'google-credentials' | 'oauth2';
type: 'apiKey' | 'http' | 'google-credentials' | 'oauth';
// API Key
key?: string;
name?: string;
@@ -205,7 +205,7 @@ const googleCredentialsAuthSchema = z.object({
*/
const oauth2AuthSchema = z.object({
...baseAuthFields,
type: z.literal('oauth2'),
type: z.literal('oauth'),
client_id: z.string().optional(),
client_secret: z.string().optional(),
scopes: z.array(z.string()).optional(),
@@ -471,7 +471,7 @@ function convertFrontmatterAuthToConfig(
}
}
case 'oauth2':
case 'oauth':
return {
...base,
type: 'oauth2',
@@ -112,6 +112,7 @@ Some errors are unrecoverable and retrying will never help. When you see ANY of
- "Could not connect to Chrome" or "Failed to connect to Chrome" or "Timed out connecting to Chrome" Include the full error message with its remediation steps in your summary verbatim. Do NOT paraphrase or omit instructions.
- "Browser closed" or "Target closed" or "Session closed" The browser process has terminated. Include the error and tell the user to try again.
- "net::ERR_" network errors on the SAME URL after 2 retries the site is unreachable. Report the URL and error.
- "reached maximum action limit" You have performed too many actions in this task. Stop immediately and report this limit to the user.
- Any error that appears IDENTICALLY 3+ times in a row it will not resolve by retrying.
Do NOT keep retrying terminal errors. Report them with actionable remediation steps and exit immediately.
@@ -697,4 +697,28 @@ describe('BrowserManager', () => {
expect(injectAutomationOverlay).not.toHaveBeenCalled();
});
});
describe('Rate limiting', () => {
it('should terminate task when maxActionsPerTask is reached', async () => {
const limitedConfig = makeFakeConfig({
agents: {
browser: {
maxActionsPerTask: 3,
},
},
});
const manager = new BrowserManager(limitedConfig);
// First 3 calls should succeed
await manager.callTool('take_snapshot', {});
await manager.callTool('take_snapshot', { some: 'args' });
await manager.callTool('take_snapshot', { other: 'args' });
await manager.callTool('take_snapshot', { other: 'new args' });
// 4th call should throw
await expect(manager.callTool('take_snapshot', {})).rejects.toThrow(
/maximum action limit \(3\)/,
);
});
});
});
@@ -97,6 +97,10 @@ export class BrowserManager {
private mcpTransport: StdioClientTransport | undefined;
private discoveredTools: McpTool[] = [];
/** State for action rate limiting */
private actionCounter = 0;
private readonly maxActionsPerTask: number;
/**
* Whether to inject the automation overlay.
* Always false in headless mode (no visible window to decorate).
@@ -108,6 +112,8 @@ export class BrowserManager {
const browserConfig = config.getBrowserAgentConfig();
this.shouldInjectOverlay = !browserConfig?.customConfig?.headless;
this.shouldDisableInput = config.shouldDisableBrowserUserInput();
this.maxActionsPerTask =
browserConfig?.customConfig.maxActionsPerTask ?? 100;
}
/**
@@ -151,6 +157,16 @@ export class BrowserManager {
throw signal.reason ?? new Error('Operation cancelled');
}
// Hard enforcement of per-action rate limit
if (this.actionCounter > this.maxActionsPerTask) {
const error = new Error(
`Browser agent reached maximum action limit (${this.maxActionsPerTask}). ` +
`Task terminated to prevent runaway execution. To config the limit, use maxActionsPerTask in the settings.`,
);
throw error;
}
this.actionCounter++;
const errorMessage = this.checkNavigationRestrictions(toolName, args);
if (errorMessage) {
return {
+16
View File
@@ -1474,6 +1474,22 @@ describe('Server Config (config.ts)', () => {
expect(browserConfig.customConfig.visualModel).toBe(
'custom-visual-model',
);
expect(browserConfig.customConfig.maxActionsPerTask).toBe(100); // default
});
it('should return custom maxActionsPerTask', () => {
const params: ConfigParameters = {
...baseParams,
agents: {
browser: {
maxActionsPerTask: 50,
},
},
};
const config = new Config(params);
const browserConfig = config.getBrowserAgentConfig();
expect(browserConfig.customConfig.maxActionsPerTask).toBe(50);
});
it('should apply defaults for partial custom config', () => {
+4 -1
View File
@@ -331,6 +331,8 @@ export interface BrowserAgentCustomConfig {
allowedDomains?: string[];
/** Disable user input on the browser window during automation. Default: true in non-headless mode */
disableUserInput?: boolean;
/** Maximum number of actions (tool calls) allowed per task. Default: 100 */
maxActionsPerTask?: number;
/** Whether to confirm sensitive actions (e.g., fill_form, evaluate_script). */
confirmSensitiveActions?: boolean;
/** Whether to block file uploads. */
@@ -1027,7 +1029,7 @@ export class Config implements McpContext, AgentLoopContext {
this.model = params.model;
this.disableLoopDetection = params.disableLoopDetection ?? false;
this._activeModel = params.model;
this.enableAgents = params.enableAgents ?? false;
this.enableAgents = params.enableAgents ?? true;
this.agents = params.agents ?? {};
this.disableLLMCorrection = params.disableLLMCorrection ?? true;
this.planEnabled = params.plan ?? true;
@@ -3194,6 +3196,7 @@ export class Config implements McpContext, AgentLoopContext {
visualModel: customConfig.visualModel,
allowedDomains: customConfig.allowedDomains,
disableUserInput: customConfig.disableUserInput,
maxActionsPerTask: customConfig.maxActionsPerTask ?? 100,
confirmSensitiveActions: customConfig.confirmSensitiveActions,
blockFileUploads: customConfig.blockFileUploads,
},
+5 -1
View File
@@ -88,7 +88,11 @@ export * from './utils/approvalModeUtils.js';
export * from './utils/fileDiffUtils.js';
export * from './utils/retry.js';
export * from './utils/shell-utils.js';
export { PolicyDecision, ApprovalMode } from './policy/types.js';
export {
PolicyDecision,
ApprovalMode,
PRIORITY_YOLO_ALLOW_ALL,
} from './policy/types.js';
export * from './utils/tool-utils.js';
export * from './utils/terminalSerializer.js';
export * from './utils/systemEncoding.js';
+4 -2
View File
@@ -2184,7 +2184,8 @@ export class ApprovalModeSwitchEvent implements BaseTelemetryEvent {
toOpenTelemetryAttributes(config: Config): LogAttributes {
return {
...getCommonAttributes(config),
event_name: EVENT_APPROVAL_MODE_SWITCH,
'event.name': EVENT_APPROVAL_MODE_SWITCH,
'event.timestamp': this['event.timestamp'],
from_mode: this.from_mode,
to_mode: this.to_mode,
};
@@ -2214,7 +2215,8 @@ export class ApprovalModeDurationEvent implements BaseTelemetryEvent {
toOpenTelemetryAttributes(config: Config): LogAttributes {
return {
...getCommonAttributes(config),
event_name: EVENT_APPROVAL_MODE_DURATION,
'event.name': EVENT_APPROVAL_MODE_DURATION,
'event.timestamp': this['event.timestamp'],
mode: this.mode,
duration_ms: this.duration_ms,
};
@@ -147,6 +147,51 @@ describe('McpClientManager', () => {
expect(mockedMcpClient.discoverInto).not.toHaveBeenCalled();
});
it('should NOT set COMPLETED prematurely when startConfiguredMcpServers finishes before parallel extensions', async () => {
mockConfig.getMcpServers.mockReturnValue({});
const manager = setupManager(new McpClientManager('0.0.1', mockConfig));
let resolveExtension: (value: void) => void;
const extensionPromise = new Promise<void>((resolve) => {
resolveExtension = resolve;
});
mockedMcpClient.connect.mockImplementation(async () => {
await extensionPromise;
});
const extensionStartPromise = manager.startExtension({
name: 'test-extension',
mcpServers: {
'extension-server': { command: 'node' },
},
isActive: true,
version: '1.0.0',
path: '/some-path',
contextFiles: [],
id: '123',
});
// Wait for the state to become IN_PROGRESS (since maybeDiscoverMcpServer is async)
await vi.waitFor(() => {
if (manager.getDiscoveryState() !== MCPDiscoveryState.IN_PROGRESS) {
throw new Error('Discovery state is not IN_PROGRESS');
}
});
expect(manager.getDiscoveryState()).toBe(MCPDiscoveryState.IN_PROGRESS);
await manager.startConfiguredMcpServers();
// discoveryState should still be IN_PROGRESS because the extension is still starting
expect(manager.getDiscoveryState()).toBe(MCPDiscoveryState.IN_PROGRESS);
resolveExtension!(undefined);
await extensionStartPromise;
expect(manager.getDiscoveryState()).toBe(MCPDiscoveryState.COMPLETED);
});
it('should mark discovery completed when all configured servers are blocked', async () => {
mockConfig.getMcpServers.mockReturnValue({
'test-server': { command: 'node' },
@@ -554,8 +554,10 @@ export class McpClientManager {
);
if (Object.keys(servers).length === 0) {
this.discoveryState = MCPDiscoveryState.COMPLETED;
this.eventEmitter?.emit('mcp-client-update', this.clients);
if (!this.discoveryPromise) {
this.discoveryState = MCPDiscoveryState.COMPLETED;
this.eventEmitter?.emit('mcp-client-update', this.clients);
}
return;
}
@@ -574,7 +576,10 @@ export class McpClientManager {
// If every configured server was skipped (for example because all are
// disabled by user settings), no discovery promise is created. In that
// case we must still mark discovery complete or the UI will wait forever.
if (this.discoveryState === MCPDiscoveryState.IN_PROGRESS) {
if (
this.discoveryState === MCPDiscoveryState.IN_PROGRESS &&
!this.discoveryPromise
) {
this.discoveryState = MCPDiscoveryState.COMPLETED;
this.eventEmitter?.emit('mcp-client-update', this.clients);
}
@@ -0,0 +1,72 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Represents a test agent used in evaluations and tests.
*/
export interface TestAgent {
/** The unique name of the agent. */
readonly name: string;
/** The full YAML/Markdown definition of the agent. */
readonly definition: string;
/** The standard path where this agent should be saved in a test project. */
readonly path: string;
/** A helper to spread this agent directly into a 'files' object for evalTest. */
readonly asFile: () => Record<string, string>;
}
/**
* Helper to create a TestAgent with consistent formatting and pathing.
*/
function createAgent(options: {
name: string;
description: string;
tools: string[];
body: string;
}): TestAgent {
const definition = `---
name: ${options.name}
description: ${options.description}
tools:
${options.tools.map((t) => ` - ${t}`).join('\n')}
---
${options.body}
`;
const path = `.gemini/agents/${options.name}.md`;
return {
name: options.name,
definition,
path,
asFile: () => ({ [path]: definition }),
};
}
/**
* A collection of predefined test agents for use in evaluations and tests.
*/
export const TEST_AGENTS = {
/**
* An agent with expertise in updating documentation.
*/
DOCS_AGENT: createAgent({
name: 'docs-agent',
description: 'An agent with expertise in updating documentation.',
tools: ['read_file', 'write_file'],
body: 'You are the docs agent. Update documentation clearly and accurately.',
}),
/**
* An agent with expertise in writing and updating tests.
*/
TESTING_AGENT: createAgent({
name: 'testing-agent',
description: 'An agent with expertise in writing and updating tests.',
tools: ['read_file', 'write_file'],
body: 'You are the test agent. Add or update tests.',
}),
} as const;
+2 -1
View File
@@ -5,6 +5,7 @@
*/
export * from './file-system-test-helpers.js';
export * from './test-rig.js';
export * from './fixtures/agents.js';
export * from './mock-utils.js';
export * from './test-mcp-server.js';
export * from './test-rig.js';
+9 -2
View File
@@ -2142,6 +2142,13 @@
"default": true,
"type": "boolean"
},
"maxActionsPerTask": {
"title": "Max Actions Per Task",
"description": "The maximum number of tool calls allowed per browser task. Enforcement is hard: the agent will be terminated when the limit is reached.",
"markdownDescription": "The maximum number of tool calls allowed per browser task. Enforcement is hard: the agent will be terminated when the limit is reached.\n\n- Category: `Advanced`\n- Requires restart: `no`\n- Default: `100`",
"default": 100,
"type": "number"
},
"confirmSensitiveActions": {
"title": "Confirm Sensitive Actions",
"description": "Require manual confirmation for sensitive browser actions (e.g., fill_form, evaluate_script).",
@@ -2680,8 +2687,8 @@
"enableAgents": {
"title": "Enable Agents",
"description": "Enable local and remote subagents.",
"markdownDescription": "Enable local and remote subagents.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"markdownDescription": "Enable local and remote subagents.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"worktrees": {