feat(policy): add --admin-policy flag for supplemental admin policies (#20360)

This commit is contained in:
Gal Zahavi
2026-03-11 10:35:45 -07:00
committed by GitHub
parent 7e9e196793
commit 6900fe5527
12 changed files with 516 additions and 810 deletions
+7
View File
@@ -92,6 +92,13 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `[]` - **Default:** `[]`
- **Requires restart:** Yes - **Requires restart:** Yes
#### `adminPolicyPaths`
- **`adminPolicyPaths`** (array):
- **Description:** Additional admin policy files or directories to load.
- **Default:** `[]`
- **Requires restart:** Yes
#### `general` #### `general`
- **`general.preferredEditor`** (string): - **`general.preferredEditor`** (string):
+30 -6
View File
@@ -191,9 +191,13 @@ User, and (if configured) Admin directories.
#### System-wide policies (Admin) #### System-wide policies (Admin)
Administrators can enforce system-wide policies (Tier 3) that override all user Administrators can enforce system-wide policies (Tier 4) that override all user
and default settings. These policies must be placed in specific, secure and default settings. These policies can be loaded from standard system
directories: locations or supplemental paths.
##### Standard Locations
These are the default paths the CLI searches for admin policies:
| OS | Policy Directory Path | | OS | Policy Directory Path |
| :---------- | :------------------------------------------------ | | :---------- | :------------------------------------------------ |
@@ -201,10 +205,25 @@ directories:
| **macOS** | `/Library/Application Support/GeminiCli/policies` | | **macOS** | `/Library/Application Support/GeminiCli/policies` |
| **Windows** | `C:\ProgramData\gemini-cli\policies` | | **Windows** | `C:\ProgramData\gemini-cli\policies` |
**Security Requirements:** ##### Supplemental Admin Policies
To prevent privilege escalation, the CLI enforces strict security checks on Administrators can also specify supplemental policy paths using:
admin directories. If checks fail, system policies are **ignored**.
- The `--admin-policy` command-line flag.
- The `adminPolicyPaths` setting in a system settings file.
These supplemental policies are assigned the same **Admin** tier (Base 4) as
policies in standard locations.
**Security Guard**: Supplemental admin policies are **ignored** if any `.toml`
policy files are found in the standard system location. This prevents flag-based
overrides when a central system policy has already been established.
#### Security Requirements
To prevent privilege escalation, the CLI enforces strict security checks on the
**standard system policy directory**. If checks fail, the policies in that
directory are **ignored**.
- **Linux / macOS:** Must be owned by `root` (UID 0) and NOT writable by group - **Linux / macOS:** Must be owned by `root` (UID 0) and NOT writable by group
or others (e.g., `chmod 755`). or others (e.g., `chmod 755`).
@@ -214,6 +233,11 @@ admin directories. If checks fail, system policies are **ignored**.
for non-admin groups. You may need to "Disable inheritance" in Advanced for non-admin groups. You may need to "Disable inheritance" in Advanced
Security Settings._ Security Settings._
**Note:** Supplemental admin policies (provided via `--admin-policy` or
`adminPolicyPaths` settings) are **NOT** subject to these strict ownership
checks, as they are explicitly provided by the user or administrator in their
current execution context.
### TOML rule schema ### TOML rule schema
Here is a breakdown of the fields available in a TOML policy rule: Here is a breakdown of the fields available in a TOML policy rule:
+25 -1
View File
@@ -2195,6 +2195,7 @@
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@octokit/auth-token": "^6.0.0", "@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.2", "@octokit/graphql": "^9.0.2",
@@ -2375,6 +2376,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"engines": { "engines": {
"node": ">=8.0.0" "node": ">=8.0.0"
} }
@@ -2424,6 +2426,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
"integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0" "@opentelemetry/semantic-conventions": "^1.29.0"
}, },
@@ -2798,6 +2801,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz",
"integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@opentelemetry/core": "2.5.0", "@opentelemetry/core": "2.5.0",
"@opentelemetry/semantic-conventions": "^1.29.0" "@opentelemetry/semantic-conventions": "^1.29.0"
@@ -2831,6 +2835,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz",
"integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@opentelemetry/core": "2.5.0", "@opentelemetry/core": "2.5.0",
"@opentelemetry/resources": "2.5.0" "@opentelemetry/resources": "2.5.0"
@@ -2885,6 +2890,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz",
"integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@opentelemetry/core": "2.5.0", "@opentelemetry/core": "2.5.0",
"@opentelemetry/resources": "2.5.0", "@opentelemetry/resources": "2.5.0",
@@ -4048,6 +4054,7 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@@ -4322,6 +4329,7 @@
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0", "@typescript-eslint/types": "8.35.0",
@@ -5195,6 +5203,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -7726,6 +7735,7 @@
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -8236,6 +8246,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.1", "body-parser": "^2.2.1",
@@ -9503,6 +9514,7 @@
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz",
"integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=16.9.0" "node": ">=16.9.0"
} }
@@ -9782,6 +9794,7 @@
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.11.tgz", "resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.11.tgz",
"integrity": "sha512-93LQlzT7vvZ1XJcmOMwN4s+6W334QegendeHOMnEJBlhnpIzr8bws6/aOEHG8ZCuVD/vNeeea5m1msHIdAY6ig==", "integrity": "sha512-93LQlzT7vvZ1XJcmOMwN4s+6W334QegendeHOMnEJBlhnpIzr8bws6/aOEHG8ZCuVD/vNeeea5m1msHIdAY6ig==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.1", "@alcalzone/ansi-tokenize": "^0.2.1",
"ansi-escapes": "^7.0.0", "ansi-escapes": "^7.0.0",
@@ -13368,6 +13381,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -13378,6 +13392,7 @@
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"shell-quote": "^1.6.1", "shell-quote": "^1.6.1",
"ws": "^7" "ws": "^7"
@@ -15422,6 +15437,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -15645,7 +15661,8 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true, "dev": true,
"license": "0BSD" "license": "0BSD",
"peer": true
}, },
"node_modules/tsx": { "node_modules/tsx": {
"version": "4.20.3", "version": "4.20.3",
@@ -15653,6 +15670,7 @@
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "~0.25.0", "esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5" "get-tsconfig": "^4.7.5"
@@ -15812,6 +15830,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -16034,6 +16053,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -16147,6 +16167,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -16159,6 +16180,7 @@
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/chai": "^5.2.2", "@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4", "@vitest/expect": "3.2.4",
@@ -16800,6 +16822,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
@@ -17336,6 +17359,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
+31 -25
View File
@@ -76,6 +76,7 @@ export interface CliArgs {
yolo: boolean | undefined; yolo: boolean | undefined;
approvalMode: string | undefined; approvalMode: string | undefined;
policy: string[] | undefined; policy: string[] | undefined;
adminPolicy: string[] | undefined;
allowedMcpServerNames: string[] | undefined; allowedMcpServerNames: string[] | undefined;
allowedTools: string[] | undefined; allowedTools: string[] | undefined;
acp?: boolean; acp?: boolean;
@@ -97,6 +98,21 @@ export interface CliArgs {
isCommand: boolean | undefined; isCommand: boolean | undefined;
} }
/**
* Helper to coerce comma-separated or multiple flag values into a flat array.
*/
const coerceCommaSeparated = (values: string[]): string[] => {
if (values.length === 1 && values[0] === '') {
return [''];
}
return values.flatMap((v) =>
v
.split(',')
.map((s) => s.trim())
.filter(Boolean),
);
};
export async function parseArguments( export async function parseArguments(
settings: MergedSettings, settings: MergedSettings,
): Promise<CliArgs> { ): Promise<CliArgs> {
@@ -166,14 +182,15 @@ export async function parseArguments(
nargs: 1, nargs: 1,
description: description:
'Additional policy files or directories to load (comma-separated or multiple --policy)', 'Additional policy files or directories to load (comma-separated or multiple --policy)',
coerce: (policies: string[]) => coerce: coerceCommaSeparated,
// Handle comma-separated values })
policies.flatMap((p) => .option('admin-policy', {
p type: 'array',
.split(',') string: true,
.map((s) => s.trim()) nargs: 1,
.filter(Boolean), description:
), 'Additional admin policy files or directories to load (comma-separated or multiple --admin-policy)',
coerce: coerceCommaSeparated,
}) })
.option('acp', { .option('acp', {
type: 'boolean', type: 'boolean',
@@ -189,11 +206,7 @@ export async function parseArguments(
string: true, string: true,
nargs: 1, nargs: 1,
description: 'Allowed MCP server names', description: 'Allowed MCP server names',
coerce: (mcpServerNames: string[]) => coerce: coerceCommaSeparated,
// Handle comma-separated values
mcpServerNames.flatMap((mcpServerName) =>
mcpServerName.split(',').map((m) => m.trim()),
),
}) })
.option('allowed-tools', { .option('allowed-tools', {
type: 'array', type: 'array',
@@ -201,9 +214,7 @@ export async function parseArguments(
nargs: 1, nargs: 1,
description: description:
'[DEPRECATED: Use Policy Engine instead See https://geminicli.com/docs/core/policy-engine] Tools that are allowed to run without confirmation', '[DEPRECATED: Use Policy Engine instead See https://geminicli.com/docs/core/policy-engine] Tools that are allowed to run without confirmation',
coerce: (tools: string[]) => coerce: coerceCommaSeparated,
// Handle comma-separated values
tools.flatMap((tool) => tool.split(',').map((t) => t.trim())),
}) })
.option('extensions', { .option('extensions', {
alias: 'e', alias: 'e',
@@ -212,11 +223,7 @@ export async function parseArguments(
nargs: 1, nargs: 1,
description: description:
'A list of extensions to use. If not provided, all extensions are used.', 'A list of extensions to use. If not provided, all extensions are used.',
coerce: (extensions: string[]) => coerce: coerceCommaSeparated,
// Handle comma-separated values
extensions.flatMap((extension) =>
extension.split(',').map((e) => e.trim()),
),
}) })
.option('list-extensions', { .option('list-extensions', {
alias: 'l', alias: 'l',
@@ -258,9 +265,7 @@ export async function parseArguments(
nargs: 1, nargs: 1,
description: description:
'Additional directories to include in the workspace (comma-separated or multiple --include-directories)', 'Additional directories to include in the workspace (comma-separated or multiple --include-directories)',
coerce: (dirs: string[]) => coerce: coerceCommaSeparated,
// Handle comma-separated values
dirs.flatMap((dir) => dir.split(',').map((d) => d.trim())),
}) })
.option('screen-reader', { .option('screen-reader', {
type: 'boolean', type: 'boolean',
@@ -643,7 +648,8 @@ export async function loadCliConfig(
...settings.mcp, ...settings.mcp,
allowed: argv.allowedMcpServerNames ?? settings.mcp?.allowed, allowed: argv.allowedMcpServerNames ?? settings.mcp?.allowed,
}, },
policyPaths: argv.policy, policyPaths: argv.policy ?? settings.policyPaths,
adminPolicyPaths: argv.adminPolicy ?? settings.adminPolicyPaths,
}; };
const { workspacePoliciesDir, policyUpdateConfirmationRequest } = const { workspacePoliciesDir, policyUpdateConfirmationRequest } =
+1
View File
@@ -61,6 +61,7 @@ export async function createPolicyEngineConfig(
tools: settings.tools, tools: settings.tools,
mcpServers: settings.mcpServers, mcpServers: settings.mcpServers,
policyPaths: settings.policyPaths, policyPaths: settings.policyPaths,
adminPolicyPaths: settings.adminPolicyPaths,
workspacePoliciesDir, workspacePoliciesDir,
}; };
+25 -11
View File
@@ -134,6 +134,18 @@ export interface SettingsSchema {
export type MemoryImportFormat = 'tree' | 'flat'; export type MemoryImportFormat = 'tree' | 'flat';
export type DnsResolutionOrder = 'ipv4first' | 'verbatim'; export type DnsResolutionOrder = 'ipv4first' | 'verbatim';
const pathArraySetting = (label: string, description: string) => ({
type: 'array' as const,
label,
category: 'Advanced' as const,
requiresRestart: true as const,
default: [] as string[],
description,
showInDialog: false as const,
items: { type: 'string' as const },
mergeStrategy: MergeStrategy.UNION,
});
/** /**
* The canonical schema for all settings. * The canonical schema for all settings.
* The structure of this object defines the structure of the `Settings` type. * The structure of this object defines the structure of the `Settings` type.
@@ -156,17 +168,15 @@ const SETTINGS_SCHEMA = {
}, },
}, },
policyPaths: { policyPaths: pathArraySetting(
type: 'array', 'Policy Paths',
label: 'Policy Paths', 'Additional policy files or directories to load.',
category: 'Advanced', ),
requiresRestart: true,
default: [] as string[], adminPolicyPaths: pathArraySetting(
description: 'Additional policy files or directories to load.', 'Admin Policy Paths',
showInDialog: false, 'Additional admin policy files or directories to load.',
items: { type: 'string' }, ),
mergeStrategy: MergeStrategy.UNION,
},
general: { general: {
type: 'object', type: 'object',
@@ -2677,6 +2687,8 @@ type InferSettings<T extends SettingsSchema> = {
? boolean ? boolean
: T[K]['default'] extends string : T[K]['default'] extends string
? string ? string
: T[K]['default'] extends ReadonlyArray<infer U>
? U[]
: T[K]['default']; : T[K]['default'];
}; };
@@ -2691,6 +2703,8 @@ type InferMergedSettings<T extends SettingsSchema> = {
? boolean ? boolean
: T[K]['default'] extends string : T[K]['default'] extends string
? string ? string
: T[K]['default'] extends ReadonlyArray<infer U>
? U[]
: T[K]['default']; : T[K]['default'];
}; };
+1
View File
@@ -481,6 +481,7 @@ describe('gemini.tsx main function kitty protocol', () => {
yolo: undefined, yolo: undefined,
approvalMode: undefined, approvalMode: undefined,
policy: undefined, policy: undefined,
adminPolicy: undefined,
allowedMcpServerNames: undefined, allowedMcpServerNames: undefined,
allowedTools: undefined, allowedTools: undefined,
experimentalAcp: undefined, experimentalAcp: undefined,
@@ -29,3 +29,9 @@ exports[`ConfigInitDisplay > updates message on McpClientUpdate event 1`] = `
Spinner Connecting to MCP servers... (1/2) - Waiting for: server2 Spinner Connecting to MCP servers... (1/2) - Waiting for: server2
" "
`; `;
exports[`ConfigInitDisplay > updates message on McpClientUpdate event 2`] = `
"
Spinner Connecting to MCP servers... (1/2) - Waiting for: server2
"
`;
File diff suppressed because it is too large Load Diff
+110 -58
View File
@@ -39,6 +39,26 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
export const DEFAULT_CORE_POLICIES_DIR = path.join(__dirname, 'policies'); export const DEFAULT_CORE_POLICIES_DIR = path.join(__dirname, 'policies');
// Cache to prevent duplicate warnings in the same process
const emittedWarnings = new Set<string>();
/**
* Emits a warning feedback event only once per process.
*/
function emitWarningOnce(message: string): void {
if (!emittedWarnings.has(message)) {
coreEvents.emitFeedback('warning', message);
emittedWarnings.add(message);
}
}
/**
* Clears the emitted warnings cache. Used primarily for tests.
*/
export function clearEmittedPolicyWarnings(): void {
emittedWarnings.clear();
}
// Policy tier constants for priority calculation // Policy tier constants for priority calculation
export const DEFAULT_POLICY_TIER = 1; export const DEFAULT_POLICY_TIER = 1;
export const EXTENSION_POLICY_TIER = 2; export const EXTENSION_POLICY_TIER = 2;
@@ -89,33 +109,29 @@ export function getAlwaysAllowPriorityFraction(): number {
* @param policyPaths Optional user-provided policy paths (from --policy flag). * @param policyPaths Optional user-provided policy paths (from --policy flag).
* When provided, these replace the default user policies directory. * When provided, these replace the default user policies directory.
* @param workspacePoliciesDir Optional path to a directory containing workspace policies. * @param workspacePoliciesDir Optional path to a directory containing workspace policies.
* @param adminPolicyPaths Optional admin-provided policy paths (from --admin-policy flag).
* When provided, these supplement the default system policies directory.
*/ */
export function getPolicyDirectories( export function getPolicyDirectories(
defaultPoliciesDir?: string, defaultPoliciesDir?: string,
policyPaths?: string[], policyPaths?: string[],
workspacePoliciesDir?: string, workspacePoliciesDir?: string,
adminPolicyPaths?: string[],
): string[] { ): string[] {
const dirs = []; return [
// Admin tier (highest priority) // Admin tier (highest priority)
dirs.push(Storage.getSystemPoliciesDir()); Storage.getSystemPoliciesDir(),
...(adminPolicyPaths ?? []),
// User tier (second highest priority) // User tier (second highest priority)
if (policyPaths && policyPaths.length > 0) { ...(policyPaths ?? [Storage.getUserPoliciesDir()]),
dirs.push(...policyPaths);
} else {
dirs.push(Storage.getUserPoliciesDir());
}
// Workspace Tier (third highest) // Workspace Tier (third highest)
if (workspacePoliciesDir) { workspacePoliciesDir,
dirs.push(workspacePoliciesDir);
}
// Default tier (lowest priority) // Default tier (lowest priority)
dirs.push(defaultPoliciesDir ?? DEFAULT_CORE_POLICIES_DIR); defaultPoliciesDir ?? DEFAULT_CORE_POLICIES_DIR,
].filter((dir): dir is string => !!dir);
return dirs;
} }
/** /**
@@ -124,37 +140,40 @@ export function getPolicyDirectories(
*/ */
export function getPolicyTier( export function getPolicyTier(
dir: string, dir: string,
defaultPoliciesDir?: string, context: {
workspacePoliciesDir?: string, defaultPoliciesDir?: string;
workspacePoliciesDir?: string;
adminPolicyPaths?: Set<string>;
systemPoliciesDir: string;
userPoliciesDir: string;
},
): number { ): number {
const USER_POLICIES_DIR = Storage.getUserPoliciesDir();
const ADMIN_POLICIES_DIR = Storage.getSystemPoliciesDir();
const normalizedDir = path.resolve(dir); const normalizedDir = path.resolve(dir);
const normalizedUser = path.resolve(USER_POLICIES_DIR);
const normalizedAdmin = path.resolve(ADMIN_POLICIES_DIR);
if (normalizedDir === context.systemPoliciesDir) {
return ADMIN_POLICY_TIER;
}
if (context.adminPolicyPaths?.has(normalizedDir)) {
return ADMIN_POLICY_TIER;
}
if (normalizedDir === context.userPoliciesDir) {
return USER_POLICY_TIER;
}
if ( if (
defaultPoliciesDir && context.workspacePoliciesDir &&
normalizedDir === path.resolve(defaultPoliciesDir) normalizedDir === path.resolve(context.workspacePoliciesDir)
) {
return WORKSPACE_POLICY_TIER;
}
if (
context.defaultPoliciesDir &&
normalizedDir === path.resolve(context.defaultPoliciesDir)
) { ) {
return DEFAULT_POLICY_TIER; return DEFAULT_POLICY_TIER;
} }
if (normalizedDir === path.resolve(DEFAULT_CORE_POLICIES_DIR)) { if (normalizedDir === path.resolve(DEFAULT_CORE_POLICIES_DIR)) {
return DEFAULT_POLICY_TIER; return DEFAULT_POLICY_TIER;
} }
if (normalizedDir === normalizedUser) {
return USER_POLICY_TIER;
}
if (
workspacePoliciesDir &&
normalizedDir === path.resolve(workspacePoliciesDir)
) {
return WORKSPACE_POLICY_TIER;
}
if (normalizedDir === normalizedAdmin) {
return ADMIN_POLICY_TIER;
}
return DEFAULT_POLICY_TIER; return DEFAULT_POLICY_TIER;
} }
@@ -178,21 +197,24 @@ export function formatPolicyError(error: PolicyFileError): string {
/** /**
* Filters out insecure policy directories (specifically the system policy directory). * Filters out insecure policy directories (specifically the system policy directory).
* Supplemental admin policy paths are NOT subject to strict security checks as they
* are explicitly provided by the user/administrator via flags or settings.
* Emits warnings if insecure directories are found. * Emits warnings if insecure directories are found.
*/ */
async function filterSecurePolicyDirectories( async function filterSecurePolicyDirectories(
dirs: string[], dirs: string[],
systemPoliciesDir: string,
): Promise<string[]> { ): Promise<string[]> {
const systemPoliciesDir = path.resolve(Storage.getSystemPoliciesDir());
const results = await Promise.all( const results = await Promise.all(
dirs.map(async (dir) => { dirs.map(async (dir) => {
// Only check security for system policies const normalizedDir = path.resolve(dir);
if (path.resolve(dir) === systemPoliciesDir) { const isSystemPolicy = normalizedDir === systemPoliciesDir;
if (isSystemPolicy) {
const { secure, reason } = await isDirectorySecure(dir); const { secure, reason } = await isDirectorySecure(dir);
if (!secure) { if (!secure) {
const msg = `Security Warning: Skipping system policies from ${dir}: ${reason}`; const msg = `Security Warning: Skipping system policies from ${dir}: ${reason}`;
coreEvents.emitFeedback('warning', msg); emitWarningOnce(msg);
return null; return null;
} }
} }
@@ -271,41 +293,71 @@ export async function createPolicyEngineConfig(
approvalMode: ApprovalMode, approvalMode: ApprovalMode,
defaultPoliciesDir?: string, defaultPoliciesDir?: string,
): Promise<PolicyEngineConfig> { ): Promise<PolicyEngineConfig> {
const systemPoliciesDir = path.resolve(Storage.getSystemPoliciesDir());
const userPoliciesDir = path.resolve(Storage.getUserPoliciesDir());
let adminPolicyPaths = settings.adminPolicyPaths;
// Security: Ignore supplemental admin policies if the system directory already contains policies.
// This prevents flag-based overrides when a central system policy is established.
if (adminPolicyPaths?.length) {
try {
const files = await fs.readdir(systemPoliciesDir);
if (files.some((f) => f.endsWith('.toml'))) {
const msg = `Security Warning: Ignoring --admin-policy because system policies are already defined in ${systemPoliciesDir}`;
emitWarningOnce(msg);
adminPolicyPaths = undefined;
}
} catch (e) {
if (!isNodeError(e) || e.code !== 'ENOENT') {
debugLogger.warn(
`Failed to check system policies in ${systemPoliciesDir}`,
e,
);
}
}
}
const policyDirs = getPolicyDirectories( const policyDirs = getPolicyDirectories(
defaultPoliciesDir, defaultPoliciesDir,
settings.policyPaths, settings.policyPaths,
settings.workspacePoliciesDir, settings.workspacePoliciesDir,
adminPolicyPaths,
); );
const securePolicyDirs = await filterSecurePolicyDirectories(policyDirs);
const normalizedAdminPoliciesDir = path.resolve( const adminPolicyPathsSet = adminPolicyPaths
Storage.getSystemPoliciesDir(), ? new Set(adminPolicyPaths.map((p) => path.resolve(p)))
: undefined;
const securePolicyDirs = await filterSecurePolicyDirectories(
policyDirs,
systemPoliciesDir,
); );
const tierContext = {
defaultPoliciesDir,
workspacePoliciesDir: settings.workspacePoliciesDir,
adminPolicyPaths: adminPolicyPathsSet,
systemPoliciesDir,
userPoliciesDir,
};
const userProvidedPaths = settings.policyPaths
? new Set(settings.policyPaths.map((p) => path.resolve(p)))
: new Set<string>();
// Load policies from TOML files // Load policies from TOML files
const { const {
rules: tomlRules, rules: tomlRules,
checkers: tomlCheckers, checkers: tomlCheckers,
errors, errors,
} = await loadPoliciesFromToml(securePolicyDirs, (p) => { } = await loadPoliciesFromToml(securePolicyDirs, (p) => {
const tier = getPolicyTier(
p,
defaultPoliciesDir,
settings.workspacePoliciesDir,
);
// If it's a user-provided path that isn't already categorized as ADMIN,
// treat it as USER tier.
if (
settings.policyPaths?.some(
(userPath) => path.resolve(userPath) === path.resolve(p),
)
) {
const normalizedPath = path.resolve(p); const normalizedPath = path.resolve(p);
if (normalizedPath !== normalizedAdminPoliciesDir) { const tier = getPolicyTier(normalizedPath, tierContext);
// If it's a user-provided path that isn't already categorized as ADMIN, treat it as USER tier.
if (userProvidedPaths.has(normalizedPath) && tier !== ADMIN_POLICY_TIER) {
return USER_POLICY_TIER; return USER_POLICY_TIER;
} }
}
return tier; return tier;
}); });
+2
View File
@@ -311,6 +311,8 @@ export interface PolicySettings {
mcpServers?: Record<string, { trust?: boolean }>; mcpServers?: Record<string, { trust?: boolean }>;
// User provided policies that will replace the USER level policies in ~/.gemini/policies // User provided policies that will replace the USER level policies in ~/.gemini/policies
policyPaths?: string[]; policyPaths?: string[];
// Admin provided policies that will supplement the ADMIN level policies
adminPolicyPaths?: string[];
workspacePoliciesDir?: string; workspacePoliciesDir?: string;
} }
+10
View File
@@ -32,6 +32,16 @@
"type": "string" "type": "string"
} }
}, },
"adminPolicyPaths": {
"title": "Admin Policy Paths",
"description": "Additional admin policy files or directories to load.",
"markdownDescription": "Additional admin policy files or directories to load.\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `[]`",
"default": [],
"type": "array",
"items": {
"type": "string"
}
},
"general": { "general": {
"title": "General", "title": "General",
"description": "General application settings.", "description": "General application settings.",