mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
feat(core): Tool Confirmation Message Bus foundation (PR 1 of 3) (#7835)
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { type FunctionCall } from '@google/genai';
|
||||
import {
|
||||
PolicyDecision,
|
||||
type PolicyEngineConfig,
|
||||
type PolicyRule,
|
||||
} from './types.js';
|
||||
import { stableStringify } from './stable-stringify.js';
|
||||
|
||||
function ruleMatches(
|
||||
rule: PolicyRule,
|
||||
toolCall: FunctionCall,
|
||||
stringifiedArgs: string | undefined,
|
||||
): boolean {
|
||||
// Check tool name if specified
|
||||
if (rule.toolName && toolCall.name !== rule.toolName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check args pattern if specified
|
||||
if (rule.argsPattern) {
|
||||
// If rule has an args pattern but tool has no args, no match
|
||||
if (!toolCall.args) {
|
||||
return false;
|
||||
}
|
||||
// Use stable JSON stringification with sorted keys to ensure consistent matching
|
||||
if (
|
||||
stringifiedArgs === undefined ||
|
||||
!rule.argsPattern.test(stringifiedArgs)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export class PolicyEngine {
|
||||
private rules: PolicyRule[];
|
||||
private readonly defaultDecision: PolicyDecision;
|
||||
private readonly nonInteractive: boolean;
|
||||
|
||||
constructor(config: PolicyEngineConfig = {}) {
|
||||
this.rules = (config.rules ?? []).sort(
|
||||
(a, b) => (b.priority ?? 0) - (a.priority ?? 0),
|
||||
);
|
||||
this.defaultDecision = config.defaultDecision ?? PolicyDecision.ASK_USER;
|
||||
this.nonInteractive = config.nonInteractive ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool call is allowed based on the configured policies.
|
||||
*/
|
||||
check(toolCall: FunctionCall): PolicyDecision {
|
||||
let stringifiedArgs: string | undefined;
|
||||
// Compute stringified args once before the loop
|
||||
if (toolCall.args && this.rules.some((rule) => rule.argsPattern)) {
|
||||
stringifiedArgs = stableStringify(toolCall.args);
|
||||
}
|
||||
|
||||
// Find the first matching rule (already sorted by priority)
|
||||
for (const rule of this.rules) {
|
||||
if (ruleMatches(rule, toolCall, stringifiedArgs)) {
|
||||
return this.applyNonInteractiveMode(rule.decision);
|
||||
}
|
||||
}
|
||||
|
||||
// No matching rule found, use default decision
|
||||
return this.applyNonInteractiveMode(this.defaultDecision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new rule to the policy engine.
|
||||
*/
|
||||
addRule(rule: PolicyRule): void {
|
||||
this.rules.push(rule);
|
||||
// Re-sort rules by priority
|
||||
this.rules.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove rules for a specific tool.
|
||||
*/
|
||||
removeRulesForTool(toolName: string): void {
|
||||
this.rules = this.rules.filter((rule) => rule.toolName !== toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all current rules.
|
||||
*/
|
||||
getRules(): readonly PolicyRule[] {
|
||||
return this.rules;
|
||||
}
|
||||
|
||||
private applyNonInteractiveMode(decision: PolicyDecision): PolicyDecision {
|
||||
// In non-interactive mode, ASK_USER becomes DENY
|
||||
if (this.nonInteractive && decision === PolicyDecision.ASK_USER) {
|
||||
return PolicyDecision.DENY;
|
||||
}
|
||||
return decision;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user