mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-02 13:11:03 -07:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45cd51d3bf | |||
| ba5ce67d18 | |||
| 790a5e9203 | |||
| ee4a3e129f | |||
| 605b19af1b | |||
| 5c5bf382ec | |||
| ba6e5e876f | |||
| b96a26b4ef |
@@ -177,9 +177,9 @@ but you can customize these rules by creating your own policies in your
|
||||
#### Global vs. mode-specific rules
|
||||
|
||||
As described in the
|
||||
[policy engine documentation](../reference/policy-engine.md#approval-modes), any
|
||||
rule that does not explicitly specify `modes` is considered "always active" and
|
||||
will apply to Plan Mode as well.
|
||||
[policy engine documentation](../reference/policy-engine.md#approval-modes),
|
||||
every rule in a TOML policy file must explicitly specify the `modes` it applies
|
||||
to.
|
||||
|
||||
To maintain the integrity of Plan Mode as a safe research environment,
|
||||
persistent tool approvals are context-aware. Approvals granted in modes like
|
||||
|
||||
@@ -271,11 +271,13 @@ mcpName = "my_server"
|
||||
toolName = "dangerous_tool"
|
||||
decision = "ask_user"
|
||||
priority = 100
|
||||
modes = ["default", "autoEdit"]
|
||||
|
||||
[[safety_checker]]
|
||||
mcpName = "my_server"
|
||||
toolName = "write_data"
|
||||
priority = 200
|
||||
modes = ["default", "autoEdit"]
|
||||
[safety_checker.checker]
|
||||
type = "in-process"
|
||||
name = "allowed-path"
|
||||
|
||||
@@ -32,6 +32,7 @@ To create your first policy:
|
||||
commandPrefix = "rm -rf"
|
||||
decision = "deny"
|
||||
priority = 100
|
||||
modes = ["default"]
|
||||
```
|
||||
3. **Run a command** that triggers the policy (e.g., ask Gemini CLI to
|
||||
`rm -rf /`). The tool will now be blocked automatically.
|
||||
@@ -54,7 +55,7 @@ A rule consists of the following main components:
|
||||
win.
|
||||
|
||||
For example, this rule will ask for user confirmation before executing any `git`
|
||||
command.
|
||||
command in the default interactive mode.
|
||||
|
||||
```toml
|
||||
[[rule]]
|
||||
@@ -62,6 +63,7 @@ toolName = "run_shell_command"
|
||||
commandPrefix = "git"
|
||||
decision = "ask_user"
|
||||
priority = 100
|
||||
modes = ["default"]
|
||||
```
|
||||
|
||||
### Conditions
|
||||
@@ -158,10 +160,9 @@ For example:
|
||||
### Approval modes
|
||||
|
||||
Approval modes allow the policy engine to apply different sets of rules based on
|
||||
the CLI's operational mode. A rule in a TOML policy file can be associated with
|
||||
one or more modes (e.g., `yolo`, `autoEdit`, `plan`). The rule will only be
|
||||
active if the CLI is running in one of its specified modes. If a rule has no
|
||||
modes specified, it is always active.
|
||||
the CLI's operational mode. Every rule in a TOML policy file must be associated
|
||||
with one or more modes (for example, `yolo`, `autoEdit`, `plan`, `default`). The
|
||||
rule will only be active if the CLI is running in one of its specified modes.
|
||||
|
||||
- `default`: The standard interactive mode where most write tools require
|
||||
confirmation.
|
||||
@@ -321,8 +322,8 @@ priority = 10
|
||||
# useful for explaining *why* it was denied.
|
||||
denyMessage = "Deletion is permanent"
|
||||
|
||||
# (Optional) An array of approval modes where this rule is active.
|
||||
# If omitted or empty, the rule applies to all modes.
|
||||
# An array of approval modes where this rule is active.
|
||||
# Valid values: "default", "autoEdit", "plan", "yolo".
|
||||
modes = ["default", "autoEdit", "yolo"]
|
||||
|
||||
# (Optional) A boolean to restrict the rule to interactive (true) or
|
||||
@@ -353,6 +354,7 @@ This single rule will apply to both the `write_file` and `replace` tools.
|
||||
toolName = ["write_file", "replace"]
|
||||
decision = "ask_user"
|
||||
priority = 10
|
||||
modes = ["default", "autoEdit"]
|
||||
```
|
||||
|
||||
### Special syntax for `run_shell_command`
|
||||
@@ -375,6 +377,7 @@ toolName = "run_shell_command"
|
||||
commandPrefix = "git"
|
||||
decision = "ask_user"
|
||||
priority = 100
|
||||
modes = ["default", "autoEdit"]
|
||||
```
|
||||
|
||||
### Special syntax for MCP tools
|
||||
@@ -406,6 +409,7 @@ mcpName = "my-jira-server"
|
||||
toolName = "search"
|
||||
decision = "allow"
|
||||
priority = 200
|
||||
modes = ["default", "autoEdit", "plan"]
|
||||
```
|
||||
|
||||
**2. Targeting all tools on a specific server**
|
||||
@@ -422,6 +426,7 @@ mcpName = "untrusted-server"
|
||||
decision = "deny"
|
||||
priority = 500
|
||||
denyMessage = "This server is not trusted by the admin."
|
||||
modes = ["default", "autoEdit", "plan", "yolo"]
|
||||
```
|
||||
|
||||
**3. Targeting all MCP servers**
|
||||
@@ -436,6 +441,7 @@ toolName = "*"
|
||||
mcpName = "*"
|
||||
decision = "ask_user"
|
||||
priority = 10
|
||||
modes = ["default", "autoEdit", "plan"]
|
||||
```
|
||||
|
||||
## Default policies
|
||||
|
||||
@@ -409,11 +409,13 @@ describe('extension tests', () => {
|
||||
toolName = "deny_tool"
|
||||
decision = "deny"
|
||||
priority = 500
|
||||
modes = ["plan", "default", "autoEdit"]
|
||||
|
||||
[[rule]]
|
||||
toolName = "ask_tool"
|
||||
decision = "ask_user"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit"]
|
||||
`;
|
||||
fs.writeFileSync(
|
||||
path.join(policiesDir, 'policies.toml'),
|
||||
@@ -454,6 +456,7 @@ priority = 100
|
||||
toolName = "allow_tool"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit"]
|
||||
|
||||
[[rule]]
|
||||
toolName = "yolo_tool"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
ApprovalMode,
|
||||
MODES_BY_PERMISSIVENESS,
|
||||
PolicyDecision,
|
||||
PolicyEngine,
|
||||
} from '@google/gemini-cli-core';
|
||||
@@ -385,6 +386,7 @@ describe('Policy Engine Integration Tests', () => {
|
||||
toolAnnotations: { readOnlyHint: true },
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 10,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
});
|
||||
|
||||
const engine = new PolicyEngine(config);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
ExtensionLoader,
|
||||
AuthType,
|
||||
ApprovalMode,
|
||||
MODES_BY_PERMISSIVENESS,
|
||||
createPolicyEngineConfig,
|
||||
PolicyDecision,
|
||||
ToolConfirmationOutcome,
|
||||
@@ -452,6 +453,7 @@ export class AppRig {
|
||||
toolName,
|
||||
decision,
|
||||
priority,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
source: 'AppRig Override',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,11 @@ import {
|
||||
} from './browserAgentFactory.js';
|
||||
import { injectAutomationOverlay } from './automationOverlay.js';
|
||||
import { makeFakeConfig } from '../../test-utils/config.js';
|
||||
import { PolicyDecision, PRIORITY_SUBAGENT_TOOL } from '../../policy/types.js';
|
||||
import {
|
||||
PolicyDecision,
|
||||
PRIORITY_SUBAGENT_TOOL,
|
||||
MODES_BY_PERMISSIVENESS,
|
||||
} from '../../policy/types.js';
|
||||
import type { Config } from '../../config/config.js';
|
||||
import type { MessageBus } from '../../confirmation-bus/message-bus.js';
|
||||
import type { PolicyEngine } from '../../policy/policy-engine.js';
|
||||
@@ -431,6 +435,7 @@ describe('browserAgentFactory', () => {
|
||||
toolName: 'mcp_browser_agent_fill',
|
||||
decision: PolicyDecision.ASK_USER,
|
||||
priority: 999,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -439,6 +444,7 @@ describe('browserAgentFactory', () => {
|
||||
toolName: 'mcp_browser_agent_upload_file',
|
||||
decision: PolicyDecision.ASK_USER,
|
||||
priority: 999,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -447,6 +453,7 @@ describe('browserAgentFactory', () => {
|
||||
toolName: 'mcp_browser_agent_evaluate_script',
|
||||
decision: PolicyDecision.ASK_USER,
|
||||
priority: 999,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -467,6 +474,29 @@ describe('browserAgentFactory', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not register rule if it already exists with modes in different order', async () => {
|
||||
const existingRule = {
|
||||
toolName: 'mcp_browser_agent_fill',
|
||||
decision: PolicyDecision.ASK_USER,
|
||||
priority: 999,
|
||||
// Reverse order of MODES_BY_PERMISSIVENESS
|
||||
modes: [...MODES_BY_PERMISSIVENESS].reverse(),
|
||||
source: 'BrowserAgent (Sensitive Actions)',
|
||||
mcpName: BROWSER_AGENT_NAME,
|
||||
};
|
||||
|
||||
mockPolicyEngine.getRules.mockReturnValue([existingRule]);
|
||||
|
||||
await createBrowserAgentDefinition(mockConfig, mockMessageBus);
|
||||
|
||||
// Should NOT add 'fill' rule again because it's already there (even if modes order differs)
|
||||
expect(mockPolicyEngine.addRule).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
toolName: 'mcp_browser_agent_fill',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should register ALLOW rules for read-only tools', async () => {
|
||||
mockBrowserManager.getDiscoveredTools.mockResolvedValue([
|
||||
{
|
||||
@@ -491,6 +521,7 @@ describe('browserAgentFactory', () => {
|
||||
toolName: 'mcp_browser_agent_take_snapshot',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: PRIORITY_SUBAGENT_TOOL,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -499,6 +530,7 @@ describe('browserAgentFactory', () => {
|
||||
toolName: 'mcp_browser_agent_take_screenshot',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: PRIORITY_SUBAGENT_TOOL,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -507,6 +539,7 @@ describe('browserAgentFactory', () => {
|
||||
toolName: 'mcp_browser_agent_list_pages',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: PRIORITY_SUBAGENT_TOOL,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
PolicyDecision,
|
||||
PRIORITY_SUBAGENT_TOOL,
|
||||
type PolicyRule,
|
||||
MODES_BY_PERMISSIVENESS,
|
||||
} from '../../policy/types.js';
|
||||
|
||||
/**
|
||||
@@ -151,6 +152,7 @@ export async function createBrowserAgentDefinition(
|
||||
toolName: `${MCP_TOOL_PREFIX}${BROWSER_AGENT_NAME}_${toolName}`,
|
||||
decision: PolicyDecision.ASK_USER,
|
||||
priority: 999,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
source: 'BrowserAgent (Sensitive Actions)',
|
||||
mcpName: BROWSER_AGENT_NAME,
|
||||
};
|
||||
@@ -161,6 +163,7 @@ export async function createBrowserAgentDefinition(
|
||||
toolName: `${MCP_TOOL_PREFIX}${BROWSER_AGENT_NAME}_${toolName}`,
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: PRIORITY_SUBAGENT_TOOL,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
source: 'BrowserAgent (Read-Only)',
|
||||
mcpName: BROWSER_AGENT_NAME,
|
||||
};
|
||||
@@ -172,7 +175,9 @@ export async function createBrowserAgentDefinition(
|
||||
rule1.toolName === rule2.toolName &&
|
||||
rule1.decision === rule2.decision &&
|
||||
rule1.priority === rule2.priority &&
|
||||
rule1.mcpName === rule2.mcpName
|
||||
rule1.mcpName === rule2.mcpName &&
|
||||
rule1.modes.length === rule2.modes.length &&
|
||||
rule1.modes.every((m) => rule2.modes.includes(m))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import { SimpleExtensionLoader } from '../utils/extensionLoader.js';
|
||||
import type { ToolRegistry } from '../tools/tool-registry.js';
|
||||
import { ThinkingLevel } from '@google/genai';
|
||||
import type { AcknowledgedAgentsService } from './acknowledgedAgents.js';
|
||||
import { PolicyDecision } from '../policy/types.js';
|
||||
import { PolicyDecision, MODES_BY_PERMISSIVENESS } from '../policy/types.js';
|
||||
import { A2AAuthProviderFactory } from './auth-provider/factory.js';
|
||||
import type { A2AAuthProvider } from './auth-provider/types.js';
|
||||
|
||||
@@ -1076,6 +1076,7 @@ describe('AgentRegistry', () => {
|
||||
toolName: 'PolicyTestAgent',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 1.05,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1103,6 +1104,7 @@ describe('AgentRegistry', () => {
|
||||
toolName: 'RemotePolicyAgent',
|
||||
decision: PolicyDecision.ASK_USER,
|
||||
priority: 1.05,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1165,6 +1167,7 @@ describe('AgentRegistry', () => {
|
||||
expect.objectContaining({
|
||||
toolName: 'OverwrittenAgent',
|
||||
decision: PolicyDecision.ASK_USER,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -25,7 +25,11 @@ import {
|
||||
type ModelConfig,
|
||||
ModelConfigService,
|
||||
} from '../services/modelConfigService.js';
|
||||
import { PolicyDecision, PRIORITY_SUBAGENT_TOOL } from '../policy/types.js';
|
||||
import {
|
||||
PolicyDecision,
|
||||
PRIORITY_SUBAGENT_TOOL,
|
||||
MODES_BY_PERMISSIVENESS,
|
||||
} from '../policy/types.js';
|
||||
import { A2AAgentError, AgentAuthConfigMissingError } from './a2a-errors.js';
|
||||
|
||||
/**
|
||||
@@ -388,6 +392,7 @@ export class AgentRegistry {
|
||||
? PolicyDecision.ALLOW
|
||||
: PolicyDecision.ASK_USER,
|
||||
priority: PRIORITY_SUBAGENT_TOOL,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
source: 'AgentRegistry (Dynamic)',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -329,7 +329,8 @@ describe('createPolicyEngineConfig', () => {
|
||||
(r) =>
|
||||
r.toolName === 'replace' &&
|
||||
r.decision === PolicyDecision.ALLOW &&
|
||||
r.modes?.includes(ApprovalMode.AUTO_EDIT),
|
||||
r.modes?.includes(ApprovalMode.AUTO_EDIT) &&
|
||||
!r.argsPattern,
|
||||
);
|
||||
expect(rule).toBeDefined();
|
||||
expect(rule?.priority).toBeCloseTo(1.015, 5);
|
||||
@@ -424,7 +425,7 @@ describe('createPolicyEngineConfig', () => {
|
||||
it('should handle complex priority scenarios correctly', async () => {
|
||||
mockPolicyFile(
|
||||
nodePath.join(MOCK_DEFAULT_DIR, 'default.toml'),
|
||||
'[[rule]]\ntoolName = "glob"\ndecision = "allow"\npriority = 50\n',
|
||||
'[[rule]]\ntoolName = "glob"\ndecision = "allow"\npriority = 50\nmodes = ["default", "plan", "autoEdit", "yolo"]\n',
|
||||
);
|
||||
|
||||
const settings: PolicySettings = {
|
||||
@@ -546,6 +547,7 @@ describe('createPolicyEngineConfig', () => {
|
||||
argsPattern = "\\"command\\":\\"git (status|diff|log)\\""
|
||||
decision = "allow"
|
||||
priority = 150
|
||||
modes = ["default", "plan", "autoEdit", "yolo"]
|
||||
`,
|
||||
);
|
||||
|
||||
@@ -576,10 +578,12 @@ describe('createPolicyEngineConfig', () => {
|
||||
toolName = "write_file"
|
||||
decision = "allow"
|
||||
priority = 10
|
||||
modes = ["default", "plan", "autoEdit", "yolo"]
|
||||
|
||||
[[safety_checker]]
|
||||
toolName = "write_file"
|
||||
priority = 10
|
||||
modes = ["default", "plan", "autoEdit", "yolo"]
|
||||
[safety_checker.checker]
|
||||
type = "in-process"
|
||||
name = "allowed-path"
|
||||
@@ -613,10 +617,12 @@ required_context = ["environment"]
|
||||
toolName = "write_file"
|
||||
decision = "allow"
|
||||
priority = 10
|
||||
modes = ["default", "plan", "autoEdit", "yolo"]
|
||||
|
||||
[[safety_checker]]
|
||||
toolName = "write_file"
|
||||
priority = 10
|
||||
modes = ["default", "plan", "autoEdit", "yolo"]
|
||||
[safety_checker.checker]
|
||||
type = "in-process"
|
||||
name = "invalid-name"
|
||||
@@ -642,6 +648,7 @@ name = "invalid-name"
|
||||
mcpName = "my-server"
|
||||
decision = "allow"
|
||||
priority = 150
|
||||
modes = ["default", "plan", "autoEdit", "yolo"]
|
||||
`,
|
||||
);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { Storage } from '../config/storage.js';
|
||||
import {
|
||||
ApprovalMode,
|
||||
MODES_BY_PERMISSIVENESS,
|
||||
type PolicyEngineConfig,
|
||||
PolicyDecision,
|
||||
type PolicyRule,
|
||||
@@ -415,6 +416,7 @@ export async function createPolicyEngineConfig(
|
||||
mcpName: serverName,
|
||||
decision: PolicyDecision.DENY,
|
||||
priority: MCP_EXCLUDED_PRIORITY,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
source: 'Settings (MCP Excluded)',
|
||||
});
|
||||
}
|
||||
@@ -428,6 +430,7 @@ export async function createPolicyEngineConfig(
|
||||
toolName: tool,
|
||||
decision: PolicyDecision.DENY,
|
||||
priority: EXCLUDE_TOOLS_FLAG_PRIORITY,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
source: 'Settings (Tools Excluded)',
|
||||
});
|
||||
}
|
||||
@@ -456,6 +459,7 @@ export async function createPolicyEngineConfig(
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: ALLOWED_TOOLS_FLAG_PRIORITY,
|
||||
argsPattern: new RegExp(pattern),
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
source: 'Settings (Tools Allowed)',
|
||||
});
|
||||
}
|
||||
@@ -467,6 +471,7 @@ export async function createPolicyEngineConfig(
|
||||
toolName,
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: ALLOWED_TOOLS_FLAG_PRIORITY,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
source: 'Settings (Tools Allowed)',
|
||||
});
|
||||
}
|
||||
@@ -479,6 +484,7 @@ export async function createPolicyEngineConfig(
|
||||
toolName,
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: ALLOWED_TOOLS_FLAG_PRIORITY,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
source: 'Settings (Tools Allowed)',
|
||||
});
|
||||
}
|
||||
@@ -499,6 +505,7 @@ export async function createPolicyEngineConfig(
|
||||
mcpName: serverName,
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: TRUSTED_MCP_SERVER_PRIORITY,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
source: 'Settings (MCP Trusted)',
|
||||
});
|
||||
}
|
||||
@@ -517,6 +524,7 @@ export async function createPolicyEngineConfig(
|
||||
mcpName: serverName,
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: ALLOWED_MCP_SERVER_PRIORITY,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
source: 'Settings (MCP Allowed)',
|
||||
});
|
||||
}
|
||||
@@ -576,6 +584,7 @@ function createTomlRule(toolName: string, message: UpdatePolicy): TomlRule {
|
||||
decision: 'allow',
|
||||
priority: getAlwaysAllowPriorityFraction(),
|
||||
toolName,
|
||||
modes: message.modes ?? MODES_BY_PERMISSIVENESS,
|
||||
};
|
||||
|
||||
if (message.mcpName) {
|
||||
@@ -592,10 +601,6 @@ function createTomlRule(toolName: string, message: UpdatePolicy): TomlRule {
|
||||
rule.allowRedirection = message.allowRedirection;
|
||||
}
|
||||
|
||||
if (message.modes) {
|
||||
rule.modes = message.modes;
|
||||
}
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
@@ -638,7 +643,7 @@ export function createPolicyUpdater(
|
||||
priority,
|
||||
argsPattern: new RegExp(pattern),
|
||||
mcpName: message.mcpName,
|
||||
modes: message.modes,
|
||||
modes: message.modes ?? MODES_BY_PERMISSIVENESS,
|
||||
source: 'Dynamic (Confirmed)',
|
||||
allowRedirection: message.allowRedirection,
|
||||
});
|
||||
@@ -676,7 +681,7 @@ export function createPolicyUpdater(
|
||||
priority,
|
||||
argsPattern,
|
||||
mcpName: message.mcpName,
|
||||
modes: message.modes,
|
||||
modes: message.modes ?? MODES_BY_PERMISSIVENESS,
|
||||
source: 'Dynamic (Confirmed)',
|
||||
allowRedirection: message.allowRedirection,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[[safety_checker]]
|
||||
toolName = "*"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
[safety_checker.checker]
|
||||
type = "in-process"
|
||||
name = "conseca"
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
toolName = "discovered_tool_*"
|
||||
decision = "ask_user"
|
||||
priority = 10
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
interactive = true
|
||||
|
||||
[[rule]]
|
||||
toolName = "discovered_tool_*"
|
||||
decision = "deny"
|
||||
priority = 10
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
interactive = false
|
||||
|
||||
@@ -7,6 +7,7 @@ subagent = "save_memory"
|
||||
toolName = ["read_file", "list_directory", "glob", "grep_search"]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
argsPattern = "(^|.*/)\\.gemini/.*"
|
||||
denyMessage = "Memory Manager is only allowed to access the .gemini folder."
|
||||
|
||||
@@ -16,5 +17,6 @@ subagent = "save_memory"
|
||||
toolName = ["write_file", "replace"]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
argsPattern = "(^|.*/)\\.gemini/.*\\.md\""
|
||||
denyMessage = "Memory Manager is only allowed to write .md files in the .gemini folder."
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
toolName = "ask_user"
|
||||
decision = "deny"
|
||||
priority = 999
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
interactive = false
|
||||
|
||||
@@ -33,12 +33,14 @@
|
||||
toolName = "enter_plan_mode"
|
||||
decision = "ask_user"
|
||||
priority = 50
|
||||
modes = ["default", "autoEdit", "yolo"]
|
||||
interactive = true
|
||||
|
||||
[[rule]]
|
||||
toolName = "enter_plan_mode"
|
||||
decision = "allow"
|
||||
priority = 50
|
||||
modes = ["default", "autoEdit", "yolo"]
|
||||
interactive = false
|
||||
|
||||
[[rule]]
|
||||
@@ -59,12 +61,14 @@ interactive = true
|
||||
toolName = "exit_plan_mode"
|
||||
decision = "allow"
|
||||
priority = 70
|
||||
modes = ["plan"]
|
||||
interactive = false
|
||||
|
||||
[[rule]]
|
||||
toolName = "exit_plan_mode"
|
||||
decision = "deny"
|
||||
priority = 50
|
||||
modes = ["default", "autoEdit", "yolo"]
|
||||
denyMessage = "You are not currently in Plan Mode. Use enter_plan_mode first to design a plan."
|
||||
|
||||
|
||||
|
||||
@@ -31,40 +31,48 @@
|
||||
toolName = "glob"
|
||||
decision = "allow"
|
||||
priority = 50
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
|
||||
[[rule]]
|
||||
toolName = "grep_search"
|
||||
decision = "allow"
|
||||
priority = 50
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
|
||||
[[rule]]
|
||||
toolName = "list_directory"
|
||||
decision = "allow"
|
||||
priority = 50
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
|
||||
[[rule]]
|
||||
toolName = "read_file"
|
||||
decision = "allow"
|
||||
priority = 50
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
|
||||
[[rule]]
|
||||
toolName = "google_web_search"
|
||||
decision = "allow"
|
||||
priority = 50
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
|
||||
[[rule]]
|
||||
toolName = ["codebase_investigator", "cli_help", "get_internal_docs"]
|
||||
decision = "allow"
|
||||
priority = 50
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
|
||||
# Topic grouping tool is innocuous and used for UI organization.
|
||||
[[rule]]
|
||||
toolName = "update_topic"
|
||||
decision = "allow"
|
||||
priority = 50
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
|
||||
# Core agent lifecycle tool
|
||||
[[rule]]
|
||||
toolName = "complete_task"
|
||||
decision = "allow"
|
||||
priority = 50
|
||||
priority = 50
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
network = false
|
||||
readonly = true
|
||||
approvedTools = []
|
||||
allowOverrides = false
|
||||
allowOverrides = true
|
||||
|
||||
[modes.default]
|
||||
network = false
|
||||
|
||||
@@ -32,3 +32,4 @@ toolName = [
|
||||
]
|
||||
decision = "allow"
|
||||
priority = 50
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
|
||||
@@ -31,13 +31,14 @@
|
||||
toolName = "replace"
|
||||
decision = "ask_user"
|
||||
priority = 10
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
interactive = true
|
||||
|
||||
[[rule]]
|
||||
toolName = "replace"
|
||||
decision = "allow"
|
||||
priority = 15
|
||||
modes = ["autoEdit"]
|
||||
modes = ["autoEdit", "yolo"]
|
||||
|
||||
[rule.safety_checker]
|
||||
type = "in-process"
|
||||
@@ -48,31 +49,35 @@ required_context = ["environment"]
|
||||
toolName = "save_memory"
|
||||
decision = "ask_user"
|
||||
priority = 10
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
interactive = true
|
||||
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
decision = "ask_user"
|
||||
priority = 10
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
interactive = true
|
||||
|
||||
[[rule]]
|
||||
toolName = "write_file"
|
||||
decision = "ask_user"
|
||||
priority = 10
|
||||
modes = ["plan", "default"]
|
||||
interactive = true
|
||||
|
||||
[[rule]]
|
||||
toolName = "activate_skill"
|
||||
decision = "ask_user"
|
||||
priority = 10
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
interactive = true
|
||||
|
||||
[[rule]]
|
||||
toolName = "write_file"
|
||||
decision = "allow"
|
||||
priority = 15
|
||||
modes = ["autoEdit"]
|
||||
modes = ["autoEdit", "yolo"]
|
||||
|
||||
[rule.safety_checker]
|
||||
type = "in-process"
|
||||
@@ -83,12 +88,13 @@ required_context = ["environment"]
|
||||
toolName = "web_fetch"
|
||||
decision = "allow"
|
||||
priority = 15
|
||||
modes = ["autoEdit"]
|
||||
modes = ["autoEdit", "yolo"]
|
||||
|
||||
[[rule]]
|
||||
toolName = "web_fetch"
|
||||
decision = "ask_user"
|
||||
priority = 10
|
||||
modes = ["plan", "default"]
|
||||
interactive = true
|
||||
|
||||
# Headless Denial Rule (Priority 10)
|
||||
@@ -104,4 +110,5 @@ toolName = [
|
||||
]
|
||||
decision = "deny"
|
||||
priority = 10
|
||||
modes = ["plan", "default"]
|
||||
interactive = false
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -89,13 +89,12 @@ function ruleMatches(
|
||||
toolAnnotations?: Record<string, unknown>,
|
||||
subagent?: string,
|
||||
): boolean {
|
||||
// Check if rule applies to current approval mode
|
||||
if (rule.modes && rule.modes.length > 0) {
|
||||
if (!rule.modes.includes(currentApprovalMode)) {
|
||||
return false;
|
||||
}
|
||||
// Check if rule applies to current approval mode.
|
||||
// Legacy rules or those injected manually in tests might not have the modes field.
|
||||
// If so, we default to matching all modes for backward compatibility.
|
||||
if (rule.modes && !rule.modes.includes(currentApprovalMode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check subagent if specified (only for PolicyRule, SafetyCheckerRule doesn't have it)
|
||||
if ('subagent' in rule && rule.subagent !== undefined) {
|
||||
if (rule.subagent !== subagent) {
|
||||
@@ -571,7 +570,7 @@ export class PolicyEngine {
|
||||
|
||||
if (match) {
|
||||
debugLogger.debug(
|
||||
`[PolicyEngine.check] MATCHED rule: toolName=${rule.toolName}, decision=${rule.decision}, priority=${rule.priority}, argsPattern=${rule.argsPattern?.source || 'none'}`,
|
||||
`[PolicyEngine.check] MATCHED rule: toolName=${rule.toolName}, decision=${rule.decision}, priority=${rule.priority}, source=${rule.source}, argsPattern=${rule.argsPattern?.source || 'none'}`,
|
||||
);
|
||||
|
||||
let ruleDecision = rule.decision;
|
||||
|
||||
@@ -69,4 +69,41 @@ describe('SandboxPolicyManager', () => {
|
||||
const perms = manager.getCommandPermissions('npm');
|
||||
expect(perms.fileSystem?.read).toContain('/node_modules');
|
||||
});
|
||||
|
||||
describe('getModeConfig', () => {
|
||||
it('should return default config for plan mode', () => {
|
||||
const manager = new SandboxPolicyManager(configPath);
|
||||
const config = manager.getModeConfig('plan');
|
||||
expect(config.readonly).toBe(true);
|
||||
expect(config.network).toBe(false);
|
||||
// Regression test: allowOverrides must be true to support integration tests
|
||||
// that use the sandbox manager manually with specific permissions.
|
||||
expect(config.allowOverrides).toBe(true);
|
||||
});
|
||||
|
||||
it('should return default config for default mode', () => {
|
||||
const manager = new SandboxPolicyManager(configPath);
|
||||
const config = manager.getModeConfig('default');
|
||||
expect(config.readonly).toBe(false);
|
||||
expect(config.network).toBe(false);
|
||||
expect(config.allowOverrides).toBe(true);
|
||||
});
|
||||
|
||||
it('should return default config for autoEdit mode', () => {
|
||||
const manager = new SandboxPolicyManager(configPath);
|
||||
const config = manager.getModeConfig('autoEdit');
|
||||
expect(config.readonly).toBe(false);
|
||||
expect(config.network).toBe(false);
|
||||
expect(config.allowOverrides).toBe(true);
|
||||
});
|
||||
|
||||
it('should return yolo config', () => {
|
||||
const manager = new SandboxPolicyManager(configPath);
|
||||
const config = manager.getModeConfig('yolo');
|
||||
expect(config.readonly).toBe(false);
|
||||
expect(config.network).toBe(true);
|
||||
expect(config.allowOverrides).toBe(true);
|
||||
expect(config.yolo).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ export class SandboxPolicyManager {
|
||||
network: false,
|
||||
readonly: true,
|
||||
approvedTools: [],
|
||||
allowOverrides: false,
|
||||
allowOverrides: true,
|
||||
},
|
||||
default: {
|
||||
network: false,
|
||||
|
||||
@@ -75,7 +75,11 @@ vi.mock('../utils/shell-utils.js', async (importOriginal) => {
|
||||
});
|
||||
|
||||
import { PolicyEngine } from './policy-engine.js';
|
||||
import { PolicyDecision, ApprovalMode } from './types.js';
|
||||
import {
|
||||
PolicyDecision,
|
||||
ApprovalMode,
|
||||
MODES_BY_PERMISSIVENESS,
|
||||
} from './types.js';
|
||||
import type { FunctionCall } from '@google/genai';
|
||||
import { buildArgsPatterns } from './utils.js';
|
||||
|
||||
@@ -96,6 +100,7 @@ describe('Shell Safety Policy', () => {
|
||||
argsPattern,
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 1.01,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
],
|
||||
defaultDecision: PolicyDecision.ASK_USER,
|
||||
@@ -204,12 +209,14 @@ describe('Shell Safety Policy', () => {
|
||||
argsPattern: new RegExp(argsPatternsEcho[0]!),
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 2,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
{
|
||||
toolName: 'run_shell_command',
|
||||
argsPattern: new RegExp(argsPatternsGit[0]!),
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 2,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
],
|
||||
defaultDecision: PolicyDecision.ASK_USER,
|
||||
@@ -290,6 +297,7 @@ describe('Shell Safety Policy', () => {
|
||||
argsPattern: new RegExp(argsPatternsEcho[0]!),
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 2,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
{
|
||||
toolName: 'run_shell_command',
|
||||
@@ -297,6 +305,7 @@ describe('Shell Safety Policy', () => {
|
||||
argsPattern: new RegExp(argsPatternsGit[0]!),
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 2,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
],
|
||||
defaultDecision: PolicyDecision.ASK_USER,
|
||||
@@ -336,6 +345,7 @@ describe('Shell Safety Policy', () => {
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 2,
|
||||
allowRedirection: true,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
],
|
||||
defaultDecision: PolicyDecision.ASK_USER,
|
||||
@@ -378,6 +388,7 @@ describe('Shell Safety Policy', () => {
|
||||
argsPattern: new RegExp(argsPatternsPush[0]!),
|
||||
decision: PolicyDecision.DENY,
|
||||
priority: 2,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
],
|
||||
defaultDecision: PolicyDecision.ASK_USER,
|
||||
@@ -409,7 +420,8 @@ describe('Shell Safety Policy', () => {
|
||||
argsPattern: new RegExp(argsPatternsGitStatus[0]!),
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 2,
|
||||
name: 'allow_git_status_rule', // Give a name to easily identify
|
||||
name: 'allow_git_status_rule', // Give a name to easily identify,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
],
|
||||
defaultDecision: PolicyDecision.ASK_USER,
|
||||
@@ -447,6 +459,7 @@ describe('Shell Safety Policy', () => {
|
||||
decision: PolicyDecision.ASK_USER,
|
||||
priority: 2,
|
||||
name: 'ask_another_unknown_command_rule',
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
],
|
||||
defaultDecision: PolicyDecision.ASK_USER,
|
||||
@@ -490,6 +503,7 @@ describe('Shell Safety Policy', () => {
|
||||
decision: PolicyDecision.ASK_USER,
|
||||
priority: 2,
|
||||
name: 'ask_rule_1',
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
{
|
||||
toolName: 'run_shell_command',
|
||||
@@ -497,6 +511,7 @@ describe('Shell Safety Policy', () => {
|
||||
decision: PolicyDecision.ASK_USER,
|
||||
priority: 2,
|
||||
name: 'ask_rule_2',
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
],
|
||||
defaultDecision: PolicyDecision.ALLOW, // Set default to ALLOW to ensure rules are hit
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
PolicyDecision,
|
||||
ApprovalMode,
|
||||
PRIORITY_SUBAGENT_TOOL,
|
||||
MODES_BY_PERMISSIVENESS,
|
||||
} from './types.js';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
@@ -68,6 +69,7 @@ describe('policy-toml-loader', () => {
|
||||
toolName = "glob"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(1);
|
||||
@@ -76,6 +78,7 @@ priority = 100
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 1.1, // tier 1 + 100/1000
|
||||
source: 'Default: test.toml',
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
});
|
||||
expect(result.checkers).toHaveLength(0);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
@@ -88,6 +91,7 @@ toolName = "run_shell_command"
|
||||
commandPrefix = ["git status", "git log"]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(2);
|
||||
@@ -109,6 +113,7 @@ toolName = "annotated-tool"
|
||||
toolAnnotations = { readOnlyHint = true, custom = "value" }
|
||||
decision = "allow"
|
||||
priority = 70
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(1);
|
||||
@@ -127,6 +132,7 @@ toolName = "*"
|
||||
mcpName = "*"
|
||||
decision = "ask_user"
|
||||
priority = 10
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(1);
|
||||
@@ -142,6 +148,7 @@ mcpName = "*"
|
||||
toolName = "search"
|
||||
decision = "allow"
|
||||
priority = 10
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(1);
|
||||
@@ -156,6 +163,7 @@ toolName = "run_shell_command"
|
||||
commandRegex = "git (status|log).*"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(1);
|
||||
@@ -178,6 +186,7 @@ toolName = "run_shell_command"
|
||||
commandRegex = "^git status"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(1);
|
||||
@@ -195,6 +204,7 @@ priority = 100
|
||||
toolName = ["glob", "grep", "read"]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(3);
|
||||
@@ -213,6 +223,7 @@ mcpName = "google-workspace"
|
||||
toolName = ["calendar.list", "calendar.get"]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(2);
|
||||
@@ -257,6 +268,7 @@ commandPrefix = "echo"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
allow_redirection = true
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(1);
|
||||
@@ -272,6 +284,7 @@ commandPrefix = "echo"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
allowRedirection = true
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(1);
|
||||
@@ -285,6 +298,7 @@ toolName = "rm"
|
||||
decision = "deny"
|
||||
priority = 100
|
||||
deny_message = "Deletion is permanent"
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(1);
|
||||
@@ -300,6 +314,7 @@ toolName = "rm"
|
||||
decision = "deny"
|
||||
priority = 100
|
||||
denyMessage = "Deletion is permanent"
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(1);
|
||||
@@ -357,6 +372,7 @@ priority = 100
|
||||
[[rule]]
|
||||
toolName = "glob"
|
||||
priority = 100
|
||||
modes = ["default"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(0);
|
||||
@@ -372,6 +388,7 @@ toolName = "glob"
|
||||
commandPrefix = "git status"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.errors).toHaveLength(1);
|
||||
@@ -387,6 +404,7 @@ commandPrefix = "git status"
|
||||
argsPattern = "test"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.errors).toHaveLength(1);
|
||||
@@ -401,6 +419,7 @@ toolName = "run_shell_command"
|
||||
commandRegex = "git (status|branch"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(0);
|
||||
@@ -416,6 +435,7 @@ toolName = "run_shell_command"
|
||||
commandPrefix = "git log *.txt"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(1);
|
||||
@@ -437,6 +457,7 @@ priority = 100
|
||||
toolName = "glob"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`,
|
||||
);
|
||||
|
||||
@@ -447,6 +468,7 @@ priority = 100
|
||||
toolName = "grep"
|
||||
decision = "allow"
|
||||
priority = -1
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`,
|
||||
);
|
||||
|
||||
@@ -465,6 +487,7 @@ priority = -1
|
||||
[[safety_checker]]
|
||||
toolName = "write_file"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
[safety_checker.checker]
|
||||
type = "in-process"
|
||||
name = "allowed-path"
|
||||
@@ -477,10 +500,62 @@ name = "allowed-path"
|
||||
});
|
||||
|
||||
describe('Negative Tests', () => {
|
||||
it('should fail validation if modes field is missing in a rule', async () => {
|
||||
const result = await runLoadPoliciesFromToml(`
|
||||
[[rule]]
|
||||
toolName = "glob"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
`);
|
||||
|
||||
const errors = result.errors.filter(
|
||||
(e) => e.errorType === 'schema_validation',
|
||||
);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].message).toBe('Schema validation failed');
|
||||
expect(errors[0].details).toContain('modes');
|
||||
expect(errors[0].details).toContain('Required');
|
||||
});
|
||||
|
||||
it('should fail validation if modes field is missing in a safety checker', async () => {
|
||||
const result = await runLoadPoliciesFromToml(`
|
||||
[[safety_checker]]
|
||||
toolName = "run_shell_command"
|
||||
priority = 100
|
||||
checker = { type = "in-process", name = "allowed-path" }
|
||||
`);
|
||||
|
||||
const errors = result.errors.filter(
|
||||
(e) => e.errorType === 'schema_validation',
|
||||
);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].message).toBe('Schema validation failed');
|
||||
expect(errors[0].details).toContain('modes');
|
||||
expect(errors[0].details).toContain('Required');
|
||||
});
|
||||
|
||||
it('should not contain hardcoded required fields in the suggestion when validation fails', async () => {
|
||||
const result = await runLoadPoliciesFromToml(`
|
||||
[[rule]]
|
||||
toolName = "glob"
|
||||
# missing decision and priority and modes
|
||||
`);
|
||||
|
||||
const errors = result.errors.filter(
|
||||
(e) => e.errorType === 'schema_validation',
|
||||
);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].suggestion).not.toContain('(decision, priority)');
|
||||
expect(errors[0].suggestion).toBe(
|
||||
'Ensure all required fields are present with correct types',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a schema_validation error if toolName is missing in safety_checker', async () => {
|
||||
const result = await runLoadPoliciesFromToml(`
|
||||
[[safety_checker]]
|
||||
priority = 100
|
||||
modes = ["default"]
|
||||
[safety_checker.checker]
|
||||
type = "in-process"
|
||||
name = "allowed-path"
|
||||
@@ -497,6 +572,7 @@ name = "allowed-path"
|
||||
[[rule]]
|
||||
toolName = "test"
|
||||
decision = "allow"
|
||||
modes = ["default"]
|
||||
`);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
const error = result.errors[0];
|
||||
@@ -510,6 +586,7 @@ decision = "allow"
|
||||
toolName = "test"
|
||||
decision = "allow"
|
||||
priority = 1.5
|
||||
modes = ["default"]
|
||||
`);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
const error = result.errors[0];
|
||||
@@ -524,6 +601,7 @@ priority = 1.5
|
||||
toolName = "test"
|
||||
decision = "allow"
|
||||
priority = -1
|
||||
modes = ["default"]
|
||||
`);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
const error = result.errors[0];
|
||||
@@ -538,6 +616,7 @@ priority = -1
|
||||
toolName = "test"
|
||||
decision = "allow"
|
||||
priority = -9999
|
||||
modes = ["default"]
|
||||
`);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
const error = result.errors[0];
|
||||
@@ -552,6 +631,7 @@ priority = -9999
|
||||
toolName = "test"
|
||||
decision = "allow"
|
||||
priority = 1000
|
||||
modes = ["default"]
|
||||
`);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
const error = result.errors[0];
|
||||
@@ -566,6 +646,7 @@ priority = 1000
|
||||
toolName = "test"
|
||||
decision = "allow"
|
||||
priority = 9999
|
||||
modes = ["default"]
|
||||
`);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
const error = result.errors[0];
|
||||
@@ -580,6 +661,7 @@ priority = 9999
|
||||
toolName = "test"
|
||||
decision = "maybe"
|
||||
priority = 100
|
||||
modes = ["default"]
|
||||
`);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
const error = result.errors[0];
|
||||
@@ -592,6 +674,7 @@ priority = 100
|
||||
[[rule]]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["default"]
|
||||
`);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
const error = result.errors[0];
|
||||
@@ -606,6 +689,7 @@ priority = 100
|
||||
toolName = 123
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["default"]
|
||||
`);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
const error = result.errors[0];
|
||||
@@ -620,6 +704,7 @@ toolName = "not_shell"
|
||||
commandRegex = ".*"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["default"]
|
||||
`);
|
||||
expect(getErrors(result)).toHaveLength(1);
|
||||
const error = getErrors(result)[0];
|
||||
@@ -635,6 +720,7 @@ commandPrefix = "git"
|
||||
commandRegex = ".*"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["default"]
|
||||
`);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
const error = result.errors[0];
|
||||
@@ -649,6 +735,7 @@ toolName = "test"
|
||||
argsPattern = "([a-z)"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["default"]
|
||||
`);
|
||||
expect(getErrors(result)).toHaveLength(1);
|
||||
const error = getErrors(result)[0];
|
||||
@@ -660,7 +747,7 @@ priority = 100
|
||||
const filePath = path.join(tempDir, 'single-rule.toml');
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
'[[rule]]\ntoolName = "test-tool"\ndecision = "allow"\npriority = 500\n',
|
||||
'[[rule]]\ntoolName = "test-tool"\ndecision = "allow"\npriority = 500\nmodes = ["default"]\n',
|
||||
);
|
||||
|
||||
const getPolicyTier = (_dir: string) => 1;
|
||||
@@ -693,6 +780,7 @@ priority = 100
|
||||
toolName = "grob"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
const warnings = getWarnings(result);
|
||||
@@ -712,11 +800,13 @@ priority = 100
|
||||
toolName = "glob"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
|
||||
[[rule]]
|
||||
toolName = "read_file"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(getWarnings(result)).toHaveLength(0);
|
||||
@@ -730,6 +820,7 @@ priority = 100
|
||||
toolName = "*"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(getWarnings(result)).toHaveLength(0);
|
||||
@@ -742,11 +833,13 @@ priority = 100
|
||||
toolName = "mcp_my-server_my-tool"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
|
||||
[[rule]]
|
||||
toolName = "mcp_my-server_*"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(getWarnings(result)).toHaveLength(0);
|
||||
@@ -760,6 +853,7 @@ mcpName = "my-server"
|
||||
toolName = "nonexistent"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(getWarnings(result)).toHaveLength(0);
|
||||
@@ -772,6 +866,7 @@ priority = 100
|
||||
toolName = "search_file_content"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(getWarnings(result)).toHaveLength(0);
|
||||
@@ -784,6 +879,7 @@ priority = 100
|
||||
toolName = "discovered_tool_my_custom_tool"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(getWarnings(result)).toHaveLength(0);
|
||||
@@ -796,6 +892,7 @@ priority = 100
|
||||
toolName = ["grob", "glob", "replce"]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
const warnings = getWarnings(result);
|
||||
@@ -812,11 +909,13 @@ priority = 100
|
||||
toolName = "delegate_to_agent"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
|
||||
[[rule]]
|
||||
toolName = "my_custom_tool"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(getWarnings(result)).toHaveLength(0);
|
||||
@@ -830,6 +929,7 @@ priority = 100
|
||||
toolName = "*"
|
||||
decision = "deny"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(getWarnings(result)).toHaveLength(0);
|
||||
@@ -843,11 +943,13 @@ priority = 100
|
||||
toolName = "wrte_file"
|
||||
decision = "deny"
|
||||
priority = 50
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
|
||||
[[rule]]
|
||||
toolName = "glob"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`);
|
||||
|
||||
expect(getWarnings(result)).toHaveLength(1);
|
||||
@@ -1000,6 +1102,7 @@ priority = 100
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: PRIORITY_SUBAGENT_TOOL,
|
||||
source: 'AgentRegistry (Dynamic)',
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
});
|
||||
|
||||
// 4. Verify Behavior:
|
||||
|
||||
@@ -60,7 +60,7 @@ const PolicyRuleSchema = z.object({
|
||||
message:
|
||||
'priority must be <= 999 to prevent tier overflow. Priorities >= 1000 would jump to the next tier.',
|
||||
}),
|
||||
modes: z.array(z.nativeEnum(ApprovalMode)).optional(),
|
||||
modes: z.array(z.nativeEnum(ApprovalMode)),
|
||||
interactive: z.boolean().optional(),
|
||||
toolAnnotations: z.record(z.any()).optional(),
|
||||
allowRedirection: z.boolean().optional(),
|
||||
@@ -79,7 +79,7 @@ const SafetyCheckerRuleSchema = z.object({
|
||||
commandPrefix: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
commandRegex: z.string().optional(),
|
||||
priority: z.number().int().default(0),
|
||||
modes: z.array(z.nativeEnum(ApprovalMode)).optional(),
|
||||
modes: z.array(z.nativeEnum(ApprovalMode)),
|
||||
toolAnnotations: z.record(z.any()).optional(),
|
||||
checker: z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
@@ -383,7 +383,7 @@ export async function loadPoliciesFromToml(
|
||||
message: 'Schema validation failed',
|
||||
details: formatSchemaError(validationResult.error, 0),
|
||||
suggestion:
|
||||
'Ensure all required fields (decision, priority) are present with correct types',
|
||||
'Ensure all required fields are present with correct types',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -161,9 +161,8 @@ export interface PolicyRule {
|
||||
|
||||
/**
|
||||
* Approval modes this rule applies to.
|
||||
* If undefined or empty, it applies to all modes.
|
||||
*/
|
||||
modes?: ApprovalMode[];
|
||||
modes: ApprovalMode[];
|
||||
|
||||
/**
|
||||
* If true, this rule only applies to interactive environments.
|
||||
@@ -230,9 +229,8 @@ export interface SafetyCheckerRule {
|
||||
|
||||
/**
|
||||
* Approval modes this rule applies to.
|
||||
* If undefined or empty, it applies to all modes.
|
||||
*/
|
||||
modes?: ApprovalMode[];
|
||||
modes: ApprovalMode[];
|
||||
|
||||
/**
|
||||
* Source of the rule.
|
||||
|
||||
@@ -92,6 +92,7 @@ describe('Workspace-Level Policies', () => {
|
||||
toolName = "test_tool"
|
||||
decision = "allow"
|
||||
priority = 10
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`; // Tier 1 -> 1.010
|
||||
}
|
||||
if (path.includes('user.toml')) {
|
||||
@@ -99,6 +100,7 @@ priority = 10
|
||||
toolName = "test_tool"
|
||||
decision = "deny"
|
||||
priority = 10
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`; // Tier 4 -> 4.010
|
||||
}
|
||||
if (path.includes('workspace.toml')) {
|
||||
@@ -106,6 +108,7 @@ priority = 10
|
||||
toolName = "test_tool"
|
||||
decision = "allow"
|
||||
priority = 10
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`; // Tier 3 -> 3.010
|
||||
}
|
||||
if (path.includes('admin.toml')) {
|
||||
@@ -113,6 +116,7 @@ priority = 10
|
||||
toolName = "test_tool"
|
||||
decision = "deny"
|
||||
priority = 10
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]
|
||||
`; // Tier 5 -> 5.010
|
||||
}
|
||||
return '';
|
||||
@@ -196,7 +200,8 @@ priority = 10
|
||||
async () => `[[rule]]
|
||||
toolName="t"
|
||||
decision="allow"
|
||||
priority=10`,
|
||||
priority=10
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]`,
|
||||
);
|
||||
|
||||
vi.doMock('node:fs/promises', () => ({
|
||||
@@ -262,7 +267,8 @@ priority=10`,
|
||||
async () => `[[rule]]
|
||||
toolName="p_tool"
|
||||
decision="allow"
|
||||
priority=500`,
|
||||
priority=500
|
||||
modes = ["plan", "default", "autoEdit", "yolo"]`,
|
||||
);
|
||||
|
||||
vi.doMock('node:fs/promises', () => ({
|
||||
|
||||
@@ -21,7 +21,11 @@ import {
|
||||
MessageBusType,
|
||||
type SerializableConfirmationDetails,
|
||||
} from '../confirmation-bus/types.js';
|
||||
import { ApprovalMode, PolicyDecision } from '../policy/types.js';
|
||||
import {
|
||||
ApprovalMode,
|
||||
PolicyDecision,
|
||||
MODES_BY_PERMISSIVENESS,
|
||||
} from '../policy/types.js';
|
||||
import { escapeRegex } from '../policy/utils.js';
|
||||
import {
|
||||
ToolConfirmationOutcome,
|
||||
@@ -786,6 +790,7 @@ describe('policy.ts', () => {
|
||||
toolName: '*',
|
||||
decision: PolicyDecision.DENY,
|
||||
denyMessage: 'Custom Deny',
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
};
|
||||
|
||||
const { errorMessage, errorType } = getPolicyDenialError(
|
||||
|
||||
@@ -68,7 +68,11 @@ import type { Config } from '../config/config.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import type { PolicyEngine } from '../policy/policy-engine.js';
|
||||
import type { ToolRegistry } from '../tools/tool-registry.js';
|
||||
import { PolicyDecision, ApprovalMode } from '../policy/types.js';
|
||||
import {
|
||||
PolicyDecision,
|
||||
ApprovalMode,
|
||||
MODES_BY_PERMISSIVENESS,
|
||||
} from '../policy/types.js';
|
||||
import {
|
||||
ToolConfirmationOutcome,
|
||||
type AnyDeclarativeTool,
|
||||
@@ -684,6 +688,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
toolName: '*',
|
||||
decision: PolicyDecision.DENY,
|
||||
denyMessage: 'Custom denial reason',
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -757,7 +762,11 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
it('should return POLICY_VIOLATION error type when denied in Plan Mode', async () => {
|
||||
vi.mocked(checkPolicy).mockResolvedValue({
|
||||
decision: PolicyDecision.DENY,
|
||||
rule: { toolName: '*', decision: PolicyDecision.DENY },
|
||||
rule: {
|
||||
toolName: '*',
|
||||
decision: PolicyDecision.DENY,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
});
|
||||
|
||||
mockConfig.getApprovalMode.mockReturnValue(ApprovalMode.PLAN);
|
||||
@@ -790,6 +799,7 @@ describe('Scheduler (Orchestrator)', () => {
|
||||
toolName: '*',
|
||||
decision: PolicyDecision.DENY,
|
||||
denyMessage: customMessage,
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ExecutionLifecycleService } from './executionLifecycleService.js';
|
||||
import { PromptRegistry } from '../prompts/prompt-registry.js';
|
||||
import { ResourceRegistry } from '../resources/resource-registry.js';
|
||||
import { PolicyEngine } from '../policy/policy-engine.js';
|
||||
import { PolicyDecision } from '../policy/types.js';
|
||||
import { PolicyDecision, ApprovalMode } from '../policy/types.js';
|
||||
import { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import { Storage } from '../config/storage.js';
|
||||
import type { AgentLoopContext } from '../config/agent-loop-context.js';
|
||||
@@ -475,6 +475,7 @@ function buildAgentLoopContext(config: Config): AgentLoopContext {
|
||||
toolName: '*',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 100,
|
||||
modes: [ApprovalMode.DEFAULT],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type MockInstance,
|
||||
} from 'vitest';
|
||||
import { SimpleExtensionLoader } from './extensionLoader.js';
|
||||
import { PolicyDecision } from '../policy/types.js';
|
||||
import { PolicyDecision, MODES_BY_PERMISSIVENESS } from '../policy/types.js';
|
||||
import type { Config, GeminiCLIExtension } from '../config/config.js';
|
||||
import { type McpClientManager } from '../tools/mcp-client-manager.js';
|
||||
import type { GeminiClient } from '../core/client.js';
|
||||
@@ -59,12 +59,14 @@ describe('SimpleExtensionLoader', () => {
|
||||
toolName: 'test-tool',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
source: 'Extension (test-extension): policies.toml',
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
},
|
||||
],
|
||||
checkers: [
|
||||
{
|
||||
toolName: 'test-tool',
|
||||
checker: { type: 'external', name: 'test-checker' },
|
||||
modes: MODES_BY_PERMISSIVENESS,
|
||||
source: 'Extension (test-extension): policies.toml',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
function sanitizeJsonString(jsonStr: string): string {
|
||||
// Match a comma, optional whitespace/newlines, then another comma.
|
||||
// Replace with just a comma + the captured whitespace.
|
||||
// Loop to handle cases like `,,,` which would otherwise become `,,` on a single pass.
|
||||
// Loop to handle cases like `,,` which would otherwise become `,` on a single pass.
|
||||
let prev: string;
|
||||
do {
|
||||
prev = jsonStr;
|
||||
|
||||
Reference in New Issue
Block a user