mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 13:04:49 -07:00
feat(plan): support plan mode in non-interactive mode (#22670)
This commit is contained in:
@@ -33,6 +33,13 @@
|
||||
toolName = "enter_plan_mode"
|
||||
decision = "ask_user"
|
||||
priority = 50
|
||||
interactive = true
|
||||
|
||||
[[rule]]
|
||||
toolName = "enter_plan_mode"
|
||||
decision = "allow"
|
||||
priority = 50
|
||||
interactive = false
|
||||
|
||||
[[rule]]
|
||||
toolName = "enter_plan_mode"
|
||||
@@ -46,6 +53,13 @@ toolName = "exit_plan_mode"
|
||||
decision = "ask_user"
|
||||
priority = 70
|
||||
modes = ["plan"]
|
||||
interactive = true
|
||||
|
||||
[[rule]]
|
||||
toolName = "exit_plan_mode"
|
||||
decision = "allow"
|
||||
priority = 70
|
||||
interactive = false
|
||||
|
||||
[[rule]]
|
||||
toolName = "exit_plan_mode"
|
||||
|
||||
@@ -45,6 +45,7 @@ toolName = ["enter_plan_mode", "exit_plan_mode"]
|
||||
decision = "deny"
|
||||
priority = 999
|
||||
modes = ["yolo"]
|
||||
interactive = true
|
||||
|
||||
# Allow everything else in YOLO mode
|
||||
[[rule]]
|
||||
|
||||
@@ -3343,4 +3343,121 @@ describe('PolicyEngine', () => {
|
||||
expect(excluded.has('test-tool')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interactive matching', () => {
|
||||
it('should ignore interactive rules in non-interactive mode', async () => {
|
||||
const engine = new PolicyEngine({
|
||||
rules: [
|
||||
{
|
||||
toolName: 'my_tool',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
interactive: true,
|
||||
},
|
||||
],
|
||||
nonInteractive: true,
|
||||
defaultDecision: PolicyDecision.DENY,
|
||||
});
|
||||
|
||||
const result = await engine.check(
|
||||
{ name: 'my_tool', args: {} },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.DENY);
|
||||
});
|
||||
|
||||
it('should allow interactive rules in interactive mode', async () => {
|
||||
const engine = new PolicyEngine({
|
||||
rules: [
|
||||
{
|
||||
toolName: 'my_tool',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
interactive: true,
|
||||
},
|
||||
],
|
||||
nonInteractive: false,
|
||||
defaultDecision: PolicyDecision.DENY,
|
||||
});
|
||||
|
||||
const result = await engine.check(
|
||||
{ name: 'my_tool', args: {} },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.ALLOW);
|
||||
});
|
||||
|
||||
it('should ignore non-interactive rules in interactive mode', async () => {
|
||||
const engine = new PolicyEngine({
|
||||
rules: [
|
||||
{
|
||||
toolName: 'my_tool',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
interactive: false,
|
||||
},
|
||||
],
|
||||
nonInteractive: false,
|
||||
defaultDecision: PolicyDecision.DENY,
|
||||
});
|
||||
|
||||
const result = await engine.check(
|
||||
{ name: 'my_tool', args: {} },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.DENY);
|
||||
});
|
||||
|
||||
it('should allow non-interactive rules in non-interactive mode', async () => {
|
||||
const engine = new PolicyEngine({
|
||||
rules: [
|
||||
{
|
||||
toolName: 'my_tool',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
interactive: false,
|
||||
},
|
||||
],
|
||||
nonInteractive: true,
|
||||
defaultDecision: PolicyDecision.DENY,
|
||||
});
|
||||
|
||||
const result = await engine.check(
|
||||
{ name: 'my_tool', args: {} },
|
||||
undefined,
|
||||
);
|
||||
expect(result.decision).toBe(PolicyDecision.ALLOW);
|
||||
});
|
||||
|
||||
it('should apply rules without interactive flag to both', async () => {
|
||||
const rule: PolicyRule = {
|
||||
toolName: 'my_tool',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
};
|
||||
|
||||
const engineInteractive = new PolicyEngine({
|
||||
rules: [rule],
|
||||
nonInteractive: false,
|
||||
defaultDecision: PolicyDecision.DENY,
|
||||
});
|
||||
const engineNonInteractive = new PolicyEngine({
|
||||
rules: [rule],
|
||||
nonInteractive: true,
|
||||
defaultDecision: PolicyDecision.DENY,
|
||||
});
|
||||
|
||||
expect(
|
||||
(
|
||||
await engineInteractive.check(
|
||||
{ name: 'my_tool', args: {} },
|
||||
undefined,
|
||||
)
|
||||
).decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
expect(
|
||||
(
|
||||
await engineNonInteractive.check(
|
||||
{ name: 'my_tool', args: {} },
|
||||
undefined,
|
||||
)
|
||||
).decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,6 +74,7 @@ function ruleMatches(
|
||||
stringifiedArgs: string | undefined,
|
||||
serverName: string | undefined,
|
||||
currentApprovalMode: ApprovalMode,
|
||||
nonInteractive: boolean,
|
||||
toolAnnotations?: Record<string, unknown>,
|
||||
subagent?: string,
|
||||
): boolean {
|
||||
@@ -146,6 +147,16 @@ function ruleMatches(
|
||||
}
|
||||
}
|
||||
|
||||
// Check interactive if specified
|
||||
if ('interactive' in rule && rule.interactive !== undefined) {
|
||||
if (rule.interactive && nonInteractive) {
|
||||
return false;
|
||||
}
|
||||
if (!rule.interactive && !nonInteractive) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -443,6 +454,7 @@ export class PolicyEngine {
|
||||
stringifiedArgs,
|
||||
serverName,
|
||||
this.approvalMode,
|
||||
this.nonInteractive,
|
||||
toolAnnotations,
|
||||
subagent,
|
||||
),
|
||||
@@ -521,6 +533,7 @@ export class PolicyEngine {
|
||||
stringifiedArgs,
|
||||
serverName,
|
||||
this.approvalMode,
|
||||
this.nonInteractive,
|
||||
toolAnnotations,
|
||||
subagent,
|
||||
)
|
||||
@@ -713,6 +726,7 @@ export class PolicyEngine {
|
||||
undefined, // stringifiedArgs
|
||||
serverName,
|
||||
this.approvalMode,
|
||||
this.nonInteractive,
|
||||
annotations,
|
||||
);
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ const PolicyRuleSchema = z.object({
|
||||
'priority must be <= 999 to prevent tier overflow. Priorities >= 1000 would jump to the next tier.',
|
||||
}),
|
||||
modes: z.array(z.nativeEnum(ApprovalMode)).optional(),
|
||||
interactive: z.boolean().optional(),
|
||||
toolAnnotations: z.record(z.any()).optional(),
|
||||
allow_redirection: z.boolean().optional(),
|
||||
deny_message: z.string().optional(),
|
||||
@@ -475,6 +476,7 @@ export async function loadPoliciesFromToml(
|
||||
decision: rule.decision,
|
||||
priority: transformPriority(rule.priority, tier),
|
||||
modes: rule.modes,
|
||||
interactive: rule.interactive,
|
||||
toolAnnotations: rule.toolAnnotations,
|
||||
allowRedirection: rule.allow_redirection,
|
||||
source: `${tierName.charAt(0).toUpperCase() + tierName.slice(1)}: ${file}`,
|
||||
|
||||
@@ -152,6 +152,13 @@ export interface PolicyRule {
|
||||
*/
|
||||
modes?: ApprovalMode[];
|
||||
|
||||
/**
|
||||
* If true, this rule only applies to interactive environments.
|
||||
* If false, this rule only applies to non-interactive environments.
|
||||
* If undefined, it applies to both interactive and non-interactive environments.
|
||||
*/
|
||||
interactive?: boolean;
|
||||
|
||||
/**
|
||||
* If true, allows command redirection even if the policy engine would normally
|
||||
* downgrade ALLOW to ASK_USER for redirected commands.
|
||||
|
||||
Reference in New Issue
Block a user