fix(approval): restore session approval for Tirith-flagged commands

Adds an allow_session flag to the gateway approval payload so adapters
can render the session tier independently of the permanent tier. Matrix
gains a session reaction (🌀) and a reaction legend; pure-tirith prompts
now offer once/session/deny instead of collapsing to once/deny.

Salvaged from PR #67312, adapted to the allow_permanent semantics that
landed in #68597 (Always offered when any dangerous-pattern warning is
persistable; pure-tirith prompts stay session-max).
This commit is contained in:
Joshua 2026-07-21 06:41:29 -07:00 committed by Teknium
parent 6b54582438
commit a3297bd232
4 changed files with 30 additions and 5 deletions

View File

@ -0,0 +1 @@
faikwo

View File

@ -359,6 +359,7 @@ def _format_exec_approval_fallback(
command_prefix: str,
*,
allow_permanent: bool = True,
allow_session: bool = True,
smart_denied: bool = False,
) -> str:
"""Render the text fallback from approval capabilities, not platform names."""
@ -368,7 +369,7 @@ def _format_exec_approval_fallback(
heading = "⚠️ **Smart DENY — owner override for one operation:**"
choices = [f"Reply `{command_prefix}approve` to execute this one operation"]
if not smart_denied:
if not smart_denied and allow_session:
choices.append(
f"`{command_prefix}approve session` to approve this pattern for the session"
)
@ -20706,6 +20707,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
description=desc,
metadata=_status_thread_metadata,
allow_permanent=approval_data.get("allow_permanent", True),
allow_session=approval_data.get("allow_session", True),
smart_denied=approval_data.get("smart_denied", False),
),
_loop_for_step,
@ -20736,6 +20738,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
desc,
_p,
allow_permanent=approval_data.get("allow_permanent", True),
allow_session=approval_data.get("allow_session", True),
smart_denied=approval_data.get("smart_denied", False),
)
try:

View File

@ -996,6 +996,7 @@ class MatrixAdapter(BasePlatformAdapter):
# Matrix reaction-based dangerous command approvals.
self._approval_reaction_map = {
"": "once",
"🌀": "session",
"♾️": "always",
"": "always",
"\u267e\ufe0f": "always",
@ -2063,6 +2064,7 @@ class MatrixAdapter(BasePlatformAdapter):
description: str = "dangerous command",
metadata: Optional[dict] = None,
allow_permanent: bool = True,
allow_session: bool = True,
smart_denied: bool = False,
) -> SendResult:
"""Send a reaction-based exec approval prompt for Matrix."""
@ -2075,17 +2077,24 @@ class MatrixAdapter(BasePlatformAdapter):
if smart_denied:
scope_choices = "Smart DENY: owner override applies to this one operation only.\n"
else:
scope_choices = "Reply `!approve session` to approve this pattern for the session, "
scope_choices = ""
if allow_session:
scope_choices += "Reply `!approve session` to approve this pattern for the session, "
if allow_permanent:
scope_choices += "`!approve always` to approve permanently, "
reaction_legend_parts = ["✅ = approve once"]
if allow_session:
reaction_legend_parts.append("🌀 = approve for this session")
if allow_permanent:
reaction_legend_parts.append("♾️ = approve always")
reaction_legend_parts.append("❎ = deny")
text = (
"⚠️ **Dangerous command requires approval**\n"
f"```\n{cmd_preview}\n```\n"
f"Reason: {description}\n\n"
f"{scope_choices}Reply `!approve` to execute once, or `!deny` to cancel.\n\n"
"You can also click the reaction to approve:\n"
"✅ = approve\n"
"❎ = deny"
+ "\n".join(reaction_legend_parts)
)
result = await self.send(chat_id, text, metadata=metadata)
@ -2105,7 +2114,12 @@ class MatrixAdapter(BasePlatformAdapter):
self._approval_prompts_by_event[result.message_id] = prompt
self._approval_prompt_by_session[session_key] = result.message_id
reactions = ("", "") if smart_denied or not allow_permanent else ("", "♾️", "")
if not allow_session:
reactions = ("", "")
elif not allow_permanent:
reactions = ("", "🌀", "")
else:
reactions = ("", "🌀", "♾️", "")
for emoji in reactions:
try:
reaction_result = await self._send_reaction(chat_id, result.message_id, emoji)

View File

@ -2769,6 +2769,7 @@ def _run_approval_gate(
"pattern_keys": [pattern_key],
"description": redact_sensitive_text(description),
"allow_permanent": True,
"allow_session": True,
}
decision = _await_gateway_decision(
session_key, notify_cb, approval_data, surface="gateway"
@ -3464,6 +3465,11 @@ def check_all_command_guards(command: str, env_type: str,
# whenever any dangerous-pattern warning can actually be
# persisted (pure-tirith prompts stay session-max).
"allow_permanent": has_permanent_capable and not smart_denied_for_owner,
# Session approval is safe for every non-Smart-DENY prompt —
# including pure-tirith ones, where the persistence layer
# already caps scope at session. Adapters use this to render
# a session tier independently of the permanent tier.
"allow_session": not smart_denied_for_owner,
}
if smart_denied_for_owner:
approval_data["smart_denied"] = True
@ -3795,6 +3801,7 @@ def check_execute_code_guard(code: str, env_type: str,
"pattern_keys": [pattern_key],
"description": display_description,
"allow_permanent": not smart_denied_for_owner,
"allow_session": not smart_denied_for_owner,
}
if smart_denied_for_owner:
approval_data["smart_denied"] = True