From 55bbf8caba44cb6fd87a756b699448d3549d6ff3 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:33:20 -0700 Subject: [PATCH] fix: include approval metadata in terminal tool results (#5141) When a dangerous command is approved (gateway, CLI, or smart approval), the terminal tool now includes an 'approval' field in the result JSON so the model knows approval was requested and granted. Previously the model only saw normal command output with no indication that approval happened, causing it to hallucinate that the approval system didn't fire. Changes: - approval.py: Return user_approved/description in all 3 approval paths (gateway blocking, CLI interactive, smart approval) - terminal_tool.py: Capture approval metadata and inject into both foreground and background command results --- tools/approval.py | 9 ++++++--- tools/terminal_tool.py | 17 +++++++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/tools/approval.py b/tools/approval.py index ab2a10927..c47672b8b 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -724,7 +724,8 @@ def check_all_command_guards(command: str, env_type: str, logger.debug("Smart approval: auto-approved '%s' (%s)", command[:60], combined_desc_for_llm) return {"approved": True, "message": None, - "smart_approved": True} + "smart_approved": True, + "description": combined_desc_for_llm} elif verdict == "deny": combined_desc_for_llm = "; ".join(desc for _, desc, _ in warnings) return { @@ -819,7 +820,8 @@ def check_all_command_guards(command: str, env_type: str, approve_permanent(key) save_permanent_allowlist(_permanent_approved) - return {"approved": True, "message": None} + return {"approved": True, "message": None, + "user_approved": True, "description": combined_desc} # Fallback: no gateway callback registered (e.g. cron, batch). # Return approval_required for backward compat. @@ -865,4 +867,5 @@ def check_all_command_guards(command: str, env_type: str, approve_permanent(key) save_permanent_allowlist(_permanent_approved) - return {"approved": True, "message": None} + return {"approved": True, "message": None, + "user_approved": True, "description": combined_desc} diff --git a/tools/terminal_tool.py b/tools/terminal_tool.py index 92581dbc4..31d7d77c6 100644 --- a/tools/terminal_tool.py +++ b/tools/terminal_tool.py @@ -1058,6 +1058,7 @@ def terminal_tool( # Pre-exec security checks (tirith + dangerous command detection) # Skip check if force=True (user has confirmed they want to run it) + approval_note = None if not force: approval = _check_all_guards(command, env_type) if not approval["approved"]: @@ -1084,6 +1085,13 @@ def terminal_tool( "error": approval.get("message", fallback_msg), "status": "blocked" }, ensure_ascii=False) + # Track whether approval was explicitly granted by the user + if approval.get("user_approved"): + desc = approval.get("description", "flagged as dangerous") + approval_note = f"Command required approval ({desc}) and was approved by the user." + elif approval.get("smart_approved"): + desc = approval.get("description", "flagged as dangerous") + approval_note = f"Command was flagged ({desc}) and auto-approved by smart approval." # Prepare command for execution if background: @@ -1121,6 +1129,8 @@ def terminal_tool( "exit_code": 0, "error": None, } + if approval_note: + result_data["approval"] = approval_note # Transparent timeout clamping note max_timeout = effective_timeout @@ -1232,11 +1242,14 @@ def terminal_tool( from agent.redact import redact_sensitive_text output = redact_sensitive_text(output.strip()) if output else "" - return json.dumps({ + result_dict = { "output": output, "exit_code": returncode, "error": None - }, ensure_ascii=False) + } + if approval_note: + result_dict["approval"] = approval_note + return json.dumps(result_dict, ensure_ascii=False) except Exception as e: import traceback