mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-28 02:31:05 -07:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d62cdb1cc | |||
| 5cb555dd05 | |||
| 2c7a62049e | |||
| 3e5a250b6f | |||
| 90cbd0cd5b |
Generated
+27
-1
@@ -2129,6 +2129,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
|
||||
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.9",
|
||||
"ajv": "^8.17.1",
|
||||
@@ -2271,6 +2272,7 @@
|
||||
"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",
|
||||
@@ -2451,6 +2453,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -2500,6 +2503,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -2874,6 +2878,7 @@
|
||||
"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"
|
||||
@@ -2907,6 +2912,7 @@
|
||||
"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"
|
||||
@@ -2961,6 +2967,7 @@
|
||||
"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",
|
||||
@@ -4124,6 +4131,7 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4398,6 +4406,7 @@
|
||||
"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",
|
||||
@@ -5323,6 +5332,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -7863,6 +7873,7 @@
|
||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -8383,6 +8394,7 @@
|
||||
"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",
|
||||
@@ -9679,6 +9691,7 @@
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz",
|
||||
"integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -9979,6 +9992,7 @@
|
||||
"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",
|
||||
@@ -13667,6 +13681,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -13677,6 +13692,7 @@
|
||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"shell-quote": "^1.6.1",
|
||||
"ws": "^7"
|
||||
@@ -15730,6 +15746,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15953,7 +15970,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
@@ -15961,6 +15979,7 @@
|
||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -16121,6 +16140,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16328,6 +16348,7 @@
|
||||
"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",
|
||||
@@ -16441,6 +16462,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16453,6 +16475,7 @@
|
||||
"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",
|
||||
@@ -17084,6 +17107,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -17419,6 +17443,7 @@
|
||||
"shell-quote": "^1.8.3",
|
||||
"simple-git": "^3.28.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"strip-json-comments": "^3.1.1",
|
||||
"systeminformation": "^5.25.11",
|
||||
"tree-sitter-bash": "^0.25.0",
|
||||
"undici": "^7.10.0",
|
||||
@@ -17619,6 +17644,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -42,11 +42,9 @@ For example:
|
||||
You are operating in **Plan Mode** - a structured planning workflow for designing implementation strategies before execution.
|
||||
|
||||
## Available Tools
|
||||
The following read-only tools are available in Plan Mode:
|
||||
The following tools are available in Plan Mode:
|
||||
<tool>\`glob\`</tool>
|
||||
<tool>\`grep_search\`</tool>
|
||||
- \`write_file\` - Save plans to the plans directory (see Plan Storage below)
|
||||
- \`replace\` - Update plans in the plans directory
|
||||
|
||||
## Plan Storage
|
||||
- Save your plans as Markdown (.md) files ONLY within: \`/tmp/plans/\`
|
||||
@@ -174,11 +172,9 @@ For example:
|
||||
You are operating in **Plan Mode** - a structured planning workflow for designing implementation strategies before execution.
|
||||
|
||||
## Available Tools
|
||||
The following read-only tools are available in Plan Mode:
|
||||
The following tools are available in Plan Mode:
|
||||
<tool>\`glob\`</tool>
|
||||
<tool>\`grep_search\`</tool>
|
||||
- \`write_file\` - Save plans to the plans directory (see Plan Storage below)
|
||||
- \`replace\` - Update plans in the plans directory
|
||||
|
||||
## Plan Storage
|
||||
- Save your plans as Markdown (.md) files ONLY within: \`/tmp/plans/\`
|
||||
@@ -424,11 +420,9 @@ For example:
|
||||
You are operating in **Plan Mode** - a structured planning workflow for designing implementation strategies before execution.
|
||||
|
||||
## Available Tools
|
||||
The following read-only tools are available in Plan Mode:
|
||||
The following tools are available in Plan Mode:
|
||||
<tool>\`glob\`</tool>
|
||||
<tool>\`grep_search\`</tool>
|
||||
- \`write_file\` - Save plans to the plans directory (see Plan Storage below)
|
||||
- \`replace\` - Update plans in the plans directory
|
||||
|
||||
## Plan Storage
|
||||
- Save your plans as Markdown (.md) files ONLY within: \`/tmp/project-temp/plans/\`
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from '../config/models.js';
|
||||
import { ApprovalMode } from '../policy/types.js';
|
||||
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
|
||||
import type { AnyDeclarativeTool } from '../tools/tools.js';
|
||||
import type { CallableTool } from '@google/genai';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
|
||||
@@ -85,6 +86,12 @@ describe('Core System Prompt (prompts.ts)', () => {
|
||||
getToolRegistry: vi.fn().mockReturnValue({
|
||||
getAllToolNames: vi.fn().mockReturnValue(['grep_search', 'glob']),
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
getActiveTools: vi
|
||||
.fn()
|
||||
.mockReturnValue([
|
||||
{ name: 'grep_search' } as unknown as AnyDeclarativeTool,
|
||||
{ name: 'glob' } as unknown as AnyDeclarativeTool,
|
||||
]),
|
||||
}),
|
||||
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
|
||||
storage: {
|
||||
@@ -452,24 +459,11 @@ describe('Core System Prompt (prompts.ts)', () => {
|
||||
true, // isReadOnly
|
||||
);
|
||||
|
||||
const nonReadOnlyMcpTool = new DiscoveredMCPTool(
|
||||
{} as CallableTool,
|
||||
'nonreadonly-server',
|
||||
'non_read_static_value',
|
||||
'A non-read-only tool',
|
||||
{},
|
||||
{} as MessageBus,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
// Note: non-read-only MCP tools are implicitly filtered out by the logic
|
||||
// because they are not potentially allowed in Plan Mode by default.
|
||||
|
||||
vi.mocked(mockConfig.getToolRegistry().getAllTools).mockReturnValue([
|
||||
vi.mocked(mockConfig.getToolRegistry().getActiveTools).mockReturnValue([
|
||||
readOnlyMcpTool,
|
||||
nonReadOnlyMcpTool,
|
||||
]);
|
||||
vi.mocked(mockConfig.getToolRegistry().getAllToolNames).mockReturnValue([
|
||||
readOnlyMcpTool.name,
|
||||
nonReadOnlyMcpTool.name,
|
||||
]);
|
||||
|
||||
const prompt = getCoreSystemPrompt(mockConfig);
|
||||
@@ -483,10 +477,10 @@ describe('Core System Prompt (prompts.ts)', () => {
|
||||
it('should only list available tools in PLAN mode', () => {
|
||||
vi.mocked(mockConfig.getApprovalMode).mockReturnValue(ApprovalMode.PLAN);
|
||||
// Only enable a subset of tools, including ask_user
|
||||
vi.mocked(mockConfig.getToolRegistry().getAllToolNames).mockReturnValue([
|
||||
'glob',
|
||||
'read_file',
|
||||
'ask_user',
|
||||
vi.mocked(mockConfig.getToolRegistry().getActiveTools).mockReturnValue([
|
||||
{ name: 'glob' } as unknown as AnyDeclarativeTool,
|
||||
{ name: 'read_file' } as unknown as AnyDeclarativeTool,
|
||||
{ name: 'ask_user' } as unknown as AnyDeclarativeTool,
|
||||
]);
|
||||
|
||||
const prompt = getCoreSystemPrompt(mockConfig);
|
||||
|
||||
@@ -2458,4 +2458,137 @@ describe('PolicyEngine', () => {
|
||||
expect(checkers[0].priority).toBe(2.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isToolPotentiallyAllowed', () => {
|
||||
it('should return true if tool is explicitly allowed', () => {
|
||||
const engine = new PolicyEngine({
|
||||
rules: [
|
||||
{ toolName: 'tool1', decision: PolicyDecision.ALLOW, priority: 100 },
|
||||
],
|
||||
});
|
||||
expect(engine.isToolPotentiallyAllowed('tool1')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if tool is explicitly denied', () => {
|
||||
const engine = new PolicyEngine({
|
||||
rules: [
|
||||
{ toolName: 'tool1', decision: PolicyDecision.DENY, priority: 100 },
|
||||
],
|
||||
});
|
||||
expect(engine.isToolPotentiallyAllowed('tool1')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if tool is allowed by wildcard', () => {
|
||||
const engine = new PolicyEngine({
|
||||
rules: [
|
||||
{
|
||||
toolName: 'server__*',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 100,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(engine.isToolPotentiallyAllowed('tool1', 'server')).toBe(true);
|
||||
});
|
||||
|
||||
it('should respect priority: higher priority deny wins', () => {
|
||||
const engine = new PolicyEngine({
|
||||
rules: [
|
||||
{ toolName: 'tool1', decision: PolicyDecision.DENY, priority: 200 },
|
||||
{ toolName: 'tool1', decision: PolicyDecision.ALLOW, priority: 100 },
|
||||
],
|
||||
});
|
||||
expect(engine.isToolPotentiallyAllowed('tool1')).toBe(false);
|
||||
});
|
||||
|
||||
it('should respect priority: higher priority allow wins', () => {
|
||||
const engine = new PolicyEngine({
|
||||
rules: [
|
||||
{ toolName: 'tool1', decision: PolicyDecision.ALLOW, priority: 200 },
|
||||
{ toolName: 'tool1', decision: PolicyDecision.DENY, priority: 100 },
|
||||
],
|
||||
});
|
||||
expect(engine.isToolPotentiallyAllowed('tool1')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for conditional DENY (argsPattern) if lower priority is ALLOW', () => {
|
||||
const engine = new PolicyEngine({
|
||||
rules: [
|
||||
{
|
||||
toolName: 'tool1',
|
||||
decision: PolicyDecision.DENY,
|
||||
priority: 200,
|
||||
argsPattern: /something/,
|
||||
},
|
||||
{ toolName: 'tool1', decision: PolicyDecision.ALLOW, priority: 100 },
|
||||
],
|
||||
});
|
||||
// Since it's only conditionally denied, it is "potentially" allowed
|
||||
expect(engine.isToolPotentiallyAllowed('tool1')).toBe(true);
|
||||
});
|
||||
|
||||
it('should respect global rules', () => {
|
||||
const engine = new PolicyEngine({
|
||||
rules: [
|
||||
{ decision: PolicyDecision.DENY, priority: 50 }, // Global deny
|
||||
],
|
||||
});
|
||||
expect(engine.isToolPotentiallyAllowed('any_tool')).toBe(false);
|
||||
});
|
||||
|
||||
it('should ignore global rules when ignoreGlobalRules is true', () => {
|
||||
const engine = new PolicyEngine({
|
||||
rules: [
|
||||
{ decision: PolicyDecision.DENY, priority: 100 }, // Global deny
|
||||
],
|
||||
});
|
||||
// Should fallback to defaultDecision (ASK_USER by default)
|
||||
expect(engine.isToolPotentiallyAllowed('any_tool', undefined, true)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should still respect specific tool DENY when ignoreGlobalRules is true', () => {
|
||||
const engine = new PolicyEngine({
|
||||
rules: [
|
||||
{ decision: PolicyDecision.DENY, priority: 100 }, // Global deny
|
||||
{ toolName: 'tool1', decision: PolicyDecision.DENY, priority: 150 }, // Specific deny
|
||||
],
|
||||
});
|
||||
expect(engine.isToolPotentiallyAllowed('tool1', undefined, true)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should respect modes', () => {
|
||||
const engine = new PolicyEngine({
|
||||
rules: [
|
||||
{
|
||||
toolName: 'tool1',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 100,
|
||||
modes: [ApprovalMode.PLAN],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(engine.isToolPotentiallyAllowed('tool1')).toBe(true); // Default mode is usually DEFAULT which might be allowed if defaultDecision is ASK_USER
|
||||
|
||||
const engineDeny = new PolicyEngine({
|
||||
rules: [
|
||||
{
|
||||
toolName: 'tool1',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 100,
|
||||
modes: [ApprovalMode.PLAN],
|
||||
},
|
||||
{ decision: PolicyDecision.DENY, priority: 50 },
|
||||
],
|
||||
});
|
||||
engineDeny.setApprovalMode(ApprovalMode.DEFAULT);
|
||||
expect(engineDeny.isToolPotentiallyAllowed('tool1')).toBe(false);
|
||||
|
||||
engineDeny.setApprovalMode(ApprovalMode.PLAN);
|
||||
expect(engineDeny.isToolPotentiallyAllowed('tool1')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -629,6 +629,73 @@ export class PolicyEngine {
|
||||
return excludedTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a tool could potentially be allowed (or ASK_USER) under the current approval mode.
|
||||
* This is used to filter out tools that are unconditionally denied before generating the prompt schema.
|
||||
*/
|
||||
isToolPotentiallyAllowed(
|
||||
toolName: string,
|
||||
serverName?: string,
|
||||
ignoreGlobalRules = false,
|
||||
): boolean {
|
||||
const aliases = getToolAliases(toolName);
|
||||
const namesToTry = [...aliases];
|
||||
if (serverName) {
|
||||
for (const alias of aliases) {
|
||||
namesToTry.push(`${serverName}__${alias}`);
|
||||
}
|
||||
}
|
||||
|
||||
let globalVerdict: PolicyDecision | undefined;
|
||||
|
||||
for (const rule of this.rules) {
|
||||
// Check if rule applies to current approval mode
|
||||
if (rule.modes && rule.modes.length > 0) {
|
||||
if (!rule.modes.includes(this.approvalMode)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const ruleToolName = rule.toolName;
|
||||
|
||||
let isToolMatch = false;
|
||||
if (!ruleToolName) {
|
||||
if (ignoreGlobalRules) {
|
||||
continue;
|
||||
}
|
||||
isToolMatch = true; // Global rule matches all tools
|
||||
if (globalVerdict === undefined) {
|
||||
globalVerdict = rule.decision;
|
||||
}
|
||||
} else if (isWildcardPattern(ruleToolName)) {
|
||||
isToolMatch = namesToTry.some((name) =>
|
||||
matchesWildcard(ruleToolName, name),
|
||||
);
|
||||
} else {
|
||||
isToolMatch = namesToTry.includes(ruleToolName);
|
||||
}
|
||||
|
||||
if (isToolMatch) {
|
||||
if (rule.decision !== PolicyDecision.DENY) {
|
||||
// Found a rule that might allow it (could be conditional on args)
|
||||
return true;
|
||||
} else if (!rule.argsPattern) {
|
||||
// Unconditional DENY that applies to this tool (either specific or global)
|
||||
return false;
|
||||
}
|
||||
// Conditional DENY (depends on args). Keep checking lower priority rules
|
||||
// because if the args DON'T match this DENY, a lower priority rule might ALLOW it.
|
||||
}
|
||||
}
|
||||
|
||||
// No specific rule allowed it or unconditionally denied it.
|
||||
if (globalVerdict !== undefined && !ignoreGlobalRules) {
|
||||
return globalVerdict !== PolicyDecision.DENY;
|
||||
}
|
||||
|
||||
return this.defaultDecision !== PolicyDecision.DENY;
|
||||
}
|
||||
|
||||
private applyNonInteractiveMode(decision: PolicyDecision): PolicyDecision {
|
||||
// In non-interactive mode, ASK_USER becomes DENY
|
||||
if (this.nonInteractive && decision === PolicyDecision.ASK_USER) {
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import { CodebaseInvestigatorAgent } from '../agents/codebase-investigator.js';
|
||||
import { isGitRepository } from '../utils/gitUtils.js';
|
||||
import {
|
||||
PLAN_MODE_TOOLS,
|
||||
WRITE_TODOS_TOOL_NAME,
|
||||
READ_FILE_TOOL_NAME,
|
||||
ENTER_PLAN_MODE_TOOL_NAME,
|
||||
@@ -67,25 +66,17 @@ export class PromptProvider {
|
||||
const contextFilenames = getAllGeminiMdFilenames();
|
||||
|
||||
// --- Context Gathering ---
|
||||
let planModeToolsList = PLAN_MODE_TOOLS.filter((t) =>
|
||||
enabledToolNames.has(t),
|
||||
)
|
||||
.map((t) => ` <tool>\`${t}\`</tool>`)
|
||||
.join('\n');
|
||||
|
||||
// Add read-only MCP tools to the list
|
||||
let planModeToolsList = '';
|
||||
if (isPlanMode) {
|
||||
const allTools = config.getToolRegistry().getAllTools();
|
||||
const readOnlyMcpTools = allTools.filter(
|
||||
(t): t is DiscoveredMCPTool =>
|
||||
t instanceof DiscoveredMCPTool && !!t.isReadOnly,
|
||||
);
|
||||
if (readOnlyMcpTools.length > 0) {
|
||||
const mcpToolsList = readOnlyMcpTools
|
||||
.map((t) => ` <tool>\`${t.name}\` (${t.serverName})</tool>`)
|
||||
.join('\n');
|
||||
planModeToolsList += `\n${mcpToolsList}`;
|
||||
}
|
||||
const activeTools = config.getToolRegistry().getActiveTools();
|
||||
activeTools.sort((a, b) => a.name.localeCompare(b.name));
|
||||
planModeToolsList = activeTools
|
||||
.map((t) => {
|
||||
const serverSuffix =
|
||||
t instanceof DiscoveredMCPTool ? ` (${t.serverName})` : '';
|
||||
return ` <tool>\`${t.name}\`${serverSuffix}</tool>`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
let basePrompt: string;
|
||||
|
||||
@@ -398,10 +398,8 @@ export function renderPlanningWorkflow(
|
||||
You are operating in **Plan Mode** - a structured planning workflow for designing implementation strategies before execution.
|
||||
|
||||
## Available Tools
|
||||
The following read-only tools are available in Plan Mode:
|
||||
The following tools are available in Plan Mode:
|
||||
${options.planModeToolsList}
|
||||
- \`${WRITE_FILE_TOOL_NAME}\` - Save plans to the plans directory (see Plan Storage below)
|
||||
- \`${EDIT_TOOL_NAME}\` - Update plans in the plans directory
|
||||
|
||||
## Plan Storage
|
||||
- Save your plans as Markdown (.md) files ONLY within: \`${options.plansDir}/\`
|
||||
|
||||
@@ -452,11 +452,9 @@ export function renderPlanningWorkflow(
|
||||
You are operating in **Plan Mode**. Your goal is to produce a detailed implementation plan in \`${options.plansDir}/\` and get user approval before editing source code.
|
||||
|
||||
## Available Tools
|
||||
The following read-only tools are available in Plan Mode:
|
||||
The following tools are available in Plan Mode:
|
||||
<available_tools>
|
||||
${options.planModeToolsList}
|
||||
<tool>${formatToolName(WRITE_FILE_TOOL_NAME)} - Save plans to the plans directory</tool>
|
||||
<tool>${formatToolName(EDIT_TOOL_NAME)} - Update plans in the plans directory</tool>
|
||||
</available_tools>
|
||||
|
||||
## Rules
|
||||
|
||||
@@ -112,22 +112,6 @@ export const ALL_BUILTIN_TOOL_NAMES = [
|
||||
EXIT_PLAN_MODE_TOOL_NAME,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Read-only tools available in Plan Mode.
|
||||
* This list is used to dynamically generate the Plan Mode prompt,
|
||||
* filtered by what tools are actually enabled in the current configuration.
|
||||
*/
|
||||
export const PLAN_MODE_TOOLS = [
|
||||
GLOB_TOOL_NAME,
|
||||
GREP_TOOL_NAME,
|
||||
READ_FILE_TOOL_NAME,
|
||||
LS_TOOL_NAME,
|
||||
WEB_SEARCH_TOOL_NAME,
|
||||
ASK_USER_TOOL_NAME,
|
||||
ACTIVATE_SKILL_TOOL_NAME,
|
||||
EXIT_PLAN_MODE_TOOL_NAME,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Validates if a tool name is syntactically valid.
|
||||
* Checks against built-in tools, discovered tools, and MCP naming conventions.
|
||||
|
||||
@@ -379,6 +379,46 @@ describe('ToolRegistry', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveTools', () => {
|
||||
it('should include tools in Plan Mode when allowed by policy', () => {
|
||||
// Setup Plan Mode
|
||||
vi.spyOn(config, 'getApprovalMode').mockReturnValue(ApprovalMode.PLAN);
|
||||
|
||||
const mockTool = new MockTool({ name: 'mock-tool' });
|
||||
toolRegistry.registerTool(mockTool);
|
||||
|
||||
// Mock PolicyEngine to explicitly allow this tool
|
||||
const policyEngine = config.getPolicyEngine();
|
||||
vi.spyOn(policyEngine, 'isToolPotentiallyAllowed').mockImplementation(
|
||||
(toolName) => toolName === 'mock-tool',
|
||||
);
|
||||
|
||||
const activeTools = toolRegistry.getActiveTools();
|
||||
const activeToolNames = activeTools.map((t) => t.name);
|
||||
|
||||
expect(activeToolNames).toContain('mock-tool');
|
||||
});
|
||||
|
||||
it('should exclude tools in Plan Mode when denied by policy', () => {
|
||||
// Setup Plan Mode
|
||||
vi.spyOn(config, 'getApprovalMode').mockReturnValue(ApprovalMode.PLAN);
|
||||
|
||||
const mockTool = new MockTool({ name: 'mock-tool' });
|
||||
toolRegistry.registerTool(mockTool);
|
||||
|
||||
// Mock PolicyEngine to explicitly deny this tool
|
||||
const policyEngine = config.getPolicyEngine();
|
||||
vi.spyOn(policyEngine, 'isToolPotentiallyAllowed').mockImplementation(
|
||||
(toolName) => toolName !== 'mock-tool',
|
||||
);
|
||||
|
||||
const activeTools = toolRegistry.getActiveTools();
|
||||
const activeToolNames = activeTools.map((t) => t.name);
|
||||
|
||||
expect(activeToolNames).not.toContain('mock-tool');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllToolNames', () => {
|
||||
it('should return all registered tool names', () => {
|
||||
// Register tools with displayNames in non-alphabetical order
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
DISCOVERED_TOOL_PREFIX,
|
||||
TOOL_LEGACY_ALIASES,
|
||||
getToolAliases,
|
||||
PLAN_MODE_TOOLS,
|
||||
WRITE_FILE_TOOL_NAME,
|
||||
EDIT_TOOL_NAME,
|
||||
} from './tool-names.js';
|
||||
@@ -442,9 +441,12 @@ export class ToolRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of currently active tools.
|
||||
* This is filtered by the current policies and approval modes (e.g. Plan Mode).
|
||||
* It is public so that PromptProvider can dynamically construct accurate tool lists.
|
||||
* @returns All the tools that are not excluded.
|
||||
*/
|
||||
private getActiveTools(): AnyDeclarativeTool[] {
|
||||
getActiveTools(): AnyDeclarativeTool[] {
|
||||
const excludedTools =
|
||||
this.expandExcludeToolsWithAliases(this.config.getExcludeTools()) ??
|
||||
new Set([]);
|
||||
@@ -490,28 +492,28 @@ export class ToolRegistry {
|
||||
this.expandExcludeToolsWithAliases(this.config.getExcludeTools()) ??
|
||||
new Set([]);
|
||||
|
||||
// Filter tools in Plan Mode to only allow approved read-only tools.
|
||||
let serverName: string | undefined;
|
||||
const isMcpTool = tool instanceof DiscoveredMCPTool;
|
||||
if (isMcpTool) {
|
||||
serverName = tool.getFullyQualifiedPrefix().slice(0, -2);
|
||||
}
|
||||
|
||||
const isPlanMode =
|
||||
typeof this.config.getApprovalMode === 'function' &&
|
||||
this.config.getApprovalMode() === ApprovalMode.PLAN;
|
||||
if (isPlanMode) {
|
||||
const allowedToolNames = new Set<string>(PLAN_MODE_TOOLS);
|
||||
// We allow write_file and replace for writing plans specifically.
|
||||
allowedToolNames.add(WRITE_FILE_TOOL_NAME);
|
||||
allowedToolNames.add(EDIT_TOOL_NAME);
|
||||
|
||||
// Discovered MCP tools are allowed if they are read-only.
|
||||
if (
|
||||
tool instanceof DiscoveredMCPTool &&
|
||||
tool.isReadOnly &&
|
||||
!allowedToolNames.has(tool.name)
|
||||
) {
|
||||
allowedToolNames.add(tool.name);
|
||||
}
|
||||
const isReadOnlyMcp = isMcpTool && tool.isReadOnly;
|
||||
|
||||
if (!allowedToolNames.has(tool.name)) {
|
||||
return false;
|
||||
}
|
||||
// Read-only MCP tools bypass the Plan Mode global deny policy,
|
||||
// but still respect specific tool or wildcard rules.
|
||||
const ignoreGlobalRules = isPlanMode && isReadOnlyMcp;
|
||||
|
||||
if (
|
||||
!this.config
|
||||
.getPolicyEngine()
|
||||
.isToolPotentiallyAllowed(tool.name, serverName, ignoreGlobalRules)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedClassName = tool.constructor.name.replace(/^_+/, '');
|
||||
|
||||
Reference in New Issue
Block a user