diff --git a/src/timmy/cli.py b/src/timmy/cli.py index 554e3a26..b546e13b 100644 --- a/src/timmy/cli.py +++ b/src/timmy/cli.py @@ -37,6 +37,35 @@ def _is_interactive() -> bool: return hasattr(sys.stdin, "isatty") and sys.stdin.isatty() +def _prompt_interactive(req, tool_name: str, tool_args: dict) -> None: + """Display tool details and prompt the human for approval.""" + description = format_action_description(tool_name, tool_args) + impact = get_impact_level(tool_name) + + typer.echo() + typer.echo(typer.style("Tool confirmation required", bold=True)) + typer.echo(f" Impact: {impact.upper()}") + typer.echo(f" {description}") + typer.echo() + + if typer.confirm("Allow this action?", default=False): + req.confirm() + logger.info("CLI: approved %s", tool_name) + else: + req.reject(note="User rejected from CLI") + logger.info("CLI: rejected %s", tool_name) + + +def _decide_autonomous(req, tool_name: str, tool_args: dict) -> None: + """Auto-approve allowlisted tools; reject everything else.""" + if is_allowlisted(tool_name, tool_args): + req.confirm() + logger.info("AUTO-APPROVED (allowlist): %s", tool_name) + else: + req.reject(note="Auto-rejected: not in allowlist") + logger.info("AUTO-REJECTED (not allowlisted): %s %s", tool_name, str(tool_args)[:100]) + + def _handle_tool_confirmation(agent, run_output, session_id: str, *, autonomous: bool = False): """Prompt user to approve/reject dangerous tool calls. @@ -51,6 +80,7 @@ def _handle_tool_confirmation(agent, run_output, session_id: str, *, autonomous: Returns the final RunOutput after all confirmations are resolved. """ interactive = _is_interactive() and not autonomous + decide = _prompt_interactive if interactive else _decide_autonomous max_rounds = 10 # safety limit for _ in range(max_rounds): @@ -66,39 +96,10 @@ def _handle_tool_confirmation(agent, run_output, session_id: str, *, autonomous: for req in reqs: if not getattr(req, "needs_confirmation", False): continue - te = req.tool_execution tool_name = getattr(te, "tool_name", "unknown") tool_args = getattr(te, "tool_args", {}) or {} - - if interactive: - # Human present — prompt for approval - description = format_action_description(tool_name, tool_args) - impact = get_impact_level(tool_name) - - typer.echo() - typer.echo(typer.style("Tool confirmation required", bold=True)) - typer.echo(f" Impact: {impact.upper()}") - typer.echo(f" {description}") - typer.echo() - - approved = typer.confirm("Allow this action?", default=False) - if approved: - req.confirm() - logger.info("CLI: approved %s", tool_name) - else: - req.reject(note="User rejected from CLI") - logger.info("CLI: rejected %s", tool_name) - else: - # Autonomous mode — check allowlist - if is_allowlisted(tool_name, tool_args): - req.confirm() - logger.info("AUTO-APPROVED (allowlist): %s", tool_name) - else: - req.reject(note="Auto-rejected: not in allowlist") - logger.info( - "AUTO-REJECTED (not allowlisted): %s %s", tool_name, str(tool_args)[:100] - ) + decide(req, tool_name, tool_args) # Resume the run so the agent sees the confirmation result try: