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
This commit is contained in:
Teknium
2026-04-04 16:33:20 -07:00
committed by GitHub
parent 2556cfdab1
commit 55bbf8caba
2 changed files with 21 additions and 5 deletions

View File

@@ -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}

View File

@@ -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