From 999ae7827d2708158acacc53146928ef7f7b4e6a Mon Sep 17 00:00:00 2001 From: Adam Weidman <65992621+adamfweidman@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:46:12 -0400 Subject: [PATCH 1/8] feat(core): change user-facing auth type from oauth2 to oauth (#23639) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- docs/core/remote-agents.md | 10 +++++----- packages/core/src/agents/agentLoader.test.ts | 18 +++++++++--------- packages/core/src/agents/agentLoader.ts | 6 +++--- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/core/remote-agents.md b/docs/core/remote-agents.md index 2e34a9dbc4..05975421fe 100644 --- a/docs/core/remote-agents.md +++ b/docs/core/remote-agents.md @@ -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 --- ``` diff --git a/packages/core/src/agents/agentLoader.test.ts b/packages/core/src/agents/agentLoader.test.ts index ea7ef0b2c3..917628f7e7 100644 --- a/packages/core/src/agents/agentLoader.test.ts +++ b/packages/core/src/agents/agentLoader.test.ts @@ -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', diff --git a/packages/core/src/agents/agentLoader.ts b/packages/core/src/agents/agentLoader.ts index 2cb7b3c439..1b9eb1ea4e 100644 --- a/packages/core/src/agents/agentLoader.ts +++ b/packages/core/src/agents/agentLoader.ts @@ -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', From 122b8d43ca109f850ba530cbbcf9c9a41d8f06ec Mon Sep 17 00:00:00 2001 From: Tommaso Sciortino Date: Tue, 24 Mar 2026 18:53:03 +0000 Subject: [PATCH 2/8] chore(deps): fix npm audit vulnerabilities (#23679) --- package-lock.json | 76 +++++++++++++++-------------------------------- 1 file changed, 24 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index ff6b8fee23..b4fdfdb439 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, From bf80e27dbc156512e68c8f718c6c6d81b9c831eb Mon Sep 17 00:00:00 2001 From: Adib234 <30782825+Adib234@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:12:22 -0400 Subject: [PATCH 3/8] test(evals): fix overlapping act() deadlock in app-test-helper (#23666) --- evals/app-test-helper.ts | 2 +- packages/cli/test-setup.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/evals/app-test-helper.ts b/evals/app-test-helper.ts index 2bcff41924..8ea842aa38 100644 --- a/evals/app-test-helper.ts +++ b/evals/app-test-helper.ts @@ -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(); diff --git a/packages/cli/test-setup.ts b/packages/cli/test-setup.ts index 452493559a..f2e1bd4586 100644 --- a/packages/cli/test-setup.ts +++ b/packages/cli/test-setup.ts @@ -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; } From 84f40768a15614f79a60b3226c1e2b953029133d Mon Sep 17 00:00:00 2001 From: Samee Zahid Date: Tue, 24 Mar 2026 12:50:48 -0700 Subject: [PATCH 4/8] feat(evals): centralize test agents into test-utils for reuse (#23616) Co-authored-by: Samee Zahid --- evals/subagents.eval.ts | 49 +++++---------- packages/test-utils/src/fixtures/agents.ts | 72 ++++++++++++++++++++++ packages/test-utils/src/index.ts | 3 +- 3 files changed, 91 insertions(+), 33 deletions(-) create mode 100644 packages/test-utils/src/fixtures/agents.ts diff --git a/evals/subagents.eval.ts b/evals/subagents.eval.ts index 3a7d8fa44f..140925964b 100644 --- a/evals/subagents.eval.ts +++ b/evals/subagents.eval.ts @@ -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, diff --git a/packages/test-utils/src/fixtures/agents.ts b/packages/test-utils/src/fixtures/agents.ts new file mode 100644 index 0000000000..9469457227 --- /dev/null +++ b/packages/test-utils/src/fixtures/agents.ts @@ -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; +} + +/** + * 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; diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts index 42dd12bb43..7bae818040 100644 --- a/packages/test-utils/src/index.ts +++ b/packages/test-utils/src/index.ts @@ -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'; From 055ff92276cffb57988cecf0f3ca3951413609b9 Mon Sep 17 00:00:00 2001 From: Abhi <43648792+abhipatel12@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:14:48 -0400 Subject: [PATCH 5/8] revert: chore(config): disable agents by default (#23672) --- docs/reference/configuration.md | 2 +- integration-tests/browser-policy.test.ts | 6 ------ packages/a2a-server/src/config/config.test.ts | 5 +++-- packages/a2a-server/src/config/config.ts | 2 +- packages/cli/src/config/settingsSchema.test.ts | 2 +- packages/cli/src/config/settingsSchema.ts | 2 +- packages/core/src/config/config.ts | 2 +- packages/core/src/index.ts | 6 +++++- schemas/settings.schema.json | 4 ++-- 9 files changed, 15 insertions(+), 16 deletions(-) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index a5533e199c..89f7502502 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1540,7 +1540,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): diff --git a/integration-tests/browser-policy.test.ts b/integration-tests/browser-policy.test.ts index bb66b10aab..f533cb3f5e 100644 --- a/integration-tests/browser-policy.test.ts +++ b/integration-tests/browser-policy.test.ts @@ -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, }, diff --git a/packages/a2a-server/src/config/config.test.ts b/packages/a2a-server/src/config/config.test.ts index 370c859944..007f1d5f06 100644 --- a/packages/a2a-server/src/config/config.test.ts +++ b/packages/a2a-server/src/config/config.test.ts @@ -29,6 +29,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { await importOriginal(); 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, }), ); }); diff --git a/packages/a2a-server/src/config/config.ts b/packages/a2a-server/src/config/config.ts index 1fe55258fc..c3561629b6 100644 --- a/packages/a2a-server/src/config/config.ts +++ b/packages/a2a-server/src/config/config.ts @@ -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, { diff --git a/packages/cli/src/config/settingsSchema.test.ts b/packages/cli/src/config/settingsSchema.test.ts index 9b643396ae..c358cd65aa 100644 --- a/packages/cli/src/config/settingsSchema.test.ts +++ b/packages/cli/src/config/settingsSchema.test.ts @@ -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.'); diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index b886dfccf3..0d0672a227 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1932,7 +1932,7 @@ const SETTINGS_SCHEMA = { label: 'Enable Agents', category: 'Experimental', requiresRestart: true, - default: false, + default: true, description: 'Enable local and remote subagents.', showInDialog: false, }, diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index e32205d070..f4f186ff8f 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -1027,7 +1027,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; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e607775345..2d48eeffe9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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'; diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 3789b64d52..287d2b3f76 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -2680,8 +2680,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": { From e591b51919fc4f798a7620ec14e92ed3434f206b Mon Sep 17 00:00:00 2001 From: Adib234 <30782825+Adib234@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:49:50 -0400 Subject: [PATCH 6/8] fix(plan): update telemetry attribute keys and add timestamp (#23685) --- packages/core/src/telemetry/types.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index ffca3a2698..3a038b2482 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -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, }; From 11dc33eab793a6259b422168d180d2ea37d5a8f5 Mon Sep 17 00:00:00 2001 From: Jack Wotherspoon Date: Tue, 24 Mar 2026 13:53:21 -0700 Subject: [PATCH 7/8] fix(core): prevent premature MCP discovery completion (#23637) --- .../core/src/tools/mcp-client-manager.test.ts | 45 +++++++++++++++++++ packages/core/src/tools/mcp-client-manager.ts | 11 +++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/core/src/tools/mcp-client-manager.test.ts b/packages/core/src/tools/mcp-client-manager.test.ts index 84d3e138ce..a96f3f7d29 100644 --- a/packages/core/src/tools/mcp-client-manager.test.ts +++ b/packages/core/src/tools/mcp-client-manager.test.ts @@ -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((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' }, diff --git a/packages/core/src/tools/mcp-client-manager.ts b/packages/core/src/tools/mcp-client-manager.ts index 666b6d5321..3e7ef75d4c 100644 --- a/packages/core/src/tools/mcp-client-manager.ts +++ b/packages/core/src/tools/mcp-client-manager.ts @@ -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); } From 466671eed483f1bdac13f817dcd5ef7df401ab82 Mon Sep 17 00:00:00 2001 From: cynthialong0-0 <82900738+cynthialong0-0@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:40:48 -0700 Subject: [PATCH 8/8] feat(browser): add maxActionsPerTask for browser agent setting (#23216) --- docs/reference/configuration.md | 5 ++++ packages/cli/src/config/settingsSchema.ts | 10 ++++++++ .../agents/browser/browserAgentDefinition.ts | 1 + .../src/agents/browser/browserManager.test.ts | 24 +++++++++++++++++++ .../core/src/agents/browser/browserManager.ts | 16 +++++++++++++ packages/core/src/config/config.test.ts | 16 +++++++++++++ packages/core/src/config/config.ts | 3 +++ schemas/settings.schema.json | 7 ++++++ 8 files changed, 82 insertions(+) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 89f7502502..f8382ee28c 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -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). diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 0d0672a227..c0f2395110 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -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', diff --git a/packages/core/src/agents/browser/browserAgentDefinition.ts b/packages/core/src/agents/browser/browserAgentDefinition.ts index 064d66dfbc..b04b2a3ede 100644 --- a/packages/core/src/agents/browser/browserAgentDefinition.ts +++ b/packages/core/src/agents/browser/browserAgentDefinition.ts @@ -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. diff --git a/packages/core/src/agents/browser/browserManager.test.ts b/packages/core/src/agents/browser/browserManager.test.ts index 36652bbb64..303c07288d 100644 --- a/packages/core/src/agents/browser/browserManager.test.ts +++ b/packages/core/src/agents/browser/browserManager.test.ts @@ -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\)/, + ); + }); + }); }); diff --git a/packages/core/src/agents/browser/browserManager.ts b/packages/core/src/agents/browser/browserManager.ts index c5fc6c5053..cc059feea3 100644 --- a/packages/core/src/agents/browser/browserManager.ts +++ b/packages/core/src/agents/browser/browserManager.ts @@ -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 { diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index f8247f8377..99688eead5 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -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', () => { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index f4f186ff8f..795df747cb 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -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. */ @@ -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, }, diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 287d2b3f76..93bd8fc895 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -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).",