refactor: pass the approval request id explicitly, not through platform metadata

The runner tunnelled the approval request id to Discord as a private
`_approval_request_id` key inside the platform metadata dict. That dict
goes to every adapter that renders approval buttons. The relay adapter
forwards it verbatim, so the private key reached the external connector
in every approval prompt frame.

Add an optional `request_id` to `send_exec_approval` and to
`ExecApprovalPrompt`, have the runner pass `approval_data["request_id"]`,
and have Discord read `prompt.request_id`. The metadata passed to the
adapters is back to the thread routing it carried before the PR (`None`
in a plain chat). The always-true `"timeout_s" in approval_data` guard
goes with it.

Native-button adapters render through `_send_exec_approval_prompt`,
whose signature is unchanged. The only `send_exec_approval` override,
`LocalSessionAdapter`, takes `**kwargs`, as does every test double, so
no other adapter or double changes.
This commit is contained in:
kshitijk4poor authored and kshitij committed 2026-10-28 15:28:40 +05:30
1 parent ac50e08b54
commit cfed2ca71f
6 files changed
+18 -19

No files matched your search

+5 -5
View File
@@ -1578,6 +1578,7 @@ class ExecApprovalPrompt:
description: str
smart_denied: bool
metadata: Optional[dict[str, Any]] = None
request_id: Optional[str] = None # the ``tools.approval`` request this card answers (and only that one)
@property
def choices(self) -> list[str]:
@@ -2790,16 +2791,15 @@ class BasePlatformAdapter(ABC):
async def send_exec_approval(
self, chat_id: str, command: str, session_key: str, description: Optional[str] = None,
metadata: Optional[dict[str, Any]] = None, allow_permanent: bool = True, allow_session: bool = True,
smart_denied: bool = False,
smart_denied: bool = False, request_id: Optional[str] = None,
) -> SendResult:
"""Interactive exec-approval prompt; a press resolves via
``tools.approval.resolve_gateway_approval``. Text and choice set are shared; adapters
render them natively in ``_send_exec_approval_prompt``."""
"""Interactive exec-approval prompt; a press resolves via ``tools.approval.resolve_gateway_approval``.
Text and choice set are shared; adapters render them natively in ``_send_exec_approval_prompt``."""
if description is None:
description = ea_default_reason_text()
prompt = ExecApprovalPrompt(
chat_id=chat_id, session_key=session_key, metadata=metadata, command=str(command or ""),
description=description, smart_denied=smart_denied,
description=description, smart_denied=smart_denied, request_id=request_id,
text=self._format_exec_approval(command, description, smart_denied),
actions=self._exec_approval_actions(
allow_permanent=allow_permanent, allow_session=allow_session, smart_denied=smart_denied))
+2 -5
View File
@@ -725,14 +725,11 @@ class TurnRunner(GatewayTurnProgressMixin, GatewaySessionAgentMixin):
# Check the *class*, not the instance — MagicMock auto-creates attributes in tests.
if _renders_exec_approval_buttons(type(adapter)):
try:
approval_metadata = ctx._status_thread_metadata
if "timeout_s" in approval_data:
approval_metadata = dict(approval_metadata or {})
approval_metadata["_approval_request_id"] = approval_data.get("request_id")
fut = self._schedule(
adapter.send_exec_approval(
chat_id=ctx._status_chat_id, command=cmd, session_key=ctx.session_key or "",
description=desc, metadata=approval_metadata, **flags,
description=desc, metadata=ctx._status_thread_metadata,
request_id=approval_data.get("request_id"), **flags,
),
"send_exec_approval scheduling error",
)
+1 -1
View File
@@ -5727,7 +5727,7 @@ class DiscordAdapter(DiscordAuthorizationMixin, DiscordMediaMixin, DiscordThread
allowed_role_ids=self._allowed_role_ids, require_admin=require_admin,
admin_user_ids=admin_user_ids, allow_permanent="always" in choices,
allow_session="session" in choices, smart_denied=prompt.smart_denied,
request_id=(prompt.metadata or {}).get("_approval_request_id"),
request_id=prompt.request_id,
)
view_holder.append(view)
send_kwargs: dict[str, Any] = {"content": content, "embed": embed, "view": view}
@@ -30,7 +30,7 @@ async def _pending_card(monkeypatch):
adapter._client = SimpleNamespace(get_channel=lambda _id: channel)
await adapter.send_exec_approval(
chat_id="555", command="ls", session_key=session,
metadata={"_approval_request_id": "request-1"},
request_id="request-1",
)
return adapter, sent["view"], entry, message
@@ -192,7 +192,7 @@ async def test_external_answer_then_stale_click_preserves_resolved_observer(monk
adapter._client = SimpleNamespace(get_channel=lambda _id: channel)
await adapter.send_exec_approval(
chat_id="555", command="ls", session_key=session,
metadata={"_approval_request_id": "request-1"},
request_id="request-1",
)
view = sent["view"]
@@ -77,7 +77,7 @@ async def test_exec_approval_view_honors_discord_timeout_with_request_id(monkeyp
result = await adapter.send_exec_approval(
chat_id="555", command="ls", session_key="agent:other:discord:555",
metadata={"_approval_request_id": "request-1"},
request_id="request-1",
)
assert result.success
@@ -96,7 +96,7 @@ async def test_expired_card_cannot_resolve_a_different_pending_request(monkeypat
session = "agent:other:discord:555"
await adapter.send_exec_approval(
chat_id="555", command="ls", session_key=session,
metadata={"_approval_request_id": "expired"},
request_id="expired",
)
other = _ApprovalEntry({"request_id": "new", "command": "rm other"})
monkeypatch.setitem(approval._gateway_queues, session, [other])
@@ -148,7 +148,7 @@ async def test_card_retires_when_backend_request_ends(monkeypatch, expire_during
adapter._client = SimpleNamespace(get_channel=lambda _id: channel)
await adapter.send_exec_approval(
chat_id="555", command="ls", session_key=session,
metadata={"_approval_request_id": "request-1"},
request_id="request-1",
)
view = sent["view"]
if not expire_during_send:
@@ -31,6 +31,7 @@ class _ButtonAdapter:
self.sends: list[str] = []
self.edits: list[tuple] = []
self.card_metadata: list[Any] = []
self.card_request_ids: list[Any] = []
self.send_metadata: list[Any] = []
self._editable = editable
@@ -39,6 +40,7 @@ class _ButtonAdapter:
async def send_exec_approval(self, *a: Any, **k: Any) -> SendResult:
self.card_metadata.append(k.get("metadata"))
self.card_request_ids.append(k.get("request_id"))
return SendResult(success=True, message_id="card-1")
async def send(self, chat_id: str, message: str, **k: Any) -> SendResult:
@@ -112,9 +114,9 @@ def test_prompt_carries_request_identity(pending_entry):
data = dict(pending_entry.data, timeout_s=3600)
_runner(adapter)._approval_notify_sync(data)
assert adapter.card_metadata == [{
"thread_id": "t1", "_approval_request_id": pending_entry.data["request_id"],
}]
assert adapter.card_request_ids == [pending_entry.data["request_id"]]
# Platform metadata stays the thread routing only: the relay forwards it verbatim.
assert adapter.card_metadata == [{"thread_id": "t1"}]
@pytest.mark.parametrize("text_fallback", [False, True])