diff --git a/packages/core/src/core/prompts.test.ts b/packages/core/src/core/prompts.test.ts index e7fa4901bde..180a7c44a87 100644 --- a/packages/core/src/core/prompts.test.ts +++ b/packages/core/src/core/prompts.test.ts @@ -483,6 +483,29 @@ describe('Core System Prompt (prompts.ts)', () => { expect(prompt).toMatchSnapshot(); }); }); + + it('should include YOLO mode instructions in interactive mode', () => { + vi.mocked(mockConfig.getApprovalMode).mockReturnValue(ApprovalMode.YOLO); + vi.mocked(mockConfig.isInteractive).mockReturnValue(true); + const prompt = getCoreSystemPrompt(mockConfig); + expect(prompt).toContain('# Autonomous Mode (YOLO)'); + expect(prompt).toContain('Only use the `ask_user` tool if'); + }); + + it('should NOT include YOLO mode instructions in non-interactive mode', () => { + vi.mocked(mockConfig.getApprovalMode).mockReturnValue(ApprovalMode.YOLO); + vi.mocked(mockConfig.isInteractive).mockReturnValue(false); + const prompt = getCoreSystemPrompt(mockConfig); + expect(prompt).not.toContain('# Autonomous Mode (YOLO)'); + }); + + it('should NOT include YOLO mode instructions for DEFAULT mode', () => { + vi.mocked(mockConfig.getApprovalMode).mockReturnValue( + ApprovalMode.DEFAULT, + ); + const prompt = getCoreSystemPrompt(mockConfig); + expect(prompt).not.toContain('# Autonomous Mode (YOLO)'); + }); }); describe('Platform-specific and Background Process instructions', () => { diff --git a/packages/core/src/policy/config.test.ts b/packages/core/src/policy/config.test.ts index 774214d1011..25f7e4a1500 100644 --- a/packages/core/src/policy/config.test.ts +++ b/packages/core/src/policy/config.test.ts @@ -317,8 +317,8 @@ describe('createPolicyEngineConfig', () => { (r) => r.decision === PolicyDecision.ALLOW && !r.toolName, ); expect(rule).toBeDefined(); - // Priority 999 in default tier → 1.999 - expect(rule?.priority).toBeCloseTo(1.999, 5); + // Priority 998 in default tier → 1.998 (999 reserved for ask_user exception) + expect(rule?.priority).toBeCloseTo(1.998, 5); }); it('should allow edit tool in AUTO_EDIT mode', async () => { @@ -582,8 +582,8 @@ describe('createPolicyEngineConfig', () => { (r) => !r.toolName && r.decision === PolicyDecision.ALLOW, ); expect(wildcardRule).toBeDefined(); - // Priority 999 in default tier → 1.999 - expect(wildcardRule?.priority).toBeCloseTo(1.999, 5); + // Priority 998 in default tier → 1.998 (999 reserved for ask_user exception) + expect(wildcardRule?.priority).toBeCloseTo(1.998, 5); // Write tool ASK_USER rules are present (from write.toml) const writeToolRules = config.rules?.filter( diff --git a/packages/core/src/policy/policies/yolo.toml b/packages/core/src/policy/policies/yolo.toml index 052ca6c4d37..95c3b411f1d 100644 --- a/packages/core/src/policy/policies/yolo.toml +++ b/packages/core/src/policy/policies/yolo.toml @@ -23,10 +23,21 @@ # 10: Write tools default to ASK_USER (becomes 1.010 in default tier) # 15: Auto-edit tool override (becomes 1.015 in default tier) # 50: Read-only tools (becomes 1.050 in default tier) -# 999: YOLO mode allow-all (becomes 1.999 in default tier) +# 998: YOLO mode allow-all (becomes 1.998 in default tier) +# 999: Ask-user tool (becomes 1.999 in default tier) +# Ask-user tool always requires user interaction, even in YOLO mode. +# This ensures the model can gather user preferences/decisions when needed. +# Note: In non-interactive mode, this decision is converted to DENY by the policy engine. [[rule]] -decision = "allow" +toolName = "ask_user" +decision = "ask_user" priority = 999 modes = ["yolo"] + +# Allow everything else in YOLO mode +[[rule]] +decision = "allow" +priority = 998 +modes = ["yolo"] allow_redirection = true diff --git a/packages/core/src/policy/policy-engine.test.ts b/packages/core/src/policy/policy-engine.test.ts index 93cf89536f4..6c59161af49 100644 --- a/packages/core/src/policy/policy-engine.test.ts +++ b/packages/core/src/policy/policy-engine.test.ts @@ -2030,4 +2030,60 @@ describe('PolicyEngine', () => { expect(result.decision).toBe(PolicyDecision.DENY); }); }); + + describe('YOLO mode with ask_user tool', () => { + it('should return ASK_USER for ask_user tool even in YOLO mode', async () => { + const rules: PolicyRule[] = [ + { + toolName: 'ask_user', + decision: PolicyDecision.ASK_USER, + priority: 999, + modes: [ApprovalMode.YOLO], + }, + { + decision: PolicyDecision.ALLOW, + priority: 998, + modes: [ApprovalMode.YOLO], + }, + ]; + + engine = new PolicyEngine({ + rules, + approvalMode: ApprovalMode.YOLO, + }); + + const result = await engine.check( + { name: 'ask_user', args: {} }, + undefined, + ); + expect(result.decision).toBe(PolicyDecision.ASK_USER); + }); + + it('should return ALLOW for other tools in YOLO mode', async () => { + const rules: PolicyRule[] = [ + { + toolName: 'ask_user', + decision: PolicyDecision.ASK_USER, + priority: 999, + modes: [ApprovalMode.YOLO], + }, + { + decision: PolicyDecision.ALLOW, + priority: 998, + modes: [ApprovalMode.YOLO], + }, + ]; + + engine = new PolicyEngine({ + rules, + approvalMode: ApprovalMode.YOLO, + }); + + const result = await engine.check( + { name: 'run_shell_command', args: { command: 'ls' } }, + undefined, + ); + expect(result.decision).toBe(PolicyDecision.ALLOW); + }); + }); }); diff --git a/packages/core/src/prompts/promptProvider.ts b/packages/core/src/prompts/promptProvider.ts index 8da1a7ed7d2..13c4f0374d1 100644 --- a/packages/core/src/prompts/promptProvider.ts +++ b/packages/core/src/prompts/promptProvider.ts @@ -50,6 +50,7 @@ export class PromptProvider { const interactiveMode = interactiveOverride ?? config.isInteractive(); const approvalMode = config.getApprovalMode?.() ?? ApprovalMode.DEFAULT; const isPlanMode = approvalMode === ApprovalMode.PLAN; + const isYoloMode = approvalMode === ApprovalMode.YOLO; const skills = config.getSkillManager().getSkills(); const toolNames = config.getToolRegistry().getAllToolNames(); const enabledToolNames = new Set(toolNames); @@ -183,6 +184,11 @@ export class PromptProvider { }), ), sandbox: this.withSection('sandbox', () => getSandboxMode()), + interactiveYoloMode: this.withSection( + 'interactiveYoloMode', + () => true, + isYoloMode && interactiveMode, + ), gitRepo: this.withSection( 'git', () => ({ interactive: interactiveMode }), diff --git a/packages/core/src/prompts/snippets.legacy.ts b/packages/core/src/prompts/snippets.legacy.ts index 0d6f429a6a9..8d46fd6a1a4 100644 --- a/packages/core/src/prompts/snippets.legacy.ts +++ b/packages/core/src/prompts/snippets.legacy.ts @@ -32,6 +32,7 @@ export interface SystemPromptOptions { planningWorkflow?: PlanningWorkflowOptions; operationalGuidelines?: OperationalGuidelinesOptions; sandbox?: SandboxMode; + interactiveYoloMode?: boolean; gitRepo?: GitRepoOptions; finalReminder?: FinalReminderOptions; } @@ -114,6 +115,8 @@ ${ ${renderOperationalGuidelines(options.operationalGuidelines)} +${renderInteractiveYoloMode(options.interactiveYoloMode)} + ${renderSandbox(options.sandbox)} ${renderGitRepo(options.gitRepo)} @@ -293,6 +296,25 @@ You are running outside of a sandbox container, directly on the user's system. F } } +export function renderInteractiveYoloMode(enabled?: boolean): string { + if (!enabled) return ''; + return ` +# Autonomous Mode (YOLO) + +You are operating in **autonomous mode**. The user has requested minimal interruption. + +**Only use the \`${ASK_USER_TOOL_NAME}\` tool if:** +- A wrong decision would cause significant re-work +- The request is fundamentally ambiguous with no reasonable default +- The user explicitly asks you to confirm or ask questions + +**Otherwise, work autonomously:** +- Make reasonable decisions based on context and existing code patterns +- Follow established project conventions +- If multiple valid approaches exist, choose the most robust option +`.trim(); +} + export function renderGitRepo(options?: GitRepoOptions): string { if (!options) return ''; return ` diff --git a/packages/core/src/prompts/snippets.ts b/packages/core/src/prompts/snippets.ts index 5939722069e..a0614e2aaf6 100644 --- a/packages/core/src/prompts/snippets.ts +++ b/packages/core/src/prompts/snippets.ts @@ -33,6 +33,7 @@ export interface SystemPromptOptions { planningWorkflow?: PlanningWorkflowOptions; operationalGuidelines?: OperationalGuidelinesOptions; sandbox?: SandboxMode; + interactiveYoloMode?: boolean; gitRepo?: GitRepoOptions; } @@ -111,6 +112,8 @@ ${ ${renderOperationalGuidelines(options.operationalGuidelines)} +${renderInteractiveYoloMode(options.interactiveYoloMode)} + ${renderSandbox(options.sandbox)} ${renderGitRepo(options.gitRepo)} @@ -312,6 +315,25 @@ export function renderSandbox(mode?: SandboxMode): string { return ''; } +export function renderInteractiveYoloMode(enabled?: boolean): string { + if (!enabled) return ''; + return ` +# Autonomous Mode (YOLO) + +You are operating in **autonomous mode**. The user has requested minimal interruption. + +**Only use the \`${ASK_USER_TOOL_NAME}\` tool if:** +- A wrong decision would cause significant re-work +- The request is fundamentally ambiguous with no reasonable default +- The user explicitly asks you to confirm or ask questions + +**Otherwise, work autonomously:** +- Make reasonable decisions based on context and existing code patterns +- Follow established project conventions +- If multiple valid approaches exist, choose the most robust option +`.trim(); +} + export function renderGitRepo(options?: GitRepoOptions): string { if (!options) return ''; return `