From a3297bd232175593b440ad171935d6f8e2e5a541 Mon Sep 17 00:00:00 2001 From: Joshua Date: Tue, 21 Jul 2026 06:41:29 -0700 Subject: [PATCH] fix(approval): restore session approval for Tirith-flagged commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- contributors/emails/joshua@amokk.net | 1 + gateway/run.py | 5 ++++- plugins/platforms/matrix/adapter.py | 22 ++++++++++++++++++---- tools/approval.py | 7 +++++++ 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 contributors/emails/joshua@amokk.net diff --git a/contributors/emails/joshua@amokk.net b/contributors/emails/joshua@amokk.net new file mode 100644 index 0000000000000..d1cd4849b1674 --- /dev/null +++ b/contributors/emails/joshua@amokk.net @@ -0,0 +1 @@ +faikwo diff --git a/gateway/run.py b/gateway/run.py index 9184c22b4125b..01fddf2dfaaa9 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -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: diff --git a/plugins/platforms/matrix/adapter.py b/plugins/platforms/matrix/adapter.py index a49a99bdd0e10..de264b77d6f0a 100644 --- a/plugins/platforms/matrix/adapter.py +++ b/plugins/platforms/matrix/adapter.py @@ -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) diff --git a/tools/approval.py b/tools/approval.py index c825e56f53c7d..9962bf851d8bf 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -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