Source code for oats.cli.approval

"""
Interactive approval system for coder2.

Provides Yes / Yes-to-all / No / No+instructions approval prompts
for tool operations that need user confirmation.

Modes:
  AUTO       — approve everything (default with -y flag)
  SUPERVISED — ask before write/bash/delete operations
  PLAN       — review-only, no execution
"""
from __future__ import annotations

import sys
from enum import Enum
from dataclasses import dataclass
from typing import Optional

from rich.console import Console

console = Console()

# Read-only tools that never need approval
_READ_ONLY_TOOLS = frozenset({
    "read", "glob", "grep", "tool_search", "todoread",
    "memory_read", "plan_status", "agent_status",
    "question", "askuser", "webfetch", "websearch",
    "check_certificate_expiration", "convert_pq_to_json",
    "get_app_manifest", "get_app_logs", "get_app_credentials",
    "lsp", "generate_readme", "plan_enter", "plan_exit",
})


[docs] class ApprovalMode(str, Enum): """Operating modes for the approval system.""" AUTO = "auto" # Auto-approve everything SUPERVISED = "supervised" # Ask for each operation PLAN = "plan" # Review-only, no execution
[docs] class ApprovalAction(str, Enum): """Result of an approval prompt.""" YES = "yes" YES_ALL = "yes_all" NO = "no" NO_WITH_INSTRUCTIONS = "no_with_instructions"
[docs] @dataclass class ApprovalResult: """Result from an approval prompt.""" action: ApprovalAction instructions: Optional[str] = None
[docs] class ApprovalManager: """Manages the approval flow for tool operations."""
[docs] def __init__(self, mode: ApprovalMode = ApprovalMode.AUTO): """Initialize the approval manager with the given mode. Args: mode: The operating mode (AUTO, SUPERVISED, or PLAN). """ self._mode = mode self._auto_approved_tools: set[str] = set()
@property def mode(self) -> ApprovalMode: """Return the current approval mode.""" return self._mode @mode.setter def mode(self, value: ApprovalMode): """Set the approval mode, clearing auto-approved tools if switching to AUTO.""" self._mode = value if value == ApprovalMode.AUTO: self._auto_approved_tools.clear()
[docs] def needs_approval(self, tool_name: str) -> bool: """Check if a tool operation needs user approval.""" if self._mode == ApprovalMode.AUTO: return False if tool_name in self._auto_approved_tools: return False if tool_name in _READ_ONLY_TOOLS: return False return True
[docs] def prompt_approval(self, tool_name: str, description: str) -> ApprovalResult: """ Prompt the user for approval of a tool operation. Uses plain input() to avoid conflicting with the main REPL's prompt_toolkit application (which is already running in the asyncio event loop when this is called via run_in_executor). y/Enter = yes, a = yes to all, n = no, i = no + instructions """ console.print() console.print( f" [bold yellow]? Approve:[/bold yellow] " f"[cyan]{tool_name}[/cyan] {description}" ) console.print( f" [dim] (y)es / Enter | (a)ll | (n)o | (i)nstructions[/dim]" ) try: raw = input(" > ").strip().lower() if raw in ("", "y", "yes"): return ApprovalResult(action=ApprovalAction.YES) if raw in ("a", "all"): self._mode = ApprovalMode.AUTO console.print( f" [green]auto-approve enabled for this session[/green]" ) return ApprovalResult(action=ApprovalAction.YES_ALL) if raw in ("i", "instructions", "inst"): console.print( f" [dim]Enter additional instructions (Enter to submit):[/dim]" ) instructions = input(" instructions> ").strip() return ApprovalResult( action=ApprovalAction.NO_WITH_INSTRUCTIONS, instructions=instructions if instructions else None, ) # Anything else (n, no, etc.) = decline console.print(f" [yellow]skipped[/yellow]") return ApprovalResult(action=ApprovalAction.NO) except (EOFError, KeyboardInterrupt): return ApprovalResult(action=ApprovalAction.NO)
[docs] def auto_approve_tool(self, tool_name: str): """Mark a specific tool type as auto-approved for this session.""" self._auto_approved_tools.add(tool_name)
[docs] def reset(self): """Reset the auto-approved tools set to empty.""" self._auto_approved_tools.clear()
_manager: Optional["ApprovalManager"] = None
[docs] def get_approval_manager() -> "ApprovalManager": """Return the process-wide approval manager, creating one in AUTO on first call.""" global _manager if _manager is None: _manager = ApprovalManager(mode=ApprovalMode.AUTO) return _manager
[docs] def set_approval_mode(mode: ApprovalMode) -> None: """Set the approval mode on the shared manager.""" get_approval_manager().mode = mode